Skip to content

Commit 60b5b75

Browse files
Merge pull request #438 from Smithsonian/feat/ias15-adaptive-mode-2-default
Default the IAS15 integrator to the modern (mode 2) adaptive step controller
2 parents 8f8e3f8 + 80f1653 commit 60b5b75

2 files changed

Lines changed: 45 additions & 7 deletions

File tree

src/lib/orbit_fit/orbit_fit.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,12 @@ namespace orbit_fit
6363
// the legacy value, apply_ias15_adaptive_mode() must run *after*
6464
// assist_attach, not before like apply_ias15_min_dt.
6565
//
66-
// Default -1 means "leave ASSIST's choice untouched" (no behavior
67-
// change for existing callers).
68-
static int g_ias15_adaptive_mode = -1;
66+
// Default 2 selects the newer (Pham, Rein & Spiegel 2024) controller on
67+
// every freshly-attached sim: it steps through close-Earth encounters
68+
// gracefully, whereas the legacy controller (mode 1) drives dt -> 0 and can
69+
// hang the fit, at no accuracy cost (correctness is mode-independent).
70+
// Set to -1 to leave ASSIST's legacy choice untouched, or 1 to force it.
71+
static int g_ias15_adaptive_mode = 2;
6972

7073
inline void set_ias15_adaptive_mode(int m) { g_ias15_adaptive_mode = m; }
7174
inline int get_ias15_adaptive_mode(void) { return g_ias15_adaptive_mode; }

tests/layup/test_ias15_settings.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,28 @@
88
99
The settings are process-wide (file-scope globals in orbit_fit.cpp), so each
1010
test saves and restores the prior value to avoid leaking state into other
11-
tests in the same process.
11+
tests in the same process. The one fit-based test,
12+
``test_fit_is_mode_independent``, is guarded by ``requires_ephem``.
1213
"""
1314

1415
from __future__ import annotations
1516

1617
import contextlib
1718

19+
import numpy as np
1820
import pytest
1921

22+
from _bk_guards import requires_ephem
23+
from layup.orbitfit import orbitfit
2024
from layup.routines import (
2125
get_ias15_adaptive_mode,
2226
get_ias15_min_dt,
2327
set_ias15_adaptive_mode,
2428
set_ias15_min_dt,
2529
)
30+
from layup.utilities.data_processing_utilities import parse_fit_result
31+
from layup.utilities.data_utilities_for_tests import get_test_filepath
32+
from layup.utilities.file_io.CSVReader import CSVDataReader
2633

2734

2835
@contextlib.contextmanager
@@ -65,9 +72,11 @@ def test_min_dt_reset_to_zero():
6572
assert get_ias15_min_dt() == 0.0
6673

6774

68-
def test_adaptive_mode_default_is_unset():
69-
"""By default adaptive_mode is -1, the sentinel meaning 'do not override'."""
70-
assert get_ias15_adaptive_mode() == -1
75+
def test_adaptive_mode_default_is_two():
76+
"""The default adaptive_mode is 2, the modern (Pham, Rein & Spiegel 2024)
77+
IAS15 step controller, engaged on every freshly-attached sim so close-Earth
78+
encounters do not drive the timestep to zero and hang a fit."""
79+
assert get_ias15_adaptive_mode() == 2
7180

7281

7382
@pytest.mark.parametrize("mode", [0, 1, 2, 3])
@@ -85,3 +94,29 @@ def test_adaptive_mode_reset_to_sentinel():
8594
assert get_ias15_adaptive_mode() == 2
8695
set_ias15_adaptive_mode(-1)
8796
assert get_ias15_adaptive_mode() == -1
97+
98+
99+
@requires_ephem
100+
def test_fit_is_mode_independent():
101+
"""The IAS15 adaptive controller changes only whether/how fast a fit
102+
completes, never the fitted orbit. A well-behaved object fit under the
103+
legacy controller (mode 1) and the modern controller (mode 2, the default)
104+
returns the same state (bit-for-bit for a well-conditioned arc)."""
105+
input_data = CSVDataReader(
106+
get_test_filepath("1_random_mpc_ADES_provIDs_no_sats_micro.csv"),
107+
"csv",
108+
primary_id_column_name="provID",
109+
).read_rows()
110+
111+
def _fit_state(mode):
112+
with _restore_adaptive_mode():
113+
set_ias15_adaptive_mode(mode)
114+
rows = orbitfit(input_data, cache_dir=None)
115+
for row in rows:
116+
if row["flag"] == 0:
117+
return np.array(parse_fit_result(row).state, dtype=float)
118+
raise AssertionError("fit did not converge (flag != 0)")
119+
120+
legacy = _fit_state(1)
121+
modern = _fit_state(2)
122+
np.testing.assert_allclose(modern, legacy, rtol=1e-10, atol=1e-12)

0 commit comments

Comments
 (0)