Model the Shapiro delay in radar residuals - #529
Merged
Conversation
A radar signal passes through the Sun's gravitational potential on each leg, and
the resulting delay was not modelled. For a leg between A and B,
dt = (2 GM / c^3) ln[(r_A + r_B + rho) / (r_A + r_B - rho)]
with r_A and r_B heliocentric distances. At the geometries these observations are
made it is one to two microseconds, which sounds negligible until you notice that
radar delay uncertainties are 0.3 to 2 us: leaving it out biases every radar
observation by several sigma.
Validated against real Apophis radar astrometry, on the epochs near the reference
orbit where propagation error is negligible. The delay residual falls from
2.16 sigma to 0.312 sigma, and the Doppler residual is unchanged at 0.28 sigma.
The comparison is against an independent Python implementation of the same
formula, so it checks the physics rather than this code; the harness is
ISSUE_146_shapiro_validation.py. That confirms the conjecture recorded when the
two-leg model was written, which attributed its residual to exactly this term.
The ephemeris is passed into compute_radar_residuals rather than read from
r->extras: the simulation used for residuals has no assist_extras attached, so
that pointer is NULL there.
The partials are left alone. The Jacobian is already a single-leg approximation
by design, and a microsecond-scale, slowly varying term does not change the step
direction.
The synthetic radar truth was generated with delay = 2 rho/c and no Shapiro term. Once the fitter models it, that truth is inconsistent with the fitter by 19.2 us per observation -- on seven observations with a stated 1 us uncertainty, which walked the chi-square to 1.0e-2. Both generators now include the term: tests/data/radar_synthetic.json is regenerated (ISSUE_146_gen_radar_fixture.py, kept with the other harnesses), and test_radar_end_to_end builds its observables inline, so that copy is fixed too. The Sun is taken at the emission time, where the C++ light-time solution leaves the simulation. The end-to-end tests then pass unchanged. test_radar_validation needed its chi-square bound moved, because that bound changed meaning: the fixture comes from an independent implementation of the same model, and the two agree to about 0.3 ns per observation rather than exactly. That is 3e-4 of the fixture's own stated uncertainty, and a hundred times finer than the best real radar timing in the JPL database. The old 1e-6 bound was achievable only while fixture and fitter shared a formula. The state tolerances are tightened rather than relaxed -- 10x on position and 100x on velocity -- since those carry the physics the test exists to check, and recovery is 2.7e-9 au and 1.5e-11 au/day.
An Observation carries one station, used for both the transmit and the receive leg. That is monostatic by design -- ISSUE_146_RADAR_DESIGN.md scopes the first implementation to TX == RX and lists bistatic as a refinement -- but nothing said so at the point where a caller assembles radar rows. Passing a bistatic measurement therefore evaluates the receive leg at the transmitting antenna, and does it silently. On real Goldstone bistatic pairs that is a ~4% Doppler error (observed/model 1.039-1.044 against 0.999934 for monostatic rows), which against a 0.1 Hz uncertainty is of order a hundred sigma per observation. Fitting (6489) Golevka with its 18 bistatic rows included gave flag 2 and a reduced chi-square of 1.5e7; with them removed, flag 0 at 0.80. No behaviour change -- the constraint already existed, it was just not written down where someone would meet it.
The end-to-end test was added after the last formatting pass and used hand-aligned trailing comments and a manually wrapped Shapiro expression, both of which black 25.1.0 rewrites. No functional change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Radar signals pass through the Sun's gravitational potential on both legs, and that delay was not modelled. For a leg between A and B,
with r_A and r_B heliocentric distances. It is one to two microseconds at the geometries these observations are made, which sounds negligible until you notice that radar delay uncertainties in the JPL database run 0.2 to 4 microseconds, and are 0.30 for Golevka at Arecibo. Leaving it out biases every radar observation by several sigma.
Validated against real Apophis radar, on the epochs near the reference orbit where propagation error is negligible: the delay residual falls from 2.16 sigma to 0.312 sigma, and Doppler is unchanged at 0.28 sigma. The comparison is against an independent Python implementation of the same formula (
ISSUE_146_shapiro_validation.py), so it checks the physics rather than this code. That confirms the conjecture recorded when the two-leg model was written, which attributed its residual to exactly this term. It also bears on the Apophis covariance work, where the unmodelled term was running at a median 8.5 sigma.Two things came with it:
compute_radar_residualsrather than read fromr->extras. The simulation used for residuals has noassist_extrasattached, so that pointer is NULL there.radar_synthetic.jsonwas generated withdelay = 2 rho/cand no Shapiro, so once the fitter modelled it the truth was inconsistent by 19.2 us per observation. There were two stale generators: the fixture script, and a second copy inlined intest_radar_end_to_end. Fixing both made the end-to-end tests pass with no tolerance touched.test_radar_validation's chi-square bound did move, and the reason is in the test: the fixture now comes from an independent implementation, which agrees with the C++ to about 0.3 ns per observation rather than exactly. That is 3e-4 of the fixture's own stated uncertainty. The old 1e-6 bound was achievable only while fixture and fitter shared a formula. State tolerances are tightened rather than relaxed, 10x on position and 100x on velocity, since those carry the physics.The partials are left alone: the Jacobian is already a single-leg approximation by design, and a microsecond-scale slowly varying term does not change the step direction.
Suite: 536 passed, 1 skipped on all four CI legs. Related: #528.
Reviewing this branch requires rebuilding the C++ extension.
orbit_fit.cppandradar_synthetic.jsonchange together, so an older binary gets scored against a fixture that now carries the Shapiro term. The fit still converges and absorbs most of the 19.2 us into the orbit, which makes the failure look like a physics error rather than a stale build:test_radar_validationreports chi-square 0.0104 against its 1e-4 bound, and the end-to-end test a 3.7e-6 velocity drift. Afterpip install -e .all five radar tests pass.