Stiff ODE Solvers Tutorial¶
cds.diffeq._implicit adds two A-stable implicit methods to the explicit
solvers: backward Euler (θ = 1) and the trapezoidal / Crank–Nicolson
method (θ = ½). Both solve their implicit stage equation with Newton
iteration, using an analytic Jacobian when you supply one and central
differences otherwise.
1. Why implicit? The stiff decay problem¶
For dy/dt = -1000·(y − 1) any explicit method diverges unless
dt < 2/1000. Backward Euler stays stable at the same step:
from cds.diffeq import backward_euler
k = 1000.0
sol = backward_euler(lambda t, y: -k * (y - 1.0), t0=0.0, y0=0.0, t_end=0.05, dt=0.001)
print(f"{sol.y[-1]:.6f}") # ≈ 1.000000 — exact is 1 − e⁻⁵⁰ ≈ 1.0
2. Trapezoid (Crank–Nicolson): stable AND second-order¶
Backward Euler buys stability with only first-order accuracy. The trapezoidal
rule averages the explicit and implicit slopes for O(dt²) error while staying
A-stable, on smooth problems its error shrinks ~4× when you halve dt.
import math
from cds.diffeq import backward_euler, trapezoid_method
f = lambda t, y: -y
be = backward_euler(f, 0.0, 1.0, 2.0, dt=0.05).y[-1]
cn = trapezoid_method(f, 0.0, 1.0, 2.0, dt=0.05).y[-1]
exact = math.exp(-2.0)
print(abs(cn - exact) < abs(be - exact)) # True
3. Systems: the harmonic oscillator¶
The _system variants take vector states and return (t_values, y_values)
just like :func:cds.diffeq.solve_system.
import math
from cds.diffeq import trapezoid_method_system
def harmonic(t, y):
return [y[1], -y[0]] # x'' = -x as a system
ts, ys = trapezoid_method_system(harmonic, 0.0, [1.0, 0.0], t_end=4 * math.pi, dt=0.01)
amplitude = math.hypot(ys[-1][0], ys[-1][1])
print(abs(amplitude - 1.0) < 0.01) # True — energy nearly conserved
4. Supplying an analytic Jacobian¶
Newton converges in one step on linear problems either way, but a Jacobian removes the finite-difference evaluations:
from cds.diffeq import backward_euler_system
jac = lambda t, y: [[-1000.0]] # df/dy
ts, ys = backward_euler_system(
lambda t, y: [-1000.0 * (y[0] - 1.0)],
t0=0.0,
y0=[0.0],
t_end=0.05,
dt=0.001,
jac=jac,
)
Notes¶
- Direction follows
sign(t_end - t0)exactly like the explicit solvers;dtis always a magnitude. - Newton failures raise
ValueErrorwith a clear message ("singular Jacobian" / "did not converge within max_iter") instead of returning garbage. - Rule of thumb: use RK45 until stiffness hurts, then reach for these.