This notebook is a narrative walkthrough that stays narrative-first: it measures the historical data window, shows the Uranus anomaly in arcseconds, and uses a compact Neptune search to explain why longitude tightens faster than distance before handing off to rebound_neptune_tutorial.ipynb.
The Historical Problem
Uranus was tracked from 1781 through 1846, long enough for a perturbation to accumulate into a visible departure from a smooth orbit. The bundled notebook data keeps that period self-contained: 66 annual observations, no live Horizons query required.
Code
from discoverneptune.plot_style import ( apply_style, palette, category_cycle, dual_render, SEQUENTIAL_CMAP, truth_line, annotate_stat, fig_size,)apply_style()from pathlib import Pathimport numpy as npimport pandas as pdfrom discoverneptune.simulation import ( NeptuneCandidate, StateVector, simulate_uranus_longitudes_from_vectors,)cwd = Path.cwd()observation_paths = [ cwd /"data"/"uranus_observations_1781_1846.csv", cwd.parent /"data"/"uranus_observations_1781_1846.csv",]state_vector_paths = [ cwd /"data"/"outer_planet_state_vectors_1781_01_01.csv", cwd.parent /"data"/"outer_planet_state_vectors_1781_01_01.csv",]for observation_path in observation_paths:if observation_path.exists():breakelse:raiseFileNotFoundError("Run this notebook from the repository root or the docs/ directory ""so the bundled Uranus observations can be found." )for state_vector_path in state_vector_paths:if state_vector_path.exists():breakelse:raiseFileNotFoundError("Run this notebook from the repository root or the docs/ directory ""so the bundled state vectors can be found." )obs = pd.read_csv(observation_path)state_vectors = pd.read_csv(state_vector_path)state = { row.planet: StateVector(row.x, row.y, row.z, row.vx, row.vy, row.vz)for row in state_vectors.itertuples(index=False)}jd_times = obs["datetime_jd"].to_numpy()truth_longitudes = obs["longitude_rad"].to_numpy()start_jd =float(jd_times[0])end_jd =float(jd_times[-1])cadence_days = obs["datetime_jd"].diff().dropna()timeline = pd.DataFrame( {"metric": ["first observation (JD)","last observation (JD)","span (years)","median cadence (days)", ],"value": [ start_jd, end_jd, (end_jd - start_jd) /365.25,float(cadence_days.median()), ], })timeline
metric
value
0
first observation (JD)
2.371558e+06
1
last observation (JD)
2.395298e+06
2
span (years)
6.499658e+01
3
median cadence (days)
3.650000e+02
What Le Verrier Had Access To
Le Verrier worked from observation tables and hand-built theory, not from a modern ephemeris or a numerical integrator. In this reconstruction we keep the inner problem simple too: Jupiter, Saturn, and Uranus start from JPL state vectors, and only Neptune is left as an unknown.
From Residuals to an Inverse Problem
The forward model takes a small Neptune vector, propagates the outer planets, and compares the predicted Uranus longitude against the observations. The unknowns are deliberately compact: mass, semi-major axis, eccentricity, and mean longitude.
Why Longitude Is Easier Than Distance
Longitude is the cleanest observable in the bundled data set, so the notebook measures the perturbation in angular residuals. A modern grid search lets us ask a sharper question: if we keep only the best few fits, how tightly do they cluster in longitude compared with semi-major axis and mass? That comparison is the computational version of Le Verrier’s practical advantage: sky position tightens faster than orbital scale.
The next cells keep one best longitude for each mass/a pair, then compare the top-performing solutions as a group. In this toy search the best fits keep longitude within a few degrees, even while semi-major axis and mass still move across a visibly broader valley. That is the mass-distance tradeoff the notebook is meant to surface.
The hands-on companion notebook, rebound_neptune_tutorial.ipynb, takes the same bundled data and rebuilds the search step by step inside REBOUND.
The result in one figure
Everything above compresses to this: remove Neptune from the model and Uranus drifts ~130 arcseconds out of place; add the optimizer’s Neptune back and the drift collapses to the few-arcsecond model noise floor. The quick search budget used here lands at ~2.9″ RMS; the production optimizer (see The Uranus Problem) reaches ~2.5″.