solve_ivp#
- class braintools.quad.solve_ivp(f, t_span, y0, *args, method='dopri5', rtol=1e-06, atol=1e-09, dt0=None, max_steps=4096, t_eval=None, checkpoint=True, **kwargs)#
Solve an initial value problem with an adaptive embedded Runge–Kutta method.
Integrates \(\dot y = f(y, t)\) from
t_span[0]tot_span[1]with error control. Each attempt takes one step of an embedded pair (method), measures the error, accepts or rejects the step and picks the next step size with a PI controller. The attempts run in one fixed-lengthscanofmax_stepsiterations; oncet1is reached the remaining iterations are skipped through adonemask.- Parameters:
f (
Callable) – Right-hand sidef(y, t, *args, **kwargs) -> dy/dtwith the same PyTree structure (and units[y]/[t]) asy. Note the(y, t)argument order (scipyuses(t, y)).t_span (tuple of (float or Quantity)) –
(t0, t1).t1 < t0integrates backwards in time. May be traced.y0 (PyTree) – Initial state. Leaves may be arrays or brainunit Quantities.
*args – Extra positional arguments forwarded to
f.method (
str) – Embedded pair:'dopri5'(Dormand–Prince 5(4), default),'rk45'(Cash–Karp 5(4)),'rkf45'(Fehlberg 4(5)),'rk23'or'bs32'(Bogacki–Shampine 3(2)),'dopri8'(DOP853). The aliases'rk45_dopri'and'rk87_dopri'are accepted.rtol (
float) – Relative tolerance. Default1e-6.atol (float, Quantity or PyTree, optional) – Absolute tolerance. A float applies to the mantissas of every leaf; a Quantity is converted to each leaf’s unit; a PyTree matching
y0sets one tolerance per leaf. Default1e-9.dt0 (float or Quantity, optional) – Initial step size magnitude. If
None(default) it is chosen by the Hairer–Wanner starting-step heuristic [1].max_steps (
int) – Static number of step attempts (accepted plus rejected). Default4096.t_eval (array or Quantity, optional) – 1-D array of output times (static length, any order, duplicates allowed) inside
t_span. The steps are shortened so that everyt_evalpoint is an accepted step (see Notes).checkpoint (
bool) – IfTrue(default), the differentiated replay loop is abrainstate.transform.checkpointed_scan, so reverse-mode memory no longer scales withmax_steps(at roughly 30% more gradient compute).Falsestores the residuals of every one of themax_stepsiterations. Values and gradients are identical either way; forward-only evaluation is unaffected.**kwargs – Extra keyword arguments forwarded to
f.
- Returns:
NamedTuple
(ts, ys, n_steps, n_rejected, success). Withoutt_eval,ts/yshold themax_steps + 1slots of accepted steps (ts[0] == t0), padded aftern_stepswith the last accepted value;ys[n_steps]is the final state. Witht_eval,tsist_evalandysthe interpolated states.- Return type:
- Raises:
KeyError – If
methodis not a registered ODE method.ValueError – If
methodhas no embedded error estimate,t_spanis not a pair,max_steps < 1,t_evalis not 1-D or (when concrete) lies outsidet_span,dt0is (concretely) zero, oratoldoes not matchy0.brainunit.UnitMismatchError – If times, tolerances or
f’s output have units incompatible witht_spanandy0.
See also
odeintFixed-step trajectory driver.
scipy.integrate.solve_ivpThe SciPy counterpart.
Notes
Error norm. The step error
errreturned by the embedded pair is measured on the mantissas as\[\|e\| = \sqrt{\frac{1}{N}\sum_i \left(\frac{e_i}{\mathrm{atol}_i + \mathrm{rtol}\,\max(|y_i|, |y^{new}_i|)}\right)^2},\]and the step is accepted when \(\|e\| \le 1\).
Step-size control. After an accepted step the PI controller of [1] (Sec. IV.2) proposes \(h_{new} = h \cdot s\,\|e_n\|^{-0.7/k} \|e_{n-1}\|^{0.4/k}\) with \(k\) one more than the order of the embedded error estimate, safety \(s = 0.9\) and the factor bounded to
[0.2, 10](no growth right after a rejection). A rejected step is retried with \(h \cdot \max(0.2, s\,\|e\|^{-1/k})\). Non-finite steps are rejected. If the step size falls below10 * eps * |t|the solve stops withsuccess=False; the same happens whenmax_stepsattempts are exhausted. In both cases the outputs hold the last accepted state (no NaN padding).Two passes. Pass 1 runs the adaptive loop (a fixed-length
scanofmax_stepsattempts, with acondskipping the iterations afterdone) on stop-gradient copies of all inputs and records, per attempt, whether it was accepted and its step size. Pass 2 replays only the accepted steps (without the error estimate) on the differentiated path. So each accepted step is computed twice, but a rejected attempt, even one that produced NaN, never enters a gradient, and recomputation undercheckpoint=Truecannot change a decision.Gradients. The step sizes chosen by the controller are constants, i.e. gradients are those of the solution on the accepted time grid (“discretise-then-optimise”); the steps that land on
t1or at_evalpoint carry the dependence on those times.Output at
t_eval. A cubic Hermite interpolant between accepted steps is only \(O(h^4)\) accurate, which for 5th- and 8th-order pairs is far worse than the step error. The solver therefore never steps over at_evalpoint: a step that would cross one is shortened to land on it (without shrinking the next proposed step), and that step’s state is the output, with the accuracy of the solver itself. This costs about one extra step pert_evalpoint, which counts towardsmax_steps. Points beyond the last accepted time of a failed solve return the last accepted state.Memory. With
t_evalthe forward pass holds a few copies of the state plus the(len(t_eval),) + y.shapeoutput, independent ofmax_steps. Withoutt_evalthe output itself hasmax_steps + 1rows, and the forward pass holds about two such arrays; prefert_evalfor large states. Reverse mode withcheckpoint=Falsestores the step residuals (several copies of the state) for each of themax_stepsiterations, whatever the number of steps actually taken; e.g. about 0.9 GiB for a1e4-element float32 state atmax_steps=4096, against about 7 MiB withcheckpoint=True.Backwards integration (
t1 < t0) is supported: steps are taken with negativedt.Transformations:
jax.jit(method,max_stepsand the length oft_evalare static;t_span,y0, tolerances and arguments may be traced),jax.vmapand forward/reverse-modejax.gradwith respect toy0,args,kwargs,t_spanandt_eval.References
Examples
>>> import jax.numpy as jnp >>> from braintools.quad import solve_ivp >>> sol = solve_ivp(lambda y, t: -y, (0.0, 2.0), jnp.array(1.0), ... rtol=1e-6, atol=1e-8, ... t_eval=jnp.array([0.0, 1.0, 2.0])) >>> bool(sol.success) True >>> print([round(float(v), 5) for v in sol.ys]) [1.0, 0.36788, 0.13534]
Without
t_evalthe accepted steps are returned:>>> sol = solve_ivp(lambda y, t, k: -k * y, (0.0, 2.0), jnp.array(1.0), 3.0, ... max_steps=64) >>> n = int(sol.n_steps) >>> print(float(sol.ts[n]), round(float(sol.ys[n]), 6)) 2.0 0.002479