Feature/light travel time - #182
Merged
Merged
Conversation
…lidation Implements exoplanet's analytic retarded-time method in components/ltt.py (one Kepler solve, closed-form Taylor-expansion delay, differentiable for NUTS). Adds C_LIGHT_RSUN_PER_DAY constant. Validated: delay amplitude 499s at 1 AU, secondary-eclipse offset 2a/c, finite gradient at az=0. Phase 1 only -- not yet wired into transit/rm.
Applies the Roemer delay correction to the transit geometry and reflection in both build_likelihood and compile_plotters, keeping the two paths consistent (plotted==likelihood invariant preserved). Beam/ellipsoidal stay on uncorrected time (star-emitted; reflex delay off by default). LTT hardcoded on pending config keys. Off-path reproduces pre-LTT model exactly. EXOFASTv2 flux-parity tests run with LTT off (fixtures are pre-LTT).
The OFF-equivalence test loaded pre-LTT code via git show HEAD, which stops being pre-LTT once the LTT commit lands (HEAD becomes the LTT-on code). Freeze a genuinely-pre-LTT reference (from commit 3ae5466) as a stored fixture and compare against that instead, so the test survives commits.
Applies the Roemer delay inside compute_rm_rv (both build_likelihood and compile_plotters inherit it), correcting only the occultation geometry via get_true_anomaly; the plain Keplerian RV term stays undelayed. Uses physical orbits.a and the m_primary/m_total planet factor. Off-path reproduces the pre-LTT RM output (stored fixture from df96bd9).
Replaces the hardcoded LTT flags in transit and RM with a per-file light_travel_time config option, default true. Transit group loop skips the correction when a group is fully off and the mask when fully on, paying both only for mixed groups. Off-equivalence and parity tests set the config key instead of patching a constant. Full suite green; LTT default-on shifts no committed baseline.
Two independent errors in the light-travel-time correction, both invisible
to the tests that shipped with it.
1. solve_delay entered vz with the WRONG SIGN. The expression was
transcribed from exoplanet's _get_retarded_position, but exoplanet's
`vz` is -dZ/dt for its own Z (its _rotate_vector returns
Z = -r*sin(f+omega)*sin(i), carrying a minus its vz does not), so its
`(c + vz)` is really `(c - dZ/dt)`. This module builds a genuinely
self-consistent (z, dz/dt, d2z/dt2), so the transcription flipped vz.
Measured against a brentq solve of the retardation condition
(a=0.05 AU, P=3 d, e=0.3, i=89): 14 ms error, ~1000x the truncation
error the expansion is supposed to have.
The quadratic is now solved from scratch. That also drops a second,
smaller error inherited from the same source: exoplanet's expression is
the small root of the quadratic with az -> -az, so it is correct to
first order in the acceleration and wrong at second, by
(c/az)*B^2/(4*A^3) -- 9 us for that orbit, against 4 ns for the correct
root. Negligible for exoplanet; free not to inherit.
Every earlier test evaluated the delay where vz == 0 EXACTLY (a
conjunction of a circular omega=0 orbit, or a face-on orbit where
z/vz/az all vanish), so none of them constrained vz's sign at all.
2. The occultation used m_primary/m_total. A transit is not an emission
event -- the planet emits nothing, it blocks light the STAR emitted --
so the observed separation needs both bodies at their own retarded
times, and expanding both leaves the relative orbit evaluated at
t - [(m_primary - m_companion)/m_total]*z_rel/c. The star's own delay
partially cancels the planet's; m_primary/m_total is the same thing
with the star pinned at the barycenter, i.e. with z_star/c dropped.
The two agree to O(q) for a planet (tens of ms) but differ
qualitatively for comparable masses: at equal masses the true
primary-to-secondary offset is EXACTLY zero by symmetry -- the standard
eclipsing-binary result used to measure mass ratios (Kaplan 2010,
Fabrycky 2010) -- while m_primary/m_total predicts a/c, 25 s for a
0.05 AU orbit. Applies to transit (likelihood and plotters) and to RM,
which is the same seam.
Also guards the three call sites against an orbit whose bodies did not
resolve: Orbit only registers a/m_primary/m_companion/m_total when
_validate_bodies passes, and light_travel_time defaults to ON, so reading
them unguarded turned a previously-working geometry-only config into an
AttributeError at build time. Now warns and disables.
Tests pin the delay against a brentq solve of the retardation condition at
nine phases spread over a full period (where vz is large and of both
signs), assert the answer is actually sensitive to vz's sign, and check
the occultation factor against a direct two-body solve with each body at
its own retarded time, over mass ratios from 1e-3 to 1.
Documents, but does not implement, the multi-companion cross terms: the
star's reflex from OTHER companions contributes a
sum_{i!=b} (M_i/M_tot)*z_rel,i/c time shift -- the classic LITE/LTT
eclipse-timing signal, 2.5 s for a Jupiter at 5 AU and varying on the
outer period -- plus a transverse term that is not a time shift at all.
Both need the full system state, not one orbit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two follow-ups to the Roemer-delay fix.
1. Reflection, beaming and ellipsoidal now use their own time bases.
The correction factor depends on WHICH observable is being retarded,
not on the timestamps, so one corrected time array cannot serve a whole
phase curve. Three roles, three factors (ltt.py's `factor` docs):
occultation seam (transit/eclipse shape, and via planetvisible the
thermal gating) (m_primary - m_companion)/m_total
light emitted by the planet (reflection) m_primary/m_total
light emitted by the star (beaming, ellipsoidal)
m_companion/m_total
Before this, reflection rode the occultation seam's time while beaming
and ellipsoidal used the UNCORRECTED time, so a single phase curve
mixed three references differing by ~a/c (~25 s, ~1e-4 in phase). Note
the stellar factor is ~q rather than ~1: leaving those terms
uncorrected was much closer to right than correcting them with the
planet's factor would have been, but still ~q*a/c off and inconsistent
with the geometry they are added to.
Both the likelihood and the plotter paths change together -- they must
stay identical or the plotted curve stops matching what the fit
optimized (test_plotted_model_matches_likelihood_model). Each extra
time base costs one Kepler solve and is built only when its term is
actually active, so an ordinary transit-only fit pays exactly what it
paid before.
2. Model-selecting per-file keys now reach the structural hash.
`light_travel_time`, `gp`, `likelihood`, `mask`, `mass_parameterization`,
`ld_law`, `chen`, `rm`, `data_format` and `mass_function` each flip
WHICH likelihood is built while leaving the component set, the file
list and every parameter's structure untouched -- so structural_hash
could not see them, and a trace sampled with one setting was silently
reused under the other by recompute_trace: false, exozippy-modes and
mkparam.write_param_file. light_travel_time is the sharpest case
because it defaults to ON.
Deliberately an allowlist (evaluator._STRUCTURAL_INSTANCE_KEYS): a
denylist would need no maintenance but would change the payload for
every config and so invalidate every trace on disk. The price is that a
new model-affecting per-file key must be added there in the same commit
that introduces it. Keys are recorded only when PRESENT, so a config
that sets none of them keeps its old payload byte-for-byte and adding
entries cannot stale unrelated traces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inverts the failure mode of the structural hash. Nearly every key inside a component instance selects part of the model -- light_travel_time, gp, likelihood, mask, ld_law, data_format, rm, exptime, ninterp, band, star_ndx, the SED's mag/err photometry -- so an allowlist is a hand-maintained list of everything, and one forgotten entry fails SILENTLY: the trace reloads under a model it was never sampled from, via recompute_trace: false, exozippy-modes or mkparam.write_param_file. light_travel_time was exactly that, and it defaulted to ON. A denylist fails the other way: forget a cosmetic key and the worst that happens is an honest re-run. Only `plot` and `label` are dropped as cosmetic, plus `file`/`files` because structural_payload already hashes those under its own "files" key (`path` is NOT dropped -- nothing else captures it). This invalidates every trace on disk once, deliberately: the payload shape changed for every config. That is the one-time cost of never silently reusing foreign draws again. Co-Authored-By: Claude Opus 5 (1M context) <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.
Adds light travel time (Rømer delay). Physics follows EXOFASTv2; solves the timing correction with a direct formula following the exoplanet toolkit, since a runtime loop isn't differentiable under NUTS. On by default, set per file with
light_travel_time. Turning it off reproduces the old results exactly.