Adaptive (error-controlled) timestep for the MetSim erosion model (on by default) - #86
Adaptive (error-controlled) timestep for the MetSim erosion model (on by default)#86dvida wants to merge 17 commits into
Conversation
… model The fixed RK4 step (const.dt = 0.005 s) has no convergence basis and is badly under-resolved for fast bodies and small grains: on a fast Orionid single body the dt=0.005 velocity error vs a converged run is ~6 km/s, and the light-curve shape depends on the step size. This adds an opt-in adaptive integrator so accuracy is set by a tolerance rather than a magic constant, while leaving the default behaviour (and every stored dt=0.005 fit) bit-for-bit unchanged. - New Cython adaptiveSingleBodyStep() in MetSimErosionCyTools.pyx advances one fragment across a macro step with per-fragment error-controlled adaptive sub-steps (step-doubling on the existing RK4, PI controller, atmosphere refreshed each sub-step). The m^(-1/3) drag term is floored at m_kill so near-exhaustion grains stay numerically stable. - ablateAll() is gated on const.adaptive_dt: True -> the adaptive stepper; False -> the original fixed-step code path, unchanged. The macro step stays the fixed output cadence (one results row per dt), so the output grid stays uniform and no downstream consumer (GUI residuals, Dynesty likelihood, wake) needs changes. - Erosion grains are shed per sub-step at each sub-step's resolved height/velocity/time, so the erosion light-curve shape stops depending on the step size (grain births match a finely-stepped fixed run; radiated energy conserved to <0.5%). - Constants gets adaptive_dt / adaptive_rtol / adaptive_atol_m / adaptive_atol_v / adaptive_dt_min / adaptive_dt_max / adaptive_max_substeps (default rtol 1e-5, targeting numerical error below measurement noise, not machine precision). runSimulation and loadConstants back-fill these for older JSONs so they default to fixed-step. - GUI: an "adaptive dt" checkbox next to the time-step box; when checked, adaptive_dt is on and the fixed-dt box is disabled (dt is then only the output cadence). Tolerances are set via the sim-fit JSON. - Tests: MetSim/Tests is now an importable package (__init__.py; still excluded from wmpl's import-time walk). Added adaptive-mode regression tests (stability/no-NaN/mass and radiated-energy conservation vs fixed). Validation: adaptive_dt=False reproduces master bit-for-bit (np.array_equal) on the synthetic scenario + 13 fitted Orionids. Convergence study: adaptive at rtol=1e-6 cuts the single-body velocity error from 6.2 to 0.05 km/s (130x); erosion light curves converge with mass/energy conserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "adaptive" checkbox previously overlapped the m_kill "kg" unit label and the len_kill field in the packed first row. Move the adaptive controls into the free right-hand column: "adaptive" checkbox on row 2, an "rtol" input box on row 3 (enabled only when adaptive is on), so tolerance is tunable from the GUI instead of JSON-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Designer layout tweaks to the simulation settings group. - Normalize Qt6-style fully-scoped enum names (Qt::AlignmentFlag::*, Qt::Orientation::*) back to the short form so PyQt5's loadUi can parse the .ui. - Expand the rtol tooltip with concrete tuning guidance (accuracy vs cost, target below measurement noise). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The adaptive stepper fed the net macro-step speed change (v_start - v)/dt into the luminosity deceleration term. For a massive, high-altitude fragment (drag ~ 0) that net change is dominated by the gravity/curvature velocity reallocation and can flip sign, making the m*v*decel term (and thus lum) large and negative. The fragment was then killed on tick 1 by the lum<0 check, yielding an empty simulation and a downstream interp1d crash in the GUI when adaptive stepping was enabled. Accumulate the drag-only speed change (sum of decelerationRK4 * substep) instead, matching the fixed path's use of drag deceleration. Verified on a 2100 kg fragmentation fireball: adaptive now matches the fixed light curve to <0.05 mag (was: 1 row, lum<0, crash). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A simulation that dies immediately (0-1 rows, or all-NaN magnitudes) previously crashed the whole app with an interp1d "reshape array of size 0" error. Add simulationUsable() and skip the interpolation/plotting chain with a clear warning when the current (or previous) run has too few valid points or no positive luminosity. Also fall back to the full arrays inside updateInterpolations() when too little of the trajectory reaches past the observed begin, so the normalized interpolators still build instead of crashing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r diagnostic From the PR #86 review: - F2: carry the controller's natural (pre-boundary-clamp) sub-step as the next macro step's warm-start instead of the tiny clamped leftover. Cuts sub-steps ~2-5x (Orionid 3436->740/row, fireball ~1673->527/row) with no effect on the visible light curve - only the faint, sub-detection tail shifts slightly. - F1: on 'going up' (vv>0) recompute h_new from the along-track length instead of forcing 0. The fixed path's frag.h=0 in that branch is immediately overwritten by the same recompute, so forcing 0 was killing upward-turning end-of-life fragments a tick earlier than fixed mode. - F3: count sub-steps accepted at the dt_min floor while still over tolerance (const.adaptive_floor_accepts) and warn once in runSimulation, so under-resolution is visible. - Record the erosion-begin (vel, mass, dyn_press) triple once at the end-of-step state so it is mutually consistent in adaptive mode (was mixing a rewound sub-step velocity with end-of-step mass/dyn_press). Backward compat unchanged: adaptive_dt=False is bit-for-bit vs master (13 Orionids + synthetic); regression suite passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces step-doubling (which needed ~3 RK4 solves per sub-step for its error estimate) with an embedded Dormand-Prince RK45 pair on the COUPLED [mass, vv, vh, length] system (adaptiveDP45Step + _rhsDP in MetSimErosionCyTools.pyx). One embedded step gives the 5th-order solution and a 4th-order error estimate in ~7 RHS evals, and the higher order takes larger sub-steps - ~3.8x faster than step-doubling at the same tolerance. Atmosphere density is refreshed at every stage (from the stage height), removing the operator-split/frozen-rho error; gravity drop is added once per accepted sub-step, matching the fixed model. Selected by const.adaptive_high_order (default True); the step-doubling stepper is kept and used when False. Grains are still shed per accepted sub-step. Validation vs the step-doubling stepper (within measurement noise, ~0.1-0.3 mag): - Fireball (no erosion, pure integrator): visible-LC max |dmag| = 0.065, 3.8x faster. - Orionid (erosion): visible-LC max |dmag| = 0.18 (grain-birth cadence), converged (rtol 1e-5 ~ 1e-6), 3.8x faster. Backward compat unchanged: adaptive_dt=False is bit-for-bit vs master (13 Orionids); regression suite passes with DP45 as the default adaptive method. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Full Arguments:/Return: docstrings for the new Cython functions: clampMassC, heightCurvatureC, atmDensityPolyC, advanceVelPosC, adaptiveSingleBodyStep, _rhsDP, adaptiveDP45Step (the last two reference adaptiveSingleBodyStep for the shared I/O). - Corrected the adaptiveSingleBodyStep return description to the actual 17-tuple. - Dropped the unused const_out binding in the erosion grain-spawn helper. No behaviour change; regression suite passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In adaptive mode grains are shed once per accepted sub-step, so the sub-step cadence set the grain-birth resolution - which made the erosion light-curve shape depend on dt (dt caps the sub-step via adaptive_dt_max). Add const.erosion_release_length (m, default 50): an eroding fragment's sub-step is capped at erosion_release_length/v, so grains are released ~every that many metres of flight, a physical resolution independent of dt and the error tolerance. Applied in both the DP45 and step-doubling steppers; only for eroding fragments (grains do not erode further). Effect (Orionid 084502): peak-magnitude difference between dt=0.005 and dt=0.02 drops from ~2.0 mag to ~0.01 mag, and the total grain count becomes dt-independent (+-6%). The knob is a set-and-forget JSON default (not a GUI box); tune it for finer/coarser grain sampling. Backward compat unchanged: adaptive_dt=False is bit-for-bit vs master; regression suite passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the error-controlled adaptive integrator on by default so erosion light curves are well resolved without hand-tuning dt down (the old workaround). The fixed-step path is unchanged and still bit-for-bit vs master when adaptive_dt is set False in the JSON/GUI. - Constants: adaptive_dt True, adaptive_rtol 1e-4 (was False / 1e-5). - runSimulation back-fill and GUI getattr fallbacks track the new defaults; older JSONs without the fields now pick up adaptive-on. - GUI: adaptive checkbox checked by default; rtol tooltip updated. rtol=1e-4 vs 1e-5 is indistinguishable on the 2019 Orionids (<0.0005 mag); 1e-4 is the slightly looser, safe default. Set adaptive_dt False for legacy runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
Adaptive timestep now on by default (
|
| config | wall time | LC RMS vs fine ref |
|---|---|---|
fixed dt=0.005 (old default) |
0.05 s | 0.554 mag |
fixed dt=0.001 |
0.90 s | 0.516 mag |
fixed dt=0.0005 |
3.04 s | 0.496 mag |
fixed dt=0.0002 |
15.5 s | 0.433 mag |
adaptive rtol=1e-4 |
0.26 s | 0.539 mag |
Adaptive lands at roughly the accuracy of fixed dt=0.001 but at ~3.4× less time, and ~11× less than the fixed dt (0.0005) that actually beats it. That's the case for making it the default: good resolution without per-event dt tuning.
2. rtol=1e-4 vs 1e-5 is a wash on eroding meteors — but safe
Across all 13 Orionids, loosening rtol from 1e-5 to 1e-4 gave mean 1.0× speedup (1e-4 uses ~97% of the sub-steps) and a light curve identical to <0.0005 mag (max 0.0002 mag, max 0.03 km/s). The reason: for an eroding meteor the sub-step count is set by the erosion_release_length=50 m grain-release cap, not by the error tolerance — so rtol barely moves the cost. Same holds for single-body cases (313 → 302 sub-steps over 1e-5 → 1e-3), where the dt_max=0.005 ceiling binds instead.
So rtol=1e-4 is a slightly-looser, zero-downside default (identical output), but the speedup does not come from loosening rtol on these events. If a faster-with-1e-4 result shows up elsewhere, it'd be an event class where the tolerance actually binds (few grains, short flight) rather than the grain cap — none of the fitted Orionids are in that regime.
Backward compatibility & tests
adaptive_dt=False→ fixed path, bit-for-bit vsmaster(checked withnp.array_equal,equal_nan=True, on the 13 Orionids + synthetic single-body).- Older JSONs without the adaptive fields now back-fill to adaptive-on; set
adaptive_dt=Falsefor legacy fixed-step behaviour. - Regression suite
wmpl/MetSim/Tests/test_MetSimErosion.py: 8/8 pass.
Figures saved under 2019orionids/pr86_adaptive_benchmark/ (pr86_defaults_benchmark.png, default_rtol_1e4_vs_1e5.png).
Benchmarking the 2019 Orionids showed rtol=1e-4 gives no speedup over 1e-5: for eroding meteors the sub-step count is set by erosion_release_length (the 50 m grain-release cap), not by the error tolerance, and single-body cases are bounded by dt_max. Since 1e-5 is tighter (more accuracy headroom where rtol does bind) at negligible extra cost, keep it as the default. adaptive_dt stays True. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
|
Follow-up — reverted the The benchmark above showed |
…eor cost) The adaptive steppers cap an eroding fragment's sub-step at erosion_release_length/v so grains are shed every ~50 m of flight (a dt-independent grain-birth resolution). Because that cadence is fixed in distance, a fast meteor takes tiny sub-steps and the cost grows with velocity - e.g. a 67 km/s Orionid spends ~2x the sub-steps of a slower one for no visible gain. (A slow, massive fireball is unaffected: the error controller already takes small steps, so the cap never binds.) - Add erosion_release_vref (default 30000 m/s): above this speed the cadence is frozen in TIME (cap = erosion_release_length/erosion_release_vref) instead of distance, so the sub-step count stays bounded. Fragments slower than vref keep the exact distance cadence, so their output is unchanged. vref <= 0 disables the cap (original pure-distance behaviour). - Expose erosion_release_length in the GUI (box next to rtol, enabled with the adaptive checkbox) so it can be coarsened for expensive fits; vref stays a JSON-level knob. - Thread the new parameter through both steppers and the runSimulation back-fill. Validated on the 13 fitted 2019 Orionids (all ~67 km/s): mean 1.81x faster with the vref=30000 default, max peak-magnitude shift 0.020 mag (well under the ~0.1 mag measurement noise; the change is confined to the faint erosion tail). Fixed mode is bit-for-bit invariant to vref, and the regression suite passes (8/8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
Qt Designer re-saves enum values in the Qt6 scoped form (Qt::AlignmentFlag::AlignCenter, Qt::Orientation::Horizontal), which PyQt5's uic.loadUi cannot parse. Strip the scope prefixes back to the Qt5 short form (Qt::AlignCenter, Qt::Horizontal). No layout change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
Benchmarking showed 200 m is substantially faster on eroding fits while the light-curve peak is barely affected (the difference is confined to the faint erosion tail, well under measurement noise). Update the Constants default, the runSimulation back-fill, the GUI getattr fallbacks, and the tooltips. Finer sampling is still available by lowering the value in the GUI/JSON. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
…lts) The GUI display/readback and the ablateAll dispatch used getattr(const, 'name', <literal>) with the default value hardcoded a second time, so a default change had to be edited in two places. Every const starts from Constants() and runSimulation back-fills older JSONs, so the attributes are always present - read them directly (const.adaptive_rtol, const.adaptive_dt, const.adaptive_high_order, const.erosion_release_length, const.erosion_release_vref), matching how h_init/P_0m/dt are already handled. Single source of truth is now Constants.__init__ (plus the runSimulation back-fill table for migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
Formatting-only cleanup from a review of the new code against the rest of the file/repo (no behaviour change; regression suite still 8/8, fixed mode bit-for-bit): - Split semicolon-packed multi-assignments onto one statement per line (the file has zero semicolon statements elsewhere), in both steppers and _rhsDP. - Add the blank line after the closing docstring """ in clampMassC, heightCurvatureC, atmDensityPolyC, advanceVelPosC and _rhsDP. - Replace fixed-path line-number references in comments with behaviour descriptions (line numbers drift). - Wrap the one >110-col erosion-cap line to match its DP45 twin; capitalize two lowercase-first comments. - MetSimErosion.py: drop the stale "opt-in / Default False" wording now that adaptive is the default; stop misusing the "### ... ###" section marker for a paragraph; document the tolerance fields with preceding comment lines instead of column-aligned inline comments (matches the rest of Constants); rename underscore-prefixed locals (_stepper, _saved_state, _adaptive_defaults, etc.) to plain names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0179geUg2TGRWWqpNPx2k234
Adaptive (error-controlled) timestep for the MetSim erosion model — on by default
Why
The fixed RK4 step
const.dt = 0.005 shas no convergence basis and is badly under-resolved:dt=0.005velocity error vs a converged run is ~6 km/s, converging only ~1st order (dt=1e-4 still ~0.56 km/s).dtway down (and eat the runtime) to resolve the erosion tail.erosion_mass_minovershoot their true velocity by tens of percent in a single step.This PR makes accuracy a tolerance rather than a magic constant, turns it on by default, and keeps the fixed path bit-for-bit for anyone who wants the old behaviour.
What
MetSimErosionCyTools.pyx), per fragment, within a fixed output cadence:adaptiveDP45Step()— the default: an embedded Dormand-Prince RK45 pair on the coupled[mass, velocity]system. One step yields the 5th-order solution and a 4th-order error estimate in ~7 RHS evals; the higher order takes larger steps (~3–4× faster than step-doubling).adaptiveSingleBodyStep()— step-doubling on the original operator-split RK4, kept as a fallback (adaptive_high_order=False).m_killso near-exhaustion grains stay stable, and erosion grains shed per sub-step so the light-curve shape no longer depends ondt.Constants.adaptive_dt=True,adaptive_rtol=1e-5(targets numerical error below measurement noise ~0.1 mag / ~0.1 km/s at ~25 FPS, not machine precision).ablateAll()dispatches onconst.adaptive_dt;False→ the original fixed path, untouched. The macro step stays the fixed output cadence (one results row perdt), so no downstream consumer (GUI residuals, Dynesty likelihood, wake) changes.dt:erosion_release_length(default 50 m) — an eroding fragment's sub-step is capped so grains are shed ~every this many metres of flight → grain births independent ofdt/rtol. Now editable in the GUI.erosion_release_vref(default 30000 m/s) — above this speed the cadence is frozen in time instead of distance, so the sub-step count stays bounded for fast meteors (a distance cadence makes cost grow withv). Slower fragments keep the exact distance cadence;vref ≤ 0disables the cap. JSON-level knob.rtoland grain-release-length boxes alongside, enabled with the checkbox (tooltips explain tuning). Fixed-dt box is used only when adaptive is off.adaptive_dt/adaptive_high_order/adaptive_rtol/adaptive_atol_m/adaptive_atol_v/adaptive_dt_min/adaptive_dt_max/adaptive_max_substeps/erosion_release_length/erosion_release_vref;runSimulation/loadConstantsback-fill them for older JSONs (which now pick up adaptive-on).wmpl/MetSim/Testsis an importable package; added adaptive-mode regression tests (no-NaN / mass-non-negative / grain stability, radiated-energy conservation vs fixed). Suite passes 8/8.Performance (measured on the 13 fitted 2019 Orionids)
dtfine enough to reach the same accuracy, adaptivertol=1e-4matches fixeddt≈0.001at ~3.4× less time, and ~11× less than the fixeddtthat beats it. So you get resolved erosion without hand-tuningdtdown — the old workaround.erosion_release_vref=30000) gives a further mean 1.81× over a pure-distance cadence on these fast (~67 km/s) events, with a max peak-magnitude shift of 0.020 mag (≪ measurement noise; the change is confined to the faint erosion tail).dt=0.005, adaptive costs more wall time (it actually resolves the physics), but that step was never accurate — see Why.rtol=1e-4vs1e-5is a wash on eroding meteors (the 50 m grain cap, notrtol, sets the sub-step count), so the default stays at the tighter, safe1e-5.Validation
adaptive_dt=Falsereproducesmasterbit-for-bit (np.array_equal,equal_nan=True, all output columns) on the synthetic scenario + 13 Orionids. Fixed mode is also bit-for-bit invariant to the newerosion_release_vref.dt=0.005) to well below measurement precision; DP45 agrees with step-doubling to ~0.06 mag.Tuning map
rtol= dynamics accuracy ·erosion_release_length= grain-birth resolution ·erosion_release_vref= fast-meteor cost cap ·dt= output sampling cadence.🤖 Generated with Claude Code