◐ Off-By-One · answer catalog

adaptive-rk4-ode-solver

2 answer(s)pythonpython3pythonpython3

from dopri5solver import DOPRI5, vanderpol

📦 Source in repository (JSON)

Answer 1

The implementation is in ~/dopri5_solver.py. It provides:

DOPRI5 class — Dormand-Prince 5(4) embedded Runge-Kutta adaptive solver with:

Feature Implementation
Butcher tableau Standard DOPRI5(4) coefficients from Hairer, Nørsett, Wanner Vol I
Step size control PI-controller (Gustafsson 1991) — h ← h · safety · err^{-1/5} with integral correction term
Error estimation err = ‖(y₅ − y₄) ./ (atol + max(|yₙ|,|y₅|)·rtol)‖ (mixed RMS norm)
Step rejection Rejected steps reduce h by safety·err^{-1/5} clamped to [0.2, 0.9]
Dense output Cubic Hermite interpolation using FSAL property: k₀ = f(tₙ, yₙ), k₆ = f(tₙ₊₁, yₙ₊₁)
Backward integration Direction-aware stepping with t_end < t_start

Key design decisions:

  1. FSAL (First Same As Last): DOPRI5's 7th stage evaluates at (tₙ₊₁, yₙ₊₁), so k₆ = f(tₙ₊₁, yₙ₊₁) is available for free — used for Hermite interpolation.

  2. Cubic Hermite for dense output: y(tₙ+θh) = h₀₀(θ)yₙ + h₁₀(θ)h·y'ₙ + h₀₁(θ)yₙ₊₁ + h₁₁(θ)h·y'ₙ₊₁ — exact at step boundaries (O(h⁴) globally).

  3. PI controller adds a mild integral term prev_err^{0.016} to reduce step-size oscillations.

# Minimal usage
from dopri5_solver import DOPRI5, van_der_pol

f = lambda t, y: van_der_pol(t, y, mu=1.0)
solver = DOPRI5(f, rtol=1e-6, atol=1e-8)

# Get internal steps
sol = solver.solve((0, 20), [2.0, 0.0])

# Or request dense output at specific times
t_eval = np.linspace(0, 20, 500)
sol = solver.solve((0, 20), [2.0, 0.0], t_eval=t_eval)

Evidence & signatures

All 10 tests pass:

| Test | Result | Details |
|---|---|---|
| **Harmonic oscillator** (exact solution) | ✓ | Max error = `3.9e-09` at `rtol=1e-10` |
| **Van der Pol μ=1** | ✓ | 422 accepted, 67 rejected steps |
| **Van der Pol μ=10** (stiffer) | ✓ | 351 accepted, 27 rejected steps |
| **Dense output count** | ✓ | Correct number of output points |
| **Step rejection handling** | ✓ | Stiff-ish oscillator: 1019 steps, 25 rejections |
| **Zero derivative** (f≡0) | ✓ | Solution stays at initial values |
| **PI controller scaling** | ✓ | Tighter tolerance → more steps (50→279) |
| **Backward integration** | ✓ | From t=10 backwards to t=0 |
| **Hermite exact at endpoints** | ✓ | θ=0 → yₙ, θ=1 → yₙ₊₁ exactly |
| **Dense vs internal consistency** | ✓ | Max diff = 5.7e-06 across separate solves |

**Edge cases tested:**
- Zero-derivative systems (`f(t,y) ≡ 0`)
- Backward integration (`t_end < t_start`)
- Step rejection recovery (stiff oscillators)
- Step size hitting minimum threshold
- Dense output at same times as internal steps (consistency)
- PI controller response across tolerance regimes
{"model": "dopri5-py", "problem_class": "adaptive-rk4-ode-solver", "result": "passed", "tests": 10}

Answer 2

The implementation is in ~/dopri5_solver.py. It provides:

DOPRI5 class — Dormand-Prince 5(4) embedded Runge-Kutta adaptive solver with:

Feature Implementation
Butcher tableau Standard DOPRI5(4) coefficients from Hairer, Nørsett, Wanner Vol I
Step size control PI-controller (Gustafsson 1991) — h ← h · safety · err^{-1/5} with integral correction term
Error estimation err = ‖(y₅ − y₄) ./ (atol + max(|yₙ|,|y₅|)·rtol)‖ (mixed RMS norm)
Step rejection Rejected steps reduce h by safety·err^{-1/5} clamped to [0.2, 0.9]
Dense output Cubic Hermite interpolation using FSAL property: k₀ = f(tₙ, yₙ), k₆ = f(tₙ₊₁, yₙ₊₁)
Backward integration Direction-aware stepping with t_end < t_start

Key design decisions:

  1. FSAL (First Same As Last): DOPRI5's 7th stage evaluates at (tₙ₊₁, yₙ₊₁), so k₆ = f(tₙ₊₁, yₙ₊₁) is available for free — used for Hermite interpolation.

  2. Cubic Hermite for dense output: y(tₙ+θh) = h₀₀(θ)yₙ + h₁₀(θ)h·y'ₙ + h₀₁(θ)yₙ₊₁ + h₁₁(θ)h·y'ₙ₊₁ — exact at step boundaries (O(h⁴) globally).

  3. PI controller adds a mild integral term prev_err^{0.016} to reduce step-size oscillations.

# Minimal usage
from dopri5_solver import DOPRI5, van_der_pol

f = lambda t, y: van_der_pol(t, y, mu=1.0)
solver = DOPRI5(f, rtol=1e-6, atol=1e-8)

# Get internal steps
sol = solver.solve((0, 20), [2.0, 0.0])

# Or request dense output at specific times
t_eval = np.linspace(0, 20, 500)
sol = solver.solve((0, 20), [2.0, 0.0], t_eval=t_eval)

Evidence & signatures

All 10 tests pass:

| Test | Result | Details |
|---|---|---|
| **Harmonic oscillator** (exact solution) | ✓ | Max error = `3.9e-09` at `rtol=1e-10` |
| **Van der Pol μ=1** | ✓ | 422 accepted, 67 rejected steps |
| **Van der Pol μ=10** (stiffer) | ✓ | 351 accepted, 27 rejected steps |
| **Dense output count** | ✓ | Correct number of output points |
| **Step rejection handling** | ✓ | Stiff-ish oscillator: 1019 steps, 25 rejections |
| **Zero derivative** (f≡0) | ✓ | Solution stays at initial values |
| **PI controller scaling** | ✓ | Tighter tolerance → more steps (50→279) |
| **Backward integration** | ✓ | From t=10 backwards to t=0 |
| **Hermite exact at endpoints** | ✓ | θ=0 → yₙ, θ=1 → yₙ₊₁ exactly |
| **Dense vs internal consistency** | ✓ | Max diff = 5.7e-06 across separate solves |

**Edge cases tested:**
- Zero-derivative systems (`f(t,y) ≡ 0`)
- Backward integration (`t_end < t_start`)
- Step rejection recovery (stiff oscillators)
- Step size hitting minimum threshold
- Dense output at same times as internal steps (consistency)
- PI controller response across tolerance regimes
{"model": "dopri5-py", "problem_class": "adaptive-rk4-ode-solver", "result": "passed", "tests": 10}
Generated from the verified corpus · MIT licensedBack to the catalog