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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions