Skip to content

Make predict_sequence march (integrate once, interpolate per detection) - #428

Merged
matthewholman merged 2 commits into
mainfrom
perf/predict-sequence-march
Jul 7, 2026
Merged

Make predict_sequence march (integrate once, interpolate per detection)#428
matthewholman merged 2 commits into
mainfrom
perf/predict-sequence-march

Conversation

@matthewholman

Copy link
Copy Markdown
Collaborator

Summary

predict_sequence re-integrated each orbit from its epoch for every requested time. It looped detections calling predict_from_fit_resultpredict, and predict does reb_simulation_create … assist_free … reb_simulation_free per call — so predicting an orbit to N times ran N full integrations from the epoch, each with its own IAS15 ramp-up.

residuals_at_state (#406) already solved exactly this for the fit/rejection path with a sorted forward + backward single pass that reuses one sim and lets integrate_or_interpolate interpolate at each detection. predict_sequence simply never got the same treatment. This PR gives it that treatment.

Change

  • predict_sequence now marches. It splits detections into those after the epoch (marched forward) and before it (marched backward), each sorted, and runs each pass against a single sim — so the trajectory is integrated once per direction and integrate_light_time → assist_integrate_or_interpolate interpolates at each detection, instead of a fresh integration per detection. Mirrors residuals_at_state/create_sequences.
  • Refactored the per-detection geometry + covariance out of predict() into a reusable compute_single_predict(ephem, ax, var, …) that operates on an existing sim. The bound single-observation predict() keeps its own sim create/free and is behavior-unchanged.
  • Because each detection carries its own observer position, a single predict_sequence call may now mix observatories (each detection is light-corrected against its own station).

Correctness

integrate_or_interpolate makes the integration independent of the query times (it interpolates at each), so the marching result is bit-identical to the per-detection path:

max |ΔRA|  = 0.000e+00 deg
max |ΔDec| = 0.000e+00 deg
max ellipse rel diff = 0.000e+00

(verified across times both before and after the epoch, i.e. both passes). New test test_predict_sequence_marches_equivalently asserts a one-call multi-time prediction equals per-time predictions to integrator precision.

Impact

Any multi-time prediction benefits — ephemeris generation, the predict CLI over a time grid, and covariance-aware prediction of a catalog to many epochs — turning N integrations per orbit into ~1. No API change (predict / predict_sequence signatures and outputs are unchanged).

🤖 Generated with Claude Code

predict_sequence looped detections calling predict_from_fit_result -> predict,
which does reb_simulation_create ... reb_simulation_free PER detection, so an
orbit was re-integrated from its epoch for every requested time -- N detections =
N full integrations. residuals_at_state (#406) already solved this for the fit
path with a sorted forward+backward single pass; predict_sequence just never got
the same treatment.

Rework predict_sequence to mirror residuals_at_state: split detections into those
after the epoch (marched forward) and before it (marched backward), each sorted so
one sim integrates the trajectory once per direction and integrate_light_time ->
assist_integrate_or_interpolate interpolates at each detection. Each detection
keeps its own observer position, so a single call may now mix observatories.

Refactored the per-detection geometry+covariance out of predict() into a reusable
compute_single_predict(ax, var, ...) that operates on an existing sim; the bound
single predict() keeps its own sim and behavior. Because integrate_or_interpolate
makes the integration independent of the query times, results are BIT-IDENTICAL
to the per-detection path (verified: 0 difference in RA/Dec/ellipse), at a
fraction of the integrations.

Adds test_predict_sequence_marches_equivalently (one multi-time call == per-time
calls).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_bench_residuals_runs asserted residuals_at_state is faster than
predict_sequence (speedup > 1), which held only because predict_sequence
re-integrated from the epoch per detection. Now that predict_sequence uses the
same sorted single-pass march, the two are comparable (ratio ~1, e.g. 0.998 on
CI), so that assertion is no longer valid. Relax it to guard only against a
pathological regression (> 0.5, i.e. not more than ~2x slower) -- consistent with
this file's 'not a performance assertion' intent -- and update the benchmark's
docstring/framing to reflect that both paths march.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@matthewholman
matthewholman requested a review from kjnapier July 6, 2026 23:38

@kjnapier kjnapier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good

@matthewholman
matthewholman merged commit 3614730 into main Jul 7, 2026
10 of 11 checks passed
@matthewholman
matthewholman deleted the perf/predict-sequence-march branch July 7, 2026 21:28
matthewholman added a commit that referenced this pull request Jul 7, 2026
Resolve test_predict.py conflict with #428 (predict_sequence march):
keep both new tests (mixed/single obscode + marching equivalence) as
separate self-contained functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants