API reference
Generated from the docstrings. Every Examples block below is executed
when this page is built, so the results shown are what the code returns
rather than numbers written beside it. The regression suite runs the same
blocks on every supported Python.
Core modules
The three core modules are self-contained: they need only numpy, and they
accept any Hermitian Hamiltonian. Copying one into your own project is a
supported way to use NuOscProbExact.
All three are unit-agnostic: they require only that the Hamiltonian and the baseline be given in reciprocal units, so that \(H L\) is dimensionless.
oscprob2nu
Compute the two-neutrino flavor-transition probabilities.
This module contains the routines needed to compute two-neutrino flavor-transition probabilities for an arbitrary time-independent \(2\times2\) Hermitian Hamiltonian, using the SU(2) exponential expansion described in [1].
The Hamiltonian is expanded in the basis of Pauli matrices,
and the time-evolution operator in the same basis,
The term \(h_0\) contributes only an overall phase and is dropped; all routines therefore work with the traceless part of the Hamiltonian, which leaves the oscillation probabilities unchanged.
evolution_operator_2nu and probabilities_2nu accept either a single Hamiltonian and baseline or a stack of them, in which case the whole stack is evaluated at once.
Units
The routines are unit-agnostic: they require only that the Hamiltonian
and the baseline be given in reciprocal units, so that the product
\(H L\) is dimensionless. Elsewhere in NuOscProbExact the
Hamiltonian is in eV and the baseline in eV-1; the module
globaldefs provides CONV_KM_TO_INV_EV to convert a baseline
in km into eV-1.
Routine listings
hamiltonian_2nu_coefficients - Returns the \(h_k\)
modulus - Returns the modulus \(|h|\) of a vector
evolution_operator_2nu_u_coefficients - Returns the \(u_k\)
evolution_operator_2nu - Returns the evolution operator \(U_2\)
probabilities_2nu - Returns the oscillation probabilities
References
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
- oscprob2nu.CHECK_HERMITICITY = True
bool: Module-level switch.
Whether to verify that the Hamiltonian handed in is Hermitian before evaluating anything. It is on by default, and the reason is that the failure it catches is silent: a non-Hermitian matrix does not raise, and does not produce obviously broken output either — the probabilities it returns still sum to one, so the usual sanity check a caller would apply cannot tell that anything went wrong. The expansion assumes Hermiticity; without it the result is meaningless rather than merely inaccurate.
Checking is not free, and the cost is stated here rather than buried, because it is larger than one might expect. Validating a stack is a pass over it, which is the same order of work as evaluating it — and the compiled kernel has made evaluating it fast, so on a large stack the check dominates. Measured by interleaving the two settings and taking the best of fifteen rounds each:
Stack
2 flavors
4 flavors
2 000 points
1.5x
1.3x
200 000 points
5.7x
3.2x
Three flavors sits between them, at 1.8x and 3.9x. So a scan that the compiled backend was installed to speed up can spend most of its time here instead. On a single matrix the check costs about 1.35x, and it takes its own branch to manage that: the reductions above are all fixed cost at one element, so without the branch one probability spent nine tenths of its time here. For production scans whose Hamiltonians come from a construction already known to be Hermitian — everything
hamiltonians2nubuilds is Hermitian to round-off, as the table below records — set this toFalse.The default is nevertheless
True, because the alternative is a library that silently returns meaningless numbers to anyone who makes this mistake once, and finding out costs far more than the check does.The tolerance is relative to the largest entry of the Hamiltonian, so a matrix assembled in floating point passes: the ones this library builds are Hermitian to about \(2 \times 10^{-17}\) relative, against a tolerance of \(10^{-12}\).
Added in version 1.11.0.
- oscprob2nu.SMALL_BATCH = 11
int: Module-level constant.
Stacks with at most this many elements are evaluated one at a time through the scalar path, whose fixed cost is lower than the array machinery’s. The measured crossover is twelve elements, and it governs every two-flavor stack below fastkernels.MIN_BATCH, which is fifty thousand here — so unlike
oscprob3nu.SMALL_BATCHthis one is consulted whether or not the compiled backend is installed.The threshold was 6, measured before CHECK_HERMITICITY existed and made a scalar call several times dearer; with the check given a path for a single matrix it was re-measured. It sits close to the three-flavor crossover despite that expansion doing much more work per element, because what is being amortised is the array machinery’s fixed cost rather than the arithmetic.
- oscprob2nu.hamiltonian_2nu_coefficients(hamiltonian_matrix: list | ndarray) List[float][source]
Returns the \(h_k\) of the SU(2) expansion of the Hamiltonian.
Computes the coefficients \(h_1, h_2, h_3\) of the SU(2) expansion \(H = h_0 \mathbb{1} + h_k \sigma^k\) of the two-flavor Hamiltonian hamiltonian_matrix, which is assumed to be given in the flavor basis. The coefficient \(h_0\) contributes only an overall phase to the evolution operator and is not returned.
Added in version 1.0.0.
Changed in version 1.1.0: Returns real floats. The coefficients of a Hermitian Hamiltonian are real by construction, but the routine previously returned a mixture of floats and complex numbers.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.- Parameters:
- hamiltonian_matrixarray_like
Two-flavor Hamiltonian, given as the nested list
[[H11, H12], [H21, H22]]. It must be Hermitian, i.e.H21 == conj(H12)andH11,H22real.
- Returns:
- list of float
The three coefficients
[h1, h2, h3]. They are real, because the Hamiltonian is Hermitian.
See also
modulusReturns the modulus \(|h|\) of the returned vector.
Examples
import oscprob2nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j], [0.0-2.0j, 3.0+0.0j]] h_coeffs = oscprob2nu.hamiltonian_2nu_coefficients(hamiltonian_matrix) print('%.6f %.6f %.6f' % tuple(h_coeffs))
0.000000 -2.000000 -1.000000
- oscprob2nu.modulus(h_coeffs: list | ndarray) float[source]
Returns the modulus \(|h|\) of the vector of coefficients.
Returns the modulus of the vector of coefficients \(h_k\) of the SU(2) expansion of the two-neutrino Hamiltonian, \(|h| = \sqrt{|h_1|^2 + |h_2|^2 + |h_3|^2}\).
Added in version 1.0.0.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The square root is taken withmathrather than NumPy.- Parameters:
- h_coeffsarray_like
Three-component vector of coefficients \(h_k\), as returned by hamiltonian_2nu_coefficients.
- Returns:
- float
The modulus \(|h|\).
Examples
import oscprob2nu print('%.6f' % oscprob2nu.modulus([0.0, -2.0, -1.0]))
2.236068
- oscprob2nu.evolution_operator_2nu_u_coefficients(hamiltonian_matrix: list | ndarray, L: int | float) List[float][source]
Returns the coefficients \(u_0, \ldots, u_3\).
Returns the four coefficients \(u_0, \ldots, u_3\) of the two-neutrino time-evolution operator \(U_2(L)\) in its SU(2) exponential expansion, \(U_2 = u_0 \mathbb{1} + i u_k \sigma^k\).
Added in version 1.0.0.
Changed in version 1.1.0: Degenerate Hamiltonians are handled exactly instead of returning NaN, by taking the limit \(\sin(|h|L)/|h| \to L\).
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.- Parameters:
- hamiltonian_matrixarray_like
Two-flavor Hermitian Hamiltonian, given as the nested list
[[H11, H12], [H21, H22]].- Lfloat
Baseline, in units reciprocal to those of the Hamiltonian.
- Returns:
- list of float
The four coefficients
[u0, u1, u2, u3]. They are real, because the Hamiltonian is Hermitian; the factor \(i\) that multiplies \(u_k\) is part of the expansion, not of the coefficients.
Notes
When \(|h| = 0\) the Hamiltonian is proportional to the identity, there is no flavor evolution, and the limit \(\sin(|h| L)/|h| \to L\) is used.
Examples
import oscprob2nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j], [0.0-2.0j, 3.0+0.0j]] u_coeffs = oscprob2nu.evolution_operator_2nu_u_coefficients(hamiltonian_matrix, 1.0) print('%.6f %.6f %.6f %.6f' % tuple(u+0.0 for u in u_coeffs))
-0.617273 0.000000 0.703690 0.351845
- oscprob2nu.evolution_operator_2nu(hamiltonian_matrix: list | ndarray, L: int | float | list | ndarray) List[List[complex]] | ndarray[source]
Returns the two-neutrino time-evolution operator.
Returns the two-neutrino time-evolution operator \(U_2(L)\) in its SU(2) exponential expansion \(U_2(L) = u_0 \mathbb{1} + i u_k \sigma^k\). This is a \(2\times2\) unitary matrix.
Added in version 1.0.0.
Changed in version 1.1.0: Degenerate Hamiltonians are handled exactly instead of returning NaN, by taking the limit \(\sin(|h|L)/|h| \to L\).
Changed in version 1.2.0: Accepts a stack of Hamiltonians of shape
(..., n, n), an array of baselines, or both broadcast against each other, returning an array with the broadcast leading axes. A single Hamiltonian with a scalar baseline returns exactly what it returned before.Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.Changed in version 1.5.0: Faster, with identical results; the probabilities agree with 1.4.0 to 1.6e-13 across every code path. The batched coefficients are laid out with the component index first, so that each \(h_k\) is contiguous; every downstream step works one component at a time, and reading those from a strided view cost about a third more.
- Parameters:
- hamiltonian_matrixarray_like
Two-flavor Hermitian Hamiltonian, given as the nested list
[[H11, H12], [H21, H22]], or a stack of them, of shape(..., 2, 2).- Lfloat or array_like
Baseline, in units reciprocal to those of the Hamiltonian, or an array of baselines broadcastable against the leading axes of hamiltonian_matrix.
- Returns:
- list of list of complex or numpy.ndarray
For a single Hamiltonian and baseline, the time-evolution operator \(U_2(L)\) — a \(2\times2\) unitary complex matrix — as a nested list. If either argument is a stack, an array of shape
(..., 2, 2).
See also
probabilities_2nuReturns the probabilities directly, more cheaply.
Examples
import oscprob2nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j], [0.0-2.0j, 3.0+0.0j]] U2 = oscprob2nu.evolution_operator_2nu(hamiltonian_matrix, 1.0) for row in U2: print(' '.join(['%+.6f%+.6fj' % (z.real+0.0, z.imag+0.0) for z in row]))
-0.617273+0.351845j +0.703690+0.000000j -0.703690+0.000000j -0.617273-0.351845j
- oscprob2nu.probabilities_2nu(hamiltonian_matrix: list | ndarray, L: int | float | list | ndarray) Tuple[float, float, float, float] | ndarray[source]
Returns the two-neutrino oscillation probabilities.
Returns the two-neutrino flavor-transition probabilities \(P_{ee}, P_{e\mu}, P_{\mu e}, P_{\mu\mu}\), where \(P_{\alpha\beta} \equiv P(\nu_\alpha \to \nu_\beta)\).
Added in version 1.0.0.
Changed in version 1.1.0: The \(h_2\) contribution was restored. The transition probability is \(|U_{\mu e}|^2 = u_1^2 + u_2^2\), but the routine computed only \(|h_1|^2/|h|^2 \sin^2(|h|L)\). Since \(h_2 = -\mathrm{Im}(H_{12})\), this affected every Hamiltonian with a complex off-diagonal entry; oscillations in vacuum and in matter of constant density were unaffected.
Changed in version 1.2.0: Accepts a stack of Hamiltonians of shape
(..., n, n), an array of baselines, or both broadcast against each other, returning an array with the broadcast leading axes. A single Hamiltonian with a scalar baseline returns exactly what it returned before.Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule. A scalar two-flavor probability is 3.3x quicker, measured best-of-seven interleaved against 1.3.0.Changed in version 1.5.0: Faster, with identical results; the probabilities agree with 1.4.0 to 1.6e-13 across every code path. The routine no longer builds the evolution operator, squares it, then transposes and reshapes the result. The coefficients of a Hermitian 2x2 Hamiltonian are real, so \(|U_{ee}|^2 = u_0^2 + u_3^2\) and \(|U_{\mu e}|^2 = u_1^2 + u_2^2\) are two numbers, the second the complement of the first by unitarity: neither the operator nor the coefficients are needed. The batched coefficients are laid out with the component index first, so that each \(h_k\) is contiguous; every downstream step works one component at a time, and reading those from a strided view cost about a third more. The scalar path is 3.3x quicker and a 2000-point scan 4.6x.
Changed in version 1.6.0: Two dispatch decisions, neither changing the result. A stack of at most
SMALL_BATCHelements is evaluated one at a time through the scalar path, because a batched call carries a fixed cost that a handful of points does not amortise. Abovefastkernels.MIN_BATCH[2]elements, and only if the optional numba extra is installed, the stack is evaluated by a compiled kernel instead; seefastkernelsfor the measured thresholds and why the two-flavor one is high.- Parameters:
- hamiltonian_matrixarray_like
Two-flavor Hermitian Hamiltonian, given as the nested list
[[H11, H12], [H21, H22]], or a stack of them, of shape(..., 2, 2).- Lfloat or array_like
Baseline, in units reciprocal to those of the Hamiltonian, or an array of baselines broadcastable against the leading axes of hamiltonian_matrix.
- Returns:
- tuple of float or numpy.ndarray
For a single Hamiltonian and baseline, the probabilities
(Pee, Pem, Pme, Pmm)as a tuple. If either argument is a stack, an array of shape(..., 4)in the same order.
Notes
Passing arrays evaluates the whole stack at once, which on a 2000-point scan is about ninety times faster than calling this routine in a Python loop — the figure measured in
README.mdand guarded bytests/test_documented_figures.py; see the notes onoscprob3nu.probabilities_3nu()for the two scans that broadcast naturally.The transition probability is
\[P_{e\mu} = \frac{|h_1|^2 + |h_2|^2}{|h|^2} \sin^2(|h| L) ,\]i.e. \(|U_{\mu e}|^2 = u_1^2 + u_2^2\). Both \(h_1\) and \(h_2\) contribute; \(h_2\) vanishes only when the off-diagonal entry of the Hamiltonian is real.
Examples
import oscprob2nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j], [0.0-2.0j, 3.0+0.0j]] Pee, Pem, Pme, Pmm = oscprob2nu.probabilities_2nu(hamiltonian_matrix, 1.0) print('%.6f %.6f %.6f %.6f' % (Pee, Pem, Pme, Pmm))
0.504821 0.495179 0.495179 0.504821
oscprob3nu
Compute the three-neutrino flavor-transition probabilities.
This module contains the routines needed to compute three-neutrino flavor-transition probabilities for an arbitrary time-independent \(3\times3\) Hermitian Hamiltonian, using the SU(3) exponential expansion described in [2].
The Hamiltonian is expanded in the basis of Gell-Mann matrices, whose structure constants and \(d\) tensor follow the conventions of [1],
and the time-evolution operator in the same basis,
The term \(h_0\) contributes only an overall phase and is dropped; all routines therefore work with the traceless part of the Hamiltonian, which leaves the oscillation probabilities unchanged.
evolution_operator_3nu and probabilities_3nu accept either a single Hamiltonian and baseline or a stack of them, in which case the whole stack is evaluated at once. See the notes on probabilities_3nu.
Units
The routines are unit-agnostic: they require only that the Hamiltonian
and the baseline be given in reciprocal units, so that the product
\(H L\) is dimensionless. Elsewhere in NuOscProbExact the
Hamiltonian is in eV and the baseline in eV-1; the module
globaldefs provides CONV_KM_TO_INV_EV to convert a baseline
in km into eV-1.
Routine listings
hamiltonian_3nu_coefficients - Returns the \(h_k\)
tensor_d - Returns the SU(3) tensor \(d_{ijk}\)
star - Returns the SU(3) star product \((h \star h)_i\)
su3_invariants - Returns the SU(3) invariants \(|h|^2, \langle h \rangle\)
psi_roots - Returns the roots of the characteristic equation
evolution_operator_3nu_u_coefficients - Returns the \(u_k\)
evolution_operator_3nu - Returns the evolution operator \(U_3\)
probabilities_3nu - Returns the oscillation probabilities
References
A.J. MacFarlane, A. Sudbery, and P.H. Weisz, “On Gell-Mann’s \(\lambda\)-matrices, \(d\)- and \(f\)-tensors, octets, and parametrizations of SU(3)”, Commun. Math. Phys. 11, 77 (1968).
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
- oscprob3nu.CHECK_HERMITICITY = True
bool: Module-level switch.
Whether to verify that the Hamiltonian handed in is Hermitian before evaluating anything. It is on by default, and the reason is that the failure it catches is silent: a non-Hermitian matrix does not raise, and does not produce obviously broken output either — the probabilities it returns still sum to one, so the usual sanity check a caller would apply cannot tell that anything went wrong. The expansion assumes Hermiticity; without it the result is meaningless rather than merely inaccurate.
Checking is not free, and the cost is stated here rather than buried, because it is larger than one might expect. Validating a stack is a pass over it, which is the same order of work as evaluating it — and the compiled kernel has made evaluating it fast, so on a large stack the check dominates. Measured by interleaving the two settings and taking the best of fifteen rounds each:
Stack
2 flavors
4 flavors
2 000 points
1.5x
1.3x
200 000 points
5.7x
3.2x
Three flavors sits between them, at 1.8x and 3.9x. So a scan that the compiled backend was installed to speed up can spend most of its time here instead. On a single matrix the check costs about 1.35x, and it takes its own branch to manage that: the reductions above are all fixed cost at one element, so without the branch one probability spent nine tenths of its time here. For production scans whose Hamiltonians come from a construction already known to be Hermitian — everything
hamiltonians3nubuilds is Hermitian to round-off, as the table below records — set this toFalse.The default is nevertheless
True, because the alternative is a library that silently returns meaningless numbers to anyone who makes this mistake once, and finding out costs far more than the check does.The tolerance is relative to the largest entry of the Hamiltonian, so a matrix assembled in floating point passes: the ones this library builds are Hermitian to about \(2 \times 10^{-17}\) relative, against a tolerance of \(10^{-12}\).
Added in version 1.11.0.
- oscprob3nu.SMALL_BATCH = 12
int: Module-level constant.
Stacks with at most this many elements are evaluated one at a time through the scalar path. A batched call carries a fixed cost of a couple of hundred microseconds — allocating and reducing a dozen small arrays — which for a short stack exceeds what the scalar path spends on the whole job. Measured crossover: thirteen elements.
This governs the NumPy path only. With the compiled backend installed
fastkernels.worthwhile()sends every three- and four-flavor stack to the kernel before this is consulted, because fastkernels.MIN_BATCH is one there.The threshold was 10, measured before CHECK_HERMITICITY existed. The check then made a scalar call nine times dearer, which moved the crossover below one — batching won at every size, and this constant was sending stacks the slow way round. With the check given a path for a single matrix the scalar route is cheap again, and the crossover re-measured at thirteen. The two-flavor expansion does less work per element; see
oscprob2nu.SMALL_BATCH.
- oscprob3nu.hamiltonian_3nu_coefficients(hamiltonian_matrix: list | ndarray) List[float][source]
Returns the \(h_k\) of the SU(3) expansion of the Hamiltonian.
Computes the coefficients \(h_1, \ldots, h_8\) of the SU(3) expansion \(H = h_0 \mathbb{1} + h_k \lambda^k\) of the three-flavor Hamiltonian hamiltonian_matrix, which is assumed to be given in the flavor basis. The coefficient \(h_0\) contributes only an overall phase to the evolution operator and is not returned.
Added in version 1.0.0.
Changed in version 1.1.0: Returns real floats. The coefficients of a Hermitian Hamiltonian are real by construction, but the routine previously returned a mixture of floats and complex numbers.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.- Parameters:
- hamiltonian_matrixarray_like
Three-flavor Hamiltonian, given as the nested list
[[H11, H12, H13], [H21, H22, H23], [H31, H32, H33]]. It must be Hermitian.
- Returns:
- list of float
The eight coefficients
[h1, h2, h3, h4, h5, h6, h7, h8]. They are real, because the Hamiltonian is Hermitian.
Examples
import oscprob3nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j, 0.0-1.0j], [0.0-2.0j, 3.0+0.0j, 3.0+0.0j], [0.0+1.0j, 3.0-0.0j, -5.0+0.0j]] h_coeffs = oscprob3nu.hamiltonian_3nu_coefficients(hamiltonian_matrix) print(' '.join(['%.6f' % (h+0.0) for h in h_coeffs]))
0.000000 -2.000000 -1.000000 0.000000 1.000000 3.000000 0.000000 4.041452
- oscprob3nu.tensor_d(i: int, j: int, k: int) float[source]
Returns the tensor \(d_{ijk}\) of the SU(3) algebra.
Returns the totally symmetric SU(3) tensor \(d_{ijk} = \frac{1}{4}\mathrm{Tr} (\{\lambda_i, \lambda_j\} \lambda_k)\), defined in [1].
Added in version 1.0.0.
Changed in version 1.1.0: Validates its indices and raises
IndexErroron one outside 0-7. The dispatch previously fell off the end of anelifchain and returnedNone, so the failure surfaced far from its cause.- Parameters:
- iint
First index, in the range 0–7 (i.e., \(d_{ijk}\) is indexed from zero, so that
i = 0corresponds to \(d_{1jk}\)).- jint
Second index, in the range 0–7.
- kint
Third index, in the range 0–7.
- Returns:
- float
The value of \(d_{ijk}\).
- Raises:
- IndexError
If any index lies outside the range 0–7.
References
[1]A.J. MacFarlane, A. Sudbery, and P.H. Weisz, “On Gell-Mann’s \(\lambda\)-matrices, \(d\)- and \(f\)-tensors, octets, and parametrizations of SU(3)”, Commun. Math. Phys. 11, 77 (1968).
Examples
import oscprob3nu print('%.6f' % oscprob3nu.tensor_d(0, 0, 7)) print('%.6f' % oscprob3nu.tensor_d(0, 1, 2))
0.577350 0.000000
- oscprob3nu.star(i: int, h_coeffs: list | ndarray) float[source]
Returns the SU(3) star product \((h \star h)_i\).
Returns the SU(3) star product \((h \star h)_i = d_{ijk} h^j h^k\), summed over repeated indices.
Added in version 1.0.0.
- Parameters:
- iint
Index of the star product, in the range 0–7.
- h_coeffsarray_like
Eight-component vector of coefficients \(h_k\), as returned by hamiltonian_3nu_coefficients.
- Returns:
- float
The star product \((h \star h)_i\).
Examples
import oscprob3nu print('%.6f' % oscprob3nu.star(0, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])) print('%.6f' % oscprob3nu.star(7, [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]))
0.000000 0.577350
- oscprob3nu.su3_invariants(h_coeffs: list | ndarray) Tuple[float, float][source]
Returns the two SU(3) invariants, \(|h|^2\) and \(\langle h \rangle\).
Returns the two invariants of the SU(3) expansion, \(|h|^2 = h_i h_i\) and \(\langle h \rangle = d_{ijk} h_i h_j h_k\). They equal, respectively, \(\mathrm{Tr}(H_0^2)/2\) and \(\mathrm{Tr}(H_0^3)/2\), with \(H_0\) the traceless part of the Hamiltonian.
Added in version 1.0.0.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The star product is computed once and passed on, rather than once to form \(\langle h \rangle\) and again inside the expansion, and the sparse expansion of \(d_{ijk} h_j h_k\) replaces the dense table for the eight scalar components.- Parameters:
- h_coeffsarray_like
Eight-component vector of coefficients \(h_k\), as returned by hamiltonian_3nu_coefficients.
- Returns:
- h2float
The SU(3) invariant \(|h|^2\).
- h3float
The SU(3) invariant \(\langle h \rangle\).
Examples
import oscprob3nu h2, h3 = oscprob3nu.su3_invariants([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) print('%.6f %.6f' % (h2, h3))
1.000000 0.000000
- oscprob3nu.psi_roots(h2: int | float, h3: int | float) List[float][source]
Returns the three latent roots \(\psi\).
Returns the three latent roots \(\psi\) of the characteristic equation \(\psi^3 - |h|^2 \psi - \frac{2}{3}\langle h \rangle = 0\), which are the eigenvalues of minus the traceless part of the Hamiltonian. The roots are independent of the baseline.
Added in version 1.0.0.
Changed in version 1.1.0: No longer returns NaN when the traceless part of the Hamiltonian vanishes, and the arc-cosine argument is clipped to \([-1, 1]\) so that round-off cannot make the roots complex and the evolution operator non-unitary.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.Changed in version 1.5.0: Faster, with identical results; the probabilities agree with 1.4.0 to 1.6e-13 across every code path. The prefactor takes \(\sqrt{|h|^2}\) once rather than three times. The arc-cosine argument still forms \(|h|^2\) to the power of -1.5, which is what the code does and what an earlier version of this note wrongly described as a division.
- Parameters:
- h2float
The SU(3) invariant \(|h|^2\).
- h3float
The SU(3) invariant \(\langle h \rangle\).
- Returns:
- list of float
The three roots
[psi1, psi2, psi3]. They are real, because the Hamiltonian is Hermitian.
Notes
For a Hermitian Hamiltonian the argument of the arc cosine lies in \([-1, 1]\); it is clipped to that interval so that round-off cannot produce spurious complex roots, which would spoil the unitarity of the evolution operator. When \(|h|^2 = 0\) the Hamiltonian is proportional to the identity and all three roots vanish.
Examples
import oscprob3nu psi = oscprob3nu.psi_roots(1.0, 0.0) print(' '.join(['%.6f' % (round(p, 9)+0.0) for p in sorted(psi)]))
-1.000000 0.000000 1.000000
- oscprob3nu.evolution_operator_3nu_u_coefficients(hamiltonian_matrix: list | ndarray, L: int | float) List[complex][source]
Returns the coefficients \(u_0, \ldots, u_8\).
Returns the nine coefficients \(u_0, \ldots, u_8\) of the three-neutrino time-evolution operator \(U_3(L)\) in its SU(3) exponential expansion, \(U_3 = u_0 \mathbb{1} + i u_k \lambda^k\).
Added in version 1.0.0.
Changed in version 1.1.0: Degenerate Hamiltonians are handled exactly instead of returning NaN. Lagrange interpolation over the latent roots divides by \(3\psi_m^2 - |h|^2\), which vanishes at a repeated root; the two degenerate cases are now taken in their confluent limit.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The sum over the latent roots forms its \(k\)-independent factors once instead of inside the loop over the eight \(k\), and the star product is computed once and passed in rather than recomputed.Changed in version 1.5.0: Faster, with identical results; the probabilities agree with 1.4.0 to 1.6e-13 across every code path. Around the latent roots, \(\sqrt{|h|^2}\) is taken once rather than three times, the arc-cosine argument is a division rather than a power of -1.5, and the degeneracy test uses two minima instead of stacking three gap arrays. The scalar exponentials use
cmath.rect().- Parameters:
- hamiltonian_matrixarray_like
Three-flavor Hermitian Hamiltonian, given as the nested list
[[H11, H12, H13], [H21, H22, H23], [H31, H32, H33]].- Lfloat
Baseline, in units reciprocal to those of the Hamiltonian.
- Returns:
- list of complex
The nine coefficients
[u0, u1, ..., u8].
Notes
The general expression divides by \(3\psi_m^2 - |h|^2\), which vanishes when two latent roots coincide. Two degenerate cases are therefore handled separately, and exactly:
\(|h|^2 = 0\), when the Hamiltonian is proportional to the identity and \(U_3 = \mathbb{1}\);
a doubly degenerate root \(\psi_a = \psi_b \neq \psi_c\), when the spectral decomposition reduces to a single projector and \(U_3 = e^{i\psi_a L}\mathbb{1} + (e^{i\psi_c L} - e^{i\psi_a L}) P_c\), with \(P_c = (h_k\lambda^k + \psi_a \mathbb{1})/(\psi_a - \psi_c)\).
Examples
import oscprob3nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j, 0.0-1.0j], [0.0-2.0j, 3.0+0.0j, 3.0+0.0j], [0.0+1.0j, 3.0-0.0j, -5.0+0.0j]] u_coeffs = oscprob3nu.evolution_operator_3nu_u_coefficients(hamiltonian_matrix, 1.0) print('%+.6f%+.6fj' % (u_coeffs[0].real, u_coeffs[0].imag))
+0.621522-0.047327j
- oscprob3nu.evolution_operator_3nu(hamiltonian_matrix: list | ndarray, L: int | float | list | ndarray) List[List[complex]] | ndarray[source]
Returns the three-neutrino time-evolution operator.
Returns the three-neutrino time-evolution operator \(U_3(L)\) in its SU(3) exponential expansion \(U_3(L) = u_0 \mathbb{1} + i u_k \lambda^k\). This is a \(3\times3\) unitary matrix.
Added in version 1.0.0.
Changed in version 1.1.0: Degenerate Hamiltonians are handled exactly instead of returning NaN. Lagrange interpolation over the latent roots divides by \(3\psi_m^2 - |h|^2\), which vanishes at a repeated root; the two degenerate cases are now taken in their confluent limit.
Changed in version 1.2.0: Accepts a stack of Hamiltonians of shape
(..., n, n), an array of baselines, or both broadcast against each other, returning an array with the broadcast leading axes. A single Hamiltonian with a scalar baseline returns exactly what it returned before.Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The sum over the three latent roots is rewritten to form its \(k\)-independent factors once, removing an(N, 3, 8)intermediate from the batched path.Changed in version 1.5.0: Faster, with identical results; the probabilities agree with 1.4.0 to 1.6e-13 across every code path. The batched star product uses the sparse expansion rather than an
numpy.einsum()contraction of the dense \(d\) tensor. The batched coefficients are laid out with the component index first, so that each \(h_k\) is contiguous; every downstream step works one component at a time, and reading those from a strided view cost about a third more.- Parameters:
- hamiltonian_matrixarray_like
Three-flavor Hermitian Hamiltonian, given as the nested list
[[H11, H12, H13], [H21, H22, H23], [H31, H32, H33]], or a stack of them, of shape(..., 3, 3).- Lfloat or array_like
Baseline, in units reciprocal to those of the Hamiltonian, or an array of baselines broadcastable against the leading axes of hamiltonian_matrix.
- Returns:
- list of list of complex or numpy.ndarray
For a single Hamiltonian and baseline, the time-evolution operator \(U_3(L)\) — a \(3\times3\) unitary complex matrix — as a nested list. If either argument is a stack, an array of shape
(..., 3, 3).
See also
probabilities_3nuReturns the probabilities built from this matrix.
Examples
import oscprob3nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j, 0.0-1.0j], [0.0-2.0j, 3.0+0.0j, 3.0+0.0j], [0.0+1.0j, 3.0-0.0j, -5.0+0.0j]] U3 = oscprob3nu.evolution_operator_3nu(hamiltonian_matrix, 1.0) for row in U3: print(' '.join(['%+.6f%+.6fj' % (z.real+0.0, z.imag+0.0) for z in row]))
+0.546090-0.496423j -0.600964-0.114920j -0.278885+0.056655j +0.600964+0.114920j +0.430462+0.614384j -0.171381+0.183031j +0.278885-0.056655j -0.171381+0.183031j +0.888015-0.259943j
- oscprob3nu.probabilities_3nu(hamiltonian_matrix: list | ndarray, L: int | float | list | ndarray) Tuple[float, ...] | ndarray[source]
Returns the three-neutrino oscillation probabilities.
Returns the three-neutrino flavor-transition probabilities \(P_{ee}, P_{e\mu}, P_{e\tau}, P_{\mu e}, P_{\mu\mu}, P_{\mu\tau}, P_{\tau e}, P_{\tau\mu}, P_{\tau\tau}\), where \(P_{\alpha\beta} \equiv P(\nu_\alpha \to \nu_\beta) = |[U_3]_{\beta\alpha}|^2\).
Added in version 1.0.0.
Changed in version 1.1.0: Degenerate Hamiltonians are handled exactly instead of returning NaN. Lagrange interpolation over the latent roots divides by \(3\psi_m^2 - |h|^2\), which vanishes at a repeated root; the two degenerate cases are now taken in their confluent limit.
Changed in version 1.2.0: Accepts a stack of Hamiltonians of shape
(..., n, n), an array of baselines, or both broadcast against each other, returning an array with the broadcast leading axes. A single Hamiltonian with a scalar baseline returns exactly what it returned before.Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The sum over the three latent roots is rewritten so that the two factors independent of \(k\) are formed once rather than inside the loop over the eight \(k\), which also removes an(N, 3, 8)intermediate from the batched path; the star product is computed once per call rather than twice; and \(|z|^2\) skips the square root that squaring undoes. A scalar three-flavor probability is 3.2x quicker.Changed in version 1.5.0: Faster, with identical results; the probabilities agree with 1.4.0 to 1.6e-13 across every code path. The batched star product no longer contracts the dense 8x8x8 \(d\) tensor through
numpy.einsum(), which with no path plan walked the whole table for every element and was 70% of a 2000-point energy scan; the sparse expansion the scalar path already used vectorises unchanged. The routine also forms the nine entries and squares them directly instead of building and transposing the evolution operator. The batched coefficients are laid out with the component index first, so that each \(h_k\) is contiguous; every downstream step works one component at a time, and reading those from a strided view cost about a third more. The scalar path is 3.2x quicker, a 2000-point energy scan 3.5x, and an oscillogram 1.8x.Changed in version 1.6.0: Two dispatch decisions, neither changing the result. A stack of at most
SMALL_BATCHelements is evaluated one at a time through the scalar path, because a batched call carries a fixed cost that a handful of points does not amortise. Larger stacks are evaluated by a compiled kernel instead, if the optional numba extra is installed; for three flavors that kernel wins at every size. Seefastkernels.- Parameters:
- hamiltonian_matrixarray_like
Three-flavor Hermitian Hamiltonian, given as the nested list
[[H11, H12, H13], [H21, H22, H23], [H31, H32, H33]], or a stack of them, of shape(..., 3, 3).- Lfloat or array_like
Baseline, in units reciprocal to those of the Hamiltonian, or an array of baselines broadcastable against the leading axes of hamiltonian_matrix.
- Returns:
- tuple of float or numpy.ndarray
For a single Hamiltonian and baseline, the nine probabilities
(Pee, Pem, Pet, Pme, Pmm, Pmt, Pte, Ptm, Ptt)as a tuple, ordered with the initial flavor varying slowest. If either argument is a stack, an array of shape(..., 9)in the same order, with the leading axes given by broadcasting the two arguments together.
Notes
Passing arrays evaluates the whole stack at once, which is between one and two orders of magnitude faster than calling this routine in a Python loop. The two common scans both broadcast naturally:
versus baseline, with one Hamiltonian and an array of baselines. The characteristic equation depends only on the Hamiltonian, so it is solved once for the whole scan;
versus energy, with an array of Hamiltonians — one per energy, since \(H \propto 1/E\) — and a single baseline.
An oscillogram is the outer combination of the two, obtained by giving the Hamiltonians and the baselines separate axes, e.g.
probabilities_3nu(H[:, None, :, :], L[None, :]), which returns an array of shape(len(H), len(L), 9).Examples
import oscprob3nu hamiltonian_matrix = [[1.0+0.0j, 0.0+2.0j, 0.0-1.0j], [0.0-2.0j, 3.0+0.0j, 3.0+0.0j], [0.0+1.0j, 3.0-0.0j, -5.0+0.0j]] prob = oscprob3nu.probabilities_3nu(hamiltonian_matrix, 1.0) print(' '.join(['%.6f' % p for p in prob[0:3]])) print(' '.join(['%.6f' % p for p in prob[3:6]])) print(' '.join(['%.6f' % p for p in prob[6:9]]))
0.544650 0.374364 0.080986 0.374364 0.562764 0.062872 0.080986 0.062872 0.856142
oscprob4nu
The \(n = 4\) member, and the last one that exists in closed form; see Why the method stops at four. Its accuracy on stiff 3+1 spectra deserves a look before use: Stiff spectra, and what they cost.
Compute the four-neutrino flavor-transition probabilities.
This module contains the routines needed to compute four-neutrino
flavor-transition probabilities for an arbitrary time-independent
\(4\times4\) Hermitian Hamiltonian, using the SU(4) exponential
expansion. It is the \(n = 4\) member of the family whose
\(n = 2\) and \(n = 3\) members are oscprob2nu and
oscprob3nu, following the method of [1]. It is the last one: see
Why the method stops at four in the methodology page. The Cayley-Hamilton
route at \(n = 4\) in constant matter was set out in [2].
The Hamiltonian is expanded in the basis of the fifteen generalized Gell-Mann matrices,
and the time-evolution operator in the same basis,
The term \(h_0\) contributes only an overall phase and is dropped; all routines therefore work with the traceless part of the Hamiltonian, which leaves the oscillation probabilities unchanged.
What is new at four flavors
Three ingredients have no counterpart in oscprob3nu:
A third invariant. SU(4) has rank three, so the traceless part carries three independent invariants rather than two,
\[I_2 = \tfrac12 \mathrm{Tr}\,\tilde{H}^2 , \qquad I_3 = \tfrac12 \mathrm{Tr}\,\tilde{H}^3 , \qquad I_4 = \tfrac12 \left(\mathrm{Tr}\,\tilde{H}^4 - I_2^2\right) ,\]and the cubic characteristic equation of the three-flavor case becomes the quartic
\[\psi^4 - I_2 \psi^2 - \tfrac23 I_3 \psi + \tfrac14 \left(I_2^2 - 2 I_4\right) = 0 .\]A quartic that still solves in closed form. Euler’s method reduces it to a resolvent cubic whose three roots are real and non-negative because \(\tilde{H}\) is Hermitian, so the same trigonometric formula that
oscprob3nu.psi_roots()uses solves it. The SU(3) machinery is literally nested inside the SU(4) solution.A longer star-product tower. The three-flavor identity \((h \star h) \star h = \tfrac13 |h|^2 h\) is a Cayley-Hamilton accident of \(n = 3\) and is false for SU(4) — it is off by some tens of per cent on a random Hamiltonian — so \(((h \star h) \star h)_a\) enters as independent data.
Accuracy
For a generic Hamiltonian the expansion is exact to round-off, like its two- and three-flavor counterparts. A stiff spectrum is the case that needs care, and the physically interesting 3+1 scenario is stiff: with \(\Delta m^2_{41} \sim 1\) eV2 the eigenvalues span four orders of magnitude, three of them clustering, and the information needed to separate the cluster is destroyed when \(I_2, I_3, I_4\) are formed in double precision. No amount of care in solving the quartic recovers it.
The roots are therefore refined against the matrix, by one Newton step on \(\chi(\psi) = \det(\psi \mathbb{1} - \tilde{H})\), which uses the Hamiltonian entries rather than the three compressed invariants. That restores the roots to round-off and the probabilities from about \(5 \times 10^{-7}\) to \(10^{-9}\).
Neither figure is anywhere near a measurable effect — probabilities
are confronted with data at the per-cent level — so this matters for
the exactness claim, for error accumulating when slabs and
earth compose operators across layers, and for a regression suite
tight enough to catch a real mistake. POLISH_ROOTS has the
measured comparison against the alternatives, including why LAPACK and
extended precision both lose.
evolution_operator_4nu and probabilities_4nu accept either a single Hamiltonian and baseline or a stack of them, in which case the whole stack is evaluated at once, exactly as at two and three flavors.
Units
The routines are unit-agnostic: they require only that the Hamiltonian
and the baseline be given in reciprocal units, so that the product
\(H L\) is dimensionless. Elsewhere in NuOscProbExact the
Hamiltonian is in eV and the baseline in eV-1; the module
globaldefs provides CONV_KM_TO_INV_EV to convert a baseline
in km into eV-1.
Routine listings
generators_su4 - Returns the fifteen generalized Gell-Mann matrices
hamiltonian_4nu_coefficients - Returns the \(h_a\)
su4_invariants - Returns the invariants \(I_2, I_3, I_4\)
psi_roots_4nu - Returns the roots of the characteristic equation
evolution_operator_4nu_u_coefficients - Returns the \(u_a\)
evolution_operator_4nu - Returns the evolution operator \(U_4\)
probabilities_4nu - Returns the oscillation probabilities
References
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
S. Kamo et al., “Matter enhanced transitions of active and sterile neutrinos”, Eur. Phys. J. C 28, 211 (2003), which applies the Cayley-Hamilton approach at \(n = 4\) in constant matter.
- oscprob4nu.CHECK_HERMITICITY = True
bool: Module-level switch.
Whether to verify that the Hamiltonian handed in is Hermitian before evaluating anything. It is on by default, and the reason is that the failure it catches is silent: a non-Hermitian matrix does not raise, and does not produce obviously broken output either — the probabilities it returns still sum to one, so the usual sanity check a caller would apply cannot tell that anything went wrong. The expansion assumes Hermiticity; without it the result is meaningless rather than merely inaccurate.
Checking is not free, and the cost is stated here rather than buried, because it is larger than one might expect. Validating a stack is a pass over it, which is the same order of work as evaluating it — and the compiled kernel has made evaluating it fast, so on a large stack the check dominates. Measured by interleaving the two settings and taking the best of fifteen rounds each:
Stack
2 flavors
4 flavors
2 000 points
1.5x
1.3x
200 000 points
5.7x
3.2x
Three flavors sits between them, at 1.8x and 3.9x. So a scan that the compiled backend was installed to speed up can spend most of its time here instead. On a single matrix the check costs about 1.35x, and it takes its own branch to manage that: the reductions above are all fixed cost at one element, so without the branch one probability spent nine tenths of its time here. For production scans whose Hamiltonians come from a construction already known to be Hermitian — everything
hamiltonians4nubuilds is Hermitian to round-off, as the table below records — set this toFalse.The default is nevertheless
True, because the alternative is a library that silently returns meaningless numbers to anyone who makes this mistake once, and finding out costs far more than the check does.The tolerance is relative to the largest entry of the Hamiltonian, so a matrix assembled in floating point passes: the ones this library builds are Hermitian to about \(2 \times 10^{-17}\) relative, against a tolerance of \(10^{-12}\).
Added in version 1.11.0.
- oscprob4nu.LAMBDA_SU4 = array([[[ 0. +0.j, 1. +0.j, 0. +0.j, 0. +0.j], [ 1. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, -0. -1.j, 0. +0.j, 0. +0.j], [ 0. +1.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 1. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 1. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, -0. -1.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +1.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 1. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 1. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, -0. -1.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +1.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 1. +0.j, 0. +0.j], [ 0. +0.j, 1. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, -0. -1.j, 0. +0.j], [ 0. +0.j, 0. +1.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 1. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 1. +0.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, -0. -1.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +1.j, 0. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 1. +0.j], [ 0. +0.j, 0. +0.j, 1. +0.j, 0. +0.j]], [[ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, -0. -1.j], [ 0. +0.j, 0. +0.j, 0. +1.j, 0. +0.j]], [[ 1. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, -1. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0.57735027+0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0.57735027+0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, -1.15470054+0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, 0. +0.j]], [[ 0.40824829+0.j, 0. +0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0.40824829+0.j, 0. +0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0.40824829+0.j, 0. +0.j], [ 0. +0.j, 0. +0.j, 0. +0.j, -1.22474487+0.j]]])
numpy.ndarray: Module-level constant.
The fifteen generalized Gell-Mann matrices, of shape
(15, 4, 4), tabulated once at import time. At three flavors the analogous table is the \(d\) tensor ofoscprob3nu.tensor_d(); here the generators themselves are tabulated, because every quantity this module needs is a trace against them and none needs \(d_{abc}\) explicitly.
- oscprob4nu.POLISH_ROOTS = True
bool: Module-level switch.
Whether to refine the latent roots against the Hamiltonian matrix, by one Newton step on \(\chi(\psi) = \det(\psi \mathbb{1} - \tilde{H})\), after solving the quartic in closed form.
This is on by default and should stay on. The closed-form roots are limited by the conditioning of \(I_2, I_3, I_4\), not by the quartic solver: perturbing the three invariants at the \(10^{-16}\) level moves the roots of a stiff 3+1 spectrum by \(6 \times 10^{-11}\) relative, which is what the unrefined closed form achieves and what no better root-finder can beat. The Newton step reads the matrix entries instead, and is not subject to that floor.
Measured against
mpmathat fifty decimal digits, on stiff 3+1 Hamiltonians, with cost quoted for a 200 000-point scan:Strategy for the latent roots
Rel. error
Cost
Closed form alone
8.3e-11
0.17 s
Closed form + one Newton step
1.1e-16
0.41 s
numpy.linalg.eigvalsh7.4e-16
0.17 s
Closed form in
numpy.longdouble4.5e-11
0.43 s
Note the second row beats the third: the Newton step is some seven times more accurate than LAPACK, because
eigvalshreduces by similarity transforms that each carry a backward error of order \(\epsilon\|H\|\), while this converges onto the root of \(\det(\psi\mathbb{1} - \tilde{H})\) for the matrix it was given. Extended precision was rejected for buying under a digit, being slower, and silently beingfloat64on Apple Silicon and Windows.The refined figure has been confirmed from outside: against nuSQuIDS, which integrates the density matrix numerically, the four-flavor probabilities agree to \(4 \times 10^{-16}\) on a benign spectrum and \(3 \times 10^{-10}\) on the stiffest one tested. See notebook 17.
In probabilities the difference is \(5 \times 10^{-7}\) unrefined against \(10^{-9}\) refined. Both are orders of magnitude below what any experiment resolves; the reasons to want the smaller one are the library’s claim to exactness, error accumulating when
slabsandearthcompose operators across many layers, and a regression suite with no room for a bug to hide in.A second Newton step changes nothing — one already reaches the floor — so exactly one is taken. It costs about 40% of the runtime, which brings the four-flavor closed form to parity with a batched
eighrather than ahead of it.The step is applied unconditionally rather than only where a spectrum looks stiff, and that is a measured decision rather than a lazy one. Two skip criteria were tried on 6300 Hamiltonians: the gap-based amplification that perturbation theory suggests, which misjudges doubly paired spectra by four orders of magnitude, and a matrix residual comparing \(\prod_m \psi_m\) with \(\det \tilde{H}\), which is one constraint on four roots and misses errors that cancel in the product. Neither can safely skip any elements at all. The reason is structural: a criterion complete enough to certify four roots must evaluate \(\chi\) at four roots, which is this refinement — the check and the fix are the same computation. See the methodology page.
Set to
Falseto skip it, which is useful only for reproducing the unrefined figures or for spectra known to be well separated.
- oscprob4nu.SMALL_BATCH = 10
int: Module-level constant.
Nothing reads this. At two and three flavors the constant of the same name selects between a scalar path and a batched one; four flavors has no separate scalar closed form to select, so every stack goes through the array path whatever its length, and there is no short-stack case for a threshold to name. It is exported and documented as being what it is, rather than quietly kept.
It also no longer matches
oscprob3nu.SMALL_BATCH, which it once did and was documented as doing: that one was re-measured when the scalar path was given a cheap Hermiticity check, and this one, governing nothing, was not.
- oscprob4nu.generators_su4() ndarray[source]
Returns the fifteen generalized Gell-Mann matrices of SU(4).
The generators are traceless, Hermitian, and normalized as \(\mathrm{Tr}(\lambda^a \lambda^b) = 2 \delta^{ab}\), which is the convention
oscprob3nuuses at \(n = 3\).They are ordered as six symmetric and six antisymmetric off-diagonal matrices, one pair for each of the six index pairs \((j, k)\) with \(j < k\) in lexicographic order, followed by the three diagonal matrices that span the Cartan subalgebra.
Added in version 1.9.0.
- Returns:
- numpy.ndarray
Complex array of shape
(15, 4, 4), the generators \(\lambda^1, \ldots, \lambda^{15}\).
Examples
import numpy as np import oscprob4nu lam = oscprob4nu.generators_su4() gram = np.einsum('aij,bji->ab', lam, lam).real/2.0 print('shape:', lam.shape) print('orthonormal to %.1e' % np.max(np.abs(gram - np.eye(15))))
shape: (15, 4, 4) orthonormal to 1.1e-16
- oscprob4nu.hamiltonian_4nu_coefficients(hamiltonian_matrix: list | ndarray) List[float][source]
Returns the \(h_a\) of the SU(4) expansion of the Hamiltonian.
Computes the coefficients \(h_1, \ldots, h_{15}\) of the SU(4) expansion \(H = h_0 \mathbb{1} + h_a \lambda^a\) of the four-flavor Hamiltonian hamiltonian_matrix, which is assumed to be given in the flavor basis. The coefficient \(h_0\) contributes only an overall phase to the evolution operator and is not returned.
They follow from the normalization of the generators as \(h_a = \tfrac12 \mathrm{Tr}(H \lambda^a)\), and are real for a Hermitian Hamiltonian.
Added in version 1.9.0.
- Parameters:
- hamiltonian_matrixarray_like
Four-flavor Hermitian Hamiltonian, given as a nested list or an array of shape
(4, 4).
- Returns:
- list of float
The fifteen coefficients \(h_1, \ldots, h_{15}\).
Examples
import numpy as np import oscprob4nu hamiltonian = np.diag([1.0, 2.0, 3.0, -6.0]).astype(complex) h = oscprob4nu.hamiltonian_4nu_coefficients(hamiltonian) print('%d coefficients' % len(h)) print('the three Cartan ones: %.4f, %.4f, %.4f' % tuple(h[12:]))
15 coefficients the three Cartan ones: -0.5000, -0.8660, 4.8990
- oscprob4nu.su4_invariants(hamiltonian_matrix: list | ndarray) Tuple[float, float, float][source]
Returns the three SU(4) invariants of the Hamiltonian.
Returns \(I_2 = |h|^2\), \(I_3 = \langle h \rangle\) and \(I_4 = |h \star h|^2\) of the traceless part \(\tilde{H}\) of hamiltonian_matrix, computed from traces of its powers,
\[I_2 = \tfrac12 \mathrm{Tr}\,\tilde{H}^2 , \qquad I_3 = \tfrac12 \mathrm{Tr}\,\tilde{H}^3 , \qquad I_4 = \tfrac12 \left(\mathrm{Tr}\,\tilde{H}^4 - I_2^2\right) .\]SU(4) has rank three, so there are three of them; SU(3) has rank two, which is why
oscprob3nu.su3_invariants()returns two. Taking them from traces avoids ever building the \(d\) tensor of SU(4), a \(15 \times 15 \times 15\) table that nothing else here needs.Added in version 1.9.0.
- Parameters:
- hamiltonian_matrixarray_like
Four-flavor Hermitian Hamiltonian, given as a nested list or an array of shape
(4, 4).
- Returns:
- tuple of float
The invariants
(I2, I3, I4).
Examples
import numpy as np import oscprob4nu hamiltonian = np.diag([1.0, 2.0, 3.0, -6.0]).astype(complex) i2, i3, i4 = oscprob4nu.su4_invariants(hamiltonian) print('I2 = %.4f' % i2) print('I3 = %.4f' % i3) print('I4 = %.4f' % i4)
I2 = 25.0000 I3 = -90.0000 I4 = 384.5000
- oscprob4nu.psi_roots_4nu(invariant_2: int | float, invariant_3: int | float, invariant_4: int | float) List[float][source]
Returns the roots of the four-flavor characteristic equation.
Returns the four real roots \(\psi_m\) of
\[\psi^4 - I_2 \psi^2 - \tfrac23 I_3 \psi + \tfrac14 \left(I_2^2 - 2 I_4\right) = 0 ,\]the characteristic equation of the traceless part of the Hamiltonian, obtained by Euler’s solution of the quartic: the resolvent cubic
\[z^3 - 2 I_2 z^2 + 2 I_4 z - \tfrac49 I_3^2 = 0\]has roots \(z_i = (\psi_i + \psi_j)^2\), real and non-negative because the Hamiltonian is Hermitian, and then
\[\psi_m = \tfrac12 \left(s_1 \sqrt{z_1} + s_2 \sqrt{z_2} + s_3 \sqrt{z_3}\right) ,\]with signs \(s_i = \pm1\) fixed by \(s_1 s_2 s_3 \sqrt{z_1 z_2 z_3} = \tfrac23 I_3\).
These roots carry only the accuracy that \(I_2, I_3, I_4\) carry. For a stiff spectrum that is not enough, which is why the routines that use them refine them against the matrix; see
POLISH_ROOTS.Added in version 1.9.0.
- Parameters:
- invariant_2int or float
The invariant \(I_2 = |h|^2\).
- invariant_3int or float
The invariant \(I_3 = \langle h \rangle\).
- invariant_4int or float
The invariant \(I_4 = |h \star h|^2\).
- Returns:
- list of float
The four roots, in ascending order.
Examples
import numpy as np import oscprob4nu hamiltonian = np.diag([1.0, 2.0, 3.0, -6.0]).astype(complex) i2, i3, i4 = oscprob4nu.su4_invariants(hamiltonian) psi = oscprob4nu.psi_roots_4nu(i2, i3, i4) print('roots :', np.round(psi, 6)) print('reference:', np.round(np.linalg.eigvalsh(hamiltonian) - 0.0, 6))
roots : [-6. 1. 2. 3.] reference: [-6. 1. 2. 3.]
- oscprob4nu.evolution_operator_4nu_u_coefficients(hamiltonian_matrix: list | ndarray, L: int | float) List[complex][source]
Returns the \(u_a\) of the SU(4) expansion of \(U_4\).
Returns the sixteen coefficients \(u_0, u_1, \ldots, u_{15}\) of the expansion \(U_4(L) = u_0 \mathbb{1} + i u_a \lambda^a\) of the time-evolution operator, in the same convention as
oscprob3nu.evolution_operator_3nu_u_coefficients().The four-flavor analogue of Eqs. (10)-(11) of arXiv:1904.12391 is
\[u_0 = \frac14 \sum_m e^{-i \psi_m L} , \qquad i u_a = \sum_m e^{-i \psi_m L}\, \frac{\left(\psi_m^2 - \tfrac12 I_2\right) h_a + \psi_m (h \star h)_a + ((h \star h) \star h)_a} {\chi'(\psi_m)} ,\]with \(\chi'(\psi_m) = 4\psi_m^3 - 2 I_2 \psi_m - \tfrac23 I_3\). Note the third term in the numerator: at three flavors the tower closes on itself and no such term appears.
Added in version 1.9.0.
Changed in version 1.10.1: Results changed for a spectrum with two nearly coincident latent roots. The Newton refinement of
POLISH_ROOTSdivides by a product of gaps, and was guarded only against a gap of exactly zero; a pair separated by one unit in the last place passed that guard and was thrown across the spectrum. The step is now refused whenever it would carry a root more than halfway to its nearest neighbour. Nothing else moved: ordinary and stiff spectra are bit-for-bit what 1.10.0 gave.- Parameters:
- hamiltonian_matrixarray_like
Four-flavor Hermitian Hamiltonian, given as a nested list or an array of shape
(4, 4).- Lint or float
Baseline, in units reciprocal to those of the Hamiltonian.
- Returns:
- list of complex
The sixteen coefficients
[u0, u1, ..., u15].
Examples
import numpy as np import oscprob4nu hamiltonian = np.diag([1.0, 2.0, 3.0, -6.0]).astype(complex) u = oscprob4nu.evolution_operator_4nu_u_coefficients(hamiltonian, 1.0) print('%d coefficients' % len(u)) print('u0 = %.6f%+.6fj' % (u[0].real, u[0].imag))
16 coefficients u0 = 0.023583-0.542826j
- oscprob4nu.evolution_operator_4nu(hamiltonian_matrix: list | ndarray, L: int | float | list | ndarray) List[List[complex]] | ndarray[source]
Returns the four-neutrino time-evolution operator \(U_4\).
Returns \(U_4(L) = e^{-i \tilde{H} L}\), with \(\tilde{H}\) the traceless part of hamiltonian_matrix. The discarded trace contributes only the overall phase \(e^{-i h_0 L}\), which cancels in the probabilities.
Added in version 1.9.0.
Changed in version 1.10.1: Results changed for a spectrum with two nearly coincident latent roots. The Newton refinement of
POLISH_ROOTSdivides by a product of gaps, and was guarded only against a gap of exactly zero; a pair separated by one unit in the last place passed that guard and was thrown across the spectrum. The step is now refused whenever it would carry a root more than halfway to its nearest neighbour. Nothing else moved: ordinary and stiff spectra are bit-for-bit what 1.10.0 gave.- Parameters:
- hamiltonian_matrixarray_like
Four-flavor Hermitian Hamiltonian, given as a nested list of shape
(4, 4), or a stack of them, of shape(..., 4, 4).- Lint or float or array_like
Baseline, in units reciprocal to those of the Hamiltonian, or an array of baselines broadcastable against the leading axes of hamiltonian_matrix.
- Returns:
- list of list of complex or numpy.ndarray
For a single Hamiltonian and baseline, the \(4\times4\) evolution operator as a nested list. If either argument is a stack, an array of shape
(..., 4, 4).
Examples
import numpy as np import oscprob4nu hamiltonian = np.diag([1.0, 2.0, 3.0, -6.0]).astype(complex) operator = oscprob4nu.evolution_operator_4nu(hamiltonian, 1.0) unitarity = np.asarray(operator).conj().T @ np.asarray(operator) print('|U| unitary to %.1e' % np.max(np.abs(unitarity - np.eye(4))))
|U| unitary to 3.3e-16
- oscprob4nu.probabilities_4nu(hamiltonian_matrix: list | ndarray, L: int | float | list | ndarray) Tuple[float, ...] | ndarray[source]
Returns the four-neutrino oscillation probabilities.
Returns the sixteen flavor-transition probabilities \(P_{\alpha\beta} \equiv P(\nu_\alpha \to \nu_\beta) = |[U_4]_{\beta\alpha}|^2\), ordered with the initial flavor varying slowest, exactly as at two and three flavors. With the fourth state read as sterile, the flavor order is \((\nu_e, \nu_\mu, \nu_\tau, \nu_s)\).
Added in version 1.9.0.
Changed in version 1.10.1: Results changed for a spectrum with two nearly coincident latent roots. The Newton refinement of
POLISH_ROOTSdivides by a product of gaps, and was guarded only against a gap of exactly zero; a pair separated by one unit in the last place passed that guard and was thrown across the spectrum. The step is now refused whenever it would carry a root more than halfway to its nearest neighbour. Nothing else moved: ordinary and stiff spectra are bit-for-bit what 1.10.0 gave.- Parameters:
- hamiltonian_matrixarray_like
Four-flavor Hermitian Hamiltonian, given as a nested list of shape
(4, 4), or a stack of them, of shape(..., 4, 4).- Lint or float or array_like
Baseline, in units reciprocal to those of the Hamiltonian, or an array of baselines broadcastable against the leading axes of hamiltonian_matrix.
- Returns:
- tuple of float or numpy.ndarray
For a single Hamiltonian and baseline, the sixteen probabilities as a tuple, ordered \(P_{ee}, P_{e\mu}, P_{e\tau}, P_{es}, P_{\mu e}, \ldots, P_{ss}\). If either argument is a stack, an array of shape
(..., 16)in the same order.
Notes
Passing arrays evaluates the whole stack at once, which is far faster than calling this routine in a Python loop, exactly as at two and three flavors; see Scanning: pass arrays.
The accuracy deserves a word, because four flavors is where it stops being automatic. Against
numpy.linalg.eigh(), a generic Hamiltonian agrees to about \(2 \times 10^{-13}\). A stiff 3+1 spectrum, with \(\Delta m^2_{41}\) four orders of magnitude above \(\Delta m^2_{21}\), agrees to about \(10^{-9}\) — limited not by this expansion but by what double precision retains when the invariants are formed. SeePOLISH_ROOTS, which is what keeps that figure from being \(5 \times 10^{-7}\).Examples
import numpy as np import oscprob4nu hamiltonian = np.diag([1.0, 2.0, 3.0, -6.0]).astype(complex) prob = oscprob4nu.probabilities_4nu(hamiltonian, 1.0) print('%d probabilities' % len(prob)) print('P_ee = %.6f' % prob[0]) print('they sum to %.6f' % sum(prob[0:4]))
16 probabilities P_ee = 1.000000 they sum to 1.000000
Sample Hamiltonians
These build the Hamiltonian for a number of standard scenarios. They are examples, not a limit on what can be computed: the core routines accept any Hermitian matrix.
Energies are in eV, mass-squared differences in eV2, baselines in eV-1, and potentials in eV. See Sign conventions for the sign the vacuum Hamiltonians adopt, and why it matters once a matter potential is added.
hamiltonians2nu
Compute two-neutrino Hamiltonians for selected scenarios.
This module contains routines that build the two-neutrino Hamiltonian
for a number of standard scenarios — oscillations in vacuum, in
matter of constant density, in matter with non-standard interactions
(NSI), and in a CPT-odd Lorentz invariance-violating (LIV) background
— together with the textbook oscillation formulas for vacuum and
matter, which serve to validate the exact SU(2) computation performed by
oscprob2nu and described in [1].
The Hamiltonians built here are meant to be passed to
oscprob2nu.probabilities_2nu(). They are examples: the exact
computation accepts any Hermitian \(2\times2\) Hamiltonian.
The routines that take a neutrino energy also accept an array of
energies, and then return one Hamiltonian per energy, stacked along a
leading axis. That stack is exactly what oscprob2nu.probabilities_2nu()
expects, so a whole energy scan is two calls rather than a loop.
Units
Throughout this module,
Quantity |
Units |
|---|---|
Mass-squared difference |
eV2 |
Neutrino energy |
eV |
Baseline |
eV-1 |
Matter potential |
eV |
LIV eigenvalues and scale |
eV |
The routine
hamiltonian_2nu_vacuum_energy_independent() returns the
energy-independent part of the vacuum Hamiltonian, i.e. it has units
of eV2 and must still be divided by the neutrino energy. The
module globaldefs provides CONV_KM_TO_INV_EV to convert a
baseline in km into eV-1.
Sign convention
The vacuum Hamiltonian is
i.e. the mass eigenstate with the larger mass-squared value is the second one. This sign matters: it fixes the sign of the matter potential relative to the vacuum term, and hence whether the routines describe neutrinos (as they do) or antineutrinos. An overall sign flip of the vacuum Hamiltonian alone is invisible in vacuum but moves the Mikheyev-Smirnov-Wolfenstein resonance from neutrinos to antineutrinos.
Routine listings
mixing_matrix_2nu - Returns the \(2\times2\) rotation matrix
hamiltonian_2nu_vacuum_energy_independent - Returns \(H_{\rm vac}\) without the \(1/E\)
probabilities_2nu_vacuum_std - Vacuum probabilities, standard formula
hamiltonian_2nu_matter - Returns \(H_{\rm matter}\)
probabilities_2nu_matter_std - Matter probabilities, standard formula
hamiltonian_2nu_nsi - Returns \(H_{\rm NSI}\)
hamiltonian_2nu_liv - Returns \(H_{\rm LIV}\)
References
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
- hamiltonians2nu.mixing_matrix_2nu(sth: int | float) List[List[float]][source]
Returns the \(2\times2\) rotation matrix.
Computes and returns the real \(2\times2\) rotation matrix parametrized by a single rotation angle \(\theta\).
Added in version 1.0.0.
Changed in version 1.1.0: \(\cos\theta\) is taken as \(\sqrt{1 - \sin^2\theta}\) rather than through the angle itself. The matrix is real, and is still returned as a nested list, as it always has been.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.- Parameters:
- sthfloat
\(\sin\theta\), with \(\theta\) in the first quadrant, so that \(\cos\theta = \sqrt{1-\sin^2\theta} \geq 0\).
- Returns:
- list of list of float
The rotation matrix
[[cth, sth], [-sth, cth]], withcth= \(\cos\theta\).
Examples
import hamiltonians2nu R = hamiltonians2nu.mixing_matrix_2nu(0.6) print('%.6f %.6f' % (R[0][0], R[0][1]))
0.800000 0.600000
- hamiltonians2nu.hamiltonian_2nu_vacuum_energy_independent(sth: int | float, Dm2: int | float, compute_matrix_multiplication: bool = False) ndarray[source]
Returns the two-neutrino Hamiltonian for vacuum oscillations.
Computes and returns the energy-independent part of the real \(2\times2\) two-neutrino Hamiltonian for oscillations in vacuum, parametrized by a single mixing angle \(\theta\) and a single mass-squared difference \(\Delta m^2\). The Hamiltonian is \(H = \frac{1}{4} R M^2 R^T\), with \(R\) the rotation matrix and \(M^2 = \mathrm{diag}(-\Delta m^2, \Delta m^2)\) the traceless mass matrix. The multiplicative factor \(1/E\) is not applied.
Added in version 1.0.0.
Changed in version 1.1.0: The sign convention was corrected. The Hamiltonian was built from \(M^2 = \mathrm{diag}(\Delta m^2, -\Delta m^2)\), which yields the negative of the textbook Hamiltonian. In vacuum this is invisible, but it reverses the sign of the matter potential relative to the vacuum term, so results in matter, with NSI, or with LIV were the antineutrino ones. It also returns a complex
numpy.ndarrayrather than a nested list.Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.- Parameters:
- sthfloat
\(\sin\theta\).
- Dm2float
Mass-squared difference \(\Delta m^2\) [eV2].
- compute_matrix_multiplicationbool, optional
If
False(default), use the pre-computed closed-form expressions; ifTrue, carry out the matrix multiplication \(R M^2 R^T\) explicitly. Both give the same result; the option exists as a cross-check.
- Returns:
- numpy.ndarray
The \(2\times2\) Hamiltonian [eV2], to be divided by the neutrino energy before use.
Notes
See the module-level Sign convention section: the mass eigenstate with the larger mass-squared value is the second one, so that adding a positive matter potential to the \(ee\) entry describes neutrinos.
Examples
import hamiltonians2nu H = hamiltonians2nu.hamiltonian_2nu_vacuum_energy_independent(0.5, 1.0) print('%.6f %.6f' % (H[0][0].real, H[0][1].real))
-0.125000 0.216506
- hamiltonians2nu.probabilities_2nu_vacuum_std(sth: int | float, Dm2: int | float, energy: int | float, L: int | float) List[float][source]
Returns the 2nu vacuum probabilities, standard computation.
Returns the probabilities for two-neutrino oscillations in vacuum, computed with the standard analytical expression
\[P_{e\mu} = \sin^2 2\theta \sin^2\left(\frac{\Delta m^2 L}{4E}\right).\]This routine exists to validate the exact SU(2) computation in
oscprob2nu; the two agree to round-off.Added in version 1.0.0.
Changed in version 1.1.0: The signature changed: the energy is now given in eV and the baseline in \(\mathrm{eV}^{-1}\), like the rest of the library, rather than in GeV and km. The rounded constants 1.27 and 2.54 that folded in the old conversion overstated every phase by 0.242%.
- Parameters:
- sthfloat
\(\sin\theta\).
- Dm2float
Mass-squared difference \(\Delta m^2\) [eV2].
- energyfloat
Neutrino energy [eV].
- Lfloat
Baseline [eV-1].
- Returns:
- list of float
The probabilities
[Pee, Pem, Pme, Pmm].
See also
oscprob2nu.probabilities_2nuThe exact SU(2) computation.
Examples
import hamiltonians2nu prob = hamiltonians2nu.probabilities_2nu_vacuum_std(0.5, 2.5e-3, 1.0e9, 5.0e12) print('%.6f %.6f' % (prob[0], prob[1]))
0.999794 0.000206
- hamiltonians2nu.hamiltonian_2nu_matter(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, VCC: int | float | list | ndarray) ndarray[source]
Returns the two-neutrino Hamiltonian for matter oscillations.
Computes and returns the \(2\times2\) two-neutrino Hamiltonian for oscillations in matter of constant density, obtained by adding the charged-current matter potential to the \(ee\) entry of the vacuum Hamiltonian.
Added in version 1.0.0.
Changed in version 1.1.0: Results changed, following the sign-convention correction in hamiltonian_2nu_vacuum_energy_independent: this routine previously returned the antineutrino Hamiltonian when asked for the neutrino one, placing the MSW resonance on the wrong side. Returns a complex
numpy.ndarray.Changed in version 1.3.0: Accepts an array of energies, returning one Hamiltonian per energy stacked along a leading axis; the matter potential may be an array too. A scalar energy still returns a single matrix, and the results are bit-for-bit what the equivalent loop produced.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent part of the two-neutrino vacuum Hamiltonian [eV2], as returned by hamiltonian_2nu_vacuum_energy_independent. It is not modified.
- energyfloat or array_like
Neutrino energy [eV], or an array of energies, in which case one Hamiltonian is returned per energy.
- VCCfloat or array_like
Potential due to charged-current interactions of \(\nu_e\) with electrons [eV]. Positive for neutrinos, negative for antineutrinos. May be an array, to scan across a density profile alongside the energy.
- Returns:
- numpy.ndarray
The \(2\times2\) Hamiltonian [eV], of shape
(2, 2)for a scalar energy and(..., 2, 2)for an array of energies.
Examples
import hamiltonians2nu H_vac = hamiltonians2nu.hamiltonian_2nu_vacuum_energy_independent(0.5, 2.5e-3) H = hamiltonians2nu.hamiltonian_2nu_matter(H_vac, 1.0e9, 1.0e-13) print('%.6e' % H[0][0].real)
-2.125000e-13
- hamiltonians2nu.probabilities_2nu_matter_std(sth: int | float, Dm2: int | float, VCC: int | float, energy: int | float, L: int | float) List[float][source]
Returns the 2nu matter probabilities, standard computation.
Returns the probabilities for two-neutrino oscillations in matter of constant density, computed with the standard analytical expression in terms of the effective mixing angle \(\theta_m\) and effective mass-squared difference \(\Delta m^2_m\),
\[\sin^2 2\theta_m = \frac{\sin^2 2\theta} {\sin^2 2\theta + (\cos 2\theta - x)^2} , \quad x \equiv \frac{2 V_{\rm CC} E}{\Delta m^2} .\]This routine exists to validate the exact SU(2) computation in
oscprob2nu; the two agree to round-off.Added in version 1.0.0.
Changed in version 1.1.0: The signature changed: the energy is now given in eV and the baseline in \(\mathrm{eV}^{-1}\), like the rest of the library, rather than in GeV and km. The rounded constants 1.27 and 2.54 that folded in the old conversion overstated every phase by 0.242%. The sign of \(\cos 2\theta\) is also kept, where it was previously discarded by computing it as \(\sqrt{1 - \sin^2 2\theta}\); for \(\theta > \pi/4\) that put the matter resonance on the wrong side.
- Parameters:
- sthfloat
\(\sin\theta\).
- Dm2float
Mass-squared difference \(\Delta m^2\) [eV2].
- VCCfloat
Potential due to charged-current interactions of \(\nu_e\) with electrons [eV].
- energyfloat
Neutrino energy [eV].
- Lfloat
Baseline [eV-1].
- Returns:
- list of float
The probabilities
[Pee, Pem, Pme, Pmm].
See also
oscprob2nu.probabilities_2nuThe exact SU(2) computation.
Notes
The resonance sits at \(x = \cos 2\theta\), which for \(\theta < \pi/4\) lies at positive energy, i.e. in the neutrino channel. Note that \(\cos 2\theta\) is signed: computing it as \(\sqrt{1 - \sin^2 2\theta}\) would lose the sign and misplace the resonance for \(\theta > \pi/4\).
Examples
import hamiltonians2nu prob = hamiltonians2nu.probabilities_2nu_matter_std(0.5, 2.5e-3, 1.0e-13, 1.0e9, 5.0e12) print('%.6f %.6f' % (prob[0], prob[1]))
0.985595 0.014405
- hamiltonians2nu.hamiltonian_2nu_nsi(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, VCC: int | float | list | ndarray, eps: list | ndarray) ndarray[source]
Returns the two-neutrino Hamiltonian for oscillations with NSI.
Computes and returns the \(2\times2\) two-neutrino Hamiltonian for oscillations with non-standard interactions (NSI) in matter of constant density.
Added in version 1.0.0.
Changed in version 1.1.0: The imaginary part of a complex \(\epsilon_{e\mu}\) is no longer discarded: the vacuum Hamiltonian was real, so the array was
float64and the in-place addition truncated the value. Results also changed with the sign-convention correction described under hamiltonian_2nu_matter.Changed in version 1.3.0: Accepts an array of energies, returning one Hamiltonian per energy stacked along a leading axis; the matter potential may be an array too. A scalar energy still returns a single matrix, and the results are bit-for-bit what the equivalent loop produced.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent part of the two-neutrino vacuum Hamiltonian [eV2], as returned by hamiltonian_2nu_vacuum_energy_independent. It is not modified.
- energyfloat or array_like
Neutrino energy [eV], or an array of energies, in which case one Hamiltonian is returned per energy.
- VCCfloat or array_like
Potential due to charged-current interactions of \(\nu_e\) with electrons [eV]. May be an array, to scan across a density profile alongside the energy.
- epsarray_like
The three NSI strength parameters
[eps_ee, eps_em, eps_mm], adimensional. The diagonal parameterseps_eeandeps_mmare real; the off-diagonaleps_emmay be complex, and its complex conjugate is placed in the lower off-diagonal entry so that the Hamiltonian stays Hermitian.
- Returns:
- numpy.ndarray
The \(2\times2\) complex Hamiltonian [eV], of shape
(2, 2)for a scalar energy and(..., 2, 2)for an array of energies.
Examples
import hamiltonians2nu H_vac = hamiltonians2nu.hamiltonian_2nu_vacuum_energy_independent(0.5, 2.5e-3) H = hamiltonians2nu.hamiltonian_2nu_nsi(H_vac, 1.0e9, 1.0e-13, [0.06, -0.06+0.03j, 1.2]) print('%+.6e%+.6ej' % (H[0][1].real, H[0][1].imag))
+5.352659e-13+3.000000e-15j
- hamiltonians2nu.hamiltonian_2nu_liv(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, sxi: int | float, b1: int | float, b2: int | float, Lambda: int | float) ndarray[source]
Returns the two-neutrino Hamiltonian for oscillations with LIV.
Computes and returns the \(2\times2\) two-neutrino Hamiltonian for oscillations in a CPT-odd Lorentz invariance-violating (LIV) background. The LIV term is \((E/\Lambda) R B_2 R^T\), with \(B_2 = \mathrm{diag}(b_1, b_2)\) and \(R\) the rotation by the angle \(\xi\) between the eigenvectors of \(B_2\) and the flavor states.
Added in version 1.0.0.
Changed in version 1.1.0: \(\cos\xi\) was computed as
sqrt(1 - sxi - sxi)rather thansqrt(1 - sxi*sxi). For \(0 < \sin\xi < 1/2\) the LIV term was not a rotation at all, and for \(\sin\xi \geq 1/2\) the whole Hamiltonian became NaN. Results also changed with the sign-convention correction described under hamiltonian_2nu_matter.Changed in version 1.3.0: Accepts an array of energies, returning one Hamiltonian per energy stacked along a leading axis. The LIV term scales with the energy rather than being added at constant strength, so it is formed per entry. A scalar energy still returns a single matrix, and the results are bit-for-bit what the equivalent loop produced.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent part of the two-neutrino vacuum Hamiltonian [eV2], as returned by hamiltonian_2nu_vacuum_energy_independent. It is not modified.
- energyfloat or array_like
Neutrino energy [eV], or an array of energies, in which case one Hamiltonian is returned per energy.
- sxifloat
\(\sin\xi\), with \(\xi\) the rotation angle between the space of the eigenvectors of \(B_2\) and the flavor states.
- b1float
Eigenvalue \(b_1\) of the LIV operator \(B_2\) [eV].
- b2float
Eigenvalue \(b_2\) of the LIV operator \(B_2\) [eV].
- Lambdafloat
Energy scale \(\Lambda\) of the LIV operator \(B_2\) [eV].
- Returns:
- numpy.ndarray
The \(2\times2\) complex Hamiltonian [eV], of shape
(2, 2)for a scalar energy and(..., 2, 2)for an array of energies.
Examples
import hamiltonians2nu H_vac = hamiltonians2nu.hamiltonian_2nu_vacuum_energy_independent(0.5, 2.5e-3) H = hamiltonians2nu.hamiltonian_2nu_liv(H_vac, 1.0e9, 0.6, 1.0e-9, 2.0e-9, 1.0e12) print('%.6e' % H[0][0].real)
1.047500e-12
hamiltonians3nu
Compute three-neutrino Hamiltonians for selected scenarios.
This module contains routines that build the three-neutrino Hamiltonian
for a number of standard scenarios — oscillations in vacuum, in
matter of constant density, in matter with non-standard interactions
(NSI), and in a CPT-odd Lorentz invariance-violating (LIV) background
— together with the textbook oscillation formula in vacuum, which
serves to validate the exact SU(3) computation performed by
oscprob3nu and described in [1].
The Hamiltonians built here are meant to be passed to
oscprob3nu.probabilities_3nu(). They are examples: the exact
computation accepts any Hermitian \(3\times3\) Hamiltonian.
The routines that take a neutrino energy also accept an array of
energies, and then return one Hamiltonian per energy, stacked along a
leading axis. That stack is exactly what oscprob3nu.probabilities_3nu()
expects, so a whole energy scan is two calls rather than a loop.
Units
Throughout this module,
Quantity |
Units |
|---|---|
Mass-squared differences |
eV2 |
Neutrino energy |
eV |
Baseline |
eV-1 |
Matter potential |
eV |
LIV eigenvalues and scale |
eV |
CP-violation phases |
radian |
The routine
hamiltonian_3nu_vacuum_energy_independent() returns the
energy-independent part of the vacuum Hamiltonian, i.e. it has units
of eV2 and must still be divided by the neutrino energy. The
module globaldefs provides CONV_KM_TO_INV_EV to convert a
baseline in km into eV-1.
Sign convention
The vacuum Hamiltonian is \(H_{\rm vac} = U M^2 U^\dagger / (2E)\), with \(M^2 = \mathrm{diag}(0, \Delta m^2_{21}, \Delta m^2_{31})\) and \(U\) the PMNS matrix, so that adding a positive matter potential to the \(ee\) entry describes neutrinos, not antineutrinos.
Routine listings
pmns_mixing_matrix - Returns the \(3\times3\) PMNS matrix
hamiltonian_3nu_vacuum_energy_independent - Returns \(H_{\rm vac}\) without the \(1/E\)
delta - Kronecker delta
J - Product of four entries of the PMNS matrix
probabilities_3nu_vacuum_std - Vacuum probabilities, standard formula
hamiltonian_3nu_matter - Returns \(H_{\rm matter}\)
hamiltonian_3nu_nsi - Returns \(H_{\rm NSI}\)
hamiltonian_3nu_liv - Returns \(H_{\rm LIV}\)
References
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
- hamiltonians3nu.pmns_mixing_matrix(s12: int | float, s23: int | float, s13: int | float, dCP: int | float) List[List[complex]][source]
Returns the \(3\times3\) PMNS mixing matrix.
Computes and returns the complex \(3\times3\) PMNS mixing matrix, parametrized by the three rotation angles \(\theta_{12}\), \(\theta_{23}\), \(\theta_{13}\), and the CP-violation phase \(\delta_{\rm CP}\), in the standard PDG parametrization.
Added in version 1.0.0.
Changed in version 1.1.0: The entries are complex throughout. They were previously a mixture of complex and real numbers, depending on which of them the CP phase reached. The matrix is still returned as a nested list, as it always has been.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The scalar path stopped dispatching NumPy for single numbers:numpy.real(),numpy.imag(),numpy.arccos,numpy.clip()andnumpy.sqrton one number give way to attribute access and themathmodule.- Parameters:
- s12float
\(\sin\theta_{12}\).
- s23float
\(\sin\theta_{23}\).
- s13float
\(\sin\theta_{13}\).
- dCPfloat
CP-violation phase \(\delta_{\rm CP}\) [radian].
- Returns:
- list of list of complex
The \(3\times3\) PMNS mixing matrix, as a nested list.
Examples
import hamiltonians3nu U = hamiltonians3nu.pmns_mixing_matrix(0.55, 0.76, 0.15, 0.0) print('%.6f %.6f' % (U[0][0].real, U[0][1].real))
0.825716 0.543777
- hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent(s12: int | float, s23: int | float, s13: int | float, dCP: int | float, D21: int | float, D31: int | float, compute_matrix_multiplication: bool = False) ndarray[source]
Returns the three-neutrino Hamiltonian for vacuum oscillations.
Computes and returns the energy-independent part of the complex \(3\times3\) three-neutrino Hamiltonian for oscillations in vacuum, parametrized by three mixing angles — \(\theta_{12}\), \(\theta_{23}\), \(\theta_{13}\) — one CP-violation phase — \(\delta_{\rm CP}\) — and two mass-squared differences — \(\Delta m^2_{21}\), \(\Delta m^2_{31}\). The Hamiltonian is \(H = \frac{1}{2} U M^2 U^\dagger\), with \(U\) the PMNS matrix and \(M^2 = \mathrm{diag}(0, \Delta m^2_{21}, \Delta m^2_{31})\) the mass matrix. The multiplicative factor \(1/E\) is not applied.
Added in version 1.0.0.
Changed in version 1.1.0: Returns a complex
numpy.ndarrayrather than a nested list.Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The closed form rebuilt the CP phase fifteen times across its nine entries and recomputed two products in five places; hoisting them makes the routine 1.9x quicker.- Parameters:
- s12float
\(\sin\theta_{12}\).
- s23float
\(\sin\theta_{23}\).
- s13float
\(\sin\theta_{13}\).
- dCPfloat
CP-violation phase \(\delta_{\rm CP}\) [radian].
- D21float
Mass-squared difference \(\Delta m^2_{21}\) [eV2].
- D31float
Mass-squared difference \(\Delta m^2_{31}\) [eV2].
- compute_matrix_multiplicationbool, optional
If
False(default), use the pre-computed closed-form expressions; ifTrue, carry out the matrix multiplication \(U M^2 U^\dagger\) explicitly. Both give the same result; the option exists as a cross-check.
- Returns:
- numpy.ndarray
The \(3\times3\) complex Hamiltonian [eV2], to be divided by the neutrino energy before use.
Examples
import hamiltonians3nu H = hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent(0.55, 0.76, 0.15, 0.0, 7.4e-5, 2.5e-3) print('%.6e' % H[0][0].real)
3.906567e-05
- hamiltonians3nu.delta(a: int, b: int) int[source]
Returns the Kronecker delta \(\delta_{ab}\).
Added in version 1.0.0.
- Parameters:
- aint
First index.
- bint
Second index.
- Returns:
- int
1 if
a == b, 0 otherwise.
Examples
import hamiltonians3nu print(hamiltonians3nu.delta(0, 0), hamiltonians3nu.delta(0, 1))
1 0
- hamiltonians3nu.J(U: list | ndarray, alpha: int, beta: int, k: int, j: int) complex[source]
Returns the quartic product of PMNS matrix entries.
Returns \(J = U_{\alpha k}^* U_{\beta k} U_{\alpha j} U_{\beta j}^*\), with \(U\) the PMNS mixing matrix. This product appears in the standard expression for the three-neutrino oscillation probability in vacuum; its imaginary part is the Jarlskog invariant, up to a sign.
Added in version 1.0.0.
- Parameters:
- Uarray_like
The \(3\times3\) complex PMNS mixing matrix.
- alphaint
Index of the initial flavor (0: \(e\), 1: \(\mu\), 2: \(\tau\)).
- betaint
Index of the final flavor (0: \(e\), 1: \(\mu\), 2: \(\tau\)).
- kint
First index of the sum over mass eigenstates (0, 1, or 2).
- jint
Second index of the sum over mass eigenstates (0, 1, or 2).
- Returns:
- complex
The product \(U_{\alpha k}^* U_{\beta k} U_{\alpha j} U_{\beta j}^*\).
Examples
import hamiltonians3nu U = hamiltonians3nu.pmns_mixing_matrix(0.55, 0.76, 0.15, 0.0) print('%.6f' % hamiltonians3nu.J(U, 0, 1, 1, 0).real)
-0.097579
- hamiltonians3nu.probabilities_3nu_vacuum_std(U: list | ndarray, D21: int | float, D31: int | float, energy: int | float, L: int | float) List[float][source]
Returns the 3nu vacuum probabilities, standard computation.
Returns the probabilities for three-neutrino oscillations in vacuum, computed with the standard analytical expression
\[P_{\alpha\beta} = \delta_{\alpha\beta} - 4 \sum_{k>j} \mathrm{Re}(J_{kj}) \sin^2\left(\frac{\Delta m^2_{kj} L}{4E}\right) + 2 \sum_{k>j} \mathrm{Im}(J_{kj}) \sin\left(\frac{\Delta m^2_{kj} L}{2E}\right) .\]This routine exists to validate the exact SU(3) computation in
oscprob3nu; the two agree to round-off.Added in version 1.0.0.
Changed in version 1.1.0: The signature changed: the energy is now given in eV and the baseline in \(\mathrm{eV}^{-1}\), like the rest of the library, rather than in GeV and km. The rounded constants 1.27 and 2.54 that folded in the old conversion overstated every phase by 0.242%.
- Parameters:
- Uarray_like
The \(3\times3\) complex PMNS mixing matrix, as returned by pmns_mixing_matrix.
- D21float
Mass-squared difference \(\Delta m^2_{21}\) [eV2].
- D31float
Mass-squared difference \(\Delta m^2_{31}\) [eV2].
- energyfloat
Neutrino energy [eV].
- Lfloat
Baseline [eV-1].
- Returns:
- list of float
The nine probabilities
[Pee, Pem, Pet, Pme, Pmm, Pmt, Pte, Ptm, Ptt], ordered with the initial flavor varying slowest.
See also
oscprob3nu.probabilities_3nuThe exact SU(3) computation.
Examples
import hamiltonians3nu U = hamiltonians3nu.pmns_mixing_matrix(0.55, 0.76, 0.15, 0.0) prob = hamiltonians3nu.probabilities_3nu_vacuum_std(U, 7.4e-5, 2.5e-3, 1.0e9, 5.0e12) print('%.6f %.6f' % (prob[0], prob[1]))
0.992787 0.001981
- hamiltonians3nu.hamiltonian_3nu_matter(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, VCC: int | float | list | ndarray) ndarray[source]
Returns the three-neutrino Hamiltonian for matter oscillations.
Computes and returns the \(3\times3\) three-neutrino Hamiltonian for oscillations in matter of constant density, obtained by adding the charged-current matter potential to the \(ee\) entry of the vacuum Hamiltonian.
Added in version 1.0.0.
Changed in version 1.1.0: Returns a complex
numpy.ndarrayrather than a nested list.Changed in version 1.3.0: Accepts an array of energies, returning one Hamiltonian per energy stacked along a leading axis; the matter potential may be an array too. A scalar energy still returns a single matrix, and the results are bit-for-bit what the equivalent loop produced.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent part of the three-neutrino vacuum Hamiltonian [eV2], as returned by hamiltonian_3nu_vacuum_energy_independent. It is not modified.
- energyfloat or array_like
Neutrino energy [eV], or an array of energies, in which case one Hamiltonian is returned per energy.
- VCCfloat or array_like
Potential due to charged-current interactions of \(\nu_e\) with electrons [eV]. Positive for neutrinos, negative for antineutrinos. May be an array, to scan across a density profile alongside the energy.
- Returns:
- numpy.ndarray
The \(3\times3\) complex Hamiltonian [eV], of shape
(3, 3)for a scalar energy and(..., 3, 3)for an array of energies.
Examples
import hamiltonians3nu H_vac = hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent(0.55, 0.76, 0.15, 0.0, 7.4e-5, 2.5e-3) H = hamiltonians3nu.hamiltonian_3nu_matter(H_vac, 1.0e9, 1.0e-13) print('%.6e' % H[0][0].real)
1.390657e-13
- hamiltonians3nu.hamiltonian_3nu_nsi(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, VCC: int | float | list | ndarray, eps: list | ndarray) ndarray[source]
Returns the three-neutrino Hamiltonian for oscillations w/ NSI.
Computes and returns the \(3\times3\) three-neutrino Hamiltonian for oscillations with non-standard interactions (NSI) in matter of constant density.
Added in version 1.0.0.
Changed in version 1.1.0: Returns a complex
numpy.ndarrayrather than a nested list.Changed in version 1.3.0: Accepts an array of energies, returning one Hamiltonian per energy stacked along a leading axis; the matter potential may be an array too. A scalar energy still returns a single matrix, and the results are bit-for-bit what the equivalent loop produced.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent part of the three-neutrino vacuum Hamiltonian [eV2], as returned by hamiltonian_3nu_vacuum_energy_independent. It is not modified.
- energyfloat or array_like
Neutrino energy [eV], or an array of energies, in which case one Hamiltonian is returned per energy.
- VCCfloat or array_like
Potential due to charged-current interactions of \(\nu_e\) with electrons [eV]. May be an array, to scan across a density profile alongside the energy.
- epsarray_like
The six NSI strength parameters
[eps_ee, eps_em, eps_et, eps_mm, eps_mt, eps_tt], adimensional. The diagonal parameters are real; the off-diagonal ones may be complex, and their complex conjugates are placed in the lower off-diagonal entries so that the Hamiltonian stays Hermitian.
- Returns:
- numpy.ndarray
The \(3\times3\) complex Hamiltonian [eV], of shape
(3, 3)for a scalar energy and(..., 3, 3)for an array of energies.
Examples
import hamiltonians3nu H_vac = hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent(0.55, 0.76, 0.15, 0.0, 7.4e-5, 2.5e-3) H = hamiltonians3nu.hamiltonian_3nu_nsi(H_vac, 1.0e9, 1.0e-13, [0.06, -0.06+0.03j, 0.0, 1.2, 0.0, 0.0]) print('%+.6e%+.6ej' % (H[0][1].real, H[0][1].imag))
+1.445471e-13+3.000000e-15j
- hamiltonians3nu.hamiltonian_3nu_liv(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, sxi12: int | float, sxi23: int | float, sxi13: int | float, dxiCP: int | float, b1: int | float, b2: int | float, b3: int | float, Lambda: int | float) ndarray[source]
Returns the three-neutrino Hamiltonian for oscillations w/ LIV.
Computes and returns the \(3\times3\) three-neutrino Hamiltonian for oscillations in a CPT-odd Lorentz invariance-violating (LIV) background. The LIV term is \((E/\Lambda) R B_3 R^\dagger\), with \(B_3 = \mathrm{diag}(b_1, b_2, b_3)\) and \(R\) a PMNS-like matrix built from the mixing angles \(\xi_{ij}\) and the phase \(\delta_{\xi,\rm CP}\) that relate the eigenvectors of \(B_3\) to the flavor states.
Added in version 1.0.0.
Changed in version 1.1.0: Returns a complex
numpy.ndarrayrather than a nested list.Changed in version 1.3.0: Accepts an array of energies, returning one Hamiltonian per energy stacked along a leading axis. The LIV term scales with the energy rather than being added at constant strength, so it is formed per entry. A scalar energy still returns a single matrix, and the results are bit-for-bit what the equivalent loop produced.
Changed in version 1.4.0: Faster, with identical results — all 42 figures generated by
run_testsuite.pyare byte-for-byte those of 1.3.0. The gain is indirect, through pmns_mixing_matrix, which this routine builds the rotation from.- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent part of the three-neutrino vacuum Hamiltonian [eV2], as returned by hamiltonian_3nu_vacuum_energy_independent. It is not modified.
- energyfloat or array_like
Neutrino energy [eV], or an array of energies, in which case one Hamiltonian is returned per energy.
- sxi12float
\(\sin\xi_{12}\), with \(\xi_{12}\) one of the mixing angles between the space of the eigenvectors of \(B_3\) and the flavor states.
- sxi23float
\(\sin\xi_{23}\), likewise.
- sxi13float
\(\sin\xi_{13}\), likewise.
- dxiCPfloat
CP-violation phase of the LIV operator \(B_3\) [radian].
- b1float
Eigenvalue \(b_1\) of the LIV operator \(B_3\) [eV].
- b2float
Eigenvalue \(b_2\) of the LIV operator \(B_3\) [eV].
- b3float
Eigenvalue \(b_3\) of the LIV operator \(B_3\) [eV].
- Lambdafloat
Energy scale \(\Lambda\) of the LIV operator \(B_3\) [eV].
- Returns:
- numpy.ndarray
The \(3\times3\) complex Hamiltonian [eV], of shape
(3, 3)for a scalar energy and(..., 3, 3)for an array of energies.
Examples
import hamiltonians3nu H_vac = hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent(0.55, 0.76, 0.15, 0.0, 7.4e-5, 2.5e-3) H = hamiltonians3nu.hamiltonian_3nu_liv(H_vac, 1.0e9, 0.3, 0.4, 0.5, 0.7, 1.0e-9, 1.5e-9, 2.0e-9, 1.0e12) print('%.6e' % H[0][0].real)
1.322816e-12
hamiltonians4nu
Sample four-neutrino Hamiltonians, for 3+1 scenarios.
This module builds the \(4\times4\) Hamiltonians that
oscprob4nu evaluates, in the same spirit as
hamiltonians3nu does at three flavors and as [1] describes: they
are examples, not limitations. oscprob4nu.probabilities_4nu() takes any Hermitian
\(4\times4\) matrix, so a scenario not built here is a matrix away.
The fourth state is written as sterile throughout, so the flavor basis
is \((\nu_e, \nu_\mu, \nu_\tau, \nu_s)\), and the mixing matrix
carries three extra angles \(\theta_{14}, \theta_{24},
\theta_{34}\) on top of the three standard ones. Nothing in
oscprob4nu depends on that reading: a fourth active state, or
any other four-level system, works identically.
Why 3+1 is in scope here
A 3+1 system is often described as “leaky” from the three-flavor point of view, because probability disappears from the active block into the sterile state. That is a statement about the \(3\times3\) subsystem, not about the physics: over all four states the evolution is closed and unitary, which is exactly the assumption the SU(4) expansion needs. Treating it at \(n = 4\) therefore brings it back inside the scope of an exact closed-form method.
The matter potential
Active neutrinos feel the charged-current potential \(V_{CC}\) (the electron flavor only) and the flavor-universal neutral-current potential \(V_{NC}\) (all three); a sterile state feels neither. A term proportional to the identity contributes only a global phase, so subtracting \(V_{NC}\mathbb{1}\) from all four states costs nothing and leaves
with \(-V_{NC} = +G_F n_n/\sqrt{2}\) positive. The sterile entry is therefore not zero, and getting it wrong is the four-flavor analogue of the antineutrino sign trap: the difference is invisible in vacuum and sets the position of the matter resonance.
Routine listings
mixing_matrix_4nu - Returns the 3+1 mixing matrix
hamiltonian_4nu_vacuum_energy_independent - Vacuum Hamiltonian
hamiltonian_4nu_matter - Adds matter of constant density
hamiltonian_4nu_nsi - Adds non-standard interactions
hamiltonian_4nu_liv - Adds a Lorentz invariance-violating term
References
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
- hamiltonians4nu.mixing_matrix_4nu(s12: int | float, s23: int | float, s13: int | float, s14: int | float, s24: int | float, s34: int | float, dCP: int | float, d14: int | float = 0.0, d24: int | float = 0.0) ndarray[source]
Returns the 3+1 lepton mixing matrix.
Built in the common 3+1 ordering
\[U = R_{34} R_{24}(\delta_{24}) R_{14}(\delta_{14}) R_{23} R_{13}(\delta_{CP}) R_{12} ,\]which reduces to the standard PDG three-flavor matrix of
hamiltonians3nu.pmns_mixing_matrix()in the upper-left block when the three new angles vanish.All six mixing parameters are sines of the angles, not the angles, matching the convention used throughout NuOscProbExact.
Added in version 1.9.0.
- Parameters:
- s12int or float
Sine of \(\theta_{12}\).
- s23int or float
Sine of \(\theta_{23}\).
- s13int or float
Sine of \(\theta_{13}\).
- s14int or float
Sine of \(\theta_{14}\).
- s24int or float
Sine of \(\theta_{24}\).
- s34int or float
Sine of \(\theta_{34}\).
- dCPint or float
Standard Dirac CP-violating phase, in radian.
- d14int or float, optional
Extra phase on the 1-4 rotation, in radian. Default: 0.
- d24int or float, optional
Extra phase on the 2-4 rotation, in radian. Default: 0.
- Returns:
- numpy.ndarray
Complex array of shape
(4, 4).
Examples
import numpy as np import hamiltonians4nu mixing = hamiltonians4nu.mixing_matrix_4nu( np.sqrt(0.310), np.sqrt(0.582), np.sqrt(2.240e-2), np.sqrt(0.10), np.sqrt(0.10), 0.0, 217.0/180.0*np.pi) unitarity = mixing.conj().T @ mixing print('unitary to %.1e' % np.max(np.abs(unitarity - np.eye(4))))
unitary to 1.7e-16
- hamiltonians4nu.hamiltonian_4nu_vacuum_energy_independent(s12: int | float, s23: int | float, s13: int | float, s14: int | float, s24: int | float, s34: int | float, dCP: int | float, D21: int | float, D31: int | float, D41: int | float, d14: int | float = 0.0, d24: int | float = 0.0) ndarray[source]
Returns the energy-independent four-neutrino vacuum Hamiltonian.
Returns \(U M^2 U^\dagger / 2\), with \(M^2 = \mathrm{diag}(0, \Delta m^2_{21}, \Delta m^2_{31}, \Delta m^2_{41})\), so that the vacuum Hamiltonian at energy \(E\) is this matrix divided by \(E\).
The factor \(1/E\) is deliberately left out, so that the energy-independent part can be computed once and reused across an energy scan — the same arrangement as
hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent().Added in version 1.9.0.
- Parameters:
- s12int or float
Sine of \(\theta_{12}\).
- s23int or float
Sine of \(\theta_{23}\).
- s13int or float
Sine of \(\theta_{13}\).
- s14int or float
Sine of \(\theta_{14}\).
- s24int or float
Sine of \(\theta_{24}\).
- s34int or float
Sine of \(\theta_{34}\).
- dCPint or float
Standard Dirac CP-violating phase, in radian.
- D21int or float
Mass-squared difference \(\Delta m^2_{21}\), in eV2.
- D31int or float
Mass-squared difference \(\Delta m^2_{31}\), in eV2.
- D41int or float
Mass-squared difference \(\Delta m^2_{41}\), in eV2.
- d14int or float, optional
Extra phase on the 1-4 rotation, in radian. Default: 0.
- d24int or float, optional
Extra phase on the 2-4 rotation, in radian. Default: 0.
- Returns:
- numpy.ndarray
Complex array of shape
(4, 4), in units of eV2. Divide by the energy in eV to obtain a Hamiltonian in eV.
Examples
import numpy as np import globaldefs as gd import hamiltonians4nu import oscprob4nu h_vacuum = hamiltonians4nu.hamiltonian_4nu_vacuum_energy_independent( gd.S12_NO_BF, gd.S23_NO_BF, gd.S13_NO_BF, np.sqrt(0.10), np.sqrt(0.10), 0.0, gd.DCP_NO_BF, gd.D21_NO_BF, gd.D31_NO_BF, 1.0) prob = oscprob4nu.probabilities_4nu( h_vacuum/1.0e9, 1300.0*gd.CONV_KM_TO_INV_EV) print('P_mumu = %.6f' % prob[5]) print('P_mus = %.6f' % prob[7])
P_mumu = 0.407166 P_mus = 0.011663
- hamiltonians4nu.hamiltonian_4nu_matter(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, VCC: int | float | list | ndarray, VNC: int | float | list | ndarray) ndarray[source]
Returns the four-neutrino Hamiltonian in matter.
Adds to the vacuum term the matter potential
\[A_4 = \mathrm{diag}\left(V_{CC},\, 0,\, 0,\, -V_{NC}\right) ,\]which is what remains after the flavor-universal neutral-current potential of the three active states is removed as a global phase. Because the sterile state does not feel \(V_{NC}\), removing it leaves \(-V_{NC}\) on the sterile entry rather than nothing.
VCC is positive for neutrinos. For antineutrinos, reverse the sign of both potentials and conjugate the vacuum term, exactly as at three flavors.
Added in version 1.9.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent four-flavor vacuum Hamiltonian, of shape
(4, 4), in eV2.- energyint or float or array_like
Neutrino energy, in eV, or an array of energies.
- VCCint or float or array_like
Charged-current matter potential, in eV. Positive for neutrinos.
- VNCint or float or array_like
Neutral-current matter potential, in eV. Negative for neutrinos, equal to \(-G_F n_n/\sqrt{2}\); see globaldefs.VNC_EARTH_CRUST.
- Returns:
- numpy.ndarray
Complex array of shape
(4, 4)for a scalar energy, or(..., 4, 4)for an array of energies, in eV.
Examples
import numpy as np import globaldefs as gd import hamiltonians4nu import oscprob4nu h_vacuum = hamiltonians4nu.hamiltonian_4nu_vacuum_energy_independent( gd.S12_NO_BF, gd.S23_NO_BF, gd.S13_NO_BF, np.sqrt(0.10), np.sqrt(0.10), 0.0, gd.DCP_NO_BF, gd.D21_NO_BF, gd.D31_NO_BF, 1.0) h_matter = hamiltonians4nu.hamiltonian_4nu_matter( h_vacuum, 1.0e9, gd.VCC_EARTH_CRUST, gd.VNC_EARTH_CRUST) prob = oscprob4nu.probabilities_4nu( h_matter, 1300.0*gd.CONV_KM_TO_INV_EV) print('P_ee = %.6f' % prob[0]) print('P_mumu = %.6f' % prob[5])
P_ee = 0.837799 P_mumu = 0.387694
- hamiltonians4nu.hamiltonian_4nu_nsi(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, VCC: int | float | list | ndarray, VNC: int | float | list | ndarray, eps: list | ndarray) ndarray[source]
Returns the four-neutrino Hamiltonian with matter and NSI.
Adds non-standard interactions to the active block only. Sterile states have no standard-model interactions by construction, so they have no non-standard ones either: the sterile row and column of the NSI matrix are zero, and the sterile entry keeps the \(-V_{NC}\) of
hamiltonian_4nu_matter().The eps parameters follow
hamiltonians3nu.hamiltonian_3nu_nsi(): six of them, with the three off-diagonal ones allowed to be complex.Added in version 1.9.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent four-flavor vacuum Hamiltonian, of shape
(4, 4), in eV2.- energyint or float or array_like
Neutrino energy, in eV, or an array of energies.
- VCCint or float or array_like
Charged-current matter potential, in eV.
- VNCint or float or array_like
Neutral-current matter potential, in eV.
- epsarray_like
The six NSI strength parameters
[eps_ee, eps_em, eps_et, eps_mm, eps_mt, eps_tt], relative to VCC. The off-diagonal ones may be complex.
- Returns:
- numpy.ndarray
Complex array of shape
(4, 4)for a scalar energy, or(..., 4, 4)for an array of energies, in eV.
Examples
import numpy as np import globaldefs as gd import hamiltonians4nu import oscprob4nu h_vacuum = hamiltonians4nu.hamiltonian_4nu_vacuum_energy_independent( gd.S12_NO_BF, gd.S23_NO_BF, gd.S13_NO_BF, np.sqrt(0.10), np.sqrt(0.10), 0.0, gd.DCP_NO_BF, gd.D21_NO_BF, gd.D31_NO_BF, 1.0) h_nsi = hamiltonians4nu.hamiltonian_4nu_nsi( h_vacuum, 1.0e9, gd.VCC_EARTH_CRUST, gd.VNC_EARTH_CRUST, gd.EPS_3) prob = oscprob4nu.probabilities_4nu( h_nsi, 1300.0*gd.CONV_KM_TO_INV_EV) print('P_mue with NSI = %.6f' % prob[4])
P_mue with NSI = 0.027737
- hamiltonians4nu.hamiltonian_4nu_liv(h_vacuum_energy_independent: list | ndarray, energy: int | float | list | ndarray, sxi12: int | float, sxi23: int | float, sxi13: int | float, sxi14: int | float, sxi24: int | float, sxi34: int | float, dxiCP: int | float, b1: int | float, b2: int | float, b3: int | float, b4: int | float, Lambda: int | float) ndarray[source]
Returns the four-neutrino Hamiltonian for oscillations w/ LIV.
The four-flavor counterpart of
hamiltonians3nu.hamiltonian_3nu_liv(). The LIV term is \((E/\Lambda) R B_4 R^\dagger\), with \(B_4 = \mathrm{diag}(b_1, b_2, b_3, b_4)\) and \(R\) a mixing matrix of the same 3+1 form asmixing_matrix_4nu(), built from the angles \(\xi_{ij}\) and the phase \(\delta_{\xi,\rm CP}\) that relate the eigenvectors of \(B_4\) to the flavor states.Nothing here privileges the fourth state: \(b_4\) is an eigenvalue like the others, so a sterile neutrino may couple to the LIV background whether or not it couples to matter. Setting the three new angles to zero and \(b_4\) equal to the trace-shifted remainder recovers the three-flavor term in the active block.
Added in version 1.11.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent four-flavor vacuum Hamiltonian, of shape
(4, 4), in eV2. It is not modified.- energyint or float or array_like
Neutrino energy, in eV, or an array of energies.
- sxi12int or float
Sine of \(\xi_{12}\).
- sxi23int or float
Sine of \(\xi_{23}\).
- sxi13int or float
Sine of \(\xi_{13}\).
- sxi14int or float
Sine of \(\xi_{14}\).
- sxi24int or float
Sine of \(\xi_{24}\).
- sxi34int or float
Sine of \(\xi_{34}\).
- dxiCPint or float
CP-violation phase of the LIV operator, in radian.
- b1int or float
Eigenvalue \(b_1\) of the LIV operator \(B_4\) [eV].
- b2int or float
Eigenvalue \(b_2\) of the LIV operator \(B_4\) [eV].
- b3int or float
Eigenvalue \(b_3\) of the LIV operator \(B_4\) [eV].
- b4int or float
Eigenvalue \(b_4\) of the LIV operator \(B_4\) [eV].
- Lambdaint or float
Energy scale \(\Lambda\) of the LIV operator [eV].
- Returns:
- numpy.ndarray
Complex array of shape
(4, 4)for a scalar energy, or(..., 4, 4)for an array of energies, in eV.
Examples
import numpy as np import globaldefs as gd import hamiltonians4nu import oscprob4nu h_vacuum = hamiltonians4nu.hamiltonian_4nu_vacuum_energy_independent( gd.S12_NO_BF, gd.S23_NO_BF, gd.S13_NO_BF, np.sqrt(0.10), np.sqrt(0.10), 0.0, gd.DCP_NO_BF, gd.D21_NO_BF, gd.D31_NO_BF, 1.0) h_liv = hamiltonians4nu.hamiltonian_4nu_liv( h_vacuum, 1.0e9, 0.3, 0.4, 0.5, 0.0, 0.0, 0.0, 0.7, 1.0e-9, 1.5e-9, 2.0e-9, 2.5e-9, 1.0e12) prob = oscprob4nu.probabilities_4nu( h_liv, 1300.0*gd.CONV_KM_TO_INV_EV) print('P_ee with LIV = %.6f' % prob[0])
P_ee with LIV = 0.370795
Piecewise-constant matter
The exact expansions assume a Hamiltonian that does not change along the trajectory. A neutrino crossing the Earth does not have one, so its path is cut into slabs, each solved exactly and the results multiplied. Within a slab nothing is approximated; the only approximation is how finely a continuously varying profile is sliced, and that is an argument the caller controls.
slabs
Oscillation probabilities across a sequence of adjacent slabs.
NuOscProbExact computes the evolution operator exactly for a Hamiltonian that does not change along the trajectory. A great many interesting cases are piecewise constant instead: a neutrino crossing the Earth, a beam through a layered detector, a castle-wall profile built to enhance CP-violating effects. This module handles those by doing the only thing the exactness of the method allows — solving each slab exactly and multiplying the results.
For a trajectory divided into \(n\) slabs, the slab \(k\) having Hamiltonian \(H_k\) and width \(L_k\), the evolution operator is
with the slab the neutrino meets first applied first — rightmost,
since the operators act to the left on the initial state. Each
\(U_k\) is the exact SU(2), SU(3) or SU(4) expansion of
oscprob2nu,
oscprob3nu or oscprob4nu, so the only approximation in the
result is the one
the caller makes in choosing how finely to slice a continuously varying
profile. Within each slab there is none.
The per-slab operators are evaluated in a single batched call, so the cost of \(n\) slabs is one vectorised evaluation plus \(n-1\) small matrix products rather than \(n\) separate evaluations.
One thing carries over from the single-slab case and is easier to overlook here. The expansions return \(e^{-i H_0 L}\), with \(H_0\) the traceless part of the Hamiltonian, dropping the phase \(e^{-i h_0 L}\) that the trace contributes. Each slab therefore drops its own phase, and their product differs from \(\prod_k e^{-i H_k L_k}\) by the single scalar \(\exp(i \sum_k h_0^{(k)} L_k)\). That is still one overall phase, so every probability is unaffected — but a caller comparing the returned operator against an independent matrix exponential must compare against the traceless one, exactly as the single-slab tests do.
For the Earth specifically, earth builds the slabs for you from
the Preliminary Reference Earth Model.
Routine listings
evolution_operator_2nu_slabs - Two-flavor evolution operator
evolution_operator_3nu_slabs - Three-flavor evolution operator
evolution_operator_4nu_slabs - Four-flavor evolution operator
probabilities_2nu_slabs - Two-flavor probabilities
probabilities_3nu_slabs - Three-flavor probabilities
probabilities_4nu_slabs - Four-flavor probabilities
- slabs.evolution_operator_2nu_slabs(hamiltonian_matrices: list | ndarray, widths: list | ndarray) ndarray[source]
Returns the two-flavor evolution operator across adjacent slabs.
Returns the \(2\times2\) evolution operator \(U_2 = U_2^{(n)}(L_n) \cdots U_2^{(1)}(L_1)\) for a trajectory divided into slabs, each with its own constant Hamiltonian and width. Each slab is solved exactly by the SU(2) expansion of
oscprob2nu.Added in version 1.8.0.
- Parameters:
- hamiltonian_matricesarray_like
Hamiltonians, of shape
(n, 2, 2), one per slab and ordered along the trajectory, in units of eV.- widthsarray_like
Slab widths, of shape
(n,), in units of eV-1. Use globaldefs.CONV_KM_TO_INV_EV to convert from km.
- Returns:
- numpy.ndarray
The evolution operator, of shape
(2, 2), indexed(final, initial).
- Raises:
- ValueError
If the number of Hamiltonians and widths differ, if either is empty, if the Hamiltonians are not \(2\times2\), or if any width is negative.
Examples
import slabs import numpy as np H = np.array([[[0.0, 1.0], [1.0, 0.0]], [[0.0, 0.5], [0.5, 0.0]]]) U = slabs.evolution_operator_2nu_slabs(H, [0.3, 0.4]) print('%.6f' % abs(U[0][0]))
0.877583
- slabs.evolution_operator_3nu_slabs(hamiltonian_matrices: list | ndarray, widths: list | ndarray) ndarray[source]
Returns the three-flavor evolution operator across adjacent slabs.
Returns the \(3\times3\) evolution operator \(U_3 = U_3^{(n)}(L_n) \cdots U_3^{(1)}(L_1)\) for a trajectory divided into slabs, each with its own constant Hamiltonian and width. Each slab is solved exactly by the SU(3) expansion of
oscprob3nu.Added in version 1.8.0.
- Parameters:
- hamiltonian_matricesarray_like
Hamiltonians, of shape
(n, 3, 3), one per slab and ordered along the trajectory, in units of eV.- widthsarray_like
Slab widths, of shape
(n,), in units of eV-1. Use globaldefs.CONV_KM_TO_INV_EV to convert from km.
- Returns:
- numpy.ndarray
The evolution operator, of shape
(3, 3), indexed(final, initial).
- Raises:
- ValueError
If the number of Hamiltonians and widths differ, if either is empty, if the Hamiltonians are not \(3\times3\), or if any width is negative.
Examples
import slabs import numpy as np H = np.array([np.diag([1.0, 0.0, -1.0]), np.diag([0.5, 0.0, -0.5])], dtype=complex) U = slabs.evolution_operator_3nu_slabs(H, [0.2, 0.3]) print('%.6f' % abs(U[0][0]))
1.000000
- slabs.evolution_operator_4nu_slabs(hamiltonian_matrices: list | ndarray, widths: list | ndarray) ndarray[source]
Returns the four-flavor evolution operator across adjacent slabs.
Returns the \(4\times4\) evolution operator \(U_4 = U_4^{(n)}(L_n) \cdots U_4^{(1)}(L_1)\) for a trajectory divided into slabs, each with its own constant Hamiltonian and width. Each slab is solved exactly by the SU(4) expansion of
oscprob4nu.This is what makes a 3+1 scenario propagable through layered matter: the sterile state’s matter entry is constant within a slab like every other, so nothing about the composition changes at four flavors. See
hamiltonians4nu.hamiltonian_4nu_matter()for the entry itself, which is \(-V_{NC}\) rather than zero.Added in version 1.11.0.
- Parameters:
- hamiltonian_matricesarray_like
Hamiltonians, of shape
(n, 4, 4), one per slab and ordered along the trajectory, in units of eV.- widthsarray_like
Slab widths, of shape
(n,), in units of eV-1. Use globaldefs.CONV_KM_TO_INV_EV to convert from km.
- Returns:
- numpy.ndarray
The evolution operator, of shape
(4, 4), indexed(final, initial).
- Raises:
- ValueError
If the number of Hamiltonians and widths differ, if either is empty, if the Hamiltonians are not \(4\times4\), or if any width is negative.
Examples
import slabs import numpy as np H = np.array([np.diag([1.0, 0.0, -0.5, -0.5]), np.diag([0.5, 0.0, -0.25, -0.25])], dtype=complex) U = slabs.evolution_operator_4nu_slabs(H, [0.2, 0.3]) print('%.6f' % abs(U[0][0]))
1.000000
- slabs.probabilities_2nu_slabs(hamiltonian_matrices: list | ndarray, widths: list | ndarray) Tuple[float, float, float, float][source]
Returns the two-flavor probabilities across adjacent slabs.
Returns \(P_{ee}, P_{e\mu}, P_{\mu e}, P_{\mu\mu}\), where \(P_{\alpha\beta} \equiv P(\nu_\alpha \to \nu_\beta) = |[U_2]_{\beta\alpha}|^2\), for a trajectory divided into slabs.
Added in version 1.8.0.
- Parameters:
- hamiltonian_matricesarray_like
Hamiltonians, of shape
(n, 2, 2), one per slab and ordered along the trajectory, in units of eV.- widthsarray_like
Slab widths, of shape
(n,), in units of eV-1.
- Returns:
- tuple of float
The probabilities \(P_{ee}, P_{e\mu}, P_{\mu e}, P_{\mu\mu}\).
- Raises:
- ValueError
If the slab sequence is malformed; see evolution_operator_2nu_slabs.
Examples
import slabs import numpy as np H = np.array([[[0.0, 1.0], [1.0, 0.0]], [[0.0, 0.5], [0.5, 0.0]]]) Pee, Pem, Pme, Pmm = slabs.probabilities_2nu_slabs(H, [0.3, 0.4]) print('%.6f %.6f' % (Pee, Pem))
0.770151 0.229849
- slabs.probabilities_3nu_slabs(hamiltonian_matrices: list | ndarray, widths: list | ndarray) Tuple[float, float, float, float, float, float, float, float, float][source]
Returns the three-flavor probabilities across adjacent slabs.
Returns \(P_{ee}, P_{e\mu}, P_{e\tau}, P_{\mu e}, P_{\mu\mu}, P_{\mu\tau}, P_{\tau e}, P_{\tau\mu}, P_{\tau\tau}\), where \(P_{\alpha\beta} \equiv P(\nu_\alpha \to \nu_\beta) = |[U_3]_{\beta\alpha}|^2\), for a trajectory divided into slabs.
Added in version 1.8.0.
- Parameters:
- hamiltonian_matricesarray_like
Hamiltonians, of shape
(n, 3, 3), one per slab and ordered along the trajectory, in units of eV.- widthsarray_like
Slab widths, of shape
(n,), in units of eV-1.
- Returns:
- tuple of float
The nine probabilities, with the initial flavor varying slowest.
- Raises:
- ValueError
If the slab sequence is malformed; see evolution_operator_3nu_slabs.
Examples
import slabs import numpy as np H = np.array([np.diag([1.0, 0.0, -1.0]), np.diag([0.5, 0.0, -0.5])], dtype=complex) prob = slabs.probabilities_3nu_slabs(H, [0.2, 0.3]) print('%.6f %.6f' % (prob[0], prob[1]))
1.000000 0.000000
- slabs.probabilities_4nu_slabs(hamiltonian_matrices: list | ndarray, widths: list | ndarray) Tuple[float, ...][source]
Returns the four-flavor probabilities across adjacent slabs.
Returns the sixteen probabilities \(P_{\alpha\beta} \equiv P(\nu_\alpha \to \nu_\beta) = |[U_4]_{\beta\alpha}|^2\), ordered with the initial flavor varying slowest, for a trajectory divided into slabs. With the fourth state read as sterile, the flavor order is \((\nu_e, \nu_\mu, \nu_\tau, \nu_s)\).
Added in version 1.11.0.
- Parameters:
- hamiltonian_matricesarray_like
Hamiltonians, of shape
(n, 4, 4), one per slab and ordered along the trajectory, in units of eV.- widthsarray_like
Slab widths, of shape
(n,), in units of eV-1.
- Returns:
- tuple of float
The sixteen probabilities, with the initial flavor varying slowest.
- Raises:
- ValueError
If the slab sequence is malformed; see evolution_operator_4nu_slabs.
Examples
import slabs import numpy as np H = np.array([np.diag([1.0, 0.0, -0.5, -0.5]), np.diag([0.5, 0.0, -0.25, -0.25])], dtype=complex) prob = slabs.probabilities_4nu_slabs(H, [0.2, 0.3]) print('%.6f %.6f' % (prob[0], prob[1]))
1.000000 0.000000
earth
The Earth as a sequence of slabs: PREM, chord geometry, and probabilities.
A neutrino crossing the Earth sees a matter density that changes
continuously along its path, so the Hamiltonian is not constant and the
exact expansions of oscprob2nu and oscprob3nu do not apply
to the trajectory as a whole. They apply to any piece of it over which
the density is taken to be constant, which is what this module builds:
the chord is cut into slabs, each slab is solved exactly by
slabs, and the operators are multiplied.
The density comes from the Preliminary Reference Earth Model (PREM) [DA81], a piecewise-polynomial fit to seismological data, given as \(\rho(x)\) with \(x = r/R_\oplus\).
Where the slabs are cut matters, and two different things are going on.
Between shells the density jumps, so the chord is first split at every crossing of a PREM shell boundary: no amount of subdivision recovers a discontinuity that straddles a slab. This gives a set of chord segments. Note a segment is not a shell — a chord enters and leaves each shell it reaches, so a diametric chord has 19 segments across 10 shells.
Within a shell the density varies smoothly, since PREM gives it as a
polynomial in \(x = r/R_\oplus\) rather than a constant, and a
segment can be long: crossing the mantle, the density changes by 21%
over a single 2200 km segment. Each segment is therefore divided
further into n_slabs_per_segment equal sub-slabs, with the density
taken at the midpoint of each.
Midpoint sampling is second-order accurate, so the result converges to the continuous answer as the sub-slabs are refined; the routines take that number as an argument so a caller can watch it converge rather than trust it. Sampling at the midpoint rather than an end also matters for the segment that straddles the closest approach: it enters and exits at the same radius, so its two ends have identical density while the interior differs from them by 2.5%.
Units follow the rest of the library: energies in eV, baselines in eV-1, potentials in eV. The exceptions are the geometry routines, which work in km because that is how the Earth is described, and density_prem, which returns g cm-3 because that is how PREM is stated. matter_potential is the bridge between the two.
The Earth is treated as a sphere of radius globaldefs.EARTH_RADIUS.
Routine listings
dms_to_decimal - Degrees, minutes, seconds to decimal degrees
coordinates_of_named_location - Coordinates of a named site
density_prem - PREM density at a radius
matter_potential - Charged-current potential from a density
matter_potential_nc - Neutral-current potential, for a sterile state
distance_traveled_inside_earth - Chord length for a given costhz
earth_radial_distance_from_depth - Radius at a point on the chord
prem_layer_edges_along_chord - Where a chord crosses PREM shells
chord_length_inside_earth - Chord between two surface locations
costhz_between_points_on_surface - Its zenith angle
earth_slabs - Slab widths and densities along a chord
probabilities_2nu_earth - Two-flavor probabilities across the Earth
probabilities_3nu_earth - Three-flavor probabilities across the Earth
probabilities_4nu_earth - Four-flavor probabilities across the Earth
probabilities_2nu_between_locations - Between two named sites
probabilities_3nu_between_locations - Between two named sites
probabilities_4nu_between_locations - Between two named sites
- earth.LOC_COORDS_DMS = {'baikal': {'lat': (51, 45, 54), 'lon': (104, 24, 54)}, 'cern': {'lat': (46, 14, 1.8), 'lon': (6, 3, 11.4)}, 'desy': {'lat': (53, 34, 19.79), 'lon': (9, 52, 27.59)}, 'ess': {'lat': (55, 44, 6), 'lon': (13, 15, 5.04)}, 'fermilab': {'lat': (41, 49, 55), 'lon': (-88, 15, 26)}, 'gran_sasso': {'lat': (42, 25, 15.8), 'lon': (13, 30, 58.43)}, 'homestake': {'lat': (44, 21, 5.76), 'lon': (-103, 45, 4.68)}, 'kamioka': {'lat': (36, 25, 50.05), 'lon': (137, 18, 41.15)}, 'km3net_arca': {'lat': (36, 16, 0), 'lon': (16, 6, 0)}, 'km3net_orca': {'lat': (42, 48, 0), 'lon': (6, 2, 0)}, 'north_pole': {'lat': (90, 0, 0), 'lon': (0, 0, 0)}, 'pyhaasalmi': {'lat': (63, 39, 31), 'lon': (26, 2, 28)}, 'snolab': {'lat': (46, 28, 18), 'lon': (-81, 11, 12)}, 'south_pole': {'lat': (-90, 0, 0), 'lon': (0, 0, 0)}, 'tokai': {'lat': (36, 27, 59), 'lon': (140, 36, 24)}}
dict: Predefined locations, in ISO 6709 convention.
North latitudes and East longitudes are positive; South and West are negative. Each entry gives
latandlonas (degree, minute, second) tuples. The same set of sites as the sibling Magnus package, so a trajectory named in one can be reproduced in the other.
- earth.PREM_BOUNDARIES = array([1221.5, 3480. , 5701. , 5771. , 5971. , 6151. , 6346.6, 6356. , 6368. ])
numpy.ndarray: Outer radius of each PREM shell but the last.
The last shell ends at globaldefs.EARTH_RADIUS. Units: [km].
- earth.dms_to_decimal(degrees: int | float, minutes: int | float, seconds: int | float) float[source]
Returns a (degree, minute, second) coordinate in decimal degrees.
Added in version 1.8.0.
- Parameters:
- degreesint or float
Degree part of the coordinate. Carries the sign: a location at 5 degrees South is
(-5, ...). For a coordinate between zero and one degree South or West, where the degree part cannot carry a sign, negate the minutes instead: 0 deg 30’ S is(0, -30, 0).- minutesint or float
Minute part of the coordinate. Normally positive; a negative value flips the sign of the whole coordinate, which is the only way to express a southern or western coordinate smaller than one degree.
- secondsint or float
Second part of the coordinate, taken as positive.
- Returns:
- float
The coordinate in decimal degrees.
Examples
import earth print('%.6f' % earth.dms_to_decimal(36, 25, 50.05))
36.430569
- earth.coordinates_of_named_location(loc_name: str) Tuple[Tuple[float, float, float], Tuple[float, float, float]][source]
Returns the coordinates of a predefined location.
Looks
loc_nameup in LOC_COORDS_DMS, case-insensitively and treating spaces as underscores.Added in version 1.8.0.
- Parameters:
- loc_namestr
Name of the location, e.g.
'kamioka'or'south pole'.
- Returns:
- tuple of tuple of float
The latitude and longitude, each as (degree, minute, second).
- Raises:
- ValueError
If the name is not one of the predefined locations.
Examples
import earth lat, lon = earth.coordinates_of_named_location('South Pole') print(lat, lon)
(-90, 0, 0) (0, 0, 0)
- earth.density_prem(r: int | float | list | ndarray, tol: float = 1e-08) float | ndarray[source]
Returns the matter density inside the Earth, according to PREM.
Evaluates the Preliminary Reference Earth Model [DA81] at a radial distance measured from the centre of the Earth. Accepts a single radius or an array of radii, evaluated in one vectorised pass.
Added in version 1.8.0.
- Parameters:
- rint, float, list or numpy.ndarray
Radial distance(s) from the centre of the Earth, in units of km.
- tolfloat, optional
Relative tolerance by which a radius may exceed globaldefs.EARTH_RADIUS before a ValueError is raised. Radii within the tolerance are clamped onto the surface, which is what makes a chord endpoint computed in floating point safe to pass in. Default: 1e-8.
- Returns:
- float or numpy.ndarray
The matter density, in units of g cm-3.
- Raises:
- ValueError
If any radius exceeds globaldefs.EARTH_RADIUS by more than the relative tolerance, or if any radius is negative.
Examples
import earth print('%.4f' % earth.density_prem(0.0)) print('%.4f' % earth.density_prem(6371.0))
13.0885 1.0200
- earth.matter_potential(density: int | float | list | ndarray, electron_fraction: float = 0.5) float | ndarray[source]
Returns the charged-current matter potential for a density.
Returns \(V_{CC} = \sqrt{2} G_F n_e\), the potential that hamiltonians3nu.hamiltonian_3nu_matter and its two-flavor counterpart expect. It is positive for neutrinos; pass its negative for antineutrinos.
Added in version 1.8.0.
- Parameters:
- densityint, float, list or numpy.ndarray
Matter density, in units of g cm-3.
- electron_fractionfloat, optional
Electrons per nucleon. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST, which is 0.5 and is a good approximation everywhere in the Earth.
- Returns:
- float or numpy.ndarray
The potential \(V_{CC}\), in units of eV.
Examples
import earth print('%.4e' % earth.matter_potential(3.0))
1.1358e-13
- earth.matter_potential_nc(density: int | float | list | ndarray, neutron_fraction: float | None = None, electron_fraction: float = 0.5) float | ndarray[source]
Returns the neutral-current matter potential for a density.
Returns \(V_{NC} = -G_F n_n/\sqrt{2}\), which is negative for neutrinos. It is felt equally by all three active flavors, so at two and three flavors it is proportional to the identity and drops out of the probabilities entirely — which is why matter_potential alone serves them.
It does not drop out once a sterile state is present, because the sterile state does not feel it. Removing it from all four states costs only a global phase and leaves \(-V_{NC}\) on the sterile entry; see
hamiltonians4nu.hamiltonian_4nu_matter().Added in version 1.11.0.
- Parameters:
- densityint, float, list or numpy.ndarray
Matter density, in units of g cm-3.
- neutron_fractionfloat, optional
Neutrons per nucleon. Default:
1 - electron_fraction, the isoscalar value, since a nucleon is either a proton — matched by an electron — or a neutron.- electron_fractionfloat, optional
Electrons per nucleon, used only to derive neutron_fraction when that is not given. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- float or numpy.ndarray
The potential \(V_{NC}\), in units of eV. Negative for neutrinos.
Examples
import earth print('%.4e' % earth.matter_potential_nc(3.0))
-5.6791e-14
- earth.distance_traveled_inside_earth(costhz: int | float) float[source]
Returns the chord length through the Earth for a given direction.
The neutrino is assumed to arrive at a detector on the surface, not underground, so the distance is zero for any down-going direction,
costhz >= 0, which reaches the detector from the sky without entering the Earth at all.Added in version 1.8.0.
- Parameters:
- costhzint or float
Cosine of the zenith angle of the neutrino direction.
costhz = -1is straight up through the centre of the Earth.
- Returns:
- float
The chord length, in units of km.
- Raises:
- ValueError
If
costhzlies outside \([-1, 1]\), where it describes no direction.
Examples
import earth print('%.1f' % earth.distance_traveled_inside_earth(-1.0)) print('%.1f' % earth.distance_traveled_inside_earth(0.5))
12742.0 0.0
- earth.earth_radial_distance_from_depth(costhz: int | float, l: int | float | list | ndarray, tol: float = 1e-08) float | ndarray[source]
Returns the radius at a point along a chord through the Earth.
A neutrino with direction
costhztravels froml = 0at its point of entry tol =distance_traveled_inside_earth (costhz) at the detector. This returns its distance from the centre of the Earth at a givenl.Added in version 1.8.0.
- Parameters:
- costhzint or float
Cosine of the zenith angle of the neutrino direction.
- lint, float, list or numpy.ndarray
Distance(s) along the chord from the point of entry, in units of km.
- tolfloat, optional
Absolute tolerance, in km, by which
lmay exceed the chord length before a ValueError is raised; values within the tolerance are clamped onto the exit point. Default: 1e-8.
- Returns:
- float or numpy.ndarray
The radial distance from the centre of the Earth, in units of km.
- Raises:
- ValueError
If any
lis negative or exceeds the chord length for thiscosthzby more than the tolerance.
Examples
import earth print('%.1f' % earth.earth_radial_distance_from_depth(-1.0, 6371.0))
0.0
- earth.prem_layer_edges_along_chord(costhz: int | float) ndarray[source]
Returns where a chord through the Earth crosses PREM boundaries.
The density is discontinuous across a PREM shell boundary, so a slab that straddles one cannot represent it however finely the rest of the chord is divided. These positions are therefore mandatory slab edges, and earth_slabs uses them as such.
The crossings solve \(r(l) = r_b\) for each boundary radius \(r_b\), a quadratic in \(l\): with \(u = d - l\) and \(d = -2 R \cos\theta_z\),
\[u^2 + 2 R \cos\theta_z\, u + \left(R^2 - r_b^2\right) = 0 .\]Added in version 1.8.0.
- Parameters:
- costhzint or float
Cosine of the zenith angle of the neutrino direction. Crossings exist only for
costhz < 0.
- Returns:
- numpy.ndarray
Sorted crossing positions along the chord, in units of km, each strictly inside
(0, d). Empty if the chord crosses no boundary.
Examples
import earth print(len(earth.prem_layer_edges_along_chord(-1.0))) print(len(earth.prem_layer_edges_along_chord(0.5)))
18 0
- earth.chord_length_inside_earth(lat1_dms: Tuple[float, float, float], lon1_dms: Tuple[float, float, float], lat2_dms: Tuple[float, float, float], lon2_dms: Tuple[float, float, float]) float[source]
Returns the straight-line distance between two surface locations.
Computes the chord — the straight line through the Earth’s interior, not the great-circle arc over its surface — between two points on a spherical Earth, via the haversine formula for the central angle.
Added in version 1.8.0.
- Parameters:
- lat1_dmstuple of float
Latitude of the first location, as (degree, minute, second).
- lon1_dmstuple of float
Longitude of the first location, as (degree, minute, second).
- lat2_dmstuple of float
Latitude of the second location, as (degree, minute, second).
- lon2_dmstuple of float
Longitude of the second location, as (degree, minute, second).
- Returns:
- float
The chord length, in units of km.
Examples
import earth lat1, lon1 = earth.coordinates_of_named_location('fermilab') lat2, lon2 = earth.coordinates_of_named_location('homestake') print('%.1f' % earth.chord_length_inside_earth(lat1, lon1, lat2, lon2))
1284.7
- earth.costhz_between_points_on_surface(lat1_dms: Tuple[float, float, float], lon1_dms: Tuple[float, float, float], lat2_dms: Tuple[float, float, float], lon2_dms: Tuple[float, float, float]) float[source]
Returns the zenith angle of the chord between two locations.
The cosine of the zenith angle at which a neutrino must travel to reach the second location from the first through the Earth’s interior. Both are assumed to be on the surface, so the result is never positive.
Added in version 1.8.0.
- Parameters:
- lat1_dmstuple of float
Latitude of the first location, as (degree, minute, second).
- lon1_dmstuple of float
Longitude of the first location, as (degree, minute, second).
- lat2_dmstuple of float
Latitude of the second location, as (degree, minute, second).
- lon2_dmstuple of float
Longitude of the second location, as (degree, minute, second).
- Returns:
- float
Cosine of the zenith angle of the connecting chord.
Examples
import earth lat1, lon1 = earth.coordinates_of_named_location('cern') lat2, lon2 = earth.coordinates_of_named_location('gran_sasso') print('%.6f' % earth.costhz_between_points_on_surface(lat1, lon1, lat2, lon2))
-0.057179
- earth.earth_slabs(costhz: int | float, n_slabs_per_segment: int = 8) Tuple[ndarray, ndarray][source]
Returns the slab widths and densities along a chord.
Cuts the chord at every PREM shell boundary it crosses, divides each resulting segment into
n_slabs_per_segmentequal sub-slabs, and evaluates the density at the midpoint of each. The boundary cuts are what make the discretisation converge quickly: they keep every slab inside a single shell, where the density is smooth.Added in version 1.8.0.
- Parameters:
- costhzint or float
Cosine of the zenith angle of the neutrino direction. Must be negative, so that the neutrino crosses the Earth at all.
- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- Returns:
- tuple of numpy.ndarray
The slab widths, in units of km, and the density in each slab, in units of g cm-3, ordered along the trajectory.
- Raises:
- ValueError
If
costhz >= 0, so that there is no path through the Earth, or ifn_slabs_per_segmentis not positive.
Examples
import earth widths, densities = earth.earth_slabs(-1.0, n_slabs_per_segment=2) print(len(widths), '%.1f' % sum(widths))
38 12742.0
- earth.probabilities_2nu_earth(h_vacuum_energy_independent: list | ndarray, energy: int | float, costhz: int | float, n_slabs_per_segment: int = 8, electron_fraction: float = 0.5) Tuple[float, float, float, float][source]
Returns the two-flavor probabilities across the Earth.
Builds the PREM slabs along the chord for the given direction and propagates through them exactly, slab by slab.
Added in version 1.8.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent two-flavor vacuum Hamiltonian, as returned by hamiltonians2nu.hamiltonian_2nu_vacuum_energy_independent.
- energyint or float
Neutrino energy, in units of eV.
- costhzint or float
Cosine of the zenith angle of the neutrino direction. Must be negative.
- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- electron_fractionfloat, optional
Electrons per nucleon. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- tuple of float
The probabilities \(P_{ee}, P_{e\mu}, P_{\mu e}, P_{\mu\mu}\).
- Raises:
- ValueError
If
costhz >= 0orn_slabs_per_segmentis not positive.
- earth.probabilities_3nu_earth(h_vacuum_energy_independent: list | ndarray, energy: int | float, costhz: int | float, n_slabs_per_segment: int = 8, electron_fraction: float = 0.5) Tuple[float, float, float, float, float, float, float, float, float][source]
Returns the three-flavor probabilities across the Earth.
Builds the PREM slabs along the chord for the given direction and propagates through them exactly, slab by slab. Raising
n_slabs_per_segmentand watching the result settle is the way to confirm the discretisation is fine enough for the energy in question; the number needed grows as the oscillation length falls.Added in version 1.8.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent three-flavor vacuum Hamiltonian, as returned by hamiltonians3nu.hamiltonian_3nu_vacuum_energy_independent.
- energyint or float
Neutrino energy, in units of eV.
- costhzint or float
Cosine of the zenith angle of the neutrino direction. Must be negative.
- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- electron_fractionfloat, optional
Electrons per nucleon. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- tuple of float
The nine probabilities, with the initial flavor varying slowest.
- Raises:
- ValueError
If
costhz >= 0orn_slabs_per_segmentis not positive.
- earth.probabilities_4nu_earth(h_vacuum_energy_independent: list | ndarray, energy: int | float, costhz: int | float, n_slabs_per_segment: int = 8, electron_fraction: float = 0.5) Tuple[float, ...][source]
Returns the four-flavor probabilities across the Earth.
Builds the PREM slabs along the chord for the given direction and propagates through them exactly, slab by slab, exactly as at two and three flavors. The one thing that is new is the potential: a sterile state does not feel the neutral-current interaction, so \(V_{NC}\) no longer cancels between the flavors and is built per slab alongside \(V_{CC}\). This is what puts the sterile matter resonance where it belongs; see
hamiltonians4nu.hamiltonian_4nu_matter().Added in version 1.11.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent four-flavor vacuum Hamiltonian, as returned by hamiltonians4nu.hamiltonian_4nu_vacuum_energy_independent.
- energyint or float
Neutrino energy, in units of eV.
- costhzint or float
Cosine of the zenith angle of the neutrino direction. Must be negative.
- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- electron_fractionfloat, optional
Electrons per nucleon. The neutron fraction is taken as its complement, which is what sets \(V_{NC}\). Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- tuple of float
The sixteen probabilities, with the initial flavor varying slowest. With the fourth state read as sterile, the flavor order is \((\nu_e, \nu_\mu, \nu_\tau, \nu_s)\).
- Raises:
- ValueError
If
costhz >= 0orn_slabs_per_segmentis not positive.
- earth.probabilities_2nu_between_locations(h_vacuum_energy_independent: list | ndarray, energy: int | float, loc_name_1: str, loc_name_2: str, n_slabs_per_segment: int = 8, electron_fraction: float = 0.5) Tuple[float, float, float, float][source]
Returns the two-flavor probabilities between two named locations.
Convenience wrapper: looks both locations up in LOC_COORDS_DMS, finds the zenith angle of the chord joining them, and evaluates probabilities_2nu_earth along it.
Added in version 1.8.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent two-flavor vacuum Hamiltonian.
- energyint or float
Neutrino energy, in units of eV.
- loc_name_1str
Name of the source location, e.g.
'fermilab'.- loc_name_2str
Name of the detector location, e.g.
'homestake'.- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- electron_fractionfloat, optional
Electrons per nucleon. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- tuple of float
The probabilities \(P_{ee}, P_{e\mu}, P_{\mu e}, P_{\mu\mu}\).
- Raises:
- ValueError
If either name is not predefined, or if the two locations coincide, so that there is no chord between them.
- earth.probabilities_3nu_between_locations(h_vacuum_energy_independent: list | ndarray, energy: int | float, loc_name_1: str, loc_name_2: str, n_slabs_per_segment: int = 8, electron_fraction: float = 0.5) Tuple[float, float, float, float, float, float, float, float, float][source]
Returns the three-flavor probabilities between two named locations.
Convenience wrapper: looks both locations up in LOC_COORDS_DMS, finds the zenith angle of the chord joining them, and evaluates probabilities_3nu_earth along it.
Added in version 1.8.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent three-flavor vacuum Hamiltonian.
- energyint or float
Neutrino energy, in units of eV.
- loc_name_1str
Name of the source location, e.g.
'cern'.- loc_name_2str
Name of the detector location, e.g.
'gran_sasso'.- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- electron_fractionfloat, optional
Electrons per nucleon. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- tuple of float
The nine probabilities, with the initial flavor varying slowest.
- Raises:
- ValueError
If either name is not predefined, or if the two locations coincide, so that there is no chord between them.
- earth.probabilities_4nu_between_locations(h_vacuum_energy_independent: list | ndarray, energy: int | float, loc_name_1: str, loc_name_2: str, n_slabs_per_segment: int = 8, electron_fraction: float = 0.5) Tuple[float, ...][source]
Returns the four-flavor probabilities between two named locations.
Convenience wrapper: looks both locations up in LOC_COORDS_DMS, finds the zenith angle of the chord joining them, and evaluates probabilities_4nu_earth along it.
Added in version 1.11.0.
- Parameters:
- h_vacuum_energy_independentarray_like
Energy-independent four-flavor vacuum Hamiltonian.
- energyint or float
Neutrino energy, in units of eV.
- loc_name_1str
Name of the source location, e.g.
'cern'.- loc_name_2str
Name of the detector location, e.g.
'gran_sasso'.- n_slabs_per_segmentint, optional
Number of equal sub-slabs per chord segment. A segment runs between consecutive PREM boundary crossings; a chord crosses most shells twice, so there are more segments than shells. Default: 8.
- electron_fractionfloat, optional
Electrons per nucleon. Default: globaldefs.ELECTRON_FRACTION_EARTH_CRUST.
- Returns:
- tuple of float
The sixteen probabilities, with the initial flavor varying slowest.
- Raises:
- ValueError
If either name is not predefined, or if the two locations coincide, so that there is no chord between them.
Optional compiled backend
Optional Numba-compiled kernels for the batched evaluation paths.
NuOscProbExact needs only NumPy. If Numba
happens to be installed, this module compiles the two-, three- and
four-neutrino expansions into fused machine-code loops and
oscprob2nu, oscprob3nu and oscprob4nu use them for
large stacks; if it is not, HAVE_NUMBA is False, nothing here is
defined, and the NumPy path is used instead. Nothing else in the
library changes either way, and the results agree to round-off — see
tests/test_fastkernels.py, which runs both paths against each other
whichever is available.
Install the optional dependency with:
pip install "nuoscprobexact[fast]"
Why it is worth compiling
The NumPy path evaluates the expansion as a sequence of whole-array operations, so a stack of N Hamiltonians makes roughly fifteen passes over N-element arrays, each writing a temporary that the next pass reads back. The compiled kernel does the same arithmetic one element at a time, keeping every intermediate in registers, and spreads the elements over the available cores. Measured against the NumPy path on this library’s own benchmarks, best of seven runs with the two paths interleaved:
Stack |
Speedup |
|---|---|
200 000 energies, four flavors |
~19x |
20 000 energies, four flavors |
~18x |
200 000 energies, three flavors |
~15x |
20 000 energies, three flavors |
~9x |
100 x 100 oscillogram |
~3.5x |
200 000 baselines, two flavors |
~1.5x |
Four flavors gains the most, and not because the kernel is cleverer there: the NumPy path has the furthest to fall. Its expansion needs a quartic, a Newton refinement of the four roots against the matrix, and a Newton-form reconstruction, which as whole-array operations is some forty passes over the stack; done one element at a time none of it leaves the registers.
These are one machine on one day, and they move by tens of per cent
between runs; read them as the shape of the gain rather than as
constants. The figures quoted for 1.6.0 in CHANGELOG.md came from a
different session and differ by up to a factor of two — which is why
notebook 09 measures the comparison when it runs, on whatever machine
is running it, rather than repeating a number from here.
Costs, so that the trade is visible
importing Numba takes about 140 ms, against 65 ms for NumPy alone;
the first call compiles, which takes a few seconds. The kernels are declared with
cache=True, so that cost is paid once per machine and later runs load the compiled code from disk in milliseconds.
Both are why this is an optional extra rather than a dependency, and why the scalar path is deliberately left alone: a single probability takes about 8 microseconds, which is not worth a compilation pause.
Turning it off
Set fastkernels.USE_NUMBA = False to force the NumPy path even when
Numba is installed — useful for checking that the two agree, which is
what the test suite does.
Routine listings
available - Whether the compiled kernels can be used at all
worthwhile - Whether a stack is large enough to be worth compiling
probabilities_2nu_kernel - Two-flavor probabilities for a stack
probabilities_3nu_kernel - Three-flavor probabilities for a stack
probabilities_4nu_kernel - Four-flavor probabilities for a stack
- fastkernels.USE_NUMBA = True
bool: Module-level switch.
Set to
Falseto force the NumPy path even when Numba is installed. available reports the two together.
- fastkernels.MIN_BATCH = {2: 50000, 3: 1, 4: 1}
dict: Module-level constant.
The smallest stack for which the compiled kernel is worth using, by number of flavors. A backend that is sometimes slower than the path it replaces is worse than no backend, so these are measured rather than assumed.
For three flavors the kernel wins at every size, by between two and sixteen times, so the threshold is one. Four flavors is the same story only more so, and for a reason worth stating: the NumPy path there has no short-stack shortcut to fall back on —
oscprob4nuhas no separate scalar closed form, so even a stack of one pays for the whole array machinery, a batched determinant and all. Measured by alternating the two paths throughoscprob4nu.probabilities_4nu()and taking the best of nine rounds each, the kernel leads by 15x at a single element, falls to 5x just below PARALLEL_THRESHOLD where it is still single-threaded, and settles at 18x once the threads are in use. It is never behind, so the threshold is one.For two flavors it does not: that expansion reduces to a square root and a sine per element, which NumPy already does about as well as compiled code can, and the kernel additionally has to materialise the Hamiltonian stack — which for a scan over baselines is the same matrix repeated, costing 2.5 ms to copy at two hundred thousand points. Measured by alternating the two paths and taking the best of nine rounds each, the crossover sits between twenty and fifty thousand elements: at twenty thousand NumPy is still ahead by a few per cent, at fifty thousand the kernel leads by 1.3x and it grows slowly from there. The threshold is set at the first size where the kernel is unambiguously ahead, since the region around the crossover is broad and varies between machines.
- fastkernels.PARALLEL_THRESHOLD = 256
int: Module-level constant.
Stacks with at least this many elements are spread over the available cores; smaller ones run in a single thread, because below roughly this size the cost of waking the thread pool exceeds what it saves.
- fastkernels.available() bool[source]
Returns whether the compiled kernels can be used at all.
True when Numba was imported successfully and USE_NUMBA has not been turned off. Whether they are worth using for a given stack is a separate question; see worthwhile.
Added in version 1.6.0.
- Returns:
- bool
Whether probabilities_2nu_kernel, probabilities_3nu_kernel and probabilities_4nu_kernel may be called.
- fastkernels.worthwhile(n_flavors: int, size: int) bool[source]
Returns whether the compiled kernel should be used for a stack.
The kernels are only used where they have been measured to win. Below the per-flavor threshold in MIN_BATCH the NumPy path is quicker, and using the kernel anyway would make installing the optional extra a pessimisation for those calls.
Added in version 1.6.0.
- Parameters:
- n_flavorsint
Number of neutrino flavors, 2, 3, or 4.
- sizeint
Number of elements in the stack.
- Returns:
- bool
Whether to call the corresponding kernel.
Constants
Physical constants, unit-conversion factors, and the NuFit 4.0 best-fit oscillation parameters [EGGHC+19] for both mass orderings. The core modules do not need any of these. The sample non-standard-interaction strengths are deliberately large, so that the worked examples show a visible effect; the combination matter oscillations are sensitive to puts them in the LMA-D region [CDGG+17] rather than anywhere near a fit.
globaldefs
Physical constants, unit-conversion factors, and default parameters.
This module contains the values of the physical constants, the
unit-conversion factors, and the default oscillation parameters used by
the sample-Hamiltonian modules of NuOscProbExact, by earth,
and by the notebooks and worked examples.
The core modules oscprob2nu, oscprob3nu and
oscprob4nu do not need these constants: they accept an
arbitrary Hermitian Hamiltonian in whatever units the user prefers.
Unless stated otherwise, quantities are expressed in natural units, in which energies are in eV, mass-squared differences in eV2, and baselines in eV-1.
Notes
The lepton mixing parameters are the best-fit values from NuFit 4.0 [1],
including Super-Kamiokande atmospheric data, for both the normal
ordering (suffix _NO_BF) and the inverted ordering (suffix
_IO_BF). The non-standard-interaction strengths are taken from
[2]. These constants support the method of [3].
References
I. Esteban et al., “Global analysis of three-flavour neutrino oscillations”, JHEP 01, 106 (2019), arXiv:1811.05487 (NuFit 4.0).
P. Coloma et al., “Curtailing the dark side in non-standard neutrino interactions”, arXiv:1805.04530.
Mauricio Bustamante, “Exact neutrino oscillation probabilities with arbitrary time-independent Hamiltonians”, arXiv:1904.12391.
- globaldefs.CONV_KM_TO_INV_EV = 5067730000.0
float: Multiplicative conversion factor from km to eV-1.
Units: [eV-1 km-1].
- globaldefs.CONV_CM_TO_INV_EV = 50677.3
float: Multiplicative conversion factor from cm to eV-1.
Units: [eV-1 cm-1].
- globaldefs.CONV_INV_EV_TO_CM = 1.9732700834495916e-05
float: Multiplicative conversion factor from eV-1 to cm.
Units: [eV cm].
- globaldefs.CONV_EV_TO_G = 1.78266192e-33
float: Multiplicative conversion factor from eV to grams.
Converts a mass expressed in eV (i.e., eV/c2) into grams. Units: [g eV-1].
Changed in version 1.11.0: Given to the precision of the CODATA value, 1.78266192e-33, rather than rounded to 1.783e-33. The rounded value was off by \(1.9 \times 10^{-4}\) relative, three orders of magnitude worse than every other constant in this module, which sit between \(10^{-7}\) and \(10^{-9}\). It propagates through NUM_DENSITY_E_EARTH_CRUST into VCC_EARTH_CRUST, and through
earth.matter_potential()into every Earth crossing, so the matter potential moved by that much. Far below anything measurable, and still worth not carrying into a release.
- globaldefs.CONV_G_TO_EV = 5.609588608927037e+32
float: Multiplicative conversion factor from grams to eV.
Units: [eV g-1].
- globaldefs.GF = 1.1663787e-23
float: Fermi constant.
Units: [eV-2].
- globaldefs.MASS_ELECTRON = 510998.9461
float: Electron mass.
Units: [eV].
- globaldefs.MASS_PROTON = 938272046.0
float: Proton mass.
Units: [eV].
- globaldefs.MASS_NEUTRON = 939565379.0
float: Neutron mass.
Units: [eV].
- globaldefs.EARTH_RADIUS = 6371.0
float: Mean radius of the Earth.
The IUGG mean radius, which is what the Preliminary Reference Earth Model in
earthis normalised against and what the chord geometry there assumes. The Earth is treated as a sphere throughout; the equatorial and polar radii differ from this by about 0.3%, which is far below the accuracy of any matter-density model.Units: [km].
- globaldefs.ELECTRON_FRACTION_EARTH_CRUST = 0.5
float: Electron fraction in the Earth’s crust.
Units: [adimensional].
- globaldefs.DENSITY_MATTER_CRUST_G_PER_CM3 = 3.0
float: Average matter density in the Earth’s crust.
Units: [g cm-3].
- globaldefs.NUM_DENSITY_E_EARTH_CRUST = 6885791558.458149
float: Electron number density in the Earth’s crust.
Units: [eV3].
- globaldefs.VCC_EARTH_CRUST = np.float64(1.1358172231000782e-13)
float: Charged-current matter potential in the Earth’s crust.
Equal to \(\sqrt{2} G_F n_e\). It is positive for neutrinos; use its negative for antineutrinos. Units: [eV].
- globaldefs.NEUTRON_FRACTION_EARTH_CRUST = 0.5
float: Neutrons per nucleon in the Earth’s crust.
The crust is close to isoscalar, so with an electron fraction of one half there is about one neutron per electron. Units: [adimensional].
- globaldefs.NUM_DENSITY_N_EARTH_CRUST = 6885791558.458149
float: Neutron number density in the Earth’s crust.
Units: [eV3].
- globaldefs.VNC_EARTH_CRUST = np.float64(-5.6790861155003904e-14)
float: Neutral-current matter potential in the Earth’s crust.
Equal to \(-G_F n_n/\sqrt{2}\), and negative for neutrinos. It is felt equally by all three active flavors, so at three flavors it is proportional to the identity and drops out of the probabilities entirely — which is why
hamiltonians3nunever needs it.It matters as soon as a sterile state is present, because the sterile state does not feel it: subtracting it from all four states, which costs only a global phase, leaves \(-V_{NC}\) on the sterile entry. See
hamiltonians4nu.hamiltonian_4nu_matter().Added in version 1.9.0.
Units: [eV].
- globaldefs.S12_NO_BF = np.float64(0.5567764362830022)
float: Lepton mixing angle \(\sin\theta_{12}\), normal ordering.
Units: [adimensional].
- globaldefs.S23_NO_BF = np.float64(0.762889244910426)
float: Lepton mixing angle \(\sin\theta_{23}\), normal ordering.
Units: [adimensional].
- globaldefs.S13_NO_BF = np.float64(0.14966629547095767)
float: Lepton mixing angle \(\sin\theta_{13}\), normal ordering.
Units: [adimensional].
- globaldefs.DCP_NO_BF = 3.787364476827695
float: CP-violation phase \(\delta_{\rm CP}\), normal ordering.
Units: [radian].
- globaldefs.D21_NO_BF = 7.39e-05
float: Mass-squared difference \(\Delta m^2_{21}\), normal ordering.
Units: [eV2].
- globaldefs.D31_NO_BF = 0.002525
float: Mass-squared difference \(\Delta m^2_{31}\), normal ordering.
Units: [eV2].
- globaldefs.S12_IO_BF = np.float64(0.5567764362830022)
float: Lepton mixing angle \(\sin\theta_{12}\), inverted ordering.
Units: [adimensional].
- globaldefs.S23_IO_BF = np.float64(0.762889244910426)
float: Lepton mixing angle \(\sin\theta_{23}\), inverted ordering.
Units: [adimensional].
- globaldefs.S13_IO_BF = np.float64(0.15043270920913443)
float: Lepton mixing angle \(\sin\theta_{13}\), inverted ordering.
Units: [adimensional].
- globaldefs.DCP_IO_BF = 4.886921905584122
float: CP-violation phase \(\delta_{\rm CP}\), inverted ordering.
Units: [radian].
- globaldefs.D21_IO_BF = 7.39e-05
float: Mass-squared difference \(\Delta m^2_{21}\), inverted ordering.
Units: [eV2].
- globaldefs.D32_IO_BF = -0.002512
float: Mass-squared difference \(\Delta m^2_{32}\), inverted ordering.
Units: [eV2].
- globaldefs.D31_IO_BF = -0.0024381
float: Mass-squared difference \(\Delta m^2_{31}\), inverted ordering.
Computed as \(\Delta m^2_{32} + \Delta m^2_{21}\). Units: [eV2].
- globaldefs.EPS_EE = 0.06
float: NSI strength parameter \(\epsilon_{ee}\).
Units: [adimensional].
- globaldefs.EPS_EM = -0.06
float: NSI strength parameter \(\epsilon_{e\mu}\).
May in general be complex. Units: [adimensional].
- globaldefs.EPS_ET = 0.0
float: NSI strength parameter \(\epsilon_{e\tau}\).
May in general be complex. Units: [adimensional].
- globaldefs.EPS_MM = 1.2
float: NSI strength parameter \(\epsilon_{\mu\mu}\).
Units: [adimensional].
- globaldefs.EPS_MT = 0.0
float: NSI strength parameter \(\epsilon_{\mu\tau}\).
May in general be complex. Units: [adimensional].
- globaldefs.EPS_TT = 0.0
float: NSI strength parameter \(\epsilon_{\tau\tau}\).
Units: [adimensional].
- globaldefs.EPS_2 = [0.06, -0.06, 1.2]
list of float: NSI strengths for two-neutrino oscillations.
Ordered as
[eps_ee, eps_em, eps_mm], ready to be passed tohamiltonians2nu.hamiltonian_2nu_nsi(). Units: [adimensional].
- globaldefs.EPS_3 = [0.06, -0.06, 0.0, 1.2, 0.0, 0.0]
list of float: NSI strengths for three-neutrino oscillations.
Ordered as
[eps_ee, eps_em, eps_et, eps_mm, eps_mt, eps_tt], ready to be passed tohamiltonians3nu.hamiltonian_3nu_nsi(). Units: [adimensional].
- globaldefs.SXI12 = 0.0
float: LIV mixing angle \(\sin\xi_{12}\).
Units: [adimensional].
- globaldefs.SXI23 = 0.0
float: LIV mixing angle \(\sin\xi_{23}\).
Units: [adimensional].
- globaldefs.SXI13 = 0.0
float: LIV mixing angle \(\sin\xi_{13}\).
Units: [adimensional].
- globaldefs.DXICP = 0.0
float: LIV CP-violation phase \(\delta_{\xi,\rm CP}\).
Units: [radian].
- globaldefs.B1 = 1e-09
float: Eigenvalue \(b_1\) of the LIV operator.
Units: [eV].
- globaldefs.B2 = 1e-09
float: Eigenvalue \(b_2\) of the LIV operator.
Units: [eV].
- globaldefs.B3 = 2e-09
float: Eigenvalue \(b_3\) of the LIV operator.
Units: [eV].
- globaldefs.LAMBDA = 1000000000000.0
float: Energy scale \(\Lambda\) of the LIV operator.
Units: [eV].