Skip to content

orbitprop: initial_step_secs hint and next_step_secs warm start (numeris 0.6) - #178

Merged
ssmichael1 merged 2 commits into
mainfrom
feat/ode-warm-start
Sep 12, 2026
Merged

orbitprop: initial_step_secs hint and next_step_secs warm start (numeris 0.6)#178
ssmichael1 merged 2 commits into
mainfrom
feat/ode-warm-start

Conversation

@ssmichael1

@ssmichael1 ssmichael1 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the initial-step discussion on #175. The adaptive integrators no longer start from numeris' Hairer starting-step heuristic, which is scale sensitive: for an orbit in metres and seconds at 1e-9 tolerance it started rkv98 at a fraction of a millisecond against a ~270 s working step, and about half the force evaluations of a one-hour arc went into growing that first step.

  • propsettings.initial_step_secs (Rust PropSettings::initial_step_secs: Option<f64>) — first step the adaptive integrators attempt. Default None derives it from the initial state, the tolerances and the integrator order:

    h0 = 1.5 · |r|/|v| · tol^(1/(p+1)),   tol = rel_error + abs_error/|r|
    

    The settled stride of an order-p method scales as tol^(1/(p+1)); the constant is fit to LEO strides. Across rkts54..rkv98 and tolerances 1e-6..1e-12 the hint lands within 0.64–2.6× of the settled stride, so the controller is on stride within a step or two. Ignored by gauss_jackson8. Zero / non-finite raises.

  • propresult.next_step_secs (Rust PropagationResult::next_step_secs) — the integrator's working stride at the end of the arc (numeris Solution::next_step, i.e. the controller's last unclamped proposal; GJ8's fixed step; 0 for a zero-duration arc), signed like the propagation direction. Pass it back as initial_step_secs to warm-start a follow-on arc.

  • Python kwarg + property (pickled), stubs and docstrings, a "Starting Step and Warm Start" section in the integrators guide, CHANGELOG entry.

  • numeris requirement 0.5.18 → 0.6.0 (which adds AdaptiveSettings::initial_step / Solution::next_step).

Measured against main

Same script, same machine: one-hour arcs, 8x8 gravity. h1 = first hour from an axis-aligned state, h2 = second hour from the resulting general state, (n) = rejected steps.

Integrator Tol main h1 branch h1 saved main h2 branch h2 saved warm h2
rkv98 1e-6 422 168 60% 275 168 39% 168
rkv98 1e-9 674 315 53% 464 315 32% 294
rkv98 1e-12 1514 630 58% 884 630 29% 609
rkv87 1e-6 325 170 48% 240 170 29% 153
rkv87 1e-9 580 357 38% 444 357 20% 340
rkv87 1e-12 1226 816 33% 954 816 14% 799
rkv65 1e-6 312 200 36% 252 200 21% 200
rkv65 1e-9 792 610 23% 672 610 9% 610
rkv65 1e-12 2392 1940 (1) 19% 2042 1930 (1) 5% 1920
rkts54 1e-6 207 151 27% 183 151 17% 151
rkts54 1e-9 675 646 (3) 4% 652 (1) 627 (2) 4% 620
rkts54 1e-12 2572 (1) 2488 (3) 3% 2530 (1) 2479 2% 2485

For the #175 script's scenario (40x40 EGM96 + drag + third bodies, rkv98 at 1e-9) the first hour goes from 779 evaluations on main to 441 with the default hint and 420 warm-started; final positions agree to 2 mm.

Why not fix the controller instead: the ramp is bound by the error-estimate noise floor (round-off against a 1e-9 m absolute tolerance), not by the PID exponents — an undamped opening phase recovered only ~25–30% of the waste and made short-arc error ~10× worse. A step hint removes the ramp entirely and leaves the controller and tolerances untouched.

Also: rkv98 runs as the 16-stage tableau when interpolation is off

After the ramp is gone the step controller is within a few percent of optimal (an undamped controller gained 1.5% on a one-day arc), so the remaining lever is stage count. The five extra stages of the 21-stage Verner 9(8) tableau exist only to build the interpolant. With enable_interp=False nothing is stored to interpolate, so integrator.rkv98 now dispatches to the 16-stage rkv98_nointerp tableau: same order and error control, 24% fewer force evaluations per step. The result still reports rkv98 and interp fails with "no dense output" exactly as before. Results for that combination change at the tolerance level, since a different tableau takes different steps. Tests pin evals == 16 × steps without interpolation and 21 × steps with, and 1 cm agreement of the two final positions over an hour.

Test plan

  • New Rust test test_initial_step_and_warm_start: hint is the first accepted step, warm ≲ default < cold, same final position, backward stride sign, GJ8 reports its fixed step, invalid hint errors
  • Five new pytest cases in test_propagation.py (default / kwarg / setter, pickle round-trip, stride sign, warm-start savings, 16-stage tableau, invalid hint) — 50/50 pass on the rebuilt extension
  • cargo test --release --lib (298) and doctests (44) pass; clippy clean; python/ crate type-checks

🤖 Generated with Claude Code

https://claude.ai/code/session_017VHexyxYasfatXebfxtphw

ssmichael1 and others added 2 commits September 11, 2026 21:16
… (numeris 0.6)

The adaptive integrators no longer start from numeris' Hairer starting-step
heuristic, which is scale sensitive: for an orbit in metres and seconds at
1e-9 tolerance it started RKV98 at a fraction of a millisecond against a
~270 s working step and spent about half the force evaluations of a
one-hour arc growing into it (satkit #175 discussion).

- `PropSettings::initial_step_secs: Option<f64>` — first step to attempt.
  `None` (default) derives `1.5 · |r|/|v| · tol^(1/(p+1))` from the initial
  state, the tolerances (`tol = rel_error + abs_error/|r|`) and the
  integrator order `p`: the settled stride of an order-p method scales as
  tol^(1/(p+1)), and the constant is fit to LEO strides. Across RKTS54..RKV98
  and 1e-6..1e-12 the hint lands within 0.64–2.6× of the settled stride, so
  the controller is on stride in a step or two. Ignored by Gauss-Jackson 8.
  Zero / non-finite → error.
- `PropagationResult::next_step_secs` — the integrator's working stride at
  the end of the arc (numeris `Solution::next_step`; GJ8's fixed step;
  0 for a zero-duration arc), signed like the propagation direction, so a
  follow-on arc can warm-start by passing it back.
- Python: `propsettings.initial_step_secs` (kwarg + property, pickled) and
  `propresult.next_step_secs`; stubs and docstrings; integrators guide
  section; CHANGELOG.
- numeris requirement 0.5.18 → 0.6.0.

One-hour 550 km arc, RKV98 at 1e-9, 8x8 gravity: 525 evals with the
old-heuristic-sized start → 315 with the default hint → 294 warm-started
from the previous hour; every RK integrator at every tolerance from 1e-6
to 1e-12 is now within one step of the warm-start cost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw
The five extra stages of the 21-stage Verner 9(8) tableau exist only to
build the interpolant. With `enable_interp = false` nothing is stored to
interpolate, so `Integrator::RKV98` now dispatches to `RKV98NoInterp`:
same order and error control, 24% fewer force evaluations per step.
The result still reports `Integrator::RKV98`; `interp` fails with
"no dense output" exactly as before. Results for that combination change
at the tolerance level (a different tableau takes different steps).

Tests pin evals == 16 × steps without interpolation and 21 × steps with,
and agreement of the two final positions to 1 cm over an hour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw
@ssmichael1
ssmichael1 merged commit b5175dc into main Sep 12, 2026
8 checks passed
@ssmichael1
ssmichael1 deleted the feat/ode-warm-start branch September 12, 2026 16:46
@ssmichael1 ssmichael1 mentioned this pull request Sep 12, 2026
ssmichael1 added a commit that referenced this pull request Sep 12, 2026
Version bump in Cargo.toml, python/Cargo.toml, pyproject.toml; changelog
rolled (Unreleased -> 0.22.0 - 2026-09-12; entries added for #176 and the
#178 references; 0.20.3 pruned to keep five releases). Docs index links the
new starting-step / warm-start section of the integrators guide.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw
ssmichael1 added a commit that referenced this pull request Sep 12, 2026
* Release 0.22.0

Version bump in Cargo.toml, python/Cargo.toml, pyproject.toml; changelog
rolled (Unreleased -> 0.22.0 - 2026-09-12; entries added for #176 and the
#178 references; 0.20.3 pruned to keep five releases). Docs index links the
new starting-step / warm-start section of the integrators guide.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw

* docs: release steps in CONTRIBUTING (cargo update, version sync, tag)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw

* build: compile the extension with --locked against the committed Cargo.lock

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw

* Cargo.lock: record 0.22.0 for the workspace crates

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VHexyxYasfatXebfxtphw

---------

Co-authored-by: Claude Fable 5.1 <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.

1 participant