Differential Equations Tutorial¶
cds.diffeq solves initial-value problems with classical schemes plus an
adaptive RK45 integrator.
The fixed-step solvers take the step size as dt, not n, and return an
ODESolution dataclass whose solution values are read from .y (a list, index
-1 for the final value) and whose grid is .t. rk45 additionally accepts
rtol / atol and chooses its own steps.
1. Single ODE¶
Solve dy/dt = -y, y(0) = 1, whose exact value at t=1 is
e^-1 ≈ 0.36787944:
from cds.diffeq import euler_method, midpoint_method, rk4, rk45
f = lambda t, y: -y
print(f"euler {euler_method(f, 0.0, 1.0, 1.0, dt=0.01).y[-1]:.8f}")
print(f"midpoint {midpoint_method(f, 0.0, 1.0, 1.0, dt=0.01).y[-1]:.8f}")
print(f"rk4 {rk4(f, 0.0, 1.0, 1.0, dt=0.01).y[-1]:.8f}")
print(f"rk45 {rk45(f, 0.0, 1.0, 1.0, rtol=1e-9).y[-1]:.8f}")
print(f"exact {2.718281828459045**-1:.8f}")
The accuracy ladder is visible in the output: Euler is off in the fourth decimal, midpoint in the sixth, and both RK4 and adaptive RK45 match the exact value to the precision printed.
2. System of ODEs¶
solve_system returns (t_values, y_values) rather than a result object.
from cds.diffeq import solve_system
def lotka(t, state):
x, y = state
return [1.1 * x - 0.4 * x * y, 0.1 * x * y - 0.4 * y] # Lotka-Volterra
ts, ys = solve_system(lotka, t0=0.0, y0=[10.0, 5.0], t_end=15.0, dt=0.05)
print(f"{len(ts)} points") # 301 with this step
print(f"final [prey, predator] = {[round(v, 4) for v in ys[-1]]}")
3. Stiff problems: implicit solvers¶
When explicit methods need absurdly small steps, the implicit solvers stay stable. See the Stiff ODE Solvers tutorial for the full walkthrough.
from cds.diffeq import backward_euler
sol = backward_euler(lambda t, y: -1000.0 * (y - 1.0), 0.0, 0.0, 0.05, dt=0.001)
print(f"{sol.y[-1]:.6f}") # 1.000000 — explicit Euler diverges here
Run the full demo with: