Skip to content

Stochastic adaptive stepping (VirtualBrownianTree-style noise) #3

Description

@jlperla

solve_sde is fixed-step only, and this is an honest limitation: adaptive stepping for SDEs is not just wiring IController into the EM loop. A rejected step must retry over a sub-interval of the same Brownian path — with v1's presampled dW = sqrt(dt) * normal(key, (n_steps,) + shape), the increments are tied to a fixed grid and cannot be subdivided consistently.

What it takes

Re-evaluable noise. A Brownian-bridge data structure that can answer W(s, t) (the increment over any interval) consistently: querying [t0, tm] and [tm, t1] must sum to the previously returned [t0, t1] sample. The standard construction is diffrax's VirtualBrownianTree: a virtual binary tree over [t0, t1], descended to a fixed tolerance/depth, deriving one PRNG key per node by splitting along the path, and sampling midpoints from the bridge conditional

W(mid) | W(l), W(r)  ~  N( (W(l)+W(r))/2 + bias, (r-mid)(mid-l)/(r-l) )

Static depth = static shapes, which fits the bounded-scan design: each query costs depth iterations of a lax.scan descent, no dynamic allocation.

Controller interaction. With re-evaluable noise, the bounded scan in ode.py generalizes: the EM (or Milstein) step consumes W(t, t+h) from the tree instead of a scanned-in increment, and a rejection shrinks h and re-queries — the bridge guarantees the retried sub-increment is consistent with the rejected one. The error estimate for accept/reject can be the standard step-doubling comparison (two half-steps vs one full step, sharing the bridge increments), since EM has no embedded pair.

Reference implementation. diffrax's VirtualBrownianTree (Li et al. 2020, "Scalable Gradients for SDEs") is the model — including the Lévy-area caveats that decide which solvers may legally use adaptive steps (EM with diagonal noise is fine).

Suggested scope

  1. VirtualBrownianTree(t0, t1, tol, shape, key) with evaluate(s, t) — standalone, tested for consistency (nested queries sum, same key reproducible, increments N(0, t-s))
  2. solve_sde(..., controller=...) accepting it, sharing the ODE loop's carry structure
  3. strong-convergence test reusing the shared-path GBM construction from tests/test_sde.py, plus a consistency test that halving the tolerance leaves the path (not just the law) fixed

Until then, SaveAt(ts=...)-style output for SDEs also stays out (Hermite interpolation is wrong for rough paths; the tree would enable Brownian-bridge-correct interpolation later).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions