odeint(func: Callable[..., object], y0: State, t: Tensor | Sequence[float], rtol: float = 1e-07, atol: float = 1e-09, method: str | ButcherTableau | None = None, options: dict[str, object] | None = None, event_fn: Callable[..., Tensor] | None = None, return_trajectory: bool = True)Integrate dy/dt = f(t, y).
Four families are reachable through the same call, and which one runs
follows from method:
Adaptive (the default, and anything carrying an embedded error
estimate) treats t as output times. The solver picks its own step
sizes to hold the local error inside rtol / atol and interpolates
to each requested time, so a coarse t costs nothing in accuracy.
Fixed step (euler / midpoint / heun2 / heun3 / rk4
/ rk4_classic)
treats t as the integration grid itself by default: consecutive
entries are one step each, with no sub-stepping and no interpolation.
rtol / atol are unused there, and accuracy is controlled by
choosing a finer grid — or by handing options["step_size"], which
decouples the two the same way an adaptive method does.
Adams multistep (explicit_adams / implicit_adams /
fixed_adams) is fixed-step too, but reaches high order by reusing the
derivatives from previous steps instead of taking more of them inside one
step — one new evaluation per step regardless of order. That pays off
when func is expensive. See options["max_order"] for the caveat
that comes with it.
Gradient mode is left entirely to the caller. Under grad the whole solve
is differentiable end-to-end (discretise-then-optimise) and every stage is
retained for backward; wrap the call in lucid.no_grad for sampling,
where the stage graph is pure overhead.
Parameters
funccallablef(t, y) -> dy/dt. Receives the stage time as a
0-D tensor matching y0 in dtype and device, and must return a
tensor with the same shape and device as y0.y0Tensort[0]. Any shape; must have a floating dtype.tTensor or sequence of floatrtolfloat= 1e-7atolfloat= 1e-9None selects "dopri5". Otherwise one of "dopri5",
"dopri8", "tsit5", "bosh3", "fehlberg2",
"adaptive_heun",
"euler", "midpoint", "heun2", "heun3", "rk4",
"rk4_classic",
"explicit_adams", "implicit_adams", "fixed_adams",
"implicit_euler", "implicit_midpoint", "trapezoid",
"radauIIA3", "radauIIA5", "gl4", "gl6", "sdirk2",
"trbdf2", or a custom ButcherTableau.optionsdict or None= Nonemin_step,
max_step, first_step, step_t, jump_t, safety,
ifactor, dfactor, max_num_steps (plus dtype and
norm, accepted and ignored). Fixed-step methods accept
step_size, grid_constructor, interp and perturb —
giving either of the first two decouples the integration grid from
t, which is then reached by interpolation. Adams methods accept
all four of those plus max_order (default 12) and max_iters
(default 4, corrector sweeps, ignored by explicit_adams).
A high max_order is not simply more accurate. Explicit Adams
loses stability as the order climbs — at the default 12 its stable
step is small enough that explicit_adams diverges on problems
rk4 handles comfortably — so lower it, or use one of the
corrected variants, whose stability holds up far better. Order also
ramps from a Runge-Kutta start, which caps the accuracy the first
steps can contribute regardless of max_order.
Implicit methods accept the fixed-step keys plus max_iters
(default 100), the ceiling on iterations of the nonlinear solve. A
step whose solve runs out of iterations warns rather than passing off
an unconverged iterate as a completed step.event_fncallable or None= Noneg(t, y) returning a single-element tensor. When given, the solve
ignores every entry of t but the first, runs until g changes
sign, and returns a (event_t, solution) pair instead of a
trajectory. The direction of t still decides which way it
searches.return_trajectorybool= Truet. Set False to keep only
the final state — for a sampling run of many steps over a batch of
images, stacking the full trajectory multiplies peak memory by the
number of steps. Ignored when event_fn is given. Lucid
extension, not part of the reference interface.Returns
Tensor or tupleWithout event_fn: shape (len(t), *y0.shape) when
return_trajectory is True (index 0 is y0 itself),
otherwise y0.shape. The dtype is the promotion of y0 with
everything func returns, matching what y + dt * k would
produce.
With event_fn: a (event_t, solution) pair, where event_t
is a 0-D tensor and solution has shape (2, *y0.shape) — the
state at t[0] and the state at the event.
Raises
ValueErrort is not a strictly monotonic 1-D grid of at least two finite
points, if y0 has a non-floating dtype, if method names no
registered method, if options holds a key the method does not
accept, or if func returns a tensor whose shape or device differs
from y0.TypeErrormethod is neither a string nor a ButcherTableau, or
if func returns something other than a tensor.RuntimeErrormax_num_steps or its step size
collapses.Notes
A fixed-step solve costs exactly (len(t) - 1) * method.stages calls to
func. An adaptive solve costs as many as the tolerances demand, and
reads one scalar back to the host per step to decide whether to accept it
— that host synchronisation is intrinsic to adaptivity, which is why the
two families have different performance characters.
Higher-order differentiation works: the fused step opts into
graph-recording backward, so create_graph=True through a solve behaves
exactly as it would for the unfused arithmetic.
Examples
Exponential decay against its closed form:
>>> import lucid, lucid.diffeq as diffeq
>>> y0 = lucid.tensor([1.0], dtype=lucid.float64)
>>> y = diffeq.odeint(lambda s, y: -y, y0, [0.0, 1.0], return_trajectory=False)
>>> abs(float(y.item()) - 0.36787944117) < 1e-8
TrueSee Also
- lucid.diffeq.ButcherTableau—Coefficient table selected by
method.