Adiabatic + Magnus hybrid strategy
This page documents the strategy parameter: the problem it solves, the method
behind it (magnus.adiabatic) and the evidence that validates it. The parameter
is accepted by the matter scenario functions
(osc_prob_matter_std_potential(),
osc_prob_matter_nsi() and osc_prob_liv()),
by every wrapper built on them, and by the generic entry points
osc_prob_sun() and osc_prob_earth().
Methodology describes the plain Magnus expansion that this strategy
complements.
The problem: extreme accumulated phase
The plain Magnus engine (Methodology) partitions a trajectory into
slabs and is accurate to any desired order within each one, but it must
still resolve however many radians of phase accumulate inside a slab. For
an \(\rm MeV\)-scale solar neutrino crossing most of the Sun’s radius,
the vacuum term alone (\(\Delta m^2/2E\), growing as \(1/E\))
accumulates \(\mathcal{O}(10^3\text{–}10^6)\) radians. Reaching a
requested tolerance then requires a very large number of slabs. If a
refinement cap is reached first, osc_prob returns its best estimate and
issues ToleranceNotAchievedWarning: the warning is correct, but the answer
remains unverified.
Physically, however, the Hamiltonian changes slowly. Away from a level crossing,
the state evolves adiabatically, following the instantaneous eigenstates of the
Hamiltonian as the matter potential falls. This is the regime of the
Mikheyev–Smirnov–Wolfenstein (MSW) effect for solar neutrinos
[GK07a]: outside the resonance region, the evolution follows from the
instantaneous eigenbasis alone, with no need to resolve the oscillation phase.
The hybrid strategy of magnus.adiabatic uses adiabatic transport where it
is valid, and a Magnus computation in the narrow regions, if any, where it is
not.
Adiabatic transport
Let \(\mathbb{H}(l)\) be diagonalized at each position:
The adiabatic theorem says that, as long as no two eigenvalues become (nearly) degenerate along the trajectory, a state that starts as the \(k\)-th eigenstate stays the \(k\)-th eigenstate, up to a phase:
with \(\Phi_k\) the ordinary dynamical phase and \(\gamma_k\) the geometric (Berry) phase, generated by how the eigenvector itself turns as \(l\) varies. Assembled into an evolution operator in the original (flavor) basis, this is
magnus.adiabatic.adiabatic_propagator() computes this, integrating
\(\Phi_k\) with Simpson’s rule over a grid of diagonalized Hamiltonians.
The phase \(\Phi_k\) is a smooth integral however large it grows, so the propagator
does not resolve the phase step by step, as a Magnus slab or an ODE solver
must, and its cost barely depends on the length of the trajectory.
The geometric phase needs no separate formula. At each grid step, the phase of every column of \(\mathbb{V}\) is fixed by making its overlap with the previous step’s eigenvector real and positive. This is a discrete form of parallel transport, and it captures \(\gamma_k\) for complex Hamiltonians, with CP-violating phases, as well as for real ones.
The result is unitary, being a diagonal phase between unitary matrices, however coarse the grid. The grid sets only how closely the discretized integral and transport approach the adiabatic limit.
Two levels approach each other as a function of position \(l\) and avoid crossing, with minimum gap \(2\varepsilon\) at \(l_\star\). An adiabatic trajectory (blue) stays on its eigenvalue branch through the crossing; a diabatic trajectory (gold) instead follows the original (bare, dashed) levels straight through — the sudden, non-adiabatic transition the local Magnus patch exists to capture.
Detecting resonances without differentiating eigenvectors
Adiabatic transport breaks down where two eigenvalues nearly cross: the gap between them becomes small, and the state cannot follow the rotation of the instantaneous eigenbasis. Locating these points, and measuring how far from adiabatic they are, needs the derivatives of the eigenvalues and eigenvectors along the trajectory. Eigenvectors, however, are defined only up to a phase, and near a degeneracy only up to a rotation, so differentiating them by finite differences is unreliable. The Hellmann–Feynman theorem avoids this: for a normalized eigenvector \(|v_k(l)\rangle\) of \(\mathbb{H}(l)\),
These exact identities require only \(d\mathbb{H}/dl\), which is smooth and gauge-independent, and not the derivative of the eigenvectors.
magnus.adiabatic.find_resonance_candidates() uses the first identity to
locate every position where the gap between a pair of levels is stationary,
\(d(\lambda_j-\lambda_k)/dl = 0\), refined to machine precision by bisection.
It scans every pair \((j,k)\), at any dimension, so simultaneous resonances
do not hide one another; this was verified at 2, 3, 4 and 5 flavors against a
dense per-pair scan of the adiabaticity parameter \(\gamma_{jk}\) (defined below).
The search is limited by the probe grid. Candidates are bracketed on
n_probe evenly spaced samples, so a feature much narrower than
\((l_1-l_0)/n_\text{probe}\) can fall between two samples and go unseen. For a
known narrow feature, supply t_breakpoints there (which routes the request to the
general Magnus method), or raise n_probe0 or max_n_probe when calling
magnus.adiabatic.hybrid_propagator() directly.
The second identity gives an exact, gauge-independent adiabaticity parameter, similar to the Landau–Zener one:
A crossing with \(\gamma_{jk} \gg 1\) is non-adiabatic, and one with \(\gamma_{jk} \ll 1\) is adiabatic. Both quantities come from \(\mathbb{H}(l)\) and its eigendecomposition alone, so they apply to any Hamiltonian, from a two-level model to five flavors with NSI and LIV terms.
A gap extremum need not coincide with the peak of \(\gamma_{jk}\). For this reason,
magnus.adiabatic.find_nonadiabatic_windows() also sweeps \(\gamma_{jk}\) along the
probe grid and opens a window over every contiguous stretch above the threshold.
Important
The derivative \(d\mathbb{H}/dl\) is computed with an ordinary real finite difference, not complex-step differentiation (\(\mathrm{Im}[\mathbb{H}(l+ih)]/h\)), which is valid only for functions that are real at real input: \(\mathbb{H}(l)\) is complex whenever a CP-violating phase is nonzero.
Growing and merging non-adiabatic windows
A candidate with \(\gamma_{jk}\) above a threshold marks a position that
needs a Magnus patch, but it does not fix the window the patch must cover. The
region where adiabaticity fails has a physical width, unrelated to the spacing
of the search grid. To find it, magnus.adiabatic.find_nonadiabatic_windows()
grows a window outward from each candidate, doubling the step, until
\(\gamma_{jk}\) falls below the threshold, and then pads it by a safety
factor. The same physical case gives the same window whatever the search grid.
Windows that overlap or touch are merged. Two nearby resonances are then patched as one region, rather than counted twice or left partly uncovered.
Patching, and composing the pieces exactly
Inside a non-adiabatic window \([l_b, l_c]\), adiabatic transport is not
used. Instead, the Magnus kernel (magnus.magnus.magnus_expansion_multislab())
computes the evolution operator directly, at order 6 by default, doubling the
number of slabs until two successive levels agree to
\(\min(10^{-7}, (\text{atol} + \text{rtol})/10)\). The window is narrow, so
this computation is cheap, although the same method would be slow over the whole
trajectory.
The full hybrid propagator over \([l_0, l_1]\) with windows \([l_b^{(1)}, l_c^{(1)}], \ldots, [l_b^{(n)}, l_c^{(n)}]\) is then assembled by the exact composition law of quantum evolution,
which holds whichever method computed each factor; the plain Magnus engine chains its slabs the same way (Methodology). Every factor is unitary, so the product is unitary too, however accurate the adiabatic approximation is along the way.
The hybrid propagator’s segmentation along a trajectory with one resonance: adiabatic transport everywhere except the narrow window \([l_b, l_c]\) bracketing the crossing \(l_\star\), patched with an exact local Magnus computation.
Self-certification
Three settings control the strategy: the adiabaticity threshold, the grid that
integrates the dynamical phase and the probe grid that locates candidates. No
single value of any of them is safe for every Hamiltonian: a threshold that is
conservative for one case can leave a crossing unpatched in another. Instead of
relying on one value, magnus.adiabatic.hybrid_propagator() repeats the
whole computation, each time dividing the threshold by 3 and doubling both
grids. It certifies the result only once two successive levels agree within the
requested rtol and atol, as magnus.oscprob.osc_prob() does for its
slab count.
A window that does not move between levels holds the same patch in both, so
this comparison cannot see a patch’s own error. Each patch is instead
converged separately, to the tolerance above. If a patch fails to converge
within its slab cap, or the levels never agree, the propagator returns its best
estimate, still unitary, and reports it as not certified. The refinement caps are
min_threshold, max_n_probe and max_n_points, for the threshold and
the two grids, and max_iters (12 passes by default). They are arguments of
magnus.adiabatic.hybrid_propagator(), and osc_prob uses their defaults.
The strategy parameter
The functions listed at the top of this page accept a strategy keyword with
three values:
'magnus'Use only the Magnus-expansion engines, never the adiabatic one: the two-flavor interaction picture where it applies (two-level Hamiltonians on an exponential profile; see Engines and dispatch), the energy-batched scan or the general adaptive slab refinement. It also turns off the cumulative baseline scan; see strategy=’magnus’.
'hybrid'Try
magnus.adiabatic.hybrid_propagator()in addition to the Magnus engines, at every point where the matter potential depends on position, provided that no slab edges or breakpoints were supplied and that a tolerance was requested. Breakpoints mark a discontinuous profile, such as PREM, on which the finite-difference diagnostics fail. The method refines adaptively, so it needs a tolerance. If any point fails to certify, the result is still returned, withHybridCertificationWarning, a subclass ofToleranceNotAchievedWarning.'auto'(default)Try
'hybrid'under the same conditions; if it does not apply, or if any requested point fails to self-certify, the whole request falls back to the'magnus'engines. The fallback issues no warning, except when the hybrid declines because of an undeclared density jump, which issuesUnmarkedDiscontinuityWarning.Two rules can route a request elsewhere before the hybrid is tried. A moderate accumulated phase goes to the Magnus ladder (the ladder route). A baseline scan at one energy of at least
HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTSpoints (at leastHYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHTbelow a tolerance of 1e-6) goes to the cumulative scan, which answers every baseline from a single traversal. Dispatch gives both rules and the constants behind them.
Unlike the two-flavor interaction-picture fast path, the hybrid strategy has no restriction on the number of flavors: the resonance detector and adiabatic propagator make no assumption about the Hamiltonian’s dimension or structure. It composes correctly with any number of simultaneous or sequential resonances, of any kind (standard MSW, NSI-induced or otherwise), between any pair of levels, provided each is visible on the probe grid — see the two limits below.
Warning
This strategy cannot detect two kinds of feature. The first makes it decline to certify; the second can return a wrong answer reported as certified:
A profile that is not smooth at the probe scale. Every diagnostic here finite-differences \(\mathbb{H}(l)\) between probe points. On a density step, a kink or any feature sharp compared with the probe spacing, those derivatives are meaningless.
hybrid_propagatormeasures this directly and declines to certify, somagnus.oscprob.osc_prob()falls through to the general Magnus path, which handles such profiles correctly. Passingt_breakpointsat the discontinuities is better still.A bump narrower than the probe spacing, which rises and falls between two probe points: neither the probe nor its refinement samples it, so \(\gamma\) looks small and no window opens. On a Gaussian resonance of width \(3\times10^{-5}(l_1-l_0)\), the returned probability was wrong by 2.9e-2 although the result reported
certified=True. The general Magnus path is no better here (it misses the feature too, though it does warn). If a narrow feature’s position is known, passt_breakpoints.A step narrower than the probe spacing is detected. Its full height lies between two probe points, so the change of \(\mathbb{H}\) across that interval is much larger than the derivatives at its ends predict. Before certifying,
hybrid_propagatorcompares the two on every probe interval. Where the change is more than twice the prediction, it evaluates \(\gamma\) at the steepest point; if that point could move the answer by the tolerance, it repeats the refinement with the point included from the first level. Later passes check again, so a profile with many such steps is covered over several passes. For a shock of width 0.8 (in units of the inverse vacuum splitting) on a path of length \(3\times10^5\) in the same units, this check moved the certified result from 0.153 to 0.4921, against a true 0.4922. Where no step is found, this check does not alter the result.
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
# 8 MeV, 90% of the way through the Sun: under strategy='magnus' this
# needs a very large slab count, and can issue ToleranceNotAchievedWarning.
P = oscprob.osc_prob_3nu_sun(
8.0 * gd.UNIT_MEV, 0.9 * gd.SUN_RADIUS * gd.UNIT_KM, 0.0,
strategy='auto', # the default; shown explicitly here for clarity
)
Validation
Every claim above is checked against a tight-tolerance
scipy.integrate.solve_ivp (DOP853) solution of the same Schrödinger
equation. The validation grid covers each qualitatively different case:
zero resonances (purely adiabatic), one resonance, two well-separated resonances and two resonances close enough that their windows merge;
3-, 4- and 5-flavor Hamiltonians, with standard and NSI-induced resonances;
real and complex (CP-violating) Hamiltonians.
Case |
Windows |
Speed-up vs. |
Unitarity |
Max abs. error |
|---|---|---|---|---|
Standard 3ν (18 MeV, 0.9 \(R_\odot\)-scale baseline) |
0 |
~3600× |
\(6.7\times10^{-16}\) |
\(2.3\times10^{-4}\) |
BSM (NSI) 3ν, engineered resonance |
1 |
~88× |
\(1.8\times10^{-12}\) |
\(9.0\times10^{-4}\) |
Standard 4ν (3+1 sterile) |
0 |
~4670× |
\(8.9\times10^{-16}\) |
\(2.0\times10^{-4}\) |
BSM (NSI) 4ν, engineered resonance |
1 |
~60× |
\(1.6\times10^{-13}\) |
\(1.4\times10^{-4}\) |
Standard 5ν (3+2 sterile) |
0 |
~4800× |
\(1.1\times10^{-15}\) |
\(2.0\times10^{-4}\) |
BSM (NSI) 5ν, engineered resonance |
1 |
~52× |
\(1.7\times10^{-13}\) |
\(1.4\times10^{-4}\) |
Synthetic, two independent resonances (kept separate) |
2 |
~30× |
\(4.5\times10^{-13}\) |
\(3.2\times10^{-4}\) |
Synthetic, two nearby resonances (merged into one window) |
1 |
~91× |
\(1.3\times10^{-13}\) |
\(2.9\times10^{-3}\) |
Measured speed-up versus a tight-tolerance solve_ivp ground truth
across the validation grid (log scale), plotting exactly the numbers in
the table above — tests/test_adiabatic_validation_table.py holds the
two against each other, so they cannot drift apart. Purely adiabatic
cases (green) are fastest, since no exact patch is ever computed. Cases
needing one or more Magnus patches (red) are 30–91× faster than direct
integration. Their cost is dominated by the patch computation, which is
cheap because the window is narrow, and by the self-certification loop.
Patched cases are slower because each patch, and the certification loop around it, requires a
Magnus computation. The purely adiabatic 5ν case has the largest speed-up because it is also
where solve_ivp is slowest: the cost of solve_ivp grows with the dimension of the
Hamiltonian, while the adiabatic transport cost barely does. Across every case, the result is
unitary to \(2\times10^{-12}\) or better, and the agreement with direct integration is at
the \(10^{-4}\) level, reaching \(2.9\times10^{-3}\) only for the two merged resonances.
Limitations and scope
The hybrid strategy requires a smooth Hamiltonian: the finite-difference Hellmann–Feynman diagnostics assume that \(d\mathbb{H}/dl\) exists everywhere on the trajectory. It does not handle a profile with discontinuities, such as the layers of PREM; supplying
t_slab_edgesort_breakpointssends the request to the general method.The strategy certifies its result by refinement, so it does not apply when no tolerance is requested (
rtolandatolbothNone).
magnus.adiabatic.hybrid_propagator() works on any callable H_func(l), so the same two
conditions apply to a Hamiltonian supplied to magnus.oscprob.osc_prob_sun() or
magnus.oscprob.osc_prob_earth(): a smooth profile and a requested tolerance. In
practice:
osc_prob_sunuses the hybrid strategy as readily as the Sun wrappers, since its density profile has no breakpoints.osc_prob_earthalmost always uses the'magnus'engines, whatever is requested, because a chord through the Earth crosses PREM layer boundaries, which become breakpoints.
See Methodology for the plain Magnus-expansion machinery this
strategy sits alongside, and Code architecture for where
magnus.adiabatic fits into the package’s module layout.