Numerical uncertainty¶
A number computed by a simulation (a growth rate, a mean, a value at a point) depends on the
resolution and on the time step. cn.uncertainty reports it with an error estimate from studies
that you declare: three resolutions and three time steps, nothing chosen for you.
A value with its error¶
import numpy as np
import conatus as cn
def u_at_origin(N, dt):
"""u(0, T) of the heat equation u_t = nu u_xx, u(x, 0) = exp(cos x)."""
m = cn.Model(cn.Box(x=cn.periodic(2 * np.pi)), name="heat", nu=0.5)
m.scalar("u")
m.equations("dt(u) - nu*dx(dx(u)) = 0")
sim = m.simulate(N=N, scheme="RK222", dt=dt)
sim.initial(u=lambda x: np.exp(np.cos(x)))
sim.run(until=0.4)
return float(sim.grid("u")[0]) # x = 0 is the first grid point
rep = cn.uncertainty(u_at_origin, N=[8, 12, 16], dt=[0.04, 0.02, 0.01], scheme_order=2,
name="u(0, T)")
lo, hi = rep.interval
print(f"u(0, T) = {rep.value:.8f} +- {rep.error:.1e} trusted: {rep.trusted}")
print(f"observed order in time: {rep.temporal.order:.2f} (scheme order 2)")
u(0, T) = 2.32103549 +- 3.8e-06 trusted: True
observed order in time: 2.01 (scheme order 2)
exact value 2.32103854: inside the interval: True
The last line is the check made after the example: the exact value of this problem (a series of Bessel functions) lies inside the reported interval.
What is computed¶
quantity(N=..., dt=...) is run five times: the three time steps at the finest resolution, and the
three resolutions at the finest time step (the finest run is shared).
Time: the observed order of convergence \(p\) from the three values; the Richardson value is shown beside the result, and the error estimate is \(1.25\,|q_R - q_3|\), with the safety factor of Roache’s grid-convergence index (Roache 1994). With
scheme_order, an observed order more than 0.3 away from it is flagged: the time steps are not yet in the asymptotic range.Space: spectral convergence is geometric, \(q(N) = q_\infty + C e^{-aN}\) at the resolutions actually given; the ratio \(\rho\) of successive differences fixes \(a\), and the estimate is \(1.25\,|d_2|/(e^{a h_2}-1)\) with \(h_2\) the last increment of \(N\) (equal increments: \(1.25\,|d_2|\,\rho/(1-\rho)\)). Differences that do not decrease geometrically, and tuple resolutions refined in different proportions, are flagged.
Result: the value at the finest resolution and time step (the Richardson value is never substituted for it), the sum of both estimates, and the interval.
rep.trustedis false as soon as a study is flagged (also withoutscheme_order: the asymptotic range is then unchecked), andrep.explain()prints every number and assumption.
The time steps must have a constant ratio and the resolutions must increase, the finest last; anything else is refused.
The mean of a statistically steady run¶
cn.time_average(series, dt=..., discard=k) gives the mean of a correlated time series after its
first k samples (the transient is yours to judge: discard has no default), with its standard
error from the integrated autocorrelation time. A batch-means estimate is printed beside it as a
check, and a series shorter than about 50 correlation times is flagged.
import numpy as np
import conatus as cn
rng = np.random.default_rng(0) # stands for the Nusselt number of a run,
nusselt = np.empty(20000) # one value every 0.01: a correlated series
nusselt[0] = 6.0 # with a transient
for i in range(1, nusselt.size):
nusselt[i] = 4.9 + 0.98 * (nusselt[i - 1] - 4.9) + 0.02 * rng.standard_normal()
rep = cn.time_average(nusselt, dt=0.01, discard=2000)
print(rep.explain()) # mean, standard error, correlation time, batch-means check