Skip to content

feat: periodic-equilibrium warm-start for nuts-on-ode (gh#396)#399

Draft
vsbuffalo wants to merge 5 commits into
mainfrom
feature/ode-equilibrium-warmstart
Draft

feat: periodic-equilibrium warm-start for nuts-on-ode (gh#396)#399
vsbuffalo wants to merge 5 commits into
mainfrom
feature/ode-equilibrium-warmstart

Conversation

@vsbuffalo

Copy link
Copy Markdown
Owner

Periodic-equilibrium warm-start for nuts on ode (gh#396)

Skips the seasonal spin-up transient in gradient ODE fits by solving the limit
cycle directly, instead of re-integrating decades of burn-in on every gradient.
The measured motivation (garki mv_age, origin = 1910): one augmented-ODE
gradient ≈ 1 min, a single NUTS warm-up iteration > 7 min ⇒ a full fit = days,
zero samples. The cost is the ~63-year transient, re-integrated per eval.

Approach

The seasonal equilibrium at the data-window start T_eq is the fixed point of
the one-period stroboscopic map, X* = Φ_P(X*; θ). We solve it by Newton using
the monodromy M = ∂Φ_P/∂X₀, and seed the data-window integration with the
exact sensitivity the sampler needs, ∂X*/∂θ = (I − M)⁻¹ ∂Φ_P/∂θ (implicit
function theorem). The 63-year transient collapses to a handful of one-year
period-solves plus dense linear solves.

Reuses the gh#275 augmented forward-sensitivity RK4 verbatim — Ṡ = J_x·S is
already the monodromy variational equation; a forcing toggle on the sensitivity
seed turns off the ∂rate/∂θ term to integrate M.

Conservation (the load-bearing correction)

A closed model conserves a linear quantity (cᵀM = cᵀ), so (I − M) is singular
endemic-or-not — and garki is conserved (per-village population, k = 4). We
detect the conserved directions numerically via SVD: because any RK method
preserves linear invariants exactly, genuine conservation sits at machine-
precision singular values (~1e-12), ~10 orders below the slowest real mode, so
the threshold is robust. The fixed point is then solved on the conservation
manifold with a bordered/KKT system; the sensitivity reuses the factorization and
the ic_grad seed for the conserved directions. An endemic gate refuses a
non-hyperbolic transverse cycle.

Validity

  • Periodicity gate: an interpolated/table forcing carries no declared
    period, so warm_start_period is required and the forcing is checked
    numerically — the vector field must repeat with period P over the burn-in
    window, else a hard error (a misconfigured anchor can't silently seed a spurious
    equilibrium).
  • Endemic gate: non-hyperbolic ⇒ hard error naming the dominant multiplier.
  • Scheduled effects, rk45, dt-in-rate, real compartments — already refused by
    the existing det_grad capability gate.

Surface (fit.toml, on the nuts stage)

[stages.posterior]
algorithm         = "nuts"
backend           = "ode"
warm_start        = "equilibrium"   # default "none"
warm_start_period = 365.25          # forcing period P (required)
warm_start_at     = 1970.0          # T_eq; default = earliest observation

Consistent with the sibling stage knobs (dense_mass, warmup, …), which are
TOML-only. warm_start_* are identity-defining and re-key the run. nuts-only
in v1 — mh does not expose the option (an honest absence, a named follow-up).

Tests

Five solver oracles (all green): fixed-point residual + conservation (k, N
preserved); monodromy vs central FD; ∂X*/∂θ vs FD of the equilibrium re-solve
(the partial-vs-total seed-trap catcher); X* vs a long burn-in (confirms it's
the attracting cycle); the periodicity gate accepts the true period and refuses a
wrong one. No regression in the existing ODE gradient suite (31 + 3 det_grad FD).

Fixture is a freshly-compiled closed seasonal SEIRS. Note: the committed
ir/golden/*.ir.json predate the gh#275 rate_state_grad emission (they carry an
empty rate_state_grad, skip_serializing_if_empty) — a separate staleness worth
a make update-golden, filed as a follow-up; real fits compile fresh and are
unaffected.

Rust-only (a fit-time runtime option) — no IR/schema/OCaml/golden change.

Follow-ups

  • mh-on-ode warm-start (needs only X*, no sensitivity).
  • Real-compartment forward sensitivity → warm-start for explicit dx/dt models.
  • Multi-period / LCM P; block-sparse bordered solve for national scale.
  • Newton warm-start across evals (couples evals; land stateless first).
  • End-to-end posterior-parity validation on the real garki mv_age (garki agent).

Proposal: docs/dev/proposals/2026-07-07-ode-periodic-equilibrium-warmstart.md.

vsbuffalo added 5 commits July 7, 2026 22:54
Revise against the 3-reviewer review + garki grounding. Key changes from
v1:

- Conservation-aware bordered/KKT solve. Closed models make (I-M)
  singular (cᵀM=cᵀ); garki is conserved k=4 (per-village, dynamical
  birth=δ·Nvil balance). Detect the conserved directions numerically via
  SVD — robust because genuine conservation sits at machine-precision
  singular values, ~10 orders from the slowest real mode. Solve on the
  conservation manifold; the sensitivity reuses the same LU and the
  ic_grad seed for the conserved-direction part.

- T_eq is a single declared anchor, decoupled from the per-stream
  cond_from (which stays per-stream over [T_eq, t_end]).

- Seed trap pinned: the ∂Φ_P/∂θ read is a SensSeed::Zero (S(0)=0)
  one-period solve — the partial, not a total derivative.

- Monodromy mode is a real SensSeed branch (not a param_model_idx
  trick), threaded through sensitivity_derivs/rk4_step.

- mh-on-ode wired too (compute_ode_loglik injection), not just nuts.

- Pre-obs periodicity validity gate: refuse events/interventions in
  [origin, T_eq]; numerical C(t)≈C(t+P) check (interpolated forcings
  carry no declared period — the garki case).

- CLI + TOML surface via the run input system; warm-start re-keys the
  run. Rust-only (fit-time option, no IR/OCaml/golden change).

Measured motivation carried forward: relaxation ~56 yr ≫ 5-yr window
(frozen ICs biased, don't wash out); global coarse dt hard-blocked by
coprime survey dates; per-phase dt ~5-8x but 1%-biased.
…h#396)

The core of the ODE warm-start: solve the seasonal limit cycle as the
fixed point of the one-period stroboscopic map instead of integrating the
60-year transient, with the exact parameter-sensitivity the NUTS/mh
gradient needs. Not yet wired into a fit path (next commit).

Integrator (ode.rs):
- Phase A: `integrate_obs_sensitivity` takes `x0: Option<&[f64]>` to begin
  from an injected continuous state (overrides initial_state_continuous).
  Every existing caller passes None → byte-identical.
- Monodromy mode: a `forcing: bool` on `Sens`, threaded to
  `sensitivity_derivs`. When false the ∂rate/∂θ summand is dropped, so the
  identity-seeded run integrates Ṡ = J_x·S → M = ∂x(T)/∂x₀. Carried in the
  Sens struct so rk4_step's call sites don't change.
- `period_flow`: augmented (x,S) over one period, the solver's building
  block (monodromy with forcing off / ∂Φ/∂θ partial with a zero seed).

Solver (ode_equilibrium.rs):
- Newton on X* = Φ_P(X*;θ) via the monodromy; ∂X*/∂θ = (I−M)⁻¹∂Φ_P/∂θ (IFT).
- Conservation-aware: closed models make (I−M) singular (cᵀM=cᵀ). Detect
  the conserved directions numerically via SVD — genuine conservation sits
  at machine-precision singular values (RK preserves linear invariants
  exactly), ~10 orders below the slowest real mode, so the threshold is
  robust. Solve on the manifold {CᵀX=CᵀX_init} with a bordered/KKT system;
  the sensitivity reuses the factorization and the ic_grad seed for the
  conserved part. Endemic gate refuses a non-hyperbolic transverse cycle.

Oracles (all green): fixed-point residual + conservation (k, N preserved);
monodromy vs central FD; ∂X*/∂θ vs FD of the equilibrium re-solve (the
seed-trap catcher); X* vs a long burn-in (the attracting cycle). Fixture
is a freshly-compiled closed seasonal SEIRS — the ir/golden copies predate
the gh#275 rate_state_grad emission (a separate staleness, noted).

nalgebra added (SVD + bordered LU). Rust-only; no IR/schema change.

Proposal: docs/dev/proposals/2026-07-07-ode-periodic-equilibrium-warmstart.md
…396)

Turn the solver on from a fit. det_grad takes an optional WarmStart
{t_eq, period}: when set, it solves the equilibrium at T_eq and seeds the
[T_eq, t_end] integration with (X*, ∂X*/∂θ) instead of integrating the
transient from origin. Threaded through OdeNutsConfig → run_ode_nuts →
both det_grad call sites (value + gradient).

Surface (fit.toml, on the nuts stage — consistent with the sibling
stage knobs dense_mass/warmup/target_accept, which are TOML-only):

  [stages.posterior]
  algorithm         = "nuts"
  backend           = "ode"
  warm_start        = "equilibrium"   # default "none"
  warm_start_period = 365.25          # forcing period P (required)
  warm_start_at     = 1970.0          # T_eq; default = earliest first_obs

warm_start_* are identity-defining (they change the scored trajectory),
so they enter the stage's run-identity hash and re-key the run.

WarmStartKind enum + NutsStageOpts fields + resolution in run_stage
(require period > 0; default T_eq = min first_obs). ode_equilibrium is
now `pub` so the cli crate can name WarmStart.

v1 scope: nuts only. mh does not expose the option (an honest absence, not
a silently-ignored flag) — a named follow-up. Existing ode tests green
(31 + 3 det_grad FD), no regression from the det_grad signature change.
Refuse a non-P-periodic forcing over the burn-in window before any
gradient is taken. An `interpolated`/table forcing (the garki case)
carries no declared period, so periodicity is checked numerically:
`derivs_at` evaluates dx/dt at a fixed reference state, and the gate
(run once at fit setup) asserts the vector field repeats with period P
across [T_eq-P, T_eq]. A mismatch → hard error naming the failure, so a
misconfigured warm_start_period/warm_start_at cannot silently seed a
spurious equilibrium.

Test: the sinusoidal seasonal fixture passes at its true period 365.25
and is refused at a wrong period (100) and a non-positive period.
fit-toml.md: a "Periodic-equilibrium warm-start" section (the
warm_start / warm_start_period / warm_start_at options, when it applies,
the refusal conditions) + warm_start in the nuts stage-knob table.
inference.md: a pointer from the ODE Bayesian-fit section for the
long-seasonal-spin-up case.
@vsbuffalo
vsbuffalo marked this pull request as draft July 8, 2026 15:23
@vsbuffalo

Copy link
Copy Markdown
Owner Author

Converting to draft — this is a correct-but-not-yet-viable foundation, not for merge.

Status: correct math, validated on small models (5 oracle tests green), but does not scale to densely-coupled models.

Findings from real-model testing (garki mv_age, 196 comp) + two expert reviews:

  • Cost: the full monodromy is O(n_int) column-integrations; at n=196 one monodromy ≈ the entire 63-yr transient (measured: >10 min for a single solve). garki's dense FOI coupling makes the state-Jacobian dense, so even a matrix-free (JFNK/GMRES) rework pays that cost on every monodromy–vector product.
  • Correctness gap: the endemic gate (rcond of I−M) misses period-doubling / multi-year cycles (a Floquet multiplier near −1 leaves I−M nonsingular). Seasonally-forced epidemic systems can settle to biennial cycles; solving x=Φ_P(x) at P=1yr can return an unstable annual orbit. Needs a Φ_P-vs-Φ_2P + Floquet-magnitude check before this is trusted on any model.
  • Modeling subtlety: for models that are not actually settled at T_eq (garki's 61-yr burn-in ≈ one 56-yr relaxation time), jumping onto the limit cycle is a model change, not a faithful speedup.

Where this method belongs: sparsely-coupled seasonal models that are genuinely at their annual cycle. If we invest, build it the reviewers' way: symbolic projection/elimination for conservation (not SVD/KKT-first), Anderson-accelerated period map, matrix-free Newton–Krylov fallback, stiff-solver option, period-2/-k diagnostics.

The immediate garki unlock is being pursued separately via calendar-aware coarse-dt burn-in (simpler, faithful, no dense-coupling penalty).

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