Engines and dispatch
Which engine answers a call, how the choice is made, and why each one exists. See Performance for what they cost and Accuracy and diagnostics for what to do when one warns.
The engines
Six independent engines can answer a request. None of them is a special case of another, and each declines requests it cannot serve honestly.
Engine |
What it assumes |
When it applies |
When it declines |
|---|---|---|---|
General Magnus ladder
( |
Nothing beyond a Hermitian |
Always. Every other engine falls back to it. |
Never – it is the terminal path. |
Two-flavor interaction picture
( |
A genuine exponential profile, built by
|
Single points and multi-energy scans at one baseline. |
Non-exponential profiles, \(d > 2\), LIV, breakpoints, and whenever its own iteration fails to converge (typically near an MSW resonance). |
Constant Hamiltonian
( |
|
Vacuum and constant density, at any flavor count, for a single point or a scan, with per-point baselines allowed. |
A position-dependent potential; user slab edges; parallel, logged or verbose runs. |
Energy-batched separable scan
( |
|
Many energies sharing one baseline. |
Per-point baselines, user slab edges, parallel or logged runs, a constant potential (which the constant engine takes instead). |
Cumulative baseline scan
( |
Baselines nest: \(U(0\to L_2) = U(L_1 \to L_2)\,U(0 \to L_1)\). |
A baseline scan at a single energy, with a position-dependent |
Differing energies, |
Adiabatic + Magnus hybrid
( |
|
Any dimension, any smooth position-dependent profile, with a requested tolerance. |
Breakpoints or slab edges supplied, a constant potential, no requested tolerance, a profile that fails the resolution test, or failure to self-certify. |
A sixth reference, scipy.linalg.expm, is not an engine but is used as an oracle by
magnus.oscprob.cross_check_strategies() wherever it is exact – a constant H,
or a piecewise-constant one whose edges are declared.
Independence, and why it matters
The engines are not all independent of each other, and pretending otherwise would make
any cross-check between them worthless. magnus.oscprob.ENGINE_FAMILIES records the
grouping the package will defend:
'magnus-ladder'– the general path, the cumulative scan and the separable scan. All three walk slabs withmagnus.magnus.magnus_expansion_multislab(), and the cumulative scan additionally sizes its grid from an ordinary adaptivemagnus.oscprob.osc_prob()probe, so it inherits that path’s stopping rule as well.'interaction-picture'– the two-flavor fast path. Same Magnus core, but the fast vacuum phase is factored out analytically first, so what it must resolve is a different function.'adiabatic'– the hybrid strategy. A genuinely different method; its blind spots are the resonance detector’s, not the quadrature’s.'exact'–expm, independent of all of them.
Two engines in the same family can be wrong in the same way at the same time. Their disagreement is informative; their agreement is not.
Dispatch
Every scenario wrapper (magnus.oscprob.osc_prob_matter_std_potential(),
magnus.oscprob.osc_prob_matter_nsi(), magnus.oscprob.osc_prob_liv()) tries the
engines in a fixed order, falling through on NotImplemented:
Taken when |
Engine |
Why it is first |
|---|---|---|
|
closed-form phase average ( |
No propagation at all; the decohered limit is algebraic |
Smooth profile, a tolerance was requested, |
adiabatic + Magnus patch |
Transports along the levels instead of resolving every oscillation |
Exponential profile, two flavors – and it converges |
interaction picture |
An exact reference solution exists for this one case |
Many energies at a single baseline |
energy-batched scan |
One traversal serves every energy |
Baseline scan at one energy, at least
|
cumulative scan |
Every baseline is a prefix of the longest one |
Otherwise |
general Magnus ladder |
Always applicable; the one that is never skipped |
Each row falls through to the next on NotImplemented, so the last row is
reached whenever nothing above it applies.
Two thresholds decide the seams, and both are constants with docstrings of their own:
magnus.oscprob.HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS= 8. Understrategy='auto'the hybrid strategy stands aside for a baseline scan of at least this many points, because the cumulative scan answers all of them from one traversal. This was 25; the constant’s own docstring records why it moved, and why a later attempt to lower it to 1 was reverted.magnus.oscprob.CUMULATIVE_AUTO_MIN_POINTS= 2. Below this there is no prefix to reuse.
The accuracy steps at the seam rather than varying smoothly, and that is by design.
Adding one baseline to a scan just below it changes the answer, because it changes the engine.
Measured against solve_ivp:
Profile |
err(N = 24) |
err(N = 26) |
Ratio |
|---|---|---|---|
solar exponential |
3.30e-05 |
2.13e-08 |
1 546× |
multi-resonance |
1.58e-03 |
2.86e-09 |
552 945× |
noisy |
6.27e-04 |
1.04e-08 |
60 418× |
castle wall + breakpoints |
2.80e-11 |
2.80e-11 |
1.0× (cumulative from N = 2) |
The jump is always toward the truth, so this is a documentation matter rather than a numerical one – but a user scanning N and watching their answer move by five orders of magnitude in accuracy will otherwise assume something is broken.
Seeing which engine answered. The fallbacks are silent by design: they happen on
ordinary calls and warning about them would be noise. Pass strategy_info to any of the
three scenario wrappers to see the route without changing it:
info = {}
P = oscprob.osc_prob_matter_std_potential(..., strategy_info=info)
info['engine'] # 'hybrid', 'ip_exp', 'separable', 'cumulative', 'magnus', 'average'
info['certified'] # for the hybrid strategy
info['declined'] # [(engine, why it stood aside)]
This is the answer to “why did my result move?” and “why did this call get slow?”, both of which were previously unanswerable from outside the package.