Skip to content

plasma_plots.theory.numerics

Accuracy of the numerics: amplification and phase errors of time integrators, and the numerical dispersion of spline Galerkin discretizations.

Use these to judge a simulation result: is the energy drift or the frequency error of a run what the scheme is expected to produce? All functions are plain numpy and vectorized; frequencies follow the package convention exp(i(kx − ωt)) (a positive imaginary part is numerical growth, a negative one numerical damping).

The time integrators are characterized on the oscillator y’ = iωy, the test equation of every linear wave: one step of size dt multiplies y by the amplification factor G(ω dt), which for the exact flow is exp(iω dt). Names of the methods (method=):

  • "explicit_euler" ("forward_euler"): G = 1 + z, with z = iω dt.
  • "implicit_euler" ("backward_euler"): G = 1/(1 − z).
  • "implicit_midpoint" ("crank_nicolson", "trapezoidal", "discrete_gradient"): G = (1 + z/2)/(1 − z/2). For a linear (quadratic-energy) problem the discrete-gradient methods (average vector field, Gonzalez) coincide with it.
  • "rk2" ("heun"), "rk3" ("ssprk3"), "rk4": explicit Runge–Kutta methods with s = order stages, whose G = Σ_{n ≤ s} zⁿ/n! does not depend on the Butcher tableau.
  • "leapfrog" ("stormer_verlet", "verlet"): the staggered leapfrog / Störmer–Verlet scheme for x’’ = −ω²x (the oscillator as the pair x’ = v, v’ = −ω²x, also the Yee scheme), whose one-step map has the eigenvalues G and 1/G with G = 1 − (ω dt)²/2 + iω dt √(1 − (ω dt)²/4), stable for |ω dt| ≤ 2.
  • "two_step_leapfrog": the explicit midpoint rule y_{n+1} = y_{n−1} + 2z y_n on the complex first-order equation, principal root G = z + √(1 + z²), stable for |ω dt| ≤ 1 (its second root −1/G is the computational mode).

Functions

NameDescription
amplification_factorCompute the amplification factor of a time integrator on the oscillator y' = iωy.
amplitude_errorCompute the relative amplitude error per step, |G| − 1, of a time integrator.
numerical_frequencyCompute the complex frequency that a time integrator produces for a real frequency ω.
phase_errorCompute the relative frequency error of the numerical oscillation of a time integrator.
spline_galerkin_dispersionCompute the numerical frequency of the periodic B-spline Galerkin wave equation in 1-D.
stability_limitReturn the largest stable |ω dt| of a time integrator on the imaginary axis.

amplification_factorfunction#

def amplification_factor(omega_dt, method: str)

Compute the amplification factor of a time integrator on the oscillator y’ = iωy.

One step multiplies y by G(ω dt); the exact flow has G = exp(iω dt). See the module documentation for the formula of each method. For "leapfrog" / "stormer_verlet" G is the principal eigenvalue of the one-step map of Störmer–Verlet on x’’ = −ω²x (the one that tends to exp(iω dt)); beyond the stability limit |ω dt| > 2 it is the growing real root. For "two_step_leapfrog" it is the principal root of G² − 2iω dt G − 1 = 0.

Parameters

NameTypeDescription
omega_dtfloat or array_likeThe (real) frequency times the time step, ω dt.
methodstrThe integrator, e.g. "implicit_midpoint", "rk4", "leapfrog"; see the module documentation for all names.

Returns

complex or numpy.ndarray
G, complex.

Raises

ValueError
If method is unknown.

Examples

>>> g = amplification_factor(0.5, "implicit_midpoint")
>>> print(round(abs(g), 12))
1.0
>>> print(np.round(amplification_factor([0.5, 1.0], "rk4"), 4))
[0.8776+0.4792j 0.5417+0.8333j]

amplitude_errorfunction#

def amplitude_error(omega_dt, method: str)

Compute the relative amplitude error per step, |G| − 1, of a time integrator.

Negative values are numerical damping, positive ones numerical growth, per step of size dt. The corresponding growth rate per unit time is ln|G|/dt, the imaginary part of numerical_frequency(). Examples: explicit Euler |G| = √(1 + (ω dt)²), RK2 |G| − 1 ≈ (ω dt)⁴/8, RK3 ≈ −(ω dt)⁴/24, RK4 ≈ −(ω dt)⁶/144, implicit midpoint and (stable) leapfrog |G| = 1 exactly.

Parameters

NameTypeDescription
omega_dtfloat or array_likeThe frequency times the time step, ω dt.
methodstrThe integrator; see amplification_factor().

Returns

float or numpy.ndarray
|G| − 1.

Raises

ValueError
If method is unknown.

Examples

>>> print(round(float(amplitude_error(0.1, "explicit_euler")), 6))
0.004988
>>> print(round(float(amplitude_error(0.1, "implicit_midpoint")), 12))
0.0

numerical_frequencyfunction#

def numerical_frequency(omega, dt, method: str)

Compute the complex frequency that a time integrator produces for a real frequency ω.

The numerical solution of an oscillation at frequency ω behaves as exp(−iω_num t) at the time steps, with ω_num = (arg G + i ln|G|)/dt and G = G(ω dt) the amplification factor (the argument unwrapped continuously from 0). The real part is the frequency seen in the spectrum of a run, the imaginary part the numerical growth (> 0) or damping (< 0) rate. Beyond the stability limit of the explicit leapfrog schemes the real part locks to the Nyquist frequency π/dt (π/(2 dt) for the two-step leapfrog).

Combine it with a spatial dispersion relation for the fully discrete frequency, e.g. numerical_frequency(spline_galerkin_dispersion(k, dx, 3), dt, "crank_nicolson") for a 1-D spline wave equation advanced with Crank–Nicolson.

Parameters

NameTypeDescription
omegafloat or array_likeThe (real) frequency of the semi-discrete (time-continuous) problem.
dtfloat or array_likeThe time step.
methodstrThe integrator; see amplification_factor().

Returns

complex or numpy.ndarray
ω_num, complex.

Raises

ValueError
If method is unknown.

Examples

Crank–Nicolson is slow but undamped, RK2 fast and growing:

>>> w = numerical_frequency(1.0, 0.5, "implicit_midpoint")
>>> print(round(w.real, 5), abs(w.imag) < 1e-12)
0.97991 True
>>> w = numerical_frequency(1.0, 0.5, "rk2")
>>> print(round(w.real, 5), round(w.imag, 5))
1.03829 0.0155

phase_errorfunction#

def phase_error(omega_dt, method: str)

Compute the relative frequency error of the numerical oscillation of a time integrator.

ε = arg G/(ω dt) − 1 = Re(ω_num)/ω − 1, with the argument of G unwrapped continuously from ω dt = 0. Negative values mean the numerical oscillation is too slow (a phase lag). Leading terms: explicit and implicit Euler −(ω dt)²/3, implicit midpoint −(ω dt)²/12, leapfrog +(ω dt)²/24, RK2 +(ω dt)²/6, RK3 +(ω dt)⁴/30, RK4 −(ω dt)⁴/120.

Parameters

NameTypeDescription
omega_dtfloat or array_likeThe frequency times the time step, ω dt.
methodstrThe integrator; see amplification_factor().

Returns

float or numpy.ndarray
The relative frequency error (0 at ω dt = 0).

Raises

ValueError
If method is unknown.

Examples

>>> print(round(float(phase_error(0.1, "implicit_midpoint")), 7))
-0.0008321
>>> print(round(float(phase_error(0.1, "leapfrog")), 7))
0.0004171

spline_galerkin_dispersionfunction#

def spline_galerkin_dispersion(k, dx, degree: int, c=1.0)

Compute the numerical frequency of the periodic B-spline Galerkin wave equation in 1-D.

Galerkin discretization of u_tt = c² u_xx with uniform, maximally smooth B-splines of degree p on a periodic grid of spacing dx gives M ü = −c² K u, with the mass and stiffness matrices M_ij = ∫ B_i B_j and K_ij = ∫ B_i’ B_j’. Both are circulant; their symbols (Fourier transforms of a row, θ = k dx) follow from ∫ N_p(x) N_p(x − j) dx = N_{2p+1}(p+1+j) for the cardinal B-spline N_p on [0, p + 1]:

M(θ) = dx A_p(θ), A_p(θ) = Σ_{|j| ≤ p} N_{2p+1}(p+1+j) cos(jθ)
= Σ_m [sinc((θ + 2πm)/2)]^{2p+2},
K(θ) = 4 sin²(θ/2) A_{p−1}(θ)/dx,

the second because N_p’ = N_{p−1}(x) − N_{p−1}(x − 1): the derivative of a degree-p spline is a degree-(p − 1) spline (the discrete de Rham complex V0 → V1). Hence

(ω dx/c)² = 4 sin²(θ/2) A_{p−1}(θ)/A_p(θ),

with A_0 = 1, A_1 = (2 + cos θ)/3, A_2 = (33 + 26 cos θ + cos 2θ)/60. For p = 1 this is the linear finite-element result (ω dx/c)² = 6(1 − cos θ)/(2 + cos θ). The relative error is

ω/(c k) − 1 ≈ |B_{2p}| θ^{2p} / (2 (2p)!)

(B_n the Bernoulli numbers: θ²/24, θ⁴/1440, θ⁶/60480, θ⁸/2419200 for p = 1, …, 4): the numerical waves are too fast, and there is one branch per wavenumber (no optical modes). The same relation holds for the mixed formulation in Struphy’s discrete de Rham complex (u in V0, u_x in V1 with the V1 mass matrix), because Dᵀ M1 D = K. ω/c is the modified wavenumber of the Galerkin second derivative M⁻¹K.

Parameters

NameTypeDefaultDescription
kfloat or array_likerequiredThe wavenumber.
dxfloat or array_likerequiredThe grid spacing (the knot spacing).
degreeintrequiredThe spline degree p ≥ 1.
cfloat or array_like1.0The wave speed. Default: 1.0.

Returns

float or numpy.ndarray
The numerical frequency ω ≥ 0 (semi-discrete: exact in time; see numerical_frequency() for the time integrator’s effect).

Raises

ValueError
If degree is not a positive integer.

Examples

>>> theta = np.pi / 4
>>> print(
... np.round(spline_galerkin_dispersion(theta, 1.0, [1, 2, 3]) / theta, 6)
... )
[1.025859 1.0003 1.000005]

stability_limitfunction#

def stability_limit(method: str) -> float

Return the largest stable |ω dt| of a time integrator on the imaginary axis.

The scheme keeps |G| ≤ 1 on y’ = iωy for |ω dt| up to this value: 0 for explicit Euler and RK2 (always weakly unstable, |G|² = 1 + (ω dt)² and 1 + (ω dt)⁴/4), √3 for RK3, 2√2 for RK4, 2 for leapfrog / Störmer–Verlet, 1 for the two-step leapfrog, infinite for implicit Euler (damped) and implicit midpoint (exactly |G| = 1).

Parameters

NameTypeDescription
methodstrThe integrator; see amplification_factor().

Returns

float
The stability limit of ω dt (inf for unconditionally stable methods).

Raises

ValueError
If method is unknown.

Examples

>>> print(round(stability_limit("rk4"), 4))
2.8284
>>> stability_limit("crank_nicolson")
inf