Manufactured solutions¶
Question: does the code solve these equations, at the order it claims?
You choose a smooth function u(t), and physverify adds the source term that makes u an exact solution of the reference equations. A correct code then reproduces u, and its error shrinks at the method's order. A code that solves slightly different equations, or takes a wrong step, does not.
Example: a half-step typo in the midpoint method¶
The midpoint method takes half a step to a midpoint, then a full step using the slope there. Writing h instead of h / 2 still gives a method that runs, stays stable and looks plausible on a plot.
"""A midpoint (RK2) solver, correct and with a one-character bug, checked by a manufactured solution."""
import numpy as np
from physverify import ManufacturedODE, mms_order_study
OMEGA = 2.0
def rhs(t, y): # the code under test: a harmonic oscillator
return np.array([y[1], -OMEGA**2 * y[0]])
def integrate(source, y0, t_end, n, buggy=False):
"""Midpoint method on y' = rhs(t, y) + source(t)."""
h, y = t_end / n, np.array(y0, dtype=float)
def f(t, y):
return rhs(t, y) + source(t)
for k in range(n):
t = k * h
k1 = f(t, y)
mid = y + (h if buggy else h / 2) * k1 # bug: a full step instead of half a step
y = y + h * f(t + h / 2, mid)
return y
def reference_rhs(t, y): # written again from the physics, not copied from the code
return np.array([y[1], -4.0 * y[0]])
mms = ManufacturedODE(lambda t: np.array([np.sin(1.3 * t) + 0.2, 1.3 * np.cos(1.3 * t)]), reference_rhs)
ns = [10, 20, 40, 80, 160]
print("correct:", mms_order_study(integrate, mms, 1.0, ns, 2).message)
print("buggy: ", mms_order_study(lambda s, y0, T, n: integrate(s, y0, T, n, buggy=True), mms, 1.0, ns, 2).message)
Output:
correct: observed order 2.00 (expected 2)
buggy: observed order 0.98 is below the expected 2
The buggy version is still close to the right answer at these step sizes, so a comparison with one answer at a loose tolerance can pass it. The order study does not: the error halves when it should quarter.
What you provide¶
ManufacturedODE(u, reference_rhs): the manufactured solutionu(t)(returning an array) and the reference right-hand side, written from the physics. The source term is computed by complex-step differentiation, soumust accept complext(usenp.sin, notmath.sin).integrate(source, y0, t_end, n): runs your code for exactlynsteps from t = 0 withsource(t)added to the right-hand side, and returns the final state.
Write the reference independently
If the reference right-hand side is copied from the code under test, a wrong sign in the code is copied too and the check passes. Write it again from the equations.
Variants¶
mms_trajectory_study: the largest error over the whole run, not only at the end (catches errors that cancel by the final time).backward_mms_study: integrate fromt_endback to 0. A code that evaluates its stages att + h/2while stepping backwards passes forwards but drops to first order here.ode_checks(...): bundles the standard checks for an ODE solver; see Getting started.