Numerics¶
Pure-numerics kernels: ODE integrators, step controllers, and
interpolators. Today the only consumer is the adaptive field-line
tracer — Dormand-Prince 5(4) 1 with FSAL re-use,
an elementary order-\(p\) (I) step controller in the convention of
2 §II.4, and scipy.interpolate.RegularGridInterpolator
for trilinear field evaluation. Future consumers (particle pushers,
splitting helpers, higher-order quadrature) land here.
What's tested¶
The kernel's contract is pinned in tests/test_numerics.py:
- 5th-order convergence — halving \(h\) on \(y'' + 4y = 0\) reduces the global error by a factor in \((20, 60)\), ruling out 4th- and 6th-order rounding accidents.
- FSAL identity and savings — the last stage of an accepted step
equals \(f(y_{n+1})\); passing it as
k0saves exactly one RHS evaluation on the next step and produces bit-for-bit identical results. - Batched ↔ scalar bit-for-bit equivalence — at N=1 and across
N=4 with mixed states, the batched kernel matches a per-seed
scalar loop to
atol=1e-15. - Per-seed failure isolation — one batched seed failing at any
Butcher stage leaves the other seeds'
y_newunaffected. - Adaptive-loop integration — a minimal driver wiring DP × error norm × I controller solves \(y''+4y=0\) to tighter and looser tolerances; the tighter run achieves a strictly smaller global error than the looser one.
- Controller monotonicity —
h_newis non-increasing inerr_normacross a sweep that spans the unclamped middle and both growth clamps.
Planned additions¶
- Implicit-midpoint integrator — single-stage Gauss-Legendre Runge-Kutta for symplectic, bounded-drift field-line tracing.
- Tricubic interpolation kwarg — non-periodic
RegularGridInterpolator(method="cubic")passthrough. - Periodic tricubic splines —
periodic_axes=kwarg usingscipy.interpolate.CubicSpline(..., bc_type="periodic")per spline line; needed for seamless \(\phi\)-wrap on spherical PFSS grids. - Curvature-based step control —
step_control="curvature"alternative to the error-norm controller, keeping the unit-tangent rotation per step bounded byover_rcand clamped by the local mesh size.
The first downstream consumer of the full bundle will be a planned
pypic.maps module — squashing factor \(Q\), footpoint maps, and
open-field classification. See schema.md § Field-line map
quantities for the canonical
names of map outputs on disk.
numerics
¶
Pure-numerics kernels for pypic.
Generic ODE integrators, step controllers, and related numerical
methods, decoupled from any specific physics task. The adaptive
field-line tracer (pypic.traces.trace_field_line_adaptive) is the
current consumer. Not re-exported from the top-level pypic
namespace.
Structure-preserving (symplectic, variational) integrators follow [@HairerLubichWanner2006]; the current Dormand-Prince kernel is the classical embedded pair from [@HairerWanner1993].
DPStepResult
dataclass
¶
Outcome of one Dormand-Prince 5(4) step.
On success, failed_stage is None and y_new /
err_vec / k_last are populated. On RHS-callable failure,
failed_stage is the stage index (0-6) whose evaluation
returned None and failed_point is the point passed to
that stage so the caller can classify the failure mode (e.g.
null hit, out-of-domain) in its own vocabulary.
Attributes:
| Name | Type | Description |
|---|---|---|
y_new |
FloatArray or None
|
5th-order solution at |
err_vec |
FloatArray or None
|
Embedded 4(5) error estimate vector (same shape as |
k_last |
FloatArray or None
|
Last stage value |
failed_stage |
int or None
|
Index of the stage whose RHS evaluation returned |
failed_point |
FloatArray or None
|
The point passed to the failed stage (for caller-side
classification). |
Source code in src/pypic/numerics/_rk.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | |
DPStepResultBatched
dataclass
¶
Outcome of one batched Dormand-Prince 5(4) step over N seeds.
Same FSAL/embedded-error contract as DPStepResult, lifted
over an N-seed batch. y_new / err_vec / k_last are
always populated as (N, n) arrays; per-seed validity is read
from failed_stage (-1 = success, 0..6 = first stage at
which the RHS reported invalid).
Attributes:
| Name | Type | Description |
|---|---|---|
y_new |
FloatArray
|
5th-order solution at |
err_vec |
FloatArray
|
Embedded 4(5) error estimate, shape |
k_last |
FloatArray
|
Last-stage value |
failed_stage |
FloatArray
|
Per-seed first-failed-stage index, shape |
failed_point |
FloatArray
|
Per-seed evaluation point at which the failure occurred, shape
|
Source code in src/pypic/numerics/_rk.py
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | |
dormand_prince_step(f, y, h, *, k0=None)
¶
One Dormand-Prince 5(4) step from y over a step of size h.
Evaluates the 7 stages of the Dormand-Prince tableau and returns both the 5th-order solution and the embedded 4th-order error estimate:
where \(b_i\) are the 5th-order propagation weights (_DP_B5)
and \(\hat{b}_i\) are the embedded 4th-order weights (_DP_B4).
The RHS callable f may return None to signal that the
point is invalid (out-of-domain, at a magnetic null, ...); when
that happens the step is aborted at the failing stage and the
failure point is reported back for caller-side classification.
Operates on a 1-D state y of shape (n,). See
dormand_prince_step_batched for the vectorized form over
a leading seed axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
callable
|
RHS function |
required |
y
|
NDArray
|
Current state, shape |
required |
h
|
float
|
Step size (sign-bearing — pass a negative |
required |
k0
|
NDArray or None
|
Pre-computed first-stage value |
None
|
Returns:
| Type | Description |
|---|---|
DPStepResult
|
Step outcome. See |
Examples:
>>> import numpy as np
>>> result = dormand_prince_step(lambda y: -y, np.array([1.0]), 0.1)
>>> bool(abs(result.y_new[0] - np.exp(-0.1)) < 1e-9)
True
>>> result.failed_stage is None
True
Source code in src/pypic/numerics/_rk.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
dormand_prince_step_batched(f, y, h, *, k0=None)
¶
Batched Dormand-Prince 5(4) step over N independent seeds.
Vectorized form of dormand_prince_step — each of the seven
Butcher stages becomes a single f call on the whole (N, n)
batch instead of N calls on individual (n,) vectors. The
arithmetic for each seed is identical to the single-step kernel;
the only contract difference is the RHS callable.
The RHS f(y_batch) must return (rhs_values, valid_mask)
where rhs_values is the (N, n) RHS array and valid_mask
is an (N,) boolean array (True = valid seed). Invalid
seeds at any stage are recorded in the per-seed failed_stage
array of the returned result; the kernel still evaluates the
remaining stages for the surviving seeds (their y_new and
err_vec are unaffected by the failed seeds because the
Butcher contraction is stage-axis only).
Currently assumes y is 2-D (N, n). Field-line tracing
uses n = 3.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
callable
|
Batched RHS |
required |
y
|
NDArray
|
Current state, shape |
required |
h
|
NDArray or float
|
Step size. Scalar (shared across seeds) or shape |
required |
k0
|
NDArray or None
|
Pre-computed first-stage value from a previous accepted step's
|
None
|
Returns:
| Type | Description |
|---|---|
DPStepResultBatched
|
|
Examples:
>>> import numpy as np
>>> def rhs(y):
... return -y, np.ones(y.shape[0], dtype=bool)
>>> y0 = np.array([[1.0], [2.0]])
>>> r = dormand_prince_step_batched(rhs, y0, 0.1)
>>> bool(np.allclose(r.y_new[:, 0], y0[:, 0] * np.exp(-0.1), atol=1e-9))
True
>>> int(r.failed_stage.max())
-1
Source code in src/pypic/numerics/_rk.py
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | |
embedded_error_norm(err_vec, y_new, atol, rtol)
¶
RMS norm of err_vec scaled by atol + rtol * |y_new|.
Standard mixed absolute/relative tolerance norm for embedded
Runge-Kutta error estimators ([@HairerWanner1993] §II.4). A
returned value \(\le 1\) means the step is acceptable under the
requested tolerances. RMS (rather than the alternative max-norm)
aligns with SciPy's RK45._estimate_error_norm and the
Hairer-Nørsett-Wanner convention, so step-size sequences here
are directly comparable to other Python and Fortran ODE
integrators that follow the same recipe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
err_vec
|
NDArray
|
Embedded error vector from
|
required |
y_new
|
NDArray
|
Proposed solution at the new time (used for the relative tolerance scale). |
required |
atol
|
float
|
Absolute and relative tolerances. |
required |
rtol
|
float
|
Absolute and relative tolerances. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Scaled RMS norm of the error. For a single-component state the RMS collapses to \(|\mathrm{err}| / \mathrm{scale}\). |
Examples:
>>> import numpy as np
>>> err = np.array([1e-6, 2e-6])
>>> y = np.array([1.0, 2.0])
>>> float(round(embedded_error_norm(err, y, atol=1e-6, rtol=0.0), 6))
1.581139
Source code in src/pypic/numerics/_rk.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | |
embedded_error_norm_batched(err_vec, y_new, atol, rtol)
¶
Per-seed RMS error norm for a batched embedded RK step.
Vectorized form of embedded_error_norm. Same mixed
absolute/relative scaling and RMS reduction as the scalar form
([@HairerWanner1993] §II.4), averaged over the component axis
(axis=-1) so each seed gets its own scalar error norm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
err_vec
|
NDArray
|
Embedded error vectors, shape |
required |
y_new
|
NDArray
|
Proposed solutions at the new time, shape |
required |
atol
|
float
|
Absolute and relative tolerances (shared across seeds; per-seed tolerances are not in scope for v1). |
required |
rtol
|
float
|
Absolute and relative tolerances (shared across seeds; per-seed tolerances are not in scope for v1). |
required |
Returns:
| Type | Description |
|---|---|
FloatArray
|
Per-seed scaled RMS error norms, shape |
Examples:
>>> import numpy as np
>>> err = np.array([[1e-6, 2e-6], [3e-6, 4e-6]])
>>> y = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> norms = embedded_error_norm_batched(err, y, atol=1e-6, rtol=0.0)
>>> norms.shape
(2,)
Source code in src/pypic/numerics/_rk.py
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | |
i_step_controller(h, err_norm, *, min_step, max_step, order=5)
¶
Next step size from the current step and its error norm.
Applies the elementary order-\(p\) step-size formula
\(h_{new} = h \cdot S \cdot (\mathrm{err})^{-1/p}\) with safety
factor \(S = 0.9\), then clamps the growth ratio to
[_GROWTH_MIN, _GROWTH_MAX] and the absolute step to
[min_step, max_step]. order is the order of the embedded
method's higher-order solution (5 for Dormand-Prince 5(4)).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
h
|
float
|
Current step size. |
required |
err_norm
|
float
|
Scaled error norm from
|
required |
min_step
|
float
|
Lower / upper bounds on the returned step magnitude. |
required |
max_step
|
float
|
Lower / upper bounds on the returned step magnitude. |
required |
order
|
int
|
Order \(p\) of the embedded higher-order solution. Default 5 (Dormand-Prince 5(4)). |
5
|
Returns:
| Type | Description |
|---|---|
float
|
Next step size (always within |
Examples:
>>> # err_norm = 1 → step factor ≈ safety = 0.9
>>> float(round(i_step_controller(1.0, 1.0, min_step=1e-6, max_step=10.0), 6))
0.9
>>> # err_norm → 0 → growth clamped to _GROWTH_MAX
>>> float(i_step_controller(1.0, 0.0, min_step=1e-6, max_step=10.0))
5.0
Source code in src/pypic/numerics/_step_control.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
i_step_controller_batched(h, err_norm, *, min_step, max_step, order=5)
¶
Per-seed step-size update for a batched embedded RK integration.
Vectorized form of i_step_controller. Applies the elementary
order-\(p\) formula \(h_{new} = h \cdot S \cdot \mathrm{err}^{-1/p}\)
independently per seed, with the same safety factor, growth clamps,
and absolute-step clamps as the scalar version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
h
|
NDArray
|
Current per-seed step sizes, shape |
required |
err_norm
|
NDArray
|
Per-seed scaled error norms from
|
required |
min_step
|
float
|
Lower / upper bounds on each returned step (shared across seeds; per-seed bounds are not in scope for v1). |
required |
max_step
|
float
|
Lower / upper bounds on each returned step (shared across seeds; per-seed bounds are not in scope for v1). |
required |
order
|
int
|
Order \(p\) of the embedded higher-order solution. Default 5 (Dormand-Prince 5(4)). |
5
|
Returns:
| Type | Description |
|---|---|
FloatArray
|
Per-seed next step sizes, shape |
Examples:
>>> import numpy as np
>>> h = np.array([1.0, 1.0])
>>> err = np.array([1.0, 0.0]) # one at-tol, one zero-err
>>> h_new = i_step_controller_batched(h, err, min_step=1e-6, max_step=10.0)
>>> float(round(h_new[0], 6))
0.9
>>> float(h_new[1])
5.0
Source code in src/pypic/numerics/_step_control.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
-
J. R. Dormand and P. J. Prince. A family of embedded Runge–Kutta formulae. Journal of Computational and Applied Mathematics, 6(1):19–26, 1980. doi:10.1016/0771-050X(80)90013-3. ↩
-
E. Hairer, S. P. Nørsett, and G. Wanner. Solving Ordinary Differential Equations I: Nonstiff Problems. Volume 8 of Springer Series in Computational Mathematics. Springer-Verlag, 2nd edition, 1993. doi:10.1007/978-3-540-78862-1. ↩