Method cards
Declare the math of a numerical method once, on the class or function that implements it, and get it back in Jupyter, in the terminal, in the API docs and in the tests.
A class: the NumericalMethod mixin
Section titled “A class: the NumericalMethod mixin”A concrete class inherits from its own base (here none) and the mixin, and declares its math as class attributes. Only equations is required; everything is written as literals, so that the docs can read it without importing the code.
import math
import sympy
from methodcard import ConvergenceProblem, NumericalMethod, Rule, card, method
class Leapfrog(NumericalMethod): """Kick, drift, kick: the velocity Verlet scheme."""
equations = { "half_kick": r"v^{n+1/2} = v^n + \frac{\Delta t}{2} a(x^n)", "drift": r"x^{n+1} = x^n + v^{n+1/2} \Delta t", "kick": r"v^{n+1} = v^{n+1/2} + \frac{\Delta t}{2} a(x^{n+1})", } order = {"x": 2, "v": 2} assumptions = ("the acceleration depends on the position only",) references = ( "Hairer, Lubich and Wanner, Geometric Numerical Integration (2006), I.1.4", )
@staticmethod def step(x, v, dt, acceleration): """Advance one step.""" v = v + 0.5 * dt * acceleration(x) x = x + dt * v return x, v + 0.5 * dt * acceleration(x)
@classmethod def convergence_problem(cls): """A harmonic oscillator up to t = 1, against cos t."""
def error(dt): x, v = 1.0, 0.0 for _ in range(round(1 / dt)): x, v = cls.step(x, v, dt, lambda x: -x) return {"x": abs(x - math.cos(1)), "v": abs(v + math.sin(1))}
return ConvergenceProblem(error, steps=(0.1, 0.05, 0.025, 0.0125))An instance renders its card in Jupyter:
Leapfrog()Leapfrog: Kick, drift, kick: the velocity Verlet scheme.
half kick
drift
kick
Order of accuracy: x: 2, v: 2.
Assumptions:
- the acceleration depends on the position only
References:
- Hairer, Lubich and Wanner, Geometric Numerical Integration (2006), I.1.4
and describe() gives the same as plain text, for a terminal:
print(Leapfrog.describe())__main__.Leapfrog: Kick, drift, kick: the velocity Verlet scheme. half_kick v^{n+1/2} = v^n + \frac{\Delta t}{2} a(x^n) drift x^{n+1} = x^n + v^{n+1/2} \Delta t kick v^{n+1} = v^{n+1/2} + \frac{\Delta t}{2} a(x^{n+1}) order: x: 2, v: 2 assumes: the acceleration depends on the position only see: Hairer, Lubich and Wanner, Geometric Numerical Integration (2006), I.1.4A function: the @method decorator
Section titled “A function: the @method decorator”Functions (and classes that do not want the mixin) use the decorator with the same fields. symbolic holds SymPy rules, checked against the implementation on random inputs; the rule’s symbols are named after the function’s parameters.
x, v, dt = sympy.symbols("x v dt")
@method( equations={"update": r"x^{n+1} = x^n + v \Delta t"}, assumptions=("constant velocity over the step",), symbolic={"update": x + v * dt},)def drift(x, v, dt): """Advance a position at constant velocity.""" return x + v * dt
card(drift)drift: Advance a position at constant velocity.
update
Assumptions:
- constant velocity over the step
The checks
Section titled “The checks”The tests a project gets from method_suite run these checks for each card: the observed order of the convergence problem against order,
from methodcard.checks import check_order, check_rule
check_order(Leapfrog.convergence_problem(), Leapfrog.card().order){'x': 2.000570853596734, 'v': 2.00041611466669}and each SymPy rule against the implementation (it raises if they differ):
check_rule(x + v * dt, drift)check_rule(Rule(x + 2 * v * dt, implementation=lambda x, v, dt: x + 2 * v * dt), None)In a package, all of this is one test module:
from methodcard.testing import method_suite
TestMethodCards = method_suite("mypackage")which also checks that the cards the docs read (statically, with griffe) are the ones registered at runtime. Register the convergence marker in the project’s pytest configuration to run the slow order tests separately (pytest -m "not convergence").
The registry and the docs
Section titled “The registry and the docs”Every card is registered under its qualified name, per top-level package:
from methodcard import registry
sorted(registry("__main__"))['__main__.Leapfrog', '__main__.drift']In the docs, defineDocs({ methodcard: true }) of the shared theme adds a “Numerics” section to each method’s API page and a catalogue of all cards; both come from methodcard export <package>, which reads the cards without importing the code.