Numerical accuracy: time stepping

GRLP integrates the sediment-conservation equation with a semi-implicit scheme. By default it is second-order in time (BDF2) and second-order in space, and runs the semi-implicit iteration to convergence each step. The first-order backward-Euler scheme is available with set_time_integration(1). This page shows what the time-stepping error looks like for each, why second order is the default, and how to choose a step.

How the accuracy is measured

By self-convergence. We run the same scenario — a river adjusting to a step increase in uplift — first with a very short time step, which we treat as “the truth,” then again with a series of progressively longer steps, and measure how far each longer-step run lands from the truth. The scenario is run only part way to its new steady state (one equilibration time, after Paola et al., 1992), because at steady state the answer no longer depends on the time step and the comparison would be empty. The same setup is checked automatically by tests/test_time_accuracy.py, and the figure below is regenerated by docs/accuracy_figure.py.

Backward-Euler final-answer error grows like step; BDF2 like step-squared.

Elevation error as the time step lengthens, on log–log axes. Left: the error from a single step, measured in a smoothly-evolving part of the run. Right: the error in the final profile after a whole run to a fixed end time, for the default BDF2 scheme (∝ step²) and the first-order backward-Euler option (∝ step).

What the figure says

Per step (left). A single step of length Δt lands off the truth by an amount that grows like Δt²:

one step of…

is off by…

1,000 yr

0.26 mm

4,000 yr

3.9 mm

16,000 yr

51 mm

Double the step → roughly quadruple the error in that step.

Final answer (right). Over a whole run to a fixed end time, the first-order backward-Euler option’s error in the final profile grows like Δt:

step length

error in final profile (backward Euler)

~1,200 yr

~0.008 m

~9,400 yr

~0.07 m

~75,000 yr

~0.6 m

Double the step → roughly double the final error. The default BDF2 scheme (green curve) instead grows like Δt² — far smaller, especially at large steps (see below).

Why the two differ by one power. A single step’s error is quadratic, but a smaller step means proportionally more steps, so the little per-step errors accumulate over more of them: (T / Δt) steps × Δt² each = T · Δt, i.e. linear. So per step the accuracy degrades quadratically, while the accuracy of the final answer — the thing a modeller cares about — degrades linearly with step length for backward Euler. That linear-in-Δt behaviour is what “first-order in time” means; BDF2 removes it, leaving the Δt² scaling below.

Choosing a time step

With the default BDF2 the final error is proportional to Δt², so halving the step quarters the error — you reach a target accuracy at a much larger step than backward Euler would need. Pick Δt from the accuracy you need relative to the elevation changes you are resolving; for the transient above, BDF2 holds the final error at the centimetre level with steps of tens of thousands of years. With the backward-Euler option the error is proportional to Δt instead, so it needs proportionally smaller steps for the same accuracy.

Second order in time: BDF2 (the default)

First order ties accuracy to cost tightly: to cut the final error by 10×, you must cut Δt by 10× — ten times as many steps. GRLP therefore integrates in time with BDF2 by default, which makes the final error scale like Δt² instead of Δt — the green curve in the figure above. So for a target accuracy you can take a substantially larger step (roughly an order of magnitude) and run transients much more cheaply with no loss of fidelity; equivalently, at the same step BDF2 is far more accurate (about 7× smaller final error at a 75,000-yr step in the figure, and the gap widens as steps grow).

BDF2 is L-stable — it damps sharp transients rather than ringing — bootstraps its first step with backward Euler, and reaches its second-order rate once the semi-implicit iteration is converged (the default, below). Steady states are independent of the time step, so the scheme only affects the path through time. The two schemes also agree as Δt → 0: they differ only by their finite-step truncation error, so on a well-resolved run the choice barely matters — BDF2 simply reaches that well-resolved answer at a much larger step. To fall back to first-order backward Euler:

lp.set_time_integration(1)      # 2 = BDF2 (default), 1 = backward Euler

(also available on Network). See MNiMORPH/GRLP#16.

Iterating the solve to convergence (the default)

Each step is semi-implicit: GRLP relinearizes and re-solves a few times (Picard iteration). By default it iterates to convergence — until the inter-iteration elevation change max|z_k z_{k-1}| falls below a tolerance (default 0.1 mm) — so a step is as converged as its second-order accuracy needs, without your having to guess an iteration count:

lp.set_iteration_tolerance(1e-4)   # the default: iterate until change < 0.1 mm

The iteration is capped at max_iter, and warns if it reaches the cap without converging. That happens for a very large step: the fixed-point (Picard) iteration is only a contraction when the step is small enough, so past that it plateaus rather than converging. The warning is the signal to take a smaller step (or accept the reported residual); the run itself still relaxes to the correct steady state.

If you know how many iterations a step needs, fixing the count is a little faster:

lp.set_niter(3)                    # fixed 3 iterations per step (expert option)

Setting a tolerance and setting niter are mutually exclusive — the most recent call wins. See MNiMORPH/GRLP#17.

Adaptive time stepping (opt-in)

Everything above uses a fixed step you choose. But a transient is rarely uniform in time: it changes fast just after a perturbation and slowly as it settles, so a single step is either too small late or too large early. Adaptive stepping picks the step size for you — small where the profile moves fast, large where it is smooth — to hold a target accuracy:

lp.set_time_integration(2)          # adaptive stepping uses BDF2
lp.set_adaptive_timestep(1e-3)      # per-step error tolerance, in metres
lp.evolve_threshold_width_river_adaptive(T)   # advance a total time T

Instead of a step count and a step length, you give a total time T and a tolerance tol. Each candidate step is checked by step doubling — the step is taken once at Δt and again as two steps of Δt/2, and the difference between the two estimates the error. If it exceeds tol the step is rejected and retried smaller; otherwise it is accepted (advancing with the more accurate half-step solution) and the next step grows toward the largest size that still meets tol. The first step is bootstrapped with an error-controlled backward-Euler step, so the result does not depend on any initial-step guess.

As with an ODE solver’s tolerance, tol bounds the per-step error; the total path error is of comparable magnitude, and tightening tol reduces it monotonically (at the cost of more, smaller steps). Pair with set_iteration_tolerance so the nonlinear solve the estimate relies on is itself converged. See MNiMORPH/GRLP#16.

Adaptive stepping is a convenience, not a speed-up. For GRLP it is slower than a well-chosen fixed BDF2 step, not faster: benchmarking (benchmarks/adaptive_timestep_benchmark.py) shows it costs several times more linear solves at matched accuracy — roughly 4× for a smooth uplift-step transient and over 10× for a sharper base-level drop. The reason is that GRLP’s governing equation is diffusive, so its transients are smooth in time: uniform steps are already close to the optimal placement, and the extra solves that step doubling spends to estimate the error each step are never repaid by better placement. Reach for adaptive stepping when you want to set an accuracy and let the solver find the step — not to run faster. When speed matters, use a fixed BDF2 step (the second-order option above buys far more than adaptivity does).