# -*- coding: utf-8 -*-
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Mauricio Bustamante
"""oscprob.py
Contains routines to compute the neutrino oscillation probability.
Internally, the probability is computed using Magnus expansion, but the
user does not call the routines in the :py:mod:`magnus.magnus` module
directly. Instead, the user calls the :func:`osc_prob`, which calls the
Magnus expansion routines internally. The function :func:`osc_prob` is
generic, flexible, and computationally efficient.
- :func:`osc_prob`: Primordial function to compute the oscillation
probability, for any given Hamiltonian, either time-dependent or
-independent (or, equivalently, position-dependent or -independent).
Supports arbitrary number of neutrino flavors.
The module contains additional functions that are wrappers of
:func:`osc_prob` to compute commonly studied cases.
Neutrino oscillations in **vacuum**:
- :func:`osc_prob_2nu_vacuum`: Two-neutrino oscillation probabilities.
- :func:`osc_prob_3nu_vacuum`: Three-flavor oscillation probabilities.
- :func:`osc_prob_4nu_vacuum`: One
additional flavor (i.e., 3+1 sterile neutrino model).
- :func:`osc_prob_5nu_vacuum`: Two
additional flavors (i.e., 3+2 sterile neutrino model).
Neutrino oscillations in **constant-density matter**:
- :func:`osc_prob_2nu_matter_constant_density`: Two-neutrino oscillation
probabilities.
- :func:`osc_prob_3nu_matter_constant_density`: Three-neutrino
oscillation probabilities.
- :func:`osc_prob_4nu_matter_constant_density`: One additional flavor
(i.e., 3+1 sterile model).
- :func:`osc_prob_5nu_matter_constant_density`: Two additional flavors
(i.e., 3+2 sterile model).
Neutrino oscillations in **exponentially falling matter density
profile** (e.g., in a supernova or the Sun):
- :func:`osc_prob_2nu_matter_exp_density`: Two-neutrino oscillation
probabilities.
- :func:`osc_prob_3nu_matter_exp_density`: Three-neutrino oscillation
probabilities.
- :func:`osc_prob_4nu_matter_exp_density`: One additional flavor. Matter
potential affects only :math:`\\nu_e`.
- :func:`osc_prob_5nu_matter_exp_density`: Two additional flavors.
Matter potential affects only :math:`\\nu_e`.
Neutrino oscillations through the **Earth**, useful for long-baseline
neutrino experiments. The trajectory runs between two locations on the
surface by default; ``source_depth`` and ``detector_depth`` put either end
of it underground:
- :func:`osc_prob_2nu_earth`: Two-neutrino oscillation probabilities.
- :func:`osc_prob_3nu_earth`: Three-neutrino oscillation probabilities.
- :func:`osc_prob_4nu_earth`: One additional flavor. Matter potential
affects only :math:`\\nu_e`.
- :func:`osc_prob_5nu_earth`: Two additional flavors. Matter potential
affects only :math:`\\nu_e`.
- :func:`osc_prob_earth`: Oscillation probabilities for arbitrary number
of flavors and arbitrary Hamiltonian. Does not assume standard
oscillations.
.. note::
These routines use the `Preliminary Reference Earth Model
<https://www.cfa.harvard.edu/~lzeng/papers/PREM.pdfL>`_ for the
matter density profile inside Earth. To use a different density
profile (including also profiles for bodies other than the Earth),
use the primordial function :func:`osc_prob` instead.
Neutrino oscillations in the **Sun**:
- :func:`osc_prob_2nu_sun`: Two-neutrino oscillation probabilities.
- :func:`osc_prob_3nu_sun`: Three-neutrino oscillation probabilities.
- :func:`osc_prob_4nu_sun`: One additional flavor. Matter potential
affects only :math:`\\nu_e`.
- :func:`osc_prob_5nu_sun`: Two additional flavors. Matter potential
affects only :math:`\\nu_e`.
- :func:`osc_prob_sun`: Oscillation probabilities for arbitrary number
of flavors and arbitrary Hamiltonian. Does not assume standard
oscillations.
.. note::
These routines use a simple exponentially falling function of radial
distance for the matter density inside the Sun: :math:`N_e(r) =
N_e(0) \\exp(-r/r_0)`, with
:math:`N_e(0) = 245 N_\\text{Av}~\\text{cm}^{-3}` and
:math:`r_0 = R_\\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
To use a different density profile, use the primordial function
:func:`osc_prob` instead.
Functions designed for specific **beyond-the-Standard-Model** proposals:
- Non-standard neutrino interactions (NSI):
- :func:`osc_prob_2nu_matter_nsi_constant_density`
- :func:`osc_prob_3nu_matter_nsi_constant_density`
- :func:`osc_prob_4nu_matter_nsi_constant_density`
- :func:`osc_prob_5nu_matter_nsi_constant_density`
- :func:`osc_prob_2nu_matter_nsi_exp_density`
- :func:`osc_prob_3nu_matter_nsi_exp_density`
- :func:`osc_prob_4nu_matter_nsi_exp_density`
- :func:`osc_prob_5nu_matter_nsi_exp_density`
- :func:`osc_prob_2nu_earth_nsi`
- :func:`osc_prob_3nu_earth_nsi`
- :func:`osc_prob_4nu_earth_nsi`
- :func:`osc_prob_5nu_earth_nsi`
- :func:`osc_prob_2nu_sun_nsi`
- :func:`osc_prob_3nu_sun_nsi`
- :func:`osc_prob_4nu_sun_nsi`
- :func:`osc_prob_5nu_sun_nsi`
- Lorentz-invariance violation:
- :func:`osc_prob_2nu_matter_liv_constant_density`
- :func:`osc_prob_3nu_matter_liv_constant_density`
- :func:`osc_prob_4nu_matter_liv_constant_density`
- :func:`osc_prob_5nu_matter_liv_constant_density`
- :func:`osc_prob_2nu_matter_liv_exp_density`
- :func:`osc_prob_3nu_matter_liv_exp_density`
- :func:`osc_prob_4nu_matter_liv_exp_density`
- :func:`osc_prob_5nu_matter_liv_exp_density`
- :func:`osc_prob_2nu_earth_liv`
- :func:`osc_prob_3nu_earth_liv`
- :func:`osc_prob_4nu_earth_liv`
- :func:`osc_prob_5nu_earth_liv`
- :func:`osc_prob_2nu_sun_liv`
- :func:`osc_prob_3nu_sun_liv`
- :func:`osc_prob_4nu_sun_liv`
- :func:`osc_prob_5nu_sun_liv`
Examples
--------
.. seealso::
Find many more examples, including advanced applications and plots,
in the `Jupyter notebooks
<https://github.com/mbustama/Magnus/tree/main/notebooks>`_ that are
distributed with :math:`{\\rm Mag}{\\nu}s`.
The code blocks below run when these docs are built, so the output shown
is always current.
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
np.set_printoptions(precision=3)
# Warnings are normally prefixed with an ANSI-colored "Warning:", which is
# meant for a terminal and renders as escape-code noise in HTML. These docs
# therefore switch to plain text; in a terminal, leave this alone.
gd.set_color_output(False)
Calling :func:`osc_prob_3nu_vacuum` returns a :math:`3 \\times 3` NumPy array
of probabilities whose entry ``[i][j]`` is the probability of a neutrino
produced with flavor ``i`` being detected with flavor ``j``
For a single neutrino energy and baseline:
.. jupyter-execute::
baseline = 10.0 * gd.UNIT_KM # 10 km in natural units [eV^-1]
energy = 1.0 * gd.UNIT_MEV # [eV]
oscprob.osc_prob_3nu_vacuum(energy, baseline)
The probabilities returned by :func:`osc_prob_3nu_vacuum` (and also
:func:`osc_prob_2nu_vacuum`,
:func:`osc_prob_2nu_matter_constant_density`, and
:func:`osc_prob_3nu_matter_constant_density`) are returned with machine
(or NumPy) precision, since first-order Magnus expansion is enough to
compute them.
Pick one channel only, e.g., :math:`\\nu_e \\to \\nu_\\mu`, by passing
an initial flavor, ``nu_i``, and a final flavor ``nu_f``:
.. jupyter-execute::
oscprob.osc_prob_3nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUMU)
The flavor indices ``NUE``, ``NUMU``, and ``NUTAU`` are defined in the
:py:mod:`magnus.globaldefs` module. For anti-neutrinos, i.e.,
:math:`\\bar{\\nu}_e \\to \\bar{\\nu}_\\mu`:
.. jupyter-execute::
oscprob.osc_prob_3nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUMU,
nubar=True)
Calling :func:`osc_prob_3nu_vacuum` without specifying the values of the
oscillation parameters will compute probabilities using the default
values in :math:`{\\rm Mag}{\\nu}s` (see
``gd.OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']``.)
We can specify values of the oscillation parameters. Unspecified values
are set to their defaults (pass nonzero ``verbose`` to see this and
other warnings):
.. jupyter-execute::
oscprob.osc_prob_3nu_vacuum(energy, baseline, s12=0.0, verbose=1)
Fixed energy, multiple baselines:
.. jupyter-execute::
baselines = gd.UNIT_KM * np.array([1.0, 10.0, 100.0])
oscprob.osc_prob_3nu_vacuum(energy, baselines, nu_i=gd.NUE, nu_f=gd.NUMU)
Fixed baseline, multiple energies:
.. jupyter-execute::
energies = gd.UNIT_MEV * np.array([1.0, 10.0, 100.0])
oscprob.osc_prob_3nu_vacuum(energies, baseline, nu_i=gd.NUE, nu_f=gd.NUMU)
Multiple energies and baselines:
.. jupyter-execute::
oscprob.osc_prob_3nu_vacuum(energies, baselines, nu_i=gd.NUE, nu_f=gd.NUMU)
To compute the oscillation probabilities in constant-density matter, we
need to specify the matter density, ``rho``, i.e.,
.. jupyter-execute::
rho = 10.0 * gd.UNIT_G_PER_CM3
oscprob.osc_prob_3nu_matter_constant_density(energy, baseline, rho,
nu_i=gd.NUE, nu_f=gd.NUMU)
To compute oscillation probabilities for a time-dependent Hamiltonian,
we need to call :func:`osc_prob` directly which, while still
straightforward, requires us to pass a Hamiltonian function explicitly.
For instance, for density matter profile that is exponentially falling
with distance:
.. hint::
There is a good chance that the scenario you are interested in
calculating was already developed in the :math:`{\\rm Mag}{\\nu}s`
`Jupyter
notebooks
<https://github.com/mbustama/Magnus/tree/main/notebooks>`_.
Worked-out examples include: oscillations in various matter density
profiles, in the Earth, and in the Sun, oscillograms, biprobability
plots, and new-physics models like additional neutrino flavors (3+1
and 3+2 sterile neutrino models), non-standard neutrino interactions,
and Lorentz-invariance violation.
See :doc:`/architecture` for how the ``osc_prob_*`` functions listed above
are layered internally (primordial/middle/wrapper) and how to add a new one.
"""
__author__ = 'Mauricio Bustamante'
import functools
import numpy as np
import sys
import warnings
import weakref
from contextlib import contextmanager
from joblib import Parallel, delayed
from typing import Optional, Callable, Union, Tuple, Dict
from io import TextIOWrapper
from inspect import signature
# import numba as nb
# import numpy.typing
import magnus.magnus as magnus
import magnus.globaldefs as gd
import magnus.hamiltonians as hamiltonians
from magnus.hamiltonians import _angles
import magnus.matter as matter
import magnus.earth as earth
import magnus.adiabatic as adiabatic
import magnus.avgprob as avgprob
import magnus.solarmodels as solarmodels
from magnus import version
from magnus import authors
has_magnus_header_been_printed = False
[docs]
MAX_N_SLABS_DEFAULT = {'gl': 20000, 'trapezoid': 2000, 'simpson': 2000}
r"""dict: Module-level constant
Default cap on the number of slabs, per ``integration_method``, used when
``max_n_slabs`` is left as None.
The cap exists to bound cost, and cost per slab differs by more than an order of
magnitude between the two families of integrators. ``'gl'`` evaluates the Hamiltonian
1, 2, 3, or 4 times per slab (set by the expansion order), while ``'trapezoid'`` and
``'simpson'`` evaluate it ``n_tpts_per_slab`` times (100 by default, up to 500). A
single cap tuned for one family therefore starves the other: at 2000 slabs -- the
quadrature cap, unchanged here -- ``'gl'`` was hitting the ceiling on cases it could
resolve comfortably, and reporting that it could not verify convergence, on answers
that were in fact far more accurate than the quadrature methods managed within their
own cap.
The ``'gl'`` value is set from both directions. The hardest case in the validation
suite (5 flavors, eV-scale sterile splittings, Earth-crossing baseline) converges at
about 8,600 slabs, so 20000 leaves better than a factor of two of headroom. At the
same time, 20000 slabs at 2-4 nodes is roughly 40,000-80,000 Hamiltonian evaluations,
still well under the ~200,000 that 2000 quadrature slabs at 100 points per slab
already permit -- so the more generous cap is also the cheaper worst case.
Passing ``max_n_slabs`` explicitly always wins; this is only the fallback.
.. versionadded:: 1.0.0
"""
[docs]
IP_EXP_N_SLABS_CAP = 2_000_000
r"""int: Module-level constant
Slab ceiling for the closed-form interaction-picture integrator
(``_osc_prob_ip_exp_core``), deliberately decoupled from the caller's
``max_n_slabs``, which is calibrated for the far more expensive quadrature slabs of
the general method. Each slab here costs one 2x2 eigendecomposition, so the ceiling
is affordable in *time*.
It is not, on its own, affordable in *memory*: the integrator works on arrays of
shape ``(n_energies, n_slabs, d, d)``, so the working set scales with the number of
energies as well as the slab count, and at this ceiling it reached ~1.3 GB **per
energy** -- enough for a batched solar call to exhaust the machine. The working set
is now tiled to :data:`BATCH_WORKING_ENTRIES` independently of both, so this ceiling
bounds only time. See ``docs/dev/BUG_IP_EXP_MEMORY.md``.
Named rather than inlined so that the method's give-up behavior at the ceiling can
be exercised by a test at a small cap: reaching two million slabs to check what
happens at the boundary would cost minutes, so with the value buried in the function
body those branches could not be tested at all.
.. versionadded:: 1.0.0
"""
[docs]
MIN_EFFECTIVE_REFINEMENT = 1.25
r"""float: Module-level constant
Least ratio of *actual slab edges* between two refinement levels for their agreement to be
allowed to count as convergence.
The ladder multiplies ``n_slabs`` by ``growth_factor_n_slabs``, but ``t_breakpoints`` are
re-inserted into every level's grid, so at small counts the nominal step and the real one are
very different things: on a chord with fourteen PREM crossings a nominal 2 -> 3 slab step is a
**16 -> 17 edge** step, a 6% refinement rather than a 50% one. Two grids differing by 6% give
nearly the same answer for reasons that have nothing to do with having converged, and
``np.allclose`` reads that as success.
**This was returning answers outside the requested tolerance, silently.** Measured over 120
Earth configurations (costhz -0.15 to -0.99, 0.5-8 GeV, rtol = atol = 1e-3 / 1e-4 / 1e-5, 3nu,
scored against a 6000-slab reference verified converged to 1e-13): one silent violation, 2.1x
outside the tolerance asked for, and five more that missed but did at least warn. At 1.25 all
six become zero. The value is the smallest that clears the population:
========= ================== ================== =====================
bound silent violations missed but warned median slabs returned
========= ================== ================== =====================
1.00 1 5 9
1.15 0 1 14
**1.25** 0 0 21
1.40 0 0 108
1.50 0 0 20 000
========= ================== ================== =====================
1.40 buys nothing and costs twelve times the slabs. The levels 1.25 adds are the cheap small
ones, so wall clock rises far less than the slab count: a 60-energy Earth scan goes from 9 ms
to 10 ms.
**One constant covers every case measured; none of them wanted their own.** Repeating the
sweep at four and five flavors (same chords and energies, three tolerances, against a
4000-slab reference) gives zero silent violations at 1.25 in all of 3nu, 4nu and 5nu. Four
and five flavors each keep one case that misses but *warns*, which is the existing contract
rather than this defect. Breakpoint density does not reach it either: swept at 0, 10, 50, 200
and 1000 user-supplied breakpoints, the ladder converged with identical slab counts either
side of the gate, because its seed starts in the thousands and never visits the small counts
where dense breakpoints would dominate the ratio.
**The bound must stay below ``growth_factor_n_slabs``, and the cliff is sharp.** At 1.50 the
ratio is only reachable when rounding happens to favor it, so the ladder runs to the cap
instead: median slabs returned goes to **20 000** -- ``max_n_slabs`` -- at every flavor count,
for no accuracy gained over 1.25. That is the failure mode of a bound set too high, and it is
why the measured value sits a comfortable distance under the growth factor rather than just
beneath it.
**It is a no-op wherever there are no breakpoints**, since the edge count is then the slab count
and the ratio is ``growth_factor_n_slabs`` itself -- solar probabilities are bit-identical
either side of this change, verified to fifteen digits. It also cannot cause a spurious
non-convergence at ``max_n_slabs``, where the cap logic ends the ladder independently; checked.
A growth-rule fix was tried instead -- enlarging ``n_slabs`` so that the *edge* count grows by
``growth_factor_n_slabs`` -- and **rejected on measurement**: it left two of the six misses in
place and cost slightly more (median 22 slabs against 21).
.. versionadded:: 1.0.0
"""
[docs]
BATCH_WORKING_ENTRIES = 65_536
r"""int: Module-level constant
Ceiling on the number of complex entries in any one temporary array of the batched
scan engines -- about 1 MB at 16 bytes each. Both batched engines work on arrays
indexed by (energy, slab, ...), whose size is the product of quantities the caller
controls independently, so neither a slab cap nor an energy count bounds them on its
own. Tiling against a fixed entry budget does, and it makes peak memory a property
of the library rather than of the call.
**The value is measured, and it is small on purpose.** The batched kernels are
memory-bound rather than compute-bound: the stack is written by the Hamiltonian
builder and then streamed by the kernel, which does little arithmetic per byte, so a
working set that fits in cache is read back nearly free and one that does not is
refetched from memory. Swept over 1 MB / 4.2 MB / 12.6 MB / 67 MB / 268 MB across
fifteen workloads on three engines (microseconds per probability):
=========================== ======= ======= ======= ======= =======
workload 1.0 MB 4.2 MB 12.6 MB 67 MB 268 MB
=========================== ======= ======= ======= ======= =======
separable, 3nu, 2000 E 96.4 103.1 112.6 115.2 114.8
separable, 4nu, 20000 E 124.8 127.6 138.9 171.3 191.5
separable, 5nu, 2000 E 146.1 155.1 172.2 201.8 202.1
cumulative, 2nu, 60 L 1720.4 1909.4 1984.0 1995.5 1996.5
=========================== ======= ======= ======= ======= =======
1 MB won eight of the eleven memory-bound workloads and was never worse than the
previous 67 MB default; the gain over it runs to **1.19x-1.38x** on Earth energy
scans and grows with both flavor count and scan length. Short scans (a few hundred
points) are flat within 2%, where fixed overhead dominates. The interaction-picture
engine is flat at 1.00x throughout -- it is compute-bound, so this constant does not
reach it.
**Tiling never changes the answer**: every workload above was bit-identical at every
budget, because the tiles are independent and only concatenated. So this is a pure
performance knob, and retuning it needs no accuracy justification.
Two caveats on the number. It was measured on one machine (13 MB L3, 6.5 MB L2), and
the optimum sits *below* the last-level cache -- 1 MB beat 12.6 MB -- which suggests
L2 or streaming behavior, not last-level residency, is what actually matters. That
also means autodetecting the last-level cache and sizing to it, as NuOscProbExact
does for its own chunking, would have landed on a *worse* value here than this fixed
constant. Set the constant to retune; nothing caches it, and both batched engines
read it at call time.
.. versionadded:: 1.0.0
"""
[docs]
CUMULATIVE_AUTO_MIN_POINTS = 2
r"""int: Module-level constant
Fewest baselines at which ``cumulative='auto'`` engages the cumulative scan in
:func:`osc_prob_energy_baseline`.
A single baseline has no prefix to reuse, and would pay for the adaptive probe that sizes the
inherited grid without getting anything back -- which matters because every single-point call
through the wrapper layer is served by ``osc_prob_energy_baseline``. From two baselines
upward there is something to share.
**That argument is about sharing, and one thing it does not establish** is whether paying for
the probe once is worse than what a single point gets *instead*. Measured against the engine
``'auto'`` otherwise uses on a single point -- the adiabatic hybrid path -- it is not: the
cumulative scan at N = 1 is **cheaper on every profile of the physical population**, from 0.62x
on a tabulated profile to 0.02x on a real solar model, median 0.15x
(``docs/dev/adversarial_batteries/physical_battery.py seam_cost``). And with this constant at 1
alongside :data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS`, both silent misses the physical
population produced are repaired: 1.380e-03 -> 1.707e-05 on the BS2005-AGS,OP solar model at
5 MeV, and 1.095e-03 -> 2.586e-06 on a supernova shock.
**It is nevertheless still 2**, because lowering it changes which engine answers *most* requests
this package serves, and the measurement above does not license that -- see
:data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS` for the three reasons, of which the sharpest is
that a declining hybrid path does not hand the request to the cumulative scan but to whichever
engine applies next.
Note that this constant being 2 is what kept two latent defects in the cumulative branch
unreachable by default, both fixed here and both reachable on the shipped tree through an
explicit ``cumulative=True``: a missing scalar squeeze, and a ``convergence_info`` keyword
forwarded to an engine that rejects it.
The threshold is deliberately not set at the point where the cumulative scan becomes *faster*,
which is higher (measured against ``solve_ivp``, on a 5 MeV solar scan to one solar radius:
0.75x at N = 2, 0.87x at N = 10, 2.65x at N = 25, 84x at N = 1000). Below that crossover the
cumulative scan is at most ~1.3x slower in wall time while being one to three orders of
magnitude more accurate -- and the per-point path it replaces returns answers *outside* the
requested 1e-3 there (9.7e-3 at N = 10, 5.6e-3 at N = 25, 2.6e-3 at N = 100). Trading a few
milliseconds for that is the right way round.
.. versionadded:: 1.0.0
"""
# A weak-band self-cross-check was built here and REMOVED, on measurement. The idea: below
# the N = 25 seam, where every remaining silent miss lives, have strategy='auto' verify a
# window-free hybrid result against the general Magnus ladder instead of taking its word --
# certification with no window rests on gamma alone, and GAMMA_TO_ERROR is good only to ~2x.
#
# It does not work, and the reason is worth keeping:
#
# * with the shipped constant, over 200 random smooth profiles, 25 results were window-free
# and certified and the ladder agreed with ALL 25. Zero disagreements, so zero benefit
# against a measured 9% of calls paying for a second engine.
# * with GAMMA_TO_ERROR deliberately made optimiztic by 2x, 41 results were certified,
# 3 of them genuinely outside tolerance -- and the check still fired zero times. The
# first version's trigger was computed FROM GAMMA_TO_ERROR, so mis-calibrating the
# constant shrank the trigger in step with it: a check insured against a constant, keyed
# on that same constant. Self-referential, which is precisely the failure shape this
# whole programme exists to find, reproduced by the person writing the fix.
# * removing the circularity does not rescue it. Verifying EVERY window-free result --
# 11% of random calls, and 100% of ordinary solar single points, which are window-free --
# still fires zero times and still misses the same 3.
#
# The conclusion is structural: what is left in the weak band is not disagreement between
# engines, it is the engines being wrong TOGETHER. A cross-check detects the former by
# construction and can never detect the latter -- the same reason cross_check_strategies
# cannot see a feature narrower than every grid. The instrument that does reach that class is
# adiabatic.find_hidden_features, which looks at the profile rather than at the answers.
#
# Reproduce: docs/dev/adversarial_batteries/crosscheck_benefit.py and weak_band.py.
[docs]
HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS = 8
r"""int: Module-level constant
Fewest baselines at which ``_osc_prob_hybrid_dispatch`` stands aside, under
``strategy='auto'``, so that a single-energy baseline scan reaches the cumulative scan instead.
Deliberately **larger** than :data:`CUMULATIVE_AUTO_MIN_POINTS`, because the two thresholds
guard different trades, and because being the larger keeps the fall-through safe: whenever the
hybrid dispatcher declines on this count, ``cumulative='auto'`` is guaranteed to engage, so a
scan can never decline both paths and land on the general per-point method.
**This was 25, and both halves of the justification for that turned out to be wrong.** The
original reasoning was that below N = 25 the cumulative scan's near-constant cost is not yet
amortized, so yielding would make a small scan "several times slower (7.6x at N = 2) to buy
accuracy that was already two orders inside what the caller asked for". Both clauses were
measured on **solar profiles only**, which is the easiest case for the hybrid path and the
hardest for the cumulative scan's strict probe.
*The accuracy clause*, re-measured over 42 workloads spanning 7 profile families, d = 2 and 3,
and single points, baseline scans and energy scans
(``docs/dev/adversarial_batteries/fallback_quality.py``):
========================================== =================
engine worst error
========================================== =================
adiabatic hybrid, over 42 workloads 1.68e-03
**cumulative scan, over the 28 it serves** **1.13e-07**
========================================== =================
Not two orders inside the tolerance: on 30 of 42 workloads ``'auto'`` was more than 10x worse
than the best engine that applied, by up to **900 000x**, and on two it was **outside the
requested 1e-3 with no warning at all**. Both of those were baseline scans at N = 8, which the
cumulative scan answers to ~1e-9.
*The cost clause*, re-measured by alternating with a control that came back at 0.99x
(``seam.py``), as the median cumulative/hybrid ratio across 3 profiles x d = 2, 3:
===== ============ ===================================
N median cost worst case
===== ============ ===================================
2 0.87x 5.75x (solar, d = 3)
4 0.48x 2.84x (solar, d = 3)
**8** **0.25x** **1.44x (solar, d = 3)**
16 0.17x 0.72x
24 0.12x 0.50x
===== ============ ===================================
The cumulative scan is *cheaper on median at every size measured*, and on a multi-resonance
profile at N = 8 it is **30x** cheaper. The 7.6x figure was solar, at N = 2, where the hybrid
path is at its best.
**8 was where the worst case stopped mattering**, on that population: at N = 8 the only profile
on which yielding cost anything was solar at d = 3, at 1.44x, in exchange for three to six
orders of accuracy; at N = 4 that worst case was 2.84x and at N = 2 it was 5.75x.
**Lowering it to 1 was built, measured and REVERTED.** Eight is a threshold on *point count*,
so it repaired baseline scans and cannot, even in principle, reach a **single point** -- and
single points turned out to be where the remaining silent misses live. Over 195 configurations
of a physically-motivated population
(``docs/dev/adversarial_batteries/physical_profiles.py``: a real published solar model, a
supernova shock and turbulence from the literature, Earth with a non-PREM crust, interpolated
density tables), ``strategy='auto'`` was outside the requested 1e-3 **with no warning at all**
on exactly two, and both were single points on which the cumulative scan was already 400-1100x
more accurate and structurally unreachable:
============================================ ============== ===============
configuration hybrid, N = 1 cumulative
============================================ ============== ===============
BS2005-AGS,OP solar model, d = 2, **5 MeV** 1.380e-03 **1.707e-05**
supernova shock w = 1e-3, d = 3, 15 MeV 1.095e-03 **2.586e-06**
============================================ ============== ===============
The first is the one that decided it: a real published solar model, at an energy in the 8B
spectrum, in the two-flavor treatment that is standard for the solar problem, returning a
*certified* answer outside the caller's tolerance in silence.
*And the cost objection did not survive measurement either.* Re-measured on the physical
population by alternating the two engines with a control that returned 1.00x, 1.01x and 1.06x
across three rounds (``physical_battery.py seam_cost``), reading **minima** rather than medians
because interference can only add time:
================= ======================================
N = 1 profile cumulative / hybrid cost
================= ======================================
BS05 solar model **0.02x** (681 ms -> 15 ms)
turbulence **0.01x**
Earth crust 0.15x
supernova shock 0.27x
tabulated 0.62x
================= ======================================
The cumulative scan is **cheaper at N = 1 on every profile measured**, median 0.15x. There is
no accuracy-against-cost trade here to balance: the engine that is three orders more accurate on
the failing cases is also one to two orders cheaper on them. Across the whole sweep exactly one
row costs anything -- ``tabulated`` at N = 2, at 1.61x -- and that is a case which is already
well inside tolerance either way.
**So why is this still 8?** Because setting both constants to 1 was tried, and the routing
consequence is not the one the cost measurement implies. Three findings, in order of how much
they matter:
1. **Standing aside does not mean the cumulative scan answers.** The dispatch order is hybrid
-> interaction picture -> separable -> cumulative. When the hybrid path declines, the next
engine that *applies* takes the request, and on several workloads that is ``ip_exp``, not the
cumulative scan. The measurement above compares hybrid against cumulative; it does not
measure hybrid against *whatever comes next*, which is what actually happens. Two tests
caught this as ``assert 'ip_exp' == 'hybrid'``.
2. **The cumulative branch is under-exercised, because it has never been the default.** Making
it so immediately surfaced two latent defects, both reachable on the shipped tree via an
explicit ``cumulative=True`` and both fixed here: a missing scalar squeeze that returned
``(1, d, d)`` instead of ``(d, d)``, and a ``convergence_info`` keyword forwarded to an engine
that rejects it, raising ``TypeError`` instead of returning a probability. Two found in
minutes said the branch needed its own audit before it carried default traffic; that audit
was run on 2026-09-05 and found nothing further. Both defect classes are clean -- scalar
input returns ``(d, d)`` at every flavour count on both settings, and nine forwarded
keywords all reach the branch -- and the branch is no longer thinly covered: 24
``cumulative=True`` call sites across five test files, twenty tests named for it, fourteen
exercising ``convergence_info``, all passing. What the audit did find is that this
sentence had gone stale, and that the branch's three refusals -- differing energies, a
supplied ``t_slab_edges``, and a request it cannot serve -- are deliberate and each says
plainly why. See ``docs/dev/AUDIT_CUMULATIVE_BRANCH.md``.
3. **It is a change of default, not a dominant engine.** The cumulative scan's worst error over
the 76 physical workloads it serves is 5.10e-03, and on one the hybrid path was 15x better.
The exposure is real and remains open: see ``docs/dev/FINDINGS_ROBUSTNESS_PROGRAMME.md`` §13.
The fix is a **surgical** one -- have the hybrid path yield *to the cumulative scan specifically*
rather than merely decline -- not a threshold change.
**Accuracy still steps at this threshold, and it is a large step.** Because the two sides are
different methods rather than two settings of one method, adding a single baseline can change
every answer in a scan. Measured against ``solve_ivp`` across the old threshold, which is the
same discontinuity moved to a new place:
========================= ========== ========== ==========
profile below above step
========================= ========== ========== ==========
solar exponential 3.30e-05 2.13e-08 1 546x
noisy 6.27e-04 1.04e-08 60 418x
multi-resonance 1.58e-03 2.86e-09 552 945x
========================= ========== ========== ==========
The step is always *toward* the truth, so it is a discontinuity to know about rather than a
defect. Pass ``cumulative=True`` to take the cumulative scan below the threshold as well, or
``cumulative=False`` to stay off it entirely.
.. versionadded:: 1.0.0
"""
[docs]
AUTO_LADDER_MAX_PHASE = 1.0e4
r"""float: Module-level constant
Largest estimated accumulated phase, in radians, at which ``strategy='auto'`` hands a smooth
profile to the Magnus ladder instead of the hybrid strategy (issue #70), provided the tolerance
is no tighter than :data:`AUTO_LADDER_MIN_TOLERANCE` and the ladder's starting slab count stays
within :data:`AUTO_LADDER_MAX_FLOOR_FRACTION` of its cap. The phase is the integral of the
spread of the Hamiltonian's eigenvalues along the path, the phase of its fastest oscillation,
from 17 samples at up to five of the requested energies (see ``_estimated_phase``).
The hybrid strategy's cost is set by its search for non-adiabatic windows (87 % of a call on the
profile of the paper's Fig. 1), which does not depend on the tolerance, so at the default
``rtol = atol = 1e-3`` it returned ~1e-12 at ~10 ms per point where 1e-3 was asked for. Measured
over 20 smooth workloads with phases from 5 to 1.2e4 rad on this measure (exponential profiles
from 25 to 250 000 km, 2 to 5 flavors, NSI and LIV), each scored against ``solve_ivp``/DOP853 at
1e-12: the ladder, run at a tenth of the requested tolerance, is 2 to 60 times faster on a
single point and 12 to 500 times faster per point of a 40-energy scan, and stays inside the
tolerance on every workload (worst 1.7e-4 at 1e-3, 4.1e-8 at 1e-6). It first lost on a single
point at 7.8e4 rad; 1e4 keeps a factor of eight below that.
The measure matters on the Sun. The norm of the integrated Hamiltonian, which
:func:`magnus.magnus.suggest_n_slabs` uses, lets the matter and vacuum terms cancel through the
MSW region and reads 2.2 to 2.9 times low there: it put a two-flavor request at 10 MeV over
0.9 R_sun, whose phase is 1.46e4 rad, at 5 572.
.. versionadded:: 1.1.1
"""
[docs]
AUTO_LADDER_MAX_FLOOR_FRACTION = 0.25
r"""float: Module-level constant
Largest fraction of the slab cap (``max_n_slabs``, resolved per integration method) that the
ladder's starting slab count may take for ``strategy='auto'`` to hand it a request (issue #70).
The starting count is the one at which every slab meets the sufficient condition
:class:`magnus.magnus.MagnusConvergenceWarning` checks (see ``_PreferLadder``); the ladder refines
by a factor of 1.5 per rung, so a quarter leaves three rungs below the cap.
Every solar path measured starts at 8 300 to 21 000 slabs against the Gauss-Legendre cap of
20 000, because the core density sets the count for the whole path. Without this condition a
two-flavor request at 10 MeV over 0.9 R_sun started at 18 334, could not refine, and warned
:class:`ToleranceNotAchievedWarning` where the hybrid strategy answers in 0.05 s. The workloads
behind :data:`AUTO_LADDER_MAX_PHASE` start at 5 to 4 700.
.. versionadded:: 1.1.1
"""
[docs]
AUTO_LADDER_MIN_TOLERANCE = 1.0e-6
r"""float: Module-level constant
Tightest ``min(rtol, atol)`` at which ``strategy='auto'`` may prefer the Magnus ladder over the
hybrid strategy; see :data:`AUTO_LADDER_MAX_PHASE`. At 1e-9 the ladder's advantage shrinks or
reverses at four and five flavors (0.9 to 1.3 times the hybrid's cost), while the hybrid
strategy reaches about 1e-9 on these profiles at no extra cost.
.. versionadded:: 1.1.1
"""
[docs]
AUTO_LADDER_TOLERANCE_MARGIN = 10.0
r"""float: Module-level constant
Factor by which ``strategy='auto'`` tightens ``rtol`` and ``atol`` when it hands a request to
the Magnus ladder in place of the hybrid strategy. The ladder's tolerances are a stopping
criterion, not an error bound, and the energy-batched scan engine can land several times outside
them (issue #71: 5.8e-3 at a requested 1e-3); asked for a tenth, it stayed inside the requested
tolerance on every workload measured for :data:`AUTO_LADDER_MAX_PHASE`.
.. versionadded:: 1.1.1
"""
[docs]
CUMULATIVE_N_ACC_SAFETY = 4
r"""int: Module-level constant
Multiple of the inherited slab count used for the accuracy grid of a cumulative baseline
scan (``osc_prob_energy_baseline(..., cumulative=True)``).
The grid is sized from one adaptive :func:`osc_prob` call at the longest baseline, which
reports the slab count *that* baseline needed. Applied unmultiplied, the same uniform density
is thinner than what a per-point path would have chosen for the shorter baselines in the scan,
and the result -- while inside the requested tolerance -- comes out less accurate than the path
it replaces. Measured on a 1000-point solar scan against ``solve_ivp``, where the per-point
path takes 12.0 s for an error of 5.6e-5:
=========== ========== ========= ==========
safety ``n_acc`` time error
=========== ========== ========= ==========
1 14 883 0.049 s 2.35e-04
2 29 766 0.097 s 5.10e-06
**4** **59 532** 0.173 s **3.34e-07**
8 119 064 0.346 s 1.80e-08
=========== ========== ========= ==========
**Why four rather than two.** Two was chosen when the cumulative scan's only alternative was
the general per-point path, against which it was already 124x faster and 11x more accurate.
Since the dispatch chain routes wrapper baseline scans here (see
``HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS``), the alternative is the *hybrid* strategy instead,
which is considerably more accurate than the per-point path -- so the bar moved. At two, a
48-configuration sweep found three where the cumulative scan was less accurate than the hybrid
answer it replaced, all at high energy over a short baseline:
========================= ========== ========== ==========
configuration hybrid safety 2 safety 4
========================= ========== ========== ==========
60 MeV, N=150, 0.4 Rsun 1.57e-05 5.03e-05 8.56e-07
100 MeV, N=150, 0.4 Rsun 2.51e-05 3.77e-05 6.11e-07
100 MeV, N=40, 0.4 Rsun 9.13e-06 1.10e-05 5.58e-07
========================= ========== ========== ==========
Four removes all three and beats the hybrid answer on each, while improving the unaffected
configurations by roughly twenty times as well (5 MeV, N = 150: 8.4e-07 -> 3.7e-08). It costs
about 1.4x in wall time -- 28 ms -> 40 ms, 260 ms -> 366 ms on the cases above -- against a
path it is still tens of times faster than. Eight is better again but 2.4x, and buys accuracy
no longer needed to clear the bar.
Note that the error is **not** concentrated where the shape of this constant suggests: on the
60 MeV case it sits at the *longest* baselines (5.03e-05 there against 2.18e-06 over the
shortest third), and the grid density at the short end already matches what a probe there would
ask for to within 1%. What the multiplier buys is total resolution, not better placement.
**The probe does not always converge**, and then this multiplier is doing more work than its
name suggests. Over a full solar radius at 5 and 10 MeV the strict probe reaches
``max_n_slabs`` (20 000) without two successive levels agreeing, so the count it reports is the
cap rather than a converged requirement, and ``n_acc`` is 80 000 by way of a ceiling. The
resulting scans are accurate (~5e-08 measured against ``solve_ivp``), but the safety margin is
what makes that so. A caller who lowers ``max_n_slabs`` lowers the scan's resolution with it,
in proportion and without a separate warning.
.. versionadded:: 1.0.0
"""
[docs]
OUTPUT_GUARD_MIN_BYTES = 64*1024*1024
r"""int: Module-level constant
Requested-output size below which :func:`osc_prob_energy_baseline` does not bother
checking whether the result will fit in memory. The check itself costs one integer
multiply below this threshold and a single read of the operating system's free-memory
figure above it, so the floor exists to keep even that off the path of ordinary calls.
.. versionadded:: 1.0.0
"""
[docs]
OUTPUT_GUARD_SAFETY = 2.0
r"""float: Module-level constant
Multiple of the requested result size that must fit in available memory before
:func:`osc_prob_energy_baseline` will attempt a scan. Greater than one because the
adaptive engines hold at least the current and the previous probability matrices at
once, plus the caller's own input arrays.
Deliberately not larger: the guard exists to turn an out-of-memory kill into a
diagnosable error, not to second-guess a caller who knows their machine. It refuses
only when the answer alone would claim more than half of what is free.
.. versionadded:: 1.0.0
"""
r"""int: Module-level constant
Maximum number of refinement loops in ``_osc_prob_ip_exp_core``.
Note this ceiling is not what stops the loop in practice: the slab count doubles
each pass, so it reaches :data:`IP_EXP_N_SLABS_CAP` after about twenty passes and
returns there, well before a thirtieth pass could occur. It is a backstop against a
future change to the growth factor or the slab ceiling, not a live limit.
.. versionadded:: 1.0.0
"""
def _n_required_params(func):
r"""How many arguments ``func`` obliges its caller to supply positionally.
Every ``H_func``/``rho_func`` arity check in this module used ``len(signature(f).parameters)``,
which counts keyword parameters that already have defaults. That breaks the ordinary Python
idiom for binding a loop variable into a closure --
.. code-block:: python
def H(energy, l, VCC, _hvac=hvac, _proj=proj): # 5 parameters, 3 required
...
-- which the package's own documentation recommends the *factory* form of, precisely because
this form used to fail. With ``validate_input=True`` it raised "must be a function of either
three arguments (energy, l, VCC) or two arguments (energy, l); the provided H_func takes 5";
with ``validate_input=False`` it silently took the two-argument branch and died with a
``TypeError`` from inside the engine. Neither is the user's fault.
Counting required parameters instead makes both forms work and changes nothing for a function
written without defaults. A ``*args`` function is not counted this way -- it declares no
required parameters at all, and the old total is the better guess there -- so those keep the
previous behavior.
.. versionadded:: 1.0.0
Parameters
----------
func : Callable
The function to inspect.
Returns
-------
int
Number of positional parameters without defaults, or the total parameter count when
``func`` takes ``*args``.
"""
# A function's arity does not change, and inspect.signature is expensive enough to show up
# in a profile: it was 42 us of cumulative time on a single osc_prob call, and the routes
# that still go point by point (PREM, the cumulative scan) pay it once per point. Cached
# weakly against the function, exactly as magnus.cached_eval_mode caches the evaluation mode,
# so the per-energy lambdas these engines build do not accumulate.
try:
hit = _ARITY_CACHE.get(func)
if hit is not None:
return hit
except TypeError: # not weak-referenceable (a builtin, say)
return _n_required_params_uncached(func)
out = _n_required_params_uncached(func)
try:
_ARITY_CACHE[func] = out
except TypeError:
pass
return out
_ARITY_CACHE = weakref.WeakKeyDictionary()
r"""weakref.WeakKeyDictionary: Memo for :func:`_n_required_params`, keyed on the function.
Weak so that the short-lived per-energy closures the scan engines build are not kept alive by
having had their arity measured.
"""
def _n_required_params_uncached(func):
r"""The body of :func:`_n_required_params`; see there for what is counted and why.
Parameters
----------
func : Callable
The function to inspect.
Returns
-------
int
Number of positional parameters without defaults, or the total parameter count when
``func`` takes ``*args``.
"""
params = list(signature(func).parameters.values())
if any(q.kind is q.VAR_POSITIONAL for q in params):
return len(params)
return sum(1 for q in params
if q.kind in (q.POSITIONAL_ONLY, q.POSITIONAL_OR_KEYWORD)
and q.default is q.empty)
def _resolve_max_n_slabs(max_n_slabs, integration_method):
"""Fills in the per-method default cap when ``max_n_slabs`` is None.
An explicitly passed value always wins. Unknown method names fall back to the
quadrature cap, so an invalid ``integration_method`` still fails in the validator that
is meant to report it, rather than here with a KeyError.
"""
if max_n_slabs is not None:
return max_n_slabs
return MAX_N_SLABS_DEFAULT.get(integration_method, MAX_N_SLABS_DEFAULT['trapezoid'])
def _tile_for_working_set(n_energies, n_inner, cell_entries, live_arrays=1,
max_entries=None):
"""Split an ``(n_energies, n_inner, ...)`` batch into tiles under a fixed entry budget.
The batched engines build temporaries indexed by (energy, slab); their size is a
product of two quantities the caller sets independently, so bounding either one alone
does not bound the array. This returns the tile to iterate in instead.
Parameters
----------
n_energies, n_inner : int
Extent of the two batched axes.
cell_entries : int
Complex entries per (energy, inner) cell -- ``d*d`` for a stack of matrices,
``d*d*n_tpts_per_slab`` when each cell also carries quadrature samples.
live_arrays : int, optional
How many temporaries of this shape exist at once at the peak of the caller's
loop. The budget is divided by it, so that :data:`BATCH_WORKING_ENTRIES` bounds
the engine's whole working set rather than one array of it -- the distinction is
an eightfold one for the interaction-picture integrator, which holds the argument,
the slab integral, ``Omega``, the slab operators and the matrix-exponential's own
workspace simultaneously. Default 1, which reproduces a budget stated per array.
max_entries : int, optional
Budget; defaults to :data:`BATCH_WORKING_ENTRIES`, read at call time rather than
bound as a default argument so that the constant can be varied -- a test that
monkeypatches a module attribute consumed as a default would pass trivially,
comparing two identical runs.
Returns
-------
(int, int)
``(energy_chunk, inner_block)``, both at least 1, whose product times
``cell_entries`` times ``live_arrays`` stays within the budget whenever that is
possible at all. A single cell larger than the whole budget cannot be split
further and is returned as ``(1, 1)``: there is no tiling that helps, and refusing
to proceed would be worse than a large allocation the caller can at least see.
.. versionadded:: 1.0.0
"""
if max_entries is None:
max_entries = BATCH_WORKING_ENTRIES
per_cell = max(1, int(cell_entries))*max(1, int(live_arrays))
budget_cells = max(1, int(max_entries)//per_cell)
n_energies = max(1, int(n_energies))
n_inner = max(1, int(n_inner))
inner_block = min(n_inner, max(1, budget_cells//n_energies))
energy_chunk = min(n_energies, max(1, budget_cells//inner_block))
return energy_chunk, inner_block
def _check_output_fits(n_points, dim, source_func_name):
"""Refuse a scan whose *result* cannot fit in memory, before allocating anything.
Tiling bounds the engines' working set, but nothing can shrink the answer itself: a
scan of N points over d flavors returns ``N*d*d`` floats, and if that does not fit,
no strategy helps. Left unchecked, the failure arrives as an out-of-memory kill from
somewhere deep in an engine -- or, on an overcommitting kernel, as the machine going
down rather than the process. Checking up front turns that into a message naming the
number that is too large.
Costs one multiply for ordinary calls: the free-memory figure is only consulted once
the request passes :data:`OUTPUT_GUARD_MIN_BYTES`.
Raises
------
MemoryError
If the result would claim more than 1/:data:`OUTPUT_GUARD_SAFETY` of available
memory. Never raised when free memory cannot be determined.
.. versionadded:: 1.0.0
"""
needed = int(n_points)*int(dim)*int(dim)*8 # float64 probability matrices
if needed < OUTPUT_GUARD_MIN_BYTES:
return
available = magnus._available_memory_bytes()
if available is None:
return
if needed*OUTPUT_GUARD_SAFETY > available:
raise MemoryError(
gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": the requested "
"scan would return " + f"{n_points:,}" + " probability matrices of size " +
f"{dim}x{dim}" + " (" + f"{needed/2**30:.2f}" + " GiB), against " +
f"{available/2**30:.2f}" + " GiB of available memory. The result alone would "
"not fit, so no choice of method or tolerance can help. Split the scan into "
"batches and concatenate the results.")
[docs]
class ToleranceNotAchievedWarning(UserWarning):
r"""Warns that a refinement ladder ran out of room before it converged.
**What was detected.** A cap was reached -- ``max_num_loops``, ``max_n_slabs``, or
``max_n_tpts_per_slab`` -- while the last two refinement levels still disagreed by more
than the requested ``rtol``/``atol``.
**What it means for the answer.** The result is still exactly unitary, so it looks
plausible; its accuracy is unverified. The message says **how far from converged the ladder
stopped**, as a multiple of the tolerance asked for, because that disagreement is computed
anyway by the comparison that decides convergence and it is the difference between raising a
cap as a guess and as a decision. A ladder that stopped a few times outside tolerance is a
different situation from one that stopped thirty times outside it.
**What to change.** Raise the cap the message names; or loosen ``rtol``/``atol`` to what
the levels actually agreed to; or, if the profile has a density jump or a kink, pass
``t_breakpoints`` there -- no number of slabs fixes a slab that straddles one.
**When it is safe to ignore.** When the reported shortfall is smaller than the accuracy the
result is used at. Not otherwise: this reports a genuine failure of the convergence test,
unlike :class:`magnus.magnus.MagnusConvergenceWarning`, which reports a slab width.
Also raised by a **cumulative baseline scan** whose accuracy grid was sized from
``max_n_slabs`` rather than from a converged probe. That instance is worth its own message
because the consequence is larger than one point: the whole scan inherits the capped grid.
Two subclasses narrow the diagnosis: :class:`HybridCertificationWarning` and
:class:`UnmarkedDiscontinuityWarning`. Code filtering on this class catches both.
**Measured rates** (``docs/dev/adversarial_batteries/warn_fp.py``, 168 configurations):
fired 37 times, **16 true positives and 21 false positives -- a 57 % false-positive rate**.
A false positive here means the ladder genuinely ran out of room *and* the answer was
nonetheless inside tolerance, which is the expected shape: a cap is reached before
convergence has been *verified*, not before it has been *achieved*.
.. versionadded:: 1.0.0
"""
[docs]
class HybridCertificationWarning(ToleranceNotAchievedWarning):
r"""Warns that ``strategy='hybrid'`` was requested (forcing
:func:`magnus.adiabatic.hybrid_propagator`) but the self-certifying
refinement of at least one requested (energy, L) point did not
converge within its internal iteration/slab caps.
The returned probabilities remain exactly unitary (every piece of the
hybrid propagator -- the adiabatic transport and the local Magnus
patches -- is unitary by construction) but their accuracy relative to
the requested ``rtol``/``atol`` is not certified. This subclasses
:class:`ToleranceNotAchievedWarning` so existing code that filters on
the parent class also catches this warning; it is issued regardless
of the verbosity setting. With the default ``strategy='auto'``, this
situation instead falls back silently to the general slab-refinement
method (which raises :class:`ToleranceNotAchievedWarning` itself if
*it* also fails to converge), so this warning fires only when
``strategy='hybrid'`` was explicitly requested. See
:doc:`/adiabatic_strategy`.
**Uncertified means unverified, not wrong.** The message says so, and names three things
to change rather than leaving the reader with a disclaimer:
* ``strategy='auto'`` -- the automatic fallback, which hands exactly these points to the
general Magnus path and certifies there instead;
* ``t_breakpoints`` at any known structure -- a density jump, a kink, or a feature narrower
than 1/200 of the trajectory, which is the one cure for what the probe grid cannot
resolve;
* a looser ``rtol``/``atol``, when the accuracy needed is less than the accuracy requested.
Which one applies is visible without guessing: pass ``strategy_info`` (see
:func:`osc_prob_matter_std_potential`) and read ``'declined'``, or pass ``info`` to
:func:`magnus.adiabatic.hybrid_propagator` directly for ``'resolved'`` and ``'gamma_max'``.
.. versionadded:: 1.0.0
"""
[docs]
class UnmarkedDiscontinuityWarning(ToleranceNotAchievedWarning):
r"""Warns that a cumulative baseline scan was asked to integrate a Hamiltonian that is
discontinuous at the scale of the grid it built, without being told where the
discontinuities are.
The hybrid strategy raises it for the same reason when it declines a profile, and so does
``average=True`` when a discontinuity could move probability between levels: the
averaging engine treats the profile as smooth, and before 1.1.1 did so silently (issue
#60). In every case the cure is the same -- ``t_breakpoints`` at the discontinuity.
The cumulative scan lays a uniform accuracy grid over the trajectory (plus the requested
baselines, plus any ``t_breakpoints``). A slab that straddles a density jump degrades the
quadrature to low order no matter how high ``magnus_exp_order`` is, and refining the grid
does not fix it -- the straddling slab merely gets narrower. The cure is to put an edge
*on* the discontinuity, which is what ``t_breakpoints`` is for.
Fuzzing 150 random piecewise-constant profiles, declaring the edges gave a median error of
1.34e-12 and nothing outside tolerance; leaving them undeclared gave a median of 7.76e-04
with 59 of 150 outside it. Of those 59, all but **two** already warned for other reasons
(usually :class:`ToleranceNotAchievedWarning` from the probe). This warning exists for
those two: measured at 1.36e-03 and 2.10e-03 against a requested 1e-3, silently, and
2.33e-11 and 4.35e-14 once the edges were declared.
Detection is a measurement, not a guess: the profile is sampled at two grid densities and
the largest adjacent change in ``H`` is compared (see
``magnus.adiabatic._profile_is_resolved``). A :math:`C^1` profile halves that change when
the spacing halves; a jump does not. On the profile families this package ships --- solar
exponential, multi-resonance, noisy, sinusoidal --- the test reports "resolved" every time,
and it flagged 12 of 12 random piecewise profiles.
Subclasses :class:`ToleranceNotAchievedWarning` so that code already filtering on the parent
also catches this. Not raised when ``t_breakpoints`` was supplied: the caller has then said
where the edges are, and the grid honors them.
**Measured rates** (``docs/dev/adversarial_batteries/warn_fp.py``, 168 configurations
including 48 random piecewise-constant profiles with the edges deliberately left
undeclared): fired 56 times, **23 true positives and 33 false positives -- 59 %**. Read
that number carefully: this reports a *condition about the input*, not a prediction about
the error, and on every one of those 33 the condition was real -- there was an undeclared
discontinuity -- and the answer happened to come out inside tolerance anyway. Declaring the
edges would still have improved it (median 7.8e-04 to 1.3e-12 in ``FINDINGS`` §9.2). A
warning whose claim is true and whose advice is worth taking is not made a false alarm by
the answer surviving.
**Also raised on the hybrid path.** The same detector already ran inside
:func:`magnus.adiabatic.hybrid_propagator`, where failing it makes the strategy decline --
silently, so the caller heard about slab widths from whichever engine answered instead,
which is true and points at the wrong knob. It now says what it found there too. The
detector, its two-stage protocol and its measured false-positive rate are unchanged; only
the number of places that report it has grown. On an unmarked density step the adiabatic
answer was wrong by **0.54** in probability while reporting itself certified, and that is
the case this instance exists for.
.. versionadded:: 1.0.0
"""
def _shortfall_phrase(last_gap, rtol, atol) -> str:
r"""How far from converged a refinement ladder stopped, as a fixed phrase.
:class:`ToleranceNotAchievedWarning` used to say only *that* the ladder ran out of room.
The disagreement between the last two levels is computed anyway, by the very comparison that
decides convergence, so the warning can say by how much -- which is the difference between
"raise the cap" as a guess and as a decision.
Bucketed rather than numeric so the message stays one of four fixed strings and Python's
default filter still shows each at most once per session. The buckets are ratios to the
requested tolerance, because that is the quantity the caller chose and can change.
.. versionadded:: 1.0.0
"""
tol = (atol or 0.0) + (rtol or 0.0)
if (last_gap is None) or (tol <= 0.0):
return "with no two levels to compare"
ratio = last_gap/tol
if ratio <= 3.0:
return "the last two refinement levels still differing by a few times the tolerance"
if ratio <= 30.0:
return ("the last two refinement levels still differing by roughly ten times the "
"tolerance")
return ("the last two refinement levels still differing by more than thirty times the "
"tolerance")
[docs]
class HiddenFeatureWarning(ToleranceNotAchievedWarning):
r"""Warns that the profile has structure too narrow for **any** grid this package lays down.
**What was detected.** A feature whose variation is concentrated between samples of even the
finest grid the adaptive machinery reaches -- see
:func:`magnus.adiabatic.find_hidden_features` and
:data:`magnus.adiabatic.HIDDEN_FEATURE_CONCENTRATION`. The message names the position.
**What it means for the answer.** Possibly wrong, and *no choice of strategy or tolerance
helps*. This is the one exposure the adversarial validation
(``docs/dev/FINDINGS_ADVERSARIAL_VALIDATION.md`` §8.3) could not close: the hybrid
strategy's probe grid, the general ladder's slabs and the cumulative scan's accuracy grid all
miss the same feature, so they agree with each other and are wrong together -- which is also
why :func:`cross_check_strategies` cannot see it either. Measured on a Gaussian of width
:math:`3\times10^{-5}` of the trajectory: **wrong by 2.9e-02 against a requested 1e-3, with
no warning at all** before this existed.
**What to change.** Pass the exact ``t_breakpoints`` printed in the message. They are
built by re-sampling the flagged interval and laying seven edges across the sub-interval
that actually carries the variation (see ``_suggest_breakpoints``), so the inner slabs
come out near two feature widths -- a pair-scale padded bracket, which an earlier version
of this message suggested, was measured *not* to cure the band this class is calibrated
on: a single point stayed at 3.0e-02, a 60-point scan moved from 3.0e-02 to 5.8e-02, and
both were silent about it, since supplying breakpoints also switches the scan off. The
printed set is verified end to end through the public path -- warn, pass the printed edges
back, re-run: 3.0e-02 → 1.0e-04 on the width-3e-5 calibration case at a point and over a
60-point scan alike, and 1.5e-04 or better at every other detected centre tried. Edges of
your own at :math:`\pm 2` and :math:`\pm 8` widths do as well (measured 8.4e-04) when you
know the width. Either way the breakpoints route the call to the general slab ladder, so
expect accuracy near the requested tolerance, not the hybrid path's excess below it.
**When it is safe to ignore.** When the narrow structure is an artifact of how the profile
function was written rather than physics -- an interpolation kink, a rounding step in a
tabulated density -- and you know the physical profile is smooth there.
Subclasses :class:`ToleranceNotAchievedWarning`, so code already filtering on the parent
catches it. Not raised when ``t_breakpoints`` was supplied: the caller has then already said
where the structure is. The scan depends on the profile and the interval but not on energy,
so it runs **once per call**, not once per (energy, L) point.
.. versionadded:: 1.0.0
.. versionchanged:: 1.0.12
The message's suggested ``t_breakpoints`` now localize the feature by a local
re-sample and are verified to cure the calibration band; the pair-scale bracket they
replace was measured not to.
"""
[docs]
class PhaseAveragingWarning(UserWarning):
r"""Warns that ``average=True`` was requested at an (energy, L) point
where the oscillation has not, in fact, averaged.
The phase-averaged probability is the exact limit reached when every
pair of eigenvalues has accumulated many cycles of relative phase (see
:mod:`magnus.avgprob`). A pair whose relative phase is neither much
larger than :math:`2\pi` nor much smaller than one radian is in
neither limit, and no averaged expression describes it -- the
oscillation probability itself is the meaningful quantity there.
This is not a statement about numerical accuracy: the returned matrix
is still a valid, doubly stochastic probability matrix. It is a
statement that the *question* does not apply at that baseline, which
is why it warns rather than refining anything.
Since 1.1.1 ``average=True`` returns the phase average over a relative
energy spread ``average_spread`` (see :mod:`magnus.avgprob`), which is
defined at every baseline, and the warning says instead that the result
**depends on that spread**: some interference term has partly survived
it, so that :math:`|\sigma\, \partial P/\partial\sigma|` exceeds
:data:`magnus.avgprob.PHASE_SPREAD_SENSITIVITY_THRESHOLD`. The number is
then the average over the spread asked for, and ``average_spread`` should
match the resolution of the measurement. The original meaning remains
for a Hamiltonian that does not depend on energy, which has no spread to
average over, and the energy-window route of a profile with declared
discontinuities warns as before.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Fires where the phase average depends on the spread.
"""
[docs]
class CrossCheckInconclusiveWarning(UserWarning):
r"""Warns that :func:`cross_check_strategies` reported a spread of zero
because it made no comparison, not because the engines agreed.
The diagnostic returns ``max_spread`` and ``max_spread_independent`` as
plain floats, and both are ``0.0`` when no pair of engines was compared --
the same value they take when two engines agree perfectly. A caller who
reads either number without also reading ``ran`` cannot tell the two apart,
and the reassuring reading is the wrong one.
Three ways to get a vacuous zero:
* **No engine ran.** Every engine declined, most often because the entry
point has no ``strategy`` parameter -- :func:`osc_prob` itself is such an
entry point, so it is an easy one to reach for. Pass a wrapper such as
:func:`osc_prob_matter_std_potential` instead.
* **One engine ran.** There is nothing to compare it with.
* **Only one family ran.** ``max_spread_independent`` is zero because no
cross-family pair exists. Engines within a family share machinery, so
their agreement is the self-certification this diagnostic exists to
avoid relying on -- see :data:`ENGINE_FAMILIES`.
A warning rather than an error: the returned dictionary is still
well-formed, ``declined`` says exactly why each engine stood down, and a
caller who wants only one engine's answer is entitled to ask for it.
.. versionadded:: 1.0.0
"""
[docs]
class SolarModelRangeWarning(UserWarning):
r"""Warns that a baseline reaches past the last tabulated radius of a standard solar model
while ``stop_at_table_edge=True`` was asked for, so no probability is returned there.
Every table stops before the surface or at it -- BP2000 and BP04 at 0.95 :math:`R_\odot`,
the BS05 models at 0.98, B16 and B23 at 1.00 -- and past it the profile is a continuation of
the last tabulated interval, not a model (see :mod:`magnus.solarmodels`).
``stop_at_table_edge`` is for a caller who would rather have no number than one computed
there: those probabilities come back as NaN, and this says how many and where the edge is.
.. versionadded:: 1.1.1
"""
#-----------------------------------------------------------------------
# Helper functions
#-----------------------------------------------------------------------
[docs]
def print_banner(file: TextIOWrapper=None):
r"""Prints the Magnus ASCII banner, version, and author string.
Prints an ASCII-art banner followed by the package version (``magnus.__version__``, resolved
from ``pyproject.toml``) and author (``magnus.authors.__authors__``). Both live in internal
metadata modules that are excluded from the API reference, so they are shown as literals
rather than as cross-references. Uses ANSI color codes when printing to
stdout (``file is None``); plain text otherwise (e.g., when writing to a log file).
.. versionadded:: 1.0.0
Parameters
----------
file : TextIOWrapper, optional
File object to print to, in addition to (or instead of, depending on the caller) stdout.
If None (default), print to stdout with color.
Returns
-------
None
"""
if file is None:
print(gd.cstyle.CBLUEBG + ".----------------------------------------." + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + "| __ __ |" + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + r"| | \/ | __ _ __ _ _ __ _ _ ___ |" + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + r"| | |\/| |/ _` |/ _` | '_ \| | | / __| |" + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + r"| | | | | (_| | (_| | | | | |_| \__ \ |" + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + r"| |_| |_|\__,_|\__, |_| |_|\__,_|___/ |" + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + "| |___/ |" + gd.cstyle.CEND,
file=file)
print(gd.cstyle.CBLUEBG + "'----------------------------------------'" + gd.cstyle.CEND,
file=file)
else:
print(".----------------------------------------.", file=file)
print("| __ __ |", file=file)
print(r"| | \/ | __ _ __ _ _ __ _ _ ___ |", file=file)
print(r"| | |\/| |/ _` |/ _` | '_ \| | | / __| |", file=file)
print(r"| | | | | (_| | (_| | | | | |_| \__ \ |", file=file)
print(r"| |_| |_|\__,_|\__, |_| |_|\__,_|___/ |", file=file)
print("| |___/ |", file=file)
print("'----------------------------------------'", file=file)
print("Version: "+ version.__version__ + " | Author(s): " + authors.__authors__ + "\n",
file=file)
[docs]
def print_run_parameters(
H_func: Union[Callable, np.ndarray],
t_ini: Union[int, float],
t_fin: Union[int, float],
n_slabs: Optional[int]=1,
n_tpts_per_slab: Optional[int]=100,
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=None,
atol: Optional[Union[int, float]]=None,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
new_recursion_limit: Optional[int]=5000,
verbose: Optional[int]=0,
file_log: Optional[TextIOWrapper]=None
):
r"""Prints the banner (once per session) and the parameters passed to :func:`osc_prob`.
Diagnostic/logging helper called from :func:`osc_prob` when ``verbose >= 1`` or ``save_log``
is True. Prints (to stdout, and additionally to ``file_log`` if ``save_log`` is True) the
values of every refinement/logging parameter for the current call, to help reproduce or debug
a specific run.
.. versionadded:: 1.0.0
Parameters
----------
H_func : Callable or np.ndarray
The Hamiltonian passed to :func:`osc_prob`.
t_ini, t_fin : int or float
Integration limits passed to :func:`osc_prob`.
n_slabs : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
n_tpts_per_slab : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
t_slab_edges : list or np.ndarray, optional
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
magnus_exp_order : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
n_jobs : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
integration_method : str
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
rtol : int or float, optional
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
atol : int or float, optional
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
growth_factor_n_slabs : int or float
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
growth_factor_n_tpts_per_slab : int or float
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
max_num_loops : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
min_n_slabs : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
max_n_slabs : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
min_n_tpts_per_slab : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
max_n_tpts_per_slab : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
validate_input : bool
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
save_log : bool
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
filename_log : str
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
new_recursion_limit : int, optional
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
verbose : int
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
file_log : TextIOWrapper, optional
Forwarded verbatim from the calling :func:`osc_prob`; see its docstring.
Returns
-------
None
"""
global has_magnus_header_been_printed
for f in [None, file_log] if save_log else [None]:
if not has_magnus_header_been_printed:
print_banner(f)
has_magnus_header_been_printed = True
print("Parameters passed to function magnus.osc_prob in this run:", file=f)
if callable(H_func):
print(" H_func = " + H_func.__name__, file=f)
else:
print(" H_func = constant (time-independent)", file=f)
print(" t_ini = " + str(t_ini), file=f)
print(" t_fin = " + str(t_fin), file=f)
print(" n_slabs = " + str(n_slabs), file=f)
print(" n_tpts_per_slab = " + str(n_tpts_per_slab), file=f)
if t_slab_edges is None:
print(" t_slab_edges = None", file=f)
else:
print(" t_slab_edges = ", file=f)
for i, t_slab in enumerate(t_slab_edges):
print(" i" + ": " + str(t_slab), file=f)
print(" magnus_exp_order = " + str(magnus_exp_order), file=f)
print(" n_jobs = " + str(n_jobs), file=f)
print(" integration_method = " + integration_method, file=f)
print(" rtol = " + str(rtol), file=f)
print(" atol = " + str(atol), file=f)
print(" growth_factor_n_slabs = " + str(growth_factor_n_slabs), file=f)
print(" growth_factor_n_tpts_per_slab = " + str(growth_factor_n_tpts_per_slab), file=f)
print(" max_num_loops = " + str(max_num_loops), file=f)
print(" min_n_slabs = " + str(min_n_slabs), file=f)
print(" max_n_slabs = " + str(max_n_slabs), file=f)
print(" min_n_tpts_per_slab = " + str(min_n_tpts_per_slab), file=f)
print(" max_n_tpts_per_slab = " + str(max_n_tpts_per_slab), file=f)
print(" validate_input = " + str(validate_input), file=f)
print(" save_log = " + str(save_log), file=f)
print(" filename_log = " + filename_log, file=f)
print(" new_recursion_limit = " + str(new_recursion_limit), file=f)
print(" verbose = " + str(verbose), file=f)
return
def _warn_if_sterile_projector_disagrees_with_composition(
source_func_name, num_flavors, costhz, electron_fraction,
ratio_number_neutrons_to_protons,
core, mantle, crust, ocean,
source_depth=0.0, detector_depth=0.0):
r"""Warns when a caller's scalar builds the sterile matter entry from a different medium
than the density.
Two numbers describe the same matter. The **density** along an Earth chord takes its
neutron-to-proton ratio from :math:`Y_e`, layer by layer, because
:math:`r = (1 - Y_e)/Y_e` is the same statement about composition. Since the projector
became position-resolved, the **sterile states' entry**, :math:`r/2`, follows the same
:math:`Y_e` by default (``ratio_number_neutrons_to_protons=None``), so the default no
longer has anything to warn about. What remains is the caller who *passes a scalar*
over a layered :math:`Y_e`: that forces one medium onto a chord that crosses iron and
rock, and no scalar describes it -- near the sterile matter resonance the mismatch
reaches ~0.4 in probability at 3+1 on a core-crossing chord, and the best possible
scalar, found only by scanning against the layered answer, still leaves ~7e-3. So a
scalar over layered composition warns *unconditionally*: the earlier 2% threshold on
:math:`r` was measured to pass chords whose error matched the very figure it warned
about (2e-2 at :math:`\cos\theta_z = -0.80`), because near a resonance the map from
composition mismatch to probability error is not linear.
With a uniform ``electron_fraction`` override there *is* a single right scalar, so the
exact-match escape stays (with 2% slack for hand-rounded values). A callable ratio is
the caller taking position-resolved control, and is trusted the way ``rho_func`` is.
Three flavors are unaffected: the projector's sterile block is empty, so the ratio has
nowhere to act.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.0
``None`` (follow the composition) and callables are silent; a scalar over layered
:math:`Y_e` always warns, where it used to be silent within 2% of the path average.
"""
if not num_flavors or num_flavors <= 3:
return
# None is the default and the fix: the projector follows the same layered Y_e the
# density uses, so there is no second medium to warn about.
if ratio_number_neutrons_to_protons is None:
return
# A callable is the position-resolved form the default builds internally. A caller
# passing their own has taken explicit control, and agreement with ye_of_r cannot be
# established from a probe evaluation; the contract is documented instead of guessed.
if callable(ratio_number_neutrons_to_protons):
return
import magnus.earth as _earth
given = float(ratio_number_neutrons_to_protons)
if electron_fraction is not None:
# One uniform medium, so there is a single right answer and the test is exact,
# with 2% slack so a hand-rounded match is not nagged about.
target = float(_earth.neutron_to_proton_ratio_from_electron_fraction(
float(electron_fraction)))
if abs(given - target) <= 2.0e-2*max(1.0, abs(target)):
return
warnings.warn(
gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": electron_fraction"
" = " + format(float(electron_fraction), '.4f') + " sets one medium for the whole"
" Earth, which implies r = (1 - Y_e)/Y_e = " + format(target, '.4f') + " for the"
" sterile states' entry in the matter projector -- but ratio_number_neutrons_to_"
"protons = " + format(given, '.4f') + " builds it from a different one. Omit the"
" ratio (default None) to have it derived from Y_e for you. Three flavors are"
" unaffected.", gd.SterileMatterCompositionWarning, stacklevel=3)
return
# Layered composition plus one scalar: different media by construction, so this warns
# regardless of the scalar's value. The layer values are listed so the caller can see
# what the chord actually crosses.
chord = float(_earth.distance_traveled_inside_earth(
costhz, source_depth/gd.UNIT_KM, detector_depth/gd.UNIT_KM))
l_km = np.linspace(0.0, chord, 2001)
radii = _earth.earth_radial_distance_from_depth(
costhz, l_km, source_depth=source_depth/gd.UNIT_KM,
detector_depth=detector_depth/gd.UNIT_KM)
ye_path = _earth.electron_fraction_func_prem(
radii, electron_fraction_core=core, electron_fraction_mantle=mantle,
electron_fraction_crust=crust, electron_fraction_ocean=ocean)
implied = sorted({round(float(_earth.neutron_to_proton_ratio_from_electron_fraction(y)), 4)
for y in np.unique(ye_path)})
warnings.warn(
gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": the density along this"
" chord takes its neutron-to-proton ratio from Y_e layer by layer (r = " +
", ".join(format(r, '.4f') for r in implied) + "), but ratio_number_neutrons_to_"
"protons = " + format(given, '.4f') + " builds the sterile states' entry in the"
" matter projector from that one scalar for the whole chord. The two describe"
" different media, and no scalar fixes that: near the sterile matter resonance on a"
" core-crossing chord the mismatch is worth up to ~0.4 in probability at 3+1, and"
" the best possible scalar still leaves ~7e-3. Omit the argument (default None) to"
" have the projector follow the same Y_e the density uses, exactly and at no extra"
" cost. electron_fraction=0.5 instead makes the medium genuinely uniform, which"
" reproduces results from before composition was layered. Three flavors are"
" unaffected.", gd.SterileMatterCompositionWarning, stacklevel=3)
def _earth_composition(costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=None,
source_depth=0.0, detector_depth=0.0,
density_matter_ocean=None):
r"""The electron density along a chord, with :math:`Y_e` resolved per PREM layer.
Returns the pair ``(rho_func, ratio_resolved)`` every Earth entry point hands to
:func:`magnus.matter.vcc_func_from_rho_func` and (as
``ratio_number_neutrons_to_protons``) to its middle layer -- the density and the
sterile projector's ratio, resolved side by side from the same :math:`Y_e` so the two
cannot drift.
Two things happen here that used to be the caller's problem. :math:`Y_e` becomes a
function of radius rather than one number for the whole Earth -- the core is iron and
the mantle is rock, and assuming 0.5 for both is worth up to a factor of ten in
:math:`P(\nu_\mu \to \nu_e)` on a core-crossing chord. And the neutron-to-proton
ratio is *derived* from :math:`Y_e` rather than carried separately, because the two
are the same statement about composition, :math:`r = (1 - Y_e)/Y_e`. They were
independent arguments before, so an iron core's :math:`Y_e` alongside an isoscalar
:math:`r` described matter that cannot exist -- silently, since :math:`r` only shows
up in the sterile sector.
The derivation used to stop at the density: the projector's sterile entry stayed one
scalar for the whole chord, and the mismatch was warned about rather than fixed --
measured at up to ~0.4 in probability at 3+1 near the sterile matter resonance on a
core-crossing chord, and unfixable by any scalar. Now the default
(``ratio_number_neutrons_to_protons=None``) resolves the projector's ratio here too,
as :math:`r(l)` from the same layered :math:`Y_e`; the warning survives only for a
caller who forces a scalar over layered composition. See
:func:`_warn_if_sterile_projector_disagrees_with_composition`.
The uniform ``electron_fraction`` override is kept, because it is how an earlier
result is reproduced: ``electron_fraction=0.5`` is what every Earth number in this
library used to assume. Combining it with a per-layer value is refused rather than
silently resolved -- any precedence rule here is a rule the caller has to know, and
this is the shape of two bugs already found in this package.
.. versionchanged:: 1.1.0
Returns ``(rho_func, ratio_resolved)`` instead of ``rho_func`` alone, and accepts
``ratio_number_neutrons_to_protons=None`` (the new wrapper default) meaning
"follow the composition".
"""
# Both depths are declared Optional, so None has to mean "no depth". Normalized
# before anything reads them, the sterile-projector warning below included, since it
# samples the trajectory the caller asked for.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
_warn_if_sterile_projector_disagrees_with_composition(
source_func_name, num_flavors, costhz, electron_fraction,
ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_depth=source_depth, detector_depth=detector_depth)
layered = {
'electron_fraction_core': electron_fraction_core,
'electron_fraction_mantle': electron_fraction_mantle,
'electron_fraction_crust': electron_fraction_crust,
'electron_fraction_ocean': electron_fraction_ocean,
}
given = [k for k, v in layered.items() if v is not None]
if (electron_fraction is not None) and given:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": "
"electron_fraction sets one value for the whole Earth, and " + ", ".join(given) +
" set it per layer; pass one or the other, not both. electron_fraction=0.5 "
"reproduces the uniform composition earlier versions assumed.")
for name, value in list(layered.items()) + [('electron_fraction', electron_fraction)]:
if value is None:
continue
if not (0.0 < float(value) <= 1.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " +
name + " is an electron fraction, Y_e = <Z/A>, so it must be in (0, 1]; "
"got " + str(value) + ".")
if electron_fraction is not None:
def ye_of_r(r):
return np.full(np.shape(np.asarray(r, dtype=float)), float(electron_fraction))
else:
def ye_of_r(r):
return earth.electron_fraction_func_prem(r, **layered)
# The depths reach earth.py in kilometers, the unit its trajectory functions work in;
# they arrive here in natural units, like every other length in this module.
_depths = {'source_depth': source_depth/gd.UNIT_KM,
'detector_depth': detector_depth/gd.UNIT_KM}
# PREM's own ocean unless the caller replaced it. Bound once rather than tested inside
# rho_func, which runs at every quadrature node of every slab.
if density_matter_ocean is None:
density_of_r = earth.density_matter_func_prem
else:
def density_of_r(r):
return earth.density_matter_func_prem(
r, density_matter_ocean=density_matter_ocean)
def rho_func(l):
r = earth.earth_radial_distance_from_depth(costhz, l/gd.UNIT_KM, **_depths)
ye = ye_of_r(r)
# ALWAYS derived from Y_e here, never taken from the caller's
# `ratio_number_neutrons_to_protons`. In this conversion the ratio only sets the
# average nucleon mass, which is a property of the local composition and so has to
# follow Y_e layer by layer. The ratio's other role -- the sterile states' entry
# in the matter projector -- is resolved below, from this same Y_e by default.
return matter.num_density_e_func(
r, density_of_r,
ratio_number_neutrons_to_protons=
earth.neutron_to_proton_ratio_from_electron_fraction(ye),
electron_fraction=ye,
density_matter_is_in_g_per_cm3=True) # [eV^3] (l in eV^{-1})
# The projector's ratio, resolved next to the density's own Y_e so the two cannot
# drift. None (the wrapper default) follows the composition -- and on the Earth this
# is *exact*, not approximate: Y_e is piecewise constant on a subset of the PREM
# crossings every Earth entry point already passes as t_breakpoints, so each slab is
# homogeneous and a per-position projector costs no extra Hamiltonian structure. A
# uniform electron_fraction collapses to the one scalar it implies, keeping the
# constant-projector fast paths; three flavors or fewer take a scalar too, the
# sterile block being empty (this also keeps their call path bit-identical to before
# the projector learned to move). An explicit caller value, scalar or callable, is
# forwarded untouched -- scalars over layered Y_e having been warned about above.
if ratio_number_neutrons_to_protons is not None:
ratio_resolved = ratio_number_neutrons_to_protons
elif (electron_fraction is not None) or (not num_flavors) or (num_flavors <= 3):
ratio_resolved = float(earth.neutron_to_proton_ratio_from_electron_fraction(
float(electron_fraction) if electron_fraction is not None else 0.5))
else:
def ratio_resolved(l):
r = earth.earth_radial_distance_from_depth(costhz, l/gd.UNIT_KM, **_depths)
return earth.neutron_to_proton_ratio_from_electron_fraction(ye_of_r(r))
return rho_func, ratio_resolved
def _earth_chord_symmetry(costhz: float,
L: Union[int, float, list, np.ndarray],
source_depth: float=0.0,
detector_depth: float=0.0) -> Optional[tuple]:
r"""The interval over which an Earth chord's matter profile is mirror-symmetric, or None.
A chord through a spherically symmetric Earth meets every radius twice, so its density reads
the same from either end. That is geometry, not an assumption about the caller's input, which
is why the Earth entry points may declare it to the Magnus core (see the ``symmetric_over``
parameter of :func:`osc_prob`) where a general caller may not.
Verified rather than asserted: ``earth.earth_radial_distance_from_depth`` satisfies
:math:`r(l) = r(L-l)` to 1e-16 relative (1.2e-12 at ``costhz = -1``), and the PREM crossings
from :func:`magnus.earth.prem_layer_edges_along_chord` are symmetric about the chord's
midpoint **exactly** -- ``max|tb + tb[::-1] - d|`` is 0.0 at every zenith angle tested, because
the two roots of the crossing quadratic come out as :math:`d/2 \pm s`.
Returns None -- declining the optimization rather than risking it -- whenever the propagation
is not over the whole chord. A chord is symmetric over its full length and over no shorter
prefix, so a request at a shorter baseline must not be mirrored; the comparison is exact, and
an array of baselines qualifies only if every one of them is the full chord.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth`` and ``detector_depth``, either of which declines the symmetry.
Parameters
----------
costhz : float
Cosine of the zenith angle, which fixes the chord.
L : int, float, list or np.ndarray
Baseline(s) actually requested [eV^-1].
source_depth : float, optional
Depth of the entry point below the surface [eV^-1]. Any nonzero value declines the
symmetry. Default: 0.0.
detector_depth : float, optional
Depth of the detector below the surface [eV^-1]. Any nonzero value declines the
symmetry. Default: 0.0.
Returns
-------
tuple or None
``(0.0, chord_length)`` when every requested baseline is the full chord, else None.
"""
if costhz is None or costhz >= 0.0:
return None
# A buried endpoint breaks the mirror this optimization rests on: the trajectory starts
# at one radius and stops at another, so its profile does not read the same from both
# ends. Two endpoints buried to the *same* depth are symmetric again, and could be
# declared -- but declaring a symmetry that does not hold returns a wrong probability
# with no warning, while declining one that does costs a factor of two in Hamiltonian
# evaluations. The cheap side of that trade is to decline.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
if source_depth != 0.0 or detector_depth != 0.0:
return None
L_chord = earth.distance_traveled_inside_earth(costhz)*gd.UNIT_KM
L_arr = np.atleast_1d(np.asarray(L, dtype=float))
if L_arr.size == 0 or not bool(np.all(L_arr == L_chord)):
return None
return (0.0, L_chord)
[docs]
def valid_flavor_indices_2nu(nu_i: int, nu_f: int) -> Tuple[int, int]:
r"""Remaps 3-flavor-style flavor indices onto valid 2-flavor indices (0 or 1).
Two-flavor wrappers (e.g. :func:`osc_prob_2nu_matter_constant_density`) accept ``nu_i``/
``nu_f`` values from the same ``NUE``/``NUMU``/``NUTAU`` constants used by the 3/4/5-flavor
wrappers, for interface consistency, even though a two-flavor system only has indices 0 and 1.
This remaps the flavor not included in the two-flavor system (whichever of NUE/NUMU/NUTAU is
not being used) onto the other valid index, so that, e.g., requesting the nu_e-nu_tau channel
of a system parametrized by :math:`\theta_{13}` (which is really a nu_e-nu_x system) resolves correctly.
.. versionadded:: 1.0.0
Parameters
----------
nu_i : int
Initial flavor index, as one of ``globaldefs.NUE``, ``NUMU``, ``NUTAU``, or already 0/1.
nu_f : int
Final flavor index, same convention as ``nu_i``.
Returns
-------
(int, int)
The remapped ``(nu_i, nu_f)``, each 0 or 1.
"""
if ((nu_i == gd.NUE) and (nu_f == gd.NUTAU)):
nu_f = 1
elif ((nu_i == gd.NUTAU) and (nu_f == gd.NUE)):
nu_i = 1
elif ((nu_i == gd.NUMU) and (nu_f == gd.NUTAU)):
nu_i, nu_f = 0, 1
elif ((nu_i == gd.NUTAU) and (nu_f == gd.NUMU)):
nu_i, nu_f = 1,0
# A pair the chain above does not cover -- (NUTAU, NUTAU) is the one that reaches here --
# used to pass through unmapped and index a 2x2 matrix out of bounds, several frames away
# and with nothing naming the cause. Two flavors cannot say which state a tau-to-tau
# survival probability is about, so the request is refused where it is made.
for name, value in (('nu_i', nu_i), ('nu_f', nu_f)):
if (value is not None) and (value > 1):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.valid_flavor_indices_2nu: " +
name + " = " + str(value) + " has no unambiguous two-flavor counterpart. "
"A two-flavor system has states 0 and 1; globaldefs.NUE, NUMU and NUTAU are "
"accepted only in the mixed pairs, where which state is meant follows from "
"the other index.")
return nu_i, nu_f
[docs]
def values_to_unspecified_osc_params(
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
verbose: Optional[int]=0,
angles: Optional[str]='sin'
) -> Tuple[float, float, float, float, float, float]:
r"""Return values of unspecified standard oscillation parameters
If any of the oscillation parameters has not been given a value, assign to it the value from
the specified parameter set with name default_osc_params_set_name. The parameter-set name is
always checked against :data:`magnus.globaldefs.OSC_PARAMS_PREDEFINED`, and an unknown one
raises ``ValueError``. Only the values of the parameters passed as None are assigned from
the predefined set; other parameters are not modified.
.. versionadded:: 1.0.0
Parameters
----------
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles``; if None, taken from the predefined set.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles``; if None, taken from the predefined set.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles``; if None, taken from the predefined set.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian]; if None, taken from the predefined set.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`; if None, taken from the predefined set.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`; if None, taken from the predefined set.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set to draw missing values from (see
``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
verbose : int, optional
Verbosity level. Default: 0.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Unlike the ``osc_prob_*`` entry
points, this routine does not check the name: an unrecognized one falls through to
the degrees branch rather than raising. Under ``'deg'`` the CP phase is read as
degrees too; under the other three it stays in radians, a sine being no way to
state a phase.
Returns
-------
(float, float, float, float, float, float)
``(s12, s23, s13, dCP, D21, D31)``, with every previously-None entry filled in from the
predefined set.
"""
if default_osc_params_set_name not in list(gd.OSC_PARAMS_PREDEFINED.keys()):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.values_to_unspecified_osc_params"+ \
": the requested oscillation parameter set (default_osc_params_set_name = " + \
default_osc_params_set_name + ") is not among the predefined sets in Magnus. " + \
"Available sets are " + str(list(gd.OSC_PARAMS_PREDEFINED.keys())) + ".")
global has_magnus_header_been_printed
if ((s12 is None) or (s23 is None) or (s13 is None) or (s23 is None) or (dCP is None) or \
(D21 is None) or (D31 is None)):
default_osc_params = gd.OSC_PARAMS_PREDEFINED[default_osc_params_set_name]
# The predefined sets are stored as SINES, and the caller may not be working in
# sines. Filling one omitted angle from the stored value while the others arrived
# as degrees would hand the builder a parameter set in two conventions at once, and
# the builder converts all of them alike -- so 0.1499 would be read as 0.1499
# degrees. Wrong only for the parameters the caller happened to leave out, which is
# the hardest kind of wrong to notice.
if angles != 'sin':
_ang, _ph = _angles.from_sines(
angles,
{k: default_osc_params[k] for k in ('s12', 's23', 's13')},
{'dCP': default_osc_params['dCP']})
default_osc_params = dict(default_osc_params)
default_osc_params.update(_ang)
default_osc_params.update(_ph)
if verbose > 0:
if verbose >= 2:
if (not has_magnus_header_been_printed):
print_banner()
has_magnus_header_been_printed = True
print(gd.WARNING_MSG_IN_COLOR + " Setting unspecified standard oscillation " + \
"parameters to default values from the predefined set " + \
default_osc_params['name'] + " (" + default_osc_params['description'] + "):\n" + \
("s12 = " + str(default_osc_params['s12']) + "\n" if (s12 is None) else '') + \
("s23 = " + str(default_osc_params['s23']) + "\n" if (s23 is None) else '') + \
("s13 = " + str(default_osc_params['s13']) + "\n" if (s13 is None) else '') + \
("dCP = " + str(default_osc_params['dCP']) + " rad\n" if (dCP is None) else '') + \
("D21 = " + str(default_osc_params['D21']) + " eV^2\n" if (D21 is None) else '') + \
("D31 = " + str(default_osc_params['D31']) + " eV^2\n" if (D31 is None) else ''))
s12 = s12 if (s12 is not None) else default_osc_params['s12']
s23 = s23 if (s23 is not None) else default_osc_params['s23']
s13 = s13 if (s13 is not None) else default_osc_params['s13']
dCP = dCP if (dCP is not None) else default_osc_params['dCP']
D21 = D21 if (D21 is not None) else default_osc_params['D21']
D31 = D31 if (D31 is not None) else default_osc_params['D31']
return s12, s23, s13, dCP, D21, D31
[docs]
def unpack_oscillation_params_from_dict(
source_func_name: str,
num_flavors: int,
osc_params: Dict,
h_vac_energy_indep: Union[list, np.ndarray]
) -> np.ndarray:
r"""Unpack oscillation parameters from the osc_params dict
Extracts the standard oscillation parameters for ``num_flavors`` flavors from ``osc_params``
(as built by each ``osc_prob_{N}nu_*`` wrapper), in the fixed order expected by the matching
``hamiltonians.hamiltonian_{N}nu_vacuum_energy_independent`` function. Aborts with a
descriptive error if a required key is missing.
.. versionadded:: 1.0.0
Parameters
----------
source_func_name : str
Name of the calling function, used to build more informative error messages.
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_vac_energy_indep`` is given).
osc_params : dict
Dictionary of oscillation parameters. For ``num_flavors == 2``, must contain 'sth', 'Dm2'.
For 3, 4, 5, must contain 's12', 's23', 's13', 'dCP', 'D21', 'D31', plus, for 4:
's14', 'd14', 's24', 'd24', 's34', 'D41'; and for 5, additionally 's15', 'd15', 's25',
's34', 's35', 'd35', 'D51'.
h_vac_energy_indep : list or np.ndarray
Precomputed energy-independent vacuum Hamiltonian, required (and used, instead of
``osc_params``) when ``num_flavors`` exceeds
``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
Returns
-------
np.ndarray
The unpacked oscillation parameters, in the order expected by the matching
``hamiltonian_{N}nu_vacuum_energy_independent`` function.
"""
if (num_flavors == 2):
try:
sth = osc_params['sth']
Dm2 = osc_params['Dm2']
return np.array([sth, Dm2])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since "+ \
"num_flavors == 2, the dictionary of oscillation parameters " + \
"(osc_params) must contain the keys 'sth' and 'Dm2'.")
elif (num_flavors == 3):
try:
s12 = osc_params['s12']
s23 = osc_params['s23']
s13 = osc_params['s13']
dCP = osc_params['dCP']
D21 = osc_params['D21']
D31 = osc_params['D31']
return np.array([s12, s23, s13, dCP, D21, D31])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 3, the dictionary of oscillation parameters " + \
"(osc_params) must contain the keys 's12', 's23', 's13', 'dCP', 'D21', and " + \
"'D31', even if they are None.")
elif (num_flavors == 4):
try:
s12 = osc_params['s12']
s23 = osc_params['s23']
s13 = osc_params['s13']
dCP = osc_params['dCP']
s14 = osc_params['s14']
d14 = osc_params['d14']
s24 = osc_params['s24']
d24 = osc_params['d24']
s34 = osc_params['s34']
D21 = osc_params['D21']
D31 = osc_params['D31']
D41 = osc_params['D41']
return np.array([s12, s23, s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 4, the dictionary of oscillation parameters " + \
"(osc_params) must contain the keys 's12', 's23', 's13', 'dCP', 'D21', and " + \
"'D31' (even if they are None); and 's14', 'd14', 's24', 'd24', 's34', and " + \
"'D41'.")
elif (num_flavors == 5):
try:
s12 = osc_params['s12']
s23 = osc_params['s23']
s13 = osc_params['s13']
dCP = osc_params['dCP']
s14 = osc_params['s14']
d14 = osc_params['d14']
s15 = osc_params['s15']
d15 = osc_params['d15']
s24 = osc_params['s24']
d24 = osc_params['d24']
s25 = osc_params['s25']
s34 = osc_params['s34']
s35 = osc_params['s35']
d35 = osc_params['d35']
D21 = osc_params['D21']
D31 = osc_params['D31']
D41 = osc_params['D41']
D51 = osc_params['D51']
return np.array([s12, s23, s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, \
D21, D31, D41, D51])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 5, the dictionary of oscillation parameters " + \
"(osc_params) must contain the keys 's12', 's23', 's13', 'dCP', 'D21', and " + \
"'D31' (even if they are None); and 's14', 'd14', 's15', 'd15', 's24', " + \
"'d24', 's25', 's34', 's35', 'd35', 'D41', and 'D51'.")
elif (num_flavors > gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS):
print(gd.WARNING_MSG_IN_COLOR + " oscprob." + source_func_name + ": the number of " + \
"flavors passed (num_flavors = " + str(num_flavors) + \
") exceeds the maximum number for which Magnus has predefined vacuum Hamiltonians " + \
"(globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS = " + \
str(gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS) + "). Will use the Hamiltonian provided " + \
"in h_vac_energy_indep.")
if (h_vac_energy_indep is None):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": provided " + \
"h_vac_energy_indep is None.")
# There are no standard parameters to unpack here: the caller's Hamiltonian replaces
# them. Returned as an empty array rather than by falling off the end of the function,
# because the callers pass this straight into validate_input_battery, which iterates it:
# an implicit None made the path the warning above advertises raise TypeError instead.
return np.array([])
elif (num_flavors < 2):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": num_flavors must be " + \
">= 2.")
[docs]
def unpack_nsi_params_from_dict(
source_func_name: str,
num_flavors: int,
nsi_params: Dict,
h_nsi: Union[list, np.ndarray]
) -> np.ndarray:
r"""Unpack NSI parameters from the nsi_params dict
Extracts the NSI epsilon parameters for ``num_flavors`` flavors from ``nsi_params`` (as built
by each ``osc_prob_{N}nu_*_nsi_*`` wrapper), in the fixed order expected by the matching
``hamiltonians.hamiltonian_{N}nu_nsi`` function. Aborts with a descriptive error if a required
key is missing.
.. versionadded:: 1.0.0
Parameters
----------
source_func_name : str
Name of the calling function, used to build more informative error messages.
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_nsi`` is given).
nsi_params : dict
Dictionary of NSI parameters. For ``num_flavors == 2``, must contain 'eps_aa', 'eps_ab'.
For 3, must contain 'eps_ee', 'eps_em', 'eps_et', 'eps_mm', 'eps_mt', 'eps_tt'. For 4,
additionally 'eps_es', 'eps_ms', 'eps_ts', 'eps_ss'. For 5, instead of the sterile-flavor
keys above, 'eps_es1', 'eps_es2', 'eps_ms1', 'eps_ms2', 'eps_ts1', 'eps_ts2', 'eps_s1s1',
'eps_s1s2', 'eps_s2s2'.
h_nsi : list or np.ndarray
Precomputed NSI Hamiltonian, required when ``num_flavors`` exceeds
``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS`` (in which case there is nothing to
unpack from ``nsi_params`` and this function returns None).
Returns
-------
np.ndarray or None
The unpacked NSI parameters, in the order expected by the matching
``hamiltonian_{N}nu_nsi`` function; or None if ``num_flavors`` exceeds
``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS`` (the caller uses ``h_nsi`` directly).
"""
if (num_flavors == 2):
try:
eps_aa = nsi_params['eps_aa']
eps_ab = nsi_params['eps_ab']
return np.array([eps_aa, eps_ab])
except KeyError :
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since "+ \
"num_flavors == 2, the dictionary of NSI parameters " + \
"(nsi_params) must contain the keys 'eps_aa' and 'eps_ab'.")
elif (num_flavors == 3):
try:
eps_ee = nsi_params['eps_ee']
eps_em = nsi_params['eps_em']
eps_et = nsi_params['eps_et']
eps_mm = nsi_params['eps_mm']
eps_mt = nsi_params['eps_mt']
eps_tt = nsi_params['eps_tt']
return np.array([eps_ee, eps_em, eps_et, eps_mm, eps_mt, eps_tt])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 3, the dictionary of NSI parameters " + \
"(nsi_params) must contain the keys 'eps_ee', 'eps_em', 'eps_et', 'eps_mm'," + \
" 'eps_mt', and 'eps_tt'.")
elif (num_flavors == 4):
try:
eps_ee = nsi_params['eps_ee']
eps_em = nsi_params['eps_em']
eps_et = nsi_params['eps_et']
eps_es = nsi_params['eps_es']
eps_mm = nsi_params['eps_mm']
eps_mt = nsi_params['eps_mt']
eps_ms = nsi_params['eps_ms']
eps_tt = nsi_params['eps_tt']
eps_ts = nsi_params['eps_ts']
eps_ss = nsi_params['eps_ss']
return np.array([eps_ee, eps_em, eps_et, eps_es, eps_mm, eps_mt, eps_ms, eps_tt, eps_ts,
eps_ss])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 4, the dictionary of NSI parameters " + \
"(nsi_params) must contain the keys 'eps_ee', 'eps_em', 'eps_et', 'eps_es'," + \
" 'eps_mm', 'eps_mt', 'eps_ms', 'eps_tt', 'eps_ts', and 'eps_ss'.")
elif (num_flavors == 5):
try:
eps_ee = nsi_params['eps_ee']
eps_em = nsi_params['eps_em']
eps_et = nsi_params['eps_et']
eps_es1 = nsi_params['eps_es1']
eps_es2 = nsi_params['eps_es2']
eps_mm = nsi_params['eps_mm']
eps_mt = nsi_params['eps_mt']
eps_ms1 = nsi_params['eps_ms1']
eps_ms2 = nsi_params['eps_ms2']
eps_tt = nsi_params['eps_tt']
eps_ts1 = nsi_params['eps_ts1']
eps_ts2 = nsi_params['eps_ts2']
eps_s1s1 = nsi_params['eps_s1s1']
eps_s1s2 = nsi_params['eps_s1s2']
eps_s2s2 = nsi_params['eps_s2s2']
return np.array([eps_ee, eps_em, eps_et, eps_es1, eps_es2, eps_mm, eps_mt, eps_ms1,
eps_ms2, eps_tt, eps_ts1, eps_ts2, eps_s1s1, eps_s1s2, eps_s2s2])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 5, the dictionary of NSI parameters " + \
"(nsi_params) must contain the keys 'eps_ee', 'eps_em', 'eps_et', " + \
"'eps_es1', 'eps_es2', 'eps_mm', 'eps_mt', 'eps_ms1', 'eps_ms2', 'eps_tt', " + \
"'eps_ts1', 'eps_ts2', 'eps_s1s1', 'eps_s1s2', and 'eps_s2s2'.")
elif (num_flavors > gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS):
print(gd.WARNING_MSG_IN_COLOR + " oscprob." + source_func_name + ": the number of " + \
"flavors passed (num_flavors = " + str(num_flavors) + \
") exceeds the maximum number for which Magnus has predefined vacuum Hamiltonians " + \
"(globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS = " + \
str(gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS) + "). Will use the Hamiltonian provided " + \
"in h_nsi.")
if (h_nsi is None):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": provided " + \
"h_nsi is None.")
# num_flavors exceeds the predefined range: the caller builds its Hamiltonian directly from
# h_nsi instead of from a flat parameter list, so there is nothing to unpack here.
return None
elif (num_flavors < 2):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": num_flavors must be " + \
">= 2.")
[docs]
def unpack_liv_params_from_dict(
source_func_name: str,
num_flavors: int,
liv_params: Dict,
h_liv: Union[list, np.ndarray]
) -> np.ndarray:
r"""Unpack LIV parameters from the liv_params dict
Extracts the LIV parameters for ``num_flavors`` flavors from ``liv_params`` (as built by each
``osc_prob_{N}nu_*_liv`` wrapper), in the fixed order expected by the matching
``hamiltonians.hamiltonian_{N}nu_liv_energy_independent`` function. Validates that ``Lambda``
is positive and aborts with a descriptive error if a required key is missing.
.. versionadded:: 1.0.0
Parameters
----------
source_func_name : str
Name of the calling function, used to build more informative error messages.
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_liv`` is given).
liv_params : dict
Dictionary of LIV parameters. Always must contain 'Lambda' (LIV energy scale, must be
positive) and 'n_liv' (power of the energy dependence). For ``num_flavors == 2``, must
also contain 'sxi', 'b1', 'b2'. For 3, 'sxi12', 'sxi23', 'sxi13', 'dxiCP', 'b1', 'b2',
'b3'. For 4, additionally 'dxi13' (replacing 'dxiCP'), 'sxi14', 'dxi14', 'sxi24', 'dxi24',
'sxi34', 'b4'. For 5, additionally 'sxi15', 'dxi15', 'sxi25', 'sxi35', 'dxi35', 'b5'.
h_liv : list or np.ndarray
Precomputed LIV Hamiltonian, required when ``num_flavors`` exceeds
``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS`` (in which case there is nothing to
unpack from ``liv_params`` and this function returns None).
Returns
-------
np.ndarray or None
The unpacked LIV parameters, in the order expected by the matching
``hamiltonian_{N}nu_liv_energy_independent`` function; or None if ``num_flavors`` exceeds
``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS`` (the caller uses ``h_liv`` directly).
"""
if (num_flavors == 2):
try:
Lambda = liv_params['Lambda']
if (Lambda <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": Lambda must be positive.")
sxi = liv_params['sxi']
b1 = liv_params['b1']
b2 = liv_params['b2']
n_liv = liv_params['n_liv']
return np.array([sxi, b1, b2, Lambda, n_liv])
except KeyError :
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since "+ \
"num_flavors == 2, the dictionary of LIV parameters " + \
"(liv_params) must contain the keys 'sxi', 'b1', 'b2', 'Lambda', 'n_liv'.")
elif (num_flavors == 3):
try:
Lambda = liv_params['Lambda']
if (Lambda <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": Lambda must be positive.")
sxi12 = liv_params['sxi12']
sxi23 = liv_params['sxi23']
sxi13 = liv_params['sxi13']
dxiCP = liv_params['dxiCP']
b1 = liv_params['b1']
b2 = liv_params['b2']
b3 = liv_params['b3']
n_liv = liv_params['n_liv']
return np.array([sxi12, sxi23, sxi13, dxiCP, b1, b2, b3, Lambda, n_liv])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 3, the dictionary of LIV parameters " + \
"(liv_params) must contain the keys 'sxi12', 'sxi23', 'sxi13', 'dxiCP'," + \
" 'b1', 'b2', 'b3', 'Lambda', and 'n_liv'.")
elif (num_flavors == 4):
try:
Lambda = liv_params['Lambda']
if (Lambda <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": Lambda must be positive.")
sxi12 = liv_params['sxi12']
sxi23 = liv_params['sxi23']
sxi13 = liv_params['sxi13']
dxi13 = liv_params['dxi13']
sxi14 = liv_params['sxi14']
dxi14 = liv_params['dxi14']
sxi24 = liv_params['sxi24']
dxi24 = liv_params['dxi24']
sxi34 = liv_params['sxi34']
b1 = liv_params['b1']
b2 = liv_params['b2']
b3 = liv_params['b3']
b4 = liv_params['b4']
n_liv = liv_params['n_liv']
return np.array([sxi12, sxi23, sxi13, dxi13, sxi14, dxi14, sxi24, dxi24, sxi34, b1, b2,
b3, b4, Lambda, n_liv])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 4, the dictionary of LIV parameters " + \
"(liv_params) must contain the keys 'sxi12', 'sxi23', 'sxi13', 'dxi13'," + \
" 'sxi14', 'dxi14', 'sxi24', 'dxi24', 'sxi34', 'b1', 'b2', 'b3', 'b4'," + \
" 'Lambda', and 'n_liv'.")
elif (num_flavors == 5):
try:
Lambda = liv_params['Lambda']
if (Lambda <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": Lambda must be positive.")
sxi12 = liv_params['sxi12']
sxi23 = liv_params['sxi23']
sxi13 = liv_params['sxi13']
dxi13 = liv_params['dxi13']
sxi14 = liv_params['sxi14']
dxi14 = liv_params['dxi14']
sxi15 = liv_params['sxi15']
dxi15 = liv_params['dxi15']
sxi24 = liv_params['sxi24']
dxi24 = liv_params['dxi24']
sxi25 = liv_params['sxi25']
sxi34 = liv_params['sxi34']
sxi35 = liv_params['sxi35']
dxi35 = liv_params['dxi35']
b1 = liv_params['b1']
b2 = liv_params['b2']
b3 = liv_params['b3']
b4 = liv_params['b4']
b5 = liv_params['b5']
n_liv = liv_params['n_liv']
return np.array([sxi12, sxi23, sxi13, dxi13, sxi14, dxi14, sxi15, dxi15, sxi24, dxi24,
sxi25, sxi34, sxi35, dxi35, b1, b2, b3, b4, b5, Lambda, n_liv])
except KeyError:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": since " + \
"num_flavors == 5, the dictionary of LIV parameters " + \
"(liv_params) must contain the keys 'sxi12', 'sxi23', 'sxi13', 'dxi13'," + \
" 'sxi14', 'dxi14', 'sxi15', 'dxi15', 'sxi24', 'dxi24', 'sxi25' 'sxi34', " + \
" 'sxi35', 'dxi35', 'b1', 'b2', 'b3', 'b4', 'b5', 'Lambda', and 'n_liv'.")
elif (num_flavors > gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS):
print(gd.WARNING_MSG_IN_COLOR + " oscprob." + source_func_name + ": the number of " + \
"flavors passed (num_flavors = " + str(num_flavors) + \
") exceeds the maximum number for which Magnus has predefined vacuum Hamiltonians " + \
"(globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS = " + \
str(gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS) + "). Will use the Hamiltonian provided " + \
"in h_liv.")
if (h_liv is None):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": provided " + \
"h_liv is None.")
# num_flavors exceeds the predefined range: the caller builds its Hamiltonian directly from
# h_liv instead of from a flat parameter list, so there is nothing to unpack here.
return None
elif (num_flavors < 2):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": num_flavors must be " + \
">= 2.")
# def chunkify(lst, n):
# """Yield successive n-sized chunks from lst."""
# for i in range(0, len(lst), n):
# yield lst[i:i + n]
#-----------------------------------------------------------------------
# Primordial functions
#-----------------------------------------------------------------------
class _PositionProfileCache:
r"""Memoizes a position-profile function on repeated position grids.
Across an energy scan, the matter term of the Hamiltonian is evaluated on
the same position grids for every energy (only the 1/E vacuum part
changes), and across the adaptive refinement loops the same grids recur
between neighboring points. This tiny cache stores the profile values of
the most recent grids, keyed by the exact grid contents, so the
(comparatively expensive) density-profile chain runs once per distinct
grid instead of once per Hamiltonian evaluation. Scalar evaluations are
passed through uncached.
Cached arrays come back marked read-only, so a caller that writes into a returned
profile gets a ``ValueError`` rather than corrupting the next caller's values.
``maxsize`` bounds the store, and the oldest entry is evicted past it.
"""
def __init__(self, func: Callable, maxsize: Optional[int]=8):
r"""Wraps ``func``, carrying over any exponential-profile tag it has.
Parameters
----------
func : Callable
Position profile to memoize.
maxsize : int, optional
How many distinct grids to keep before evicting the oldest. Default: 8.
"""
self.func = func
self._cache = {}
self._keys = []
self._maxsize = maxsize
# Preserve any profile tag set by the caller (e.g., is_exp_density_profile/l_scale from
# matter.exp_density_profile, propagated through matter.vcc_func_from_rho_func), so that
# wrapping in this cache does not hide it from _osc_prob_ip_exp_dispatch.
if getattr(func, 'is_exp_density_profile', False):
self.is_exp_density_profile = True
self.l_scale = func.l_scale
def __call__(self, l: Union[int, float, np.ndarray]):
r"""Returns the profile at ``l``, from the store when the grid has been seen.
Parameters
----------
l : int, float, or np.ndarray
Position or grid of positions.
Returns
-------
float or np.ndarray
The profile's value. An array result is read-only.
"""
if np.ndim(l) == 0:
return self.func(l)
l = np.asarray(l, dtype=float)
key = (l.shape, l.tobytes())
val = self._cache.get(key)
if val is None:
val = np.asarray(self.func(l))
# Handed out by reference, to every later caller that asks for the same grid, so a
# write through any one of them would silently change what all the others receive --
# and this cache sits under the matter term of the Hamiltonian, so the result would
# be a wrong probability with nothing raised. Marking it read-only turns that into
# an exception at the point of the write. ``setflags`` refuses on an array that does
# not own its data (a view the profile function handed back from something it keeps),
# and that case is left writable rather than forced: it is the profile's memory, not
# this cache's, and raising here would break a legitimate caller.
try:
val.setflags(write=False)
except ValueError: # pragma: no cover - needs a borrowing profile
pass
self._cache[key] = val
self._keys.append(key)
if len(self._keys) > self._maxsize:
self._cache.pop(self._keys.pop(0), None)
return val
[docs]
def compute_evolution_operator(
H_func: Callable,
t_slab: Union[list, np.ndarray],
n_tpts_per_slab: int,
magnus_exp_order: int,
**kwargs
) -> np.ndarray:
r"""Computes the evolution operator inside a given time slab.
Exported so the single-slab kernel can be used on its own. It is not on
:func:`osc_prob`'s path: that routine calls
:func:`compute_evolution_operator_multiple_slabs` instead, and nothing in the
package calls this one.
.. versionadded:: 1.0.0
Parameters
----------
H_func : Callable
Hamiltonian, a function of time or position that returns a square matrix (NumPy array).
t_slab : list or np.ndarray
Start and end times or positions of the slab, ``[t0, t1]``.
n_tpts_per_slab : int
Number of time points inside the slab at which to evaluate ``H_func`` to numerically
compute the integrals required by the Magnus expansion.
magnus_exp_order : int
Highest order of the Magnus expansion used to compute the evolution operator (should not
exceed ``globaldefs.MAGNUS_EXP_ORDER_MAX``).
\**kwargs
Additional arguments passed to :func:`magnus.magnus.magnus_expansion` (e.g.,
``integration_method``).
Returns
-------
np.ndarray
The evolution operator for the given time slab.
"""
if t_slab[1] > t_slab[0]:
return magnus.magnus_expansion(
lambda t: -1j * H_func(t),
t0=t_slab[0],
t1=t_slab[1],
# t_slabs=[t_slab],
order=magnus_exp_order,
n_tpts=n_tpts_per_slab,
**kwargs,
)
else: # t_slab[1] == t_slab[0]
n = H_func(t_slab[0]).shape[0]
return np.eye(n)
[docs]
def compute_evolution_operator_multiple_slabs(
H_func: Callable,
t_slabs: Union[list, np.ndarray],
n_tpts_per_slab: int,
magnus_exp_order: int,
symmetric_over: Optional[tuple]=None,
**kwargs
) -> np.ndarray:
r"""Computes the evolution operators of a chain of time slabs. This function is not designed
to be called directly by the user, but rather internally by :func:`osc_prob`.
All slabs are computed at once by :func:`magnus.magnus.magnus_expansion_multislab`, which
batches the Hamiltonian evaluation, the quadrature, the commutator algebra, and the matrix
exponentials over the slab axis. Slabs of zero width yield identity operators.
.. versionadded:: 1.0.0
Parameters
----------
H_func : Callable
Hamiltonian, a function of time or position that returns a square matrix (NumPy array).
If it also accepts an array of times (returning a stack of matrices), the vectorized form
is detected and used automatically for speed.
t_slabs : list or np.ndarray
Pairs specifying the start and end times or positions of each slab, i.e.,
``[[t0, t1], [t1, t2], ...]``.
n_tpts_per_slab : int
Number of time points inside each slab at which to evaluate ``H_func`` to numerically
compute the integrals required by the Magnus expansion (ignored by the 'gl' integration
method).
magnus_exp_order : int
Highest order of the Magnus expansion used to compute the evolution operator (should not
exceed ``globaldefs.MAGNUS_EXP_ORDER_MAX``).
symmetric_over : tuple, optional
Forwarded to :func:`magnus.magnus.magnus_expansion_multislab`: the interval over which
the caller declares ``H_func`` to be mirror-symmetric. Not a user-facing parameter --
see that function, and ``docs/dev/PLAN_PALINDROMIC_PROFILES.md`` section 3d(ii), for why
it is a declaration rather than something detected here.
\**kwargs
Additional arguments passed to :func:`magnus.magnus.magnus_expansion_multislab` (e.g.,
``integration_method``).
Returns
-------
np.ndarray
Evolution operators, shape (n_slabs, dim, dim), ordered like ``t_slabs`` (earliest slab
first). Note that the time-ordered product over the chain is
``U_total = U[-1] @ ... @ U[1] @ U[0]``, i.e., the last slab is the leftmost factor.
"""
def hh(t):
return -1j * H_func(t)
return magnus.magnus_expansion_multislab(hh, t_slabs, n_tpts_per_slab=n_tpts_per_slab,
order=magnus_exp_order, symmetric_over=symmetric_over, **kwargs)
[docs]
def osc_prob(
H_func: Union[Callable, np.ndarray],
t_ini: Union[int, float],
t_fin: Union[int, float],
n_slabs: Optional[int]=1,
n_tpts_per_slab: Optional[int]=100,
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
new_recursion_limit: Optional[int]=5000,
verbose: Optional[int]=0,
A_eval_mode: Optional[str]=None,
convergence_info: Optional[Dict]=None,
t_breakpoints: Optional[Union[list, np.ndarray]]=None,
strict_convergence: Optional[bool]=False,
symmetric_over: Optional[tuple]=None,
return_evolution_operator: Optional[bool]=False,
**kwargs
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Computes and returns the neutrino oscillation probability.
Computes the oscillation probability of neutrinos starting at time
(or position) ``t_ini`` and ending at time (or position) ``t_fin``.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator``.
Parameters
----------
H_func : Callable or np.ndarray
The Hamiltonian, which is a function of time or position that
returns a square matrix (a NumPy array). The Hamiltonian can
have complex-valued entries.
t_ini : int or float
Initial time or position of the neutrino.
t_fin : int or float
Final time or position of the neutrino.
n_slabs : int, optional
Number of slabs, or subintervals, into which the interval
[``t_ini``, ``t_fin``] is partitioned in order to compute the
neutrino evolution operators. A higher value of ``n_slabs``
yields a more accurate probability.
If no target tolerance is requested (i.e., if ``rtol`` and
``atol`` are both ``None``), then the given value of ``n_slabs``
is the final number of slabs used in the computation.
If a target tolerance is requested (i.e., if either ``rtol`` or
``atol`` is not ``None``), then the given value of ``n_slabs``
acts as a *floor*: the number of slabs is increased
progressively, starting from ``max(min_n_slabs, n_slabs)``,
until the tolerance is achieved or until we hit ``max_n_slabs``,
whichever happens first. The refinement never runs coarser than
what was asked for, so a caller who knows the feature scale of
their profile can state it here and have it respected. With the
default, ``n_slabs = 1``, the floor is inactive and refinement
starts at ``min_n_slabs`` as before.
n_tpts_per_slab : int, optional
Number of time-points inside the slab at which to evaluate
H_func in order to numerically compute the integrals over time
required by the Magnus expansion. A higher value of
``n_tpts_per_slab`` yields a more accurate probability.
t_slab_edges : list or np.ndarray, optional
Optional list of pairs [[t0, t1], [t1, t2], ...] with the edges
of each time slab. If given, it overrides ``n_slabs`` and the
uniform partitioning of [``t_ini``, ``t_fin``]; the user must
ensure that the slabs chain without gaps. If a tolerance is
requested, only ``n_tpts_per_slab`` is grown (the user-provided
edges are kept fixed).
**Not the same as** ``t_breakpoints``, despite the similar name, and
usually not the one wanted. This is the *complete* partition and it
fixes the discretization, so refinement can no longer add slabs.
``t_breakpoints`` instead names positions that must fall on an edge and
lets the ladder fill in between, which is what a density jump, a kink or
a shock front calls for.
magnus_exp_order : int, optional
Order at which the Magnus expansion is truncated (1 to
``globaldefs.MAGNUS_EXP_ORDER_MAX``).
n_jobs : int, optional
Accepted and ignored. The per-slab parallelization it used to select was
retired: every slab is now computed in a single vectorized call, which was
faster in every case measured, and serial against two workers now agrees to
exactly 0.0. Parallelism over points lives one layer up, in
:func:`osc_prob_energy_baseline`, whose ``n_jobs`` is a live argument.
Default: 1.
integration_method : str, optional
'gl' for Gauss-Legendre collocation, which needs only 1, 2, 3, or 4
Hamiltonian evaluations per slab for orders <= 2, <= 4, <= 6, <= 8, and
ignores ``n_tpts_per_slab``; or 'trapezoid'/'simpson' for cumulative
quadrature over ``n_tpts_per_slab`` points per slab. Default: 'gl'.
rtol : int or float, optional
Relative tolerance on the *agreement between successive refinement
loops*. Set both ``rtol`` and ``atol`` to ``None`` to run once with
the given fixed parameters. If only one of the two is ``None``, it is
treated as 0.
**This is a stopping criterion, not an accuracy guarantee, and the
distinction is not pedantic.** The ladder stops when two successive
levels agree to within ``atol + rtol*|P|``; it never estimates the
error of the answer it returns. A stepping ODE integrator's ``rtol``
is a different thing: it controls an *estimated* local error per step.
Nothing here is estimated.
Usually the criterion is conservative -- for a converging sequence the
level-to-level gap overstates the error of the finer level, so an
answer that stopped at 1e-3 is typically better than 1e-3. But
agreement is evidence of convergence, not proof of it: on a sequence
that is still jumping around, two levels can agree by coincidence
while both are far from the truth. Measured on a sawtooth density,
levels at 3 and 4 slabs agreed and the returned answer was wrong by
**0.855** in probability. ``strict_convergence`` exists for that case
and requires two consecutive agreements instead of one.
If you need to know how far the last two levels actually were apart,
pass ``convergence_info`` and read ``last_gap`` from it; see that
parameter for what the number does and does not mean.
atol : int or float, optional
Absolute tolerance on the same agreement; see ``rtol``.
growth_factor_n_slabs : int or float, optional
Factor by which ``n_slabs`` is multiplied on each refinement
loop (used only when a tolerance is requested).
growth_factor_n_tpts_per_slab : int or float, optional
Factor by which ``n_tpts_per_slab`` is multiplied on each
refinement loop (used only when a tolerance is requested).
max_num_loops : int, optional
Maximum number of refinement loops.
min_n_slabs : int, optional
Number of slabs used in the first refinement loop.
max_n_slabs : int, optional
Maximum allowed number of slabs. If None (default), a cap appropriate to
``integration_method`` is used: 20000 for 'gl', 2000 for the cumulative-quadrature
methods (see :data:`MAX_N_SLABS_DEFAULT`). 'gl' costs 1-4 Hamiltonian evaluations
per slab against the quadrature methods' ``n_tpts_per_slab``, so the same cost
budget buys it far more slabs. An explicit value is always used as given.
min_n_tpts_per_slab : int, optional
Number of time points per slab in the first refinement loop.
max_n_tpts_per_slab : int, optional
Maximum allowed number of time points per slab.
validate_input : bool, optional
If True, validate the input parameters (set to False for a
small speed-up once a call is known to be well-formed).
save_log : bool, optional
If True, also write all messages to the log file.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no
``file_log`` object is given).
file_log : TextIOWrapper, optional
Optional file object to write log messages to.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning.
new_recursion_limit : int, optional
Accepted and ignored, for backward compatibility. Nothing in the package
calls ``sys.setrecursionlimit``. Default: 5000.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the
refinement loops).
A_eval_mode : str, optional
How the Hamiltonian can be evaluated: 'vector' (accepts an
array of positions), 'constant', or 'scalar'. Determined
automatically when None; pass it explicitly (e.g., from
:func:`magnus.magnus.probe_eval_mode`) to skip the probe.
convergence_info : Dict, optional
If a dict is passed, it is filled in place with what the refinement
ladder actually did. Callers use it to warm-start neighboring
computations, and to see how the answer was arrived at.
**It carries facts, never an estimate of the error**, and no key in it
is an accuracy bound. Keys:
- ``'n_slabs'``, ``'n_tpts_per_slab'`` -- the level returned.
- ``'n_slab_edges'`` -- slabs actually used, which is larger than
``n_slabs`` whenever ``t_breakpoints`` inserted edges.
- ``'n_slabs_previous'``, ``'n_tpts_per_slab_previous'``,
``'n_slab_edges_previous'`` -- the level compared against, or None
if no second level was ever computed.
- ``'last_gap'`` -- ``max|P - P_old|`` between those two levels (``max|U - U_old|``
when ``return_evolution_operator`` is set), or
None if only one level was ever computed (which happens whenever the
seed already sits at ``max_n_slabs``).
- ``'n_agreements'`` -- consecutive agreements at the point of return.
- ``'tolerance_achieved'`` -- True if the ladder stopped because it
agreed, False if it ran out of room, None if no tolerance was asked
for. The programmatic form of
:class:`ToleranceNotAchievedWarning`.
**``last_gap`` is not the error, and can be far smaller than it.** It
is the distance between the last two levels. Two failure modes are
measured and neither is rare. A sequence still jumping around can put
two levels close together by coincidence -- on a sawtooth density the
3- and 4-slab levels agreed while the answer was wrong by 0.855. And
on a breakpoint-dominated grid the gap measures the *refinement step*
rather than the error, because inserting the same fixed edges at every
level makes an apparent 2 -> 3 slab step a 16 -> 17 edge one: measured
on plain PREM chords at ``rtol=atol=1e-4``, the ladder stopped with
gaps of 1.9e-5 and 5.0e-5 while the true errors were 8.5e-4 and
8.3e-4. Compare ``n_slab_edges`` against ``n_slab_edges_previous``
to see how little a step actually was.
There is deliberately no key holding an error estimate. Converting
the gap into one by Richardson extrapolation is what the sibling
NuOscProbExact does, and it is sound there because its per-slab solver
is exact: one error mechanism, a guaranteed order, an exact refinement
ratio and nested grids. Magnus has four interacting error sources
(Magnus truncation, slab count, quadrature, breakpoint-perturbed
non-nested grids) and no stable convergence order -- fitted on Earth
chords it scatters from 1.4 to 7.2 against nominal orders of 2 and 4.
Every extrapolation tried under-reported the true error on a large
fraction of real refinement pairs, which is the dangerous direction.
See ``docs/source/performance.rst``.
t_breakpoints : list or np.ndarray, optional
Optional positions at which the Hamiltonian is known to be
non-smooth (e.g., density discontinuities such as the PREM
layer boundaries). They are inserted as mandatory slab edges
into the automatically generated slab grid at every refinement
level, so that the quadrature never integrates across them.
Ignored when ``t_slab_edges`` is given explicitly.
strict_convergence : bool, optional
Require the refinement ladder to agree **twice in a row** before
declaring convergence, instead of once. Default: False.
*What the ladder normally does.* With a tolerance requested,
``osc_prob`` computes the probability on a grid of ``n_slabs``
slabs, then again on a finer one (``n_slabs`` grows by
``growth_factor_n_slabs`` each time), and returns as soon as two
successive grids agree within ``rtol``/``atol``. The assumption
is that agreement between successive refinements means the answer
has stopped changing because it has converged.
*When that assumption fails.* It is only safe while the sequence
is settling down. If the grid is still too coarse to resolve the
Hamiltonian, successive refinements do not approach the answer
smoothly -- they jump around it -- and two neighboring jumps can
land close together by coincidence. ``np.allclose`` cannot tell
that apart from convergence, so the ladder stops early and returns
a plausible, exactly unitary, *wrong* answer with no warning.
Measured example (2 flavors, solar exponential profile, 10 MeV
over one solar radius, default ``rtol=atol=1e-3``): the errors at
successive levels run 5.9e-02, 3.8e-03, 1.6e-02, 1.7e-02, 8.1e-03,
4.5e-03, 3.5e-06. Levels 3 and 4 agree to 1.1e-03 -- inside the
requested tolerance -- while both are wrong by ~1.6e-02, and the
next level moves by 2.5e-02.
*What this flag changes.* Convergence is declared only after two
consecutive agreements, so a lone coincidence is vetoed by the
level that follows it. On the example above the ladder continues
to ``n_slabs = 20000`` and an error of 6.8e-08.
*What it costs.* One extra refinement level, each costing about
``growth_factor_n_slabs`` times the last: measured median **1.53x**
(worst 1.75x) on calls whose first agreement was already genuine.
On calls this actually rescues it costs 3.5-8.6x, because there the
second agreement is several levels away -- that cost is paid only
where the answer would otherwise have been wrong.
*When you do not need it.* If the quantity you care about is an
average over many oscillations -- the usual case for solar
neutrinos, where the survival probability oscillates thousands of
times along the trajectory -- most of the error this guards
against is in the *phase* and cancels in the average. On the 10
MeV example the pointwise error of 2.5e-02 becomes 1.9e-04 once
averaged over 25 oscillations. Prefer ``average=True`` on the
wrapper functions (see :func:`osc_prob_matter_std_potential`),
which computes the phase-averaged probability directly and far
more cheaply. Use ``strict_convergence`` when the oscillating
probability itself is the answer you want -- a probability-versus-
baseline or versus-energy curve, an oscillogram, or a fixed
baseline and energy.
*What it does not fix.* A refinement ladder of any strictness is
powerless against an **incomplete** ``t_breakpoints`` list. If the
Hamiltonian is discontinuous somewhere that is not marked as a slab
edge, every level integrates across that discontinuity, successive
levels can agree to machine precision, and the shared answer is
simply wrong. Measured on a 50-wall piecewise-constant profile
whose first boundary was left unmarked: the error sat at 1.6e-02,
bit-identical from ``n_slabs = 4`` through 32, and adding the one
missing edge moved it to 3.6e-12 at every slab count. When a
profile is discontinuous, marking *every* discontinuity -- including
where it switches on and off, which may lie inside the trajectory --
is worth more than any amount of refinement.
symmetric_over : tuple, optional
Caller's declaration that ``H_func(t) == H_func(lo + hi - t)`` on ``(lo, hi)``. When the
slab chain is found to span exactly that interval, the Magnus core evaluates ``H_func``
on its first half only and mirrors the rest, halving the Hamiltonian evaluations.
Passed as the *interval*, not as a flag, and that is what makes it safe in a scan: a
chord through the Earth is symmetric over its full length and over no shorter prefix, so
a scan point at a shorter baseline spans ``(L0, baseline)``, fails to match, and takes
the ordinary path. No extra bookkeeping is needed for that -- the check in
``magnus.magnus._mirror_applies`` is the whole of it.
**Not a user-facing switch, and unchecked**: verifying it would need the evaluations it
exists to avoid, and declaring it of a profile that is not symmetric returns a silently
wrong answer. It is set by the Earth entry points, where the symmetry is a fact of chord
geometry -- a chord meets every radius twice -- rather than a claim. Turn the mechanism
off globally with ``magnus.magnus.USE_PALINDROME = False``.
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone: ``U`` is the evolution
operator over the same interval, in the flavor basis, a complex square array with
``U[final, initial]`` the amplitude from the initial to the final state, so that
``P == abs(U)**2.T``. The refinement ladder then compares the operator itself
between levels, with the same ``rtol`` and ``atol``, so the returned operator is
converged in its own right (phases included) and not only in its moduli. Every
other setting keeps its meaning. Default: False.
\**kwargs
Additional arguments passed through to the Magnus-expansion
routines
Returns
-------
np.ndarray, or (np.ndarray, np.ndarray)
NumPy array containing the probability matrix of the same
dimensions as the Hamiltonian, ``H_func``. With
``return_evolution_operator=True``, the pair ``(P, U)``, where ``U`` is the
evolution operator from ``t_ini`` to ``t_fin`` (see that parameter).
"""
# Checked before anything forwards **kwargs onwards, and regardless of validate_input:
# these two keys are rejected several hops away otherwise, by a function the caller never
# named (see _reject_parameter_set_metadata).
_reject_parameter_set_metadata(kwargs, 'osc_prob')
_check_passthrough_kwargs(kwargs, 'osc_prob')
# Two keywords the guard admits, because the batching layer declares them, that mean
# nothing here: osc_prob computes one point, and averaging or a cumulative scan are what
# osc_prob_energy_baseline and the wrappers do with many. Caught here, where the caller can
# be named, rather than by magnus_expansion_multislab at the far end of **kwargs.
for _key in ('average', 'cumulative'):
if _key in kwargs:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: '" + _key + "' is not "
"an argument of osc_prob, which computes one point; it belongs to "
"osc_prob_energy_baseline and to the wrappers.")
# Validate input; set validate_input to False for speed-up.
# None means 'use the cap appropriate to this integration method'
# (see MAX_N_SLABS_DEFAULT); an explicit value always wins.
max_n_slabs = _resolve_max_n_slabs(max_n_slabs, integration_method)
if validate_input:
if (t_fin < t_ini):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: t_fin must be >=" + \
" t_ini.")
if (magnus_exp_order < 1):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: magnus_exp_order " + \
"must be >= 1.")
# The slab counts are checked unconditionally, unlike the refinement ceilings
# below, which are only meaningful when a tolerance was requested. `n_slabs` is
# the floor the ladder starts from and is used whether or not rtol/atol are set,
# so `n_slabs=0` and `n_slabs=-5` used to be accepted in silence: the ladder
# simply ignored them and returned the default answer, which made a typo look
# like a setting that had been honored. rtol and atol were already rejected
# when non-positive; these are the same kind of argument and are now treated the
# same way.
if (n_slabs is not None) and (n_slabs < 1):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: n_slabs must be" + \
" >= 1.")
if (min_n_slabs is not None) and (min_n_slabs < 1):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: min_n_slabs must" + \
" be >= 1.")
if (n_tpts_per_slab is not None) and (n_tpts_per_slab < 2):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: n_tpts_per_slab" + \
" must be >= 2.")
if (min_n_tpts_per_slab is not None) and (min_n_tpts_per_slab < 2):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: min_n_tpts_per_" + \
"slab must be >= 2.")
# A floor above its own ceiling is a contradiction, and it used to be answered:
# min_n_slabs=100 with max_n_slabs=5 returned a probability that differed from the
# default by 4.2e-04, so the request was neither honored nor refused. Which of
# the two the ladder ends up obeying is an implementation detail, and a caller who
# wrote both cannot have meant either.
if ((min_n_slabs is not None) and (max_n_slabs is not None) and
(min_n_slabs > max_n_slabs)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: min_n_slabs (" + \
str(min_n_slabs) + ") must be <= max_n_slabs (" + str(max_n_slabs) + ").")
if ((min_n_tpts_per_slab is not None) and (max_n_tpts_per_slab is not None) and
(min_n_tpts_per_slab > max_n_tpts_per_slab)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: min_n_tpts_per_" + \
"slab (" + str(min_n_tpts_per_slab) + ") must be <= max_n_tpts_per_slab (" + \
str(max_n_tpts_per_slab) + ").")
if ((rtol is not None) and (rtol <= 0.0)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: rtol must be None " + \
"or > 0.0.")
if ((atol is not None) and (atol <= 0.0)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: atol must be None " + \
"or > 0.0.")
if ((rtol is not None) and (atol is not None) and (growth_factor_n_slabs < 1.0)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: " + \
"growth_factor_n_slabs must be >= 1.0.")
if ((rtol is not None) and (atol is not None) and (growth_factor_n_tpts_per_slab < 1.0)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: " + \
"growth_factor_n_tpts_per_slab must be >= 1.0.")
if ( ((rtol is not None) and (atol is not None)) and \
((growth_factor_n_slabs == 1.0) and (growth_factor_n_tpts_per_slab == 1.0)) ):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: since a target " + \
"tolerance has been requested, either growth_factor_n_slabs, " + \
"growth_factor_n_tpts_per_slab, or both must be > 1.")
if ((rtol is not None) and (atol is not None) and (max_num_loops <= 1)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: max_num_loops must" + \
" be > 1.")
if ((rtol is not None) and (atol is not None) and (max_n_slabs <= 1)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: max_n_slabs must " + \
"be > 1.")
# Each ceiling has to clear its own floor: min_n_slabs defaults to 1 and
# min_n_tpts_per_slab to 2, which is what the two messages above and below say. This
# condition named max_n_slabs while its message named max_n_tpts_per_slab, so the
# latter was never validated at all (0 and 1 were accepted), the former was bounded at
# > 2 while the message three lines up promised > 1, and a caller who passed
# max_n_slabs=2 was refused in the name of a parameter they had not touched.
if ((rtol is not None) and (atol is not None) and (max_n_tpts_per_slab <= 2)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: max_n_tpts_per_slab" +\
" must be > 2.")
if ((callable(H_func)) and (_n_required_params(H_func) > 1)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: the provided H_func" +\
" is a function of more than one parameter")
H_test = H_func(t_ini) if callable(H_func) else H_func
if not isinstance(H_test, np.ndarray):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: H_func must be a " + \
"NumPy (if the Hamiltonian is time-independent) or must return a NumPy array.")
if H_test.shape[0] != H_test.shape[1]:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: H_func must be a " + \
"square matrix (if the Hamiltonian is time-independent) or must return a " + \
"square matrix.")
# If only one of rtol and atol was given (i.e., the other one is None), set the missing one to
# 0.0, so that the requested tolerance is driven by the one that was given. (Internally, the
# code treats "a tolerance was requested" as both rtol and atol being not None.)
if (rtol is None) != (atol is None):
rtol = 0.0 if rtol is None else rtol
atol = 0.0 if atol is None else atol
# The Gauss-Legendre integration method ('gl') uses a fixed, small number of Hamiltonian
# evaluations per slab (1, 2, 3, or 4, depending on magnus_exp_order), so n_tpts_per_slab plays no
# role: the accuracy is controlled by the number of slabs only. Neutralize the growth of
# n_tpts_per_slab so that the adaptive loop below grows only n_slabs.
if integration_method == 'gl':
growth_factor_n_tpts_per_slab = 1.0
min_n_tpts_per_slab = max_n_tpts_per_slab = n_tpts_per_slab = 2
# If there is no file object given (i.e., if file_log is None), open a log file if requested
if file_log is None:
file_log = open(filename_log, 'w') if save_log else None
# Print a list of all the parameters passed to the osc_prob function and their values
if (verbose > 1):
print_run_parameters(H_func, t_ini, t_fin, n_slabs, n_tpts_per_slab, t_slab_edges,
magnus_exp_order, n_jobs, integration_method, rtol, atol, growth_factor_n_slabs,
growth_factor_n_tpts_per_slab, max_num_loops, min_n_slabs, max_n_slabs,
min_n_tpts_per_slab, max_n_tpts_per_slab, validate_input, save_log, filename_log,
new_recursion_limit, verbose, file_log)
# Note: new_recursion_limit is accepted for backward compatibility but no longer used; the
# probability calculation is fully iterative (nothing recurses), so there is no need to raise
# Python's recursion limit.
# osc_prob runs at a fixed magnus_exp_order; the requested tolerance is reached by
# refining the number of slabs (and, for the quadrature methods, the points per
# slab), never by raising the order. See the note on choosing an order in
# docs/source/methodology.rst.
loop_count = 1 # Loop counter
# Probability matrix of the current and the previous refinement loop; the two are compared
# to decide convergence. Both are None until the first loop has produced a matrix, which is
# why the early-exit checks inside the loop are guarded on loop_count > 1.
P = None
P_old = None
Utot = None
Utot_old = None
last_gap = None
# Consecutive refinement levels that have agreed within (rtol, atol) so far. The ladder
# normally returns on the first agreement; strict_convergence requires two in a row, so that
# a coincidental agreement between two levels of a sequence that is still jumping around is
# vetoed by the level after it. See the strict_convergence entry in the docstring above.
n_agreements = 0
agreements_required = 2 if strict_convergence else 1
# The level ``P_old`` was computed at, so that ``convergence_info`` can report which two
# levels were compared and not merely the last one. This matters more than it looks: with
# breakpoints inserted into every grid, a nominal 2 -> 3 slab step is really 16 -> 17 edges,
# a 6% refinement rather than a 50% one, and a caller reading only the final count cannot
# see that. None until a second level exists.
n_slabs_prev_level = None
n_tpts_prev_level = None
n_edges_prev_level = None
# Copy this to remember whether the function was originally called with predefine slab edges,
# or whether we can increase the number of edges (n_slabs) progressively to reach tolerance
t_slab_edges_original = t_slab_edges
# Flags to signal whether a loop has been run with n_slabs == max_n_slabs or
# n_tpts_per_slab = max_n_tpts_per_slab
ran_with_max_n_slabs, ran_with_max_n_tpts_per_slab = False, False
# Flags to signal whether we have already printed the warning that we have reached
# n_slabs == max_n_slabs or n_tpts_per_slab = max_n_tpts_per_slab, so as not to print it again
warned_reached_max_n_slabs, warned_reached_max_n_tpts_per_slab = False, False
# If a tolerance is requested, start the iterations at the floor on the slab count. That floor
# used to be min_n_slabs alone, with the caller's n_slabs discarded outright; it is now the
# larger of the two. Discarding it was how a profile with 50 density walls, called with
# n_slabs=150, came to be integrated on 4 slabs and declared converged. The seed that replaced
# the caller's number, magnus.suggest_n_slabs, measures the *integral* of the Hamiltonian along
# the path, and an integral is blind to structure that averages out: that profile accumulates
# only ~9 radians over the whole trajectory, so it was seeded with 2 slabs. Below the count
# that resolves the walls the ladder does not converge, it thrashes -- 0.43, 0.13, 0.13, 0.64,
# 0.12 at 2, 3, 4, 5, 6 slabs -- and np.allclose fired on the accidental 3-vs-4 agreement,
# returning an answer wrong by 0.855 in probability. A stricter rtol is the wrong lever: it
# tightens a comparison between two answers that both failed to see the profile. Resolving the
# profile is the right one, and the caller is who knows its feature scale (t_breakpoints is the
# sharper tool still, where the features sit at known positions). With the default n_slabs=1
# the floor is inactive and nothing changes. Clipped at max_n_slabs so that a floor above the
# cap cannot make the ladder step *down* on its first growth; the usual "reached max_n_slabs"
# warning then fires, as it should.
if ((rtol is not None) and (atol is not None)):
min_n_slabs = int(min(max(min_n_slabs, n_slabs), max_n_slabs))
n_slabs = min_n_slabs
n_tpts_per_slab = min_n_tpts_per_slab
# The provided Hamiltonian, H_func, can be either a single-parameter function (of the neutrino
# position) or, if time-independent, a constant NumPy array (e.g., for oscillations in vacuum
# or in matter with constant density). In the latter case, we use this constant Hamiltonian to
# build a dummy one-parameter function of position that we will need later to call the function
# compute_evolution_operator. In this case, first-order Magnus expansion is enough, and so we
# can overwrite the parameters provided to n_slabs = 1, n_tpts_per_slab = 2, rtol = None,
# atol = None for speed-up.
H_constant = None
if not callable(H_func):
H = np.copy(H_func)
H_constant = H
def H_func(l: float) -> np.ndarray:
return H
magnus_exp_order = 1
n_slabs = 1
n_tpts_per_slab = 2
rtol = None
atol = None
n_jobs = 1 # No need to parallelize for this simple computation in a single slab
# A single slab is exact for a constant Hamiltonian, so drop any user-provided slab edges
t_slab_edges = None
t_slab_edges_original = None
# ...and it is constant, which is precisely what probe_eval_mode below would go and
# find out. Saying so here skips a five-point linspace, three calls into the closure
# just built, an np.allclose and a broadcast -- 41% of a constant-density call, spent
# rediscovering the branch condition that got us here.
if A_eval_mode is None:
A_eval_mode = 'constant'
if verbose > 0:
for f in [None, file_log] if save_log else [None]:
warn_msg = gd.WARNING_MSG_IN_COLOR if f is None else gd.WARNING_MSG_NO_COLOR
print("\n" + warn_msg + " The provided Hamiltonian is time-independent. " + \
"Overwriting the run parameters to magnus_exp_order = 1, n_slabs = 1, " + \
"n_tpts_per_slab = 2, rtol = None, atol = None, and n_jobs = 1 for speed-up.",
file=f)
# If the user provided the slab edges explicitly, the number of slabs is set by them
if t_slab_edges_original is not None:
n_slabs = len(t_slab_edges_original)
# Determine once how the Hamiltonian can be evaluated (vectorized over an array of positions,
# constant, or scalar-only), so that the Magnus kernel does not have to re-probe it on every
# refinement iteration below.
if A_eval_mode is None:
# Cached against H_func itself, not the -1j wrapper: the wrapper is a fresh
# lambda on every call and would never hit. How H_func may be evaluated is the
# same question either way, since scaling by a constant changes neither its
# signature nor its shape -- which is what `key` is for.
#
# This used to be a ternary whose other arm called cached_eval_mode on the
# wrapper directly, guarded by `not callable(H_func)`. That arm was dead:
# H_func is unconditionally rebound to a closure above, so the guard was never
# true, and the arm that did run was a second, *unguarded* copy of this cache in
# this module. The copy is gone; the guarded original does the work.
A_eval_mode = magnus.cached_eval_mode(
lambda t: -1j*H_func(t), t_ini, t_fin, key=H_func)
# Physics-informed starting number of slabs (Gauss-Legendre method only): rather than always
# starting the refinement from min_n_slabs and climbing the geometric ladder, start from an
# estimate based on the accumulated phase (see magnus.suggest_n_slabs). min_n_slabs still
# acts as a lower bound, so warm starts provided by the caller take precedence when they are
# larger. For the quadrature methods ('trapezoid', 'simpson') the accuracy is governed
# jointly by n_slabs and n_tpts_per_slab, and seeding only the slab count unbalances that
# ladder, so the seed is not applied there.
if ((rtol is not None) and (atol is not None) and (t_slab_edges_original is None) and \
(integration_method == 'gl')):
n_slabs = int(np.clip(max(min_n_slabs,
magnus.suggest_n_slabs(lambda t: -1j*H_func(t), t_ini, t_fin,
A_eval_mode=A_eval_mode)), 1, max_n_slabs))
while True:
# These checks only apply when osc_prob is run with a requested tolerance (rtol, atol) that
# should be achieved.
if ((rtol is not None) and (atol is not None)):
# Reached maximum allowed number of loops: exit loop, return the probability matrix.
# Guarded on loop_count > 1 because these are refinement limits: there is nothing to
# return until at least one loop has produced a probability matrix. Without the
# guard, max_num_loops < 1 with validate_input=False (the validator rejects it
# otherwise) reached this return before P existed and raised UnboundLocalError.
if (loop_count > 1) and (loop_count > max_num_loops):
warnings.warn("osc_prob: requested tolerance not achieved "
"(max_num_loops reached), " + _shortfall_phrase(last_gap, rtol, atol) +
"; the returned probabilities may be inaccurate. Raise max_num_loops, or "
"loosen rtol/atol to what the last two levels actually agreed to. Shown "
"once per session.", ToleranceNotAchievedWarning, stacklevel=2)
if (verbose > 0):
for f in [None, file_log] if save_log else [None]:
warn_msg = gd.WARNING_MSG_IN_COLOR if f is None else gd.WARNING_MSG_NO_COLOR
print(" " + warn_msg + " Number of loops (loop_count = " + \
str(loop_count-1) + ") reached maximum allowed (max_num_loops = " + \
str(max_num_loops) + "). Requested tolerance not achieved. Try " + \
"increasing max_num_loops.\n",
file=f)
if save_log and close_file_log_upon_exit: file_log.close()
return (P, Utot) if return_evolution_operator else P
# Reached maximum allowed number of slabs: continue execution
if (n_slabs == max_n_slabs):
if ((verbose > 0) and not warned_reached_max_n_slabs):
for f in [None, file_log] if save_log else [None]:
warn_msg = gd.WARNING_MSG_IN_COLOR if f is None else gd.WARNING_MSG_NO_COLOR
print(" " + warn_msg + " Number of slabs (n_slabs) reached maximum " + \
"allowed (max_n_slabs = " + str(max_n_slabs) + ").", file=f)
warned_reached_max_n_slabs = True
# Reached maximum allowed number of time-points per slab: continue execution
if (n_tpts_per_slab == max_n_tpts_per_slab):
if ((verbose > 0) and not warned_reached_max_n_tpts_per_slab):
for f in [None, file_log] if save_log else [None]:
warn_msg = gd.WARNING_MSG_IN_COLOR if f is None else gd.WARNING_MSG_NO_COLOR
print(" " + warn_msg + " Number of time-points per slab " + \
"(n_tpts_per_slab) reached maximum allowed (max_n_tpts_per_slab = " + \
str(max_n_tpts_per_slab) + ").", file=f)
warned_reached_max_n_tpts_per_slab = True
# Reached maximum allowed number of slabs and maximum allowed number of time-points per
# slab: exit loop, return the probability matrix
if (loop_count > 1) and ran_with_max_n_slabs and ran_with_max_n_tpts_per_slab:
# 'gl' pins n_tpts_per_slab (it uses a fixed 1-4 nodes per slab), so only
# max_n_slabs is a meaningful knob for it; naming max_n_tpts_per_slab in the
# message would send the reader after a setting that cannot help them.
knobs = ("max_n_slabs" if integration_method == 'gl'
else "max_n_slabs and max_n_tpts_per_slab")
warnings.warn("osc_prob: requested tolerance not achieved (" + knobs +
" reached), " + _shortfall_phrase(last_gap, rtol, atol) + ", so "
"convergence could not be verified by successive refinement and the "
"returned probabilities may be inaccurate. Raise " + knobs + ". This can "
"happen for very large accumulated phases, e.g., low-energy neutrinos over "
"very long baselines, or eV-scale sterile splittings over Earth-crossing "
"baselines. If the profile has a density jump or a kink, pass "
"t_breakpoints there as well -- a slab straddling one is never fixed by "
"more slabs. Shown once per session.",
ToleranceNotAchievedWarning, stacklevel=2)
if (verbose > 0):
for f in [None, file_log] if save_log else [None]:
warn_msg = gd.WARNING_MSG_IN_COLOR if f is None else gd.WARNING_MSG_NO_COLOR
print(" " + warn_msg + " Number of slabs (n_slabs) and time-points " + \
"per slab (n_tpts_per_slab) reached maximum allowed (max_n_slabs = " + \
str(max_n_slabs) + ", max_n_tpts_per_slab = " + \
str(max_n_tpts_per_slab) + ").", file=f)
print(" " + warn_msg + " Returning probability, but requested " + \
"tolerance (rtol = " + str(rtol) + ", atol = " + str(atol) + \
") not achieved. Try increasing max_n_slabs or max_n_tpts_per_slab.\n",
file=f)
if save_log and close_file_log_upon_exit: file_log.close()
return (P, Utot) if return_evolution_operator else P
# The array (or list) t_slab_edges contains user-provided pairs of start and end times,
# [ti, tf]_k, that define the initial and final times of each of the k-th time slab. It is
# up to the user to ensure that the chain of time slabs covers the full range [t_ini, t_fin]
# without leaving gaps. I.e., the user should ensure that ti_{k+1} = tf_k.
if (t_slab_edges_original is None):
# If t_slab_edges == None, then divide the interval [t_ini, t_fin] evenly into a number
# n_slabs of time slabs. Any t_breakpoints inside the interval (e.g., density
# discontinuities) are inserted as additional mandatory slab edges: high-order
# quadrature converges at its nominal order only if the Hamiltonian is smooth inside
# each slab.
# np.linspace is ~15x the cost of writing the two endpoints down, and for the
# common single-slab case that is all it produces. Worth special-casing because
# this is per call, not per slab: measured at 3.8 us against 0.26 us.
if n_slabs == 1:
grid = np.array([t_ini, t_fin], dtype=float)
else:
grid = np.linspace(t_ini, t_fin, n_slabs+1)
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
bp = np.atleast_1d(np.asarray(t_breakpoints, dtype=float))
bp = bp[(bp > t_ini) & (bp < t_fin)]
grid = np.unique(np.concatenate([grid, bp]))
t_slab_edges = np.column_stack([grid[:-1], grid[1:]])
# Within each slab, t_slab, we use n_tpts_per_slab time-evaluations to compute the integrals
# of the Magnus expansion, from t_slab[0] to t_slab[1]. U_chain contains the chain of time-
# ordered evolution operators, each computed in one time slab. All slabs are computed in a
# single batched call. (Note: n_jobs is accepted for backward compatibility, but the
# per-slab parallelization it used to trigger here has been retired: the batched kernel is
# faster than distributing the small per-slab tasks over joblib workers. Parallelism over
# (energy, L) points is available in osc_prob_energy_baseline instead.)
# NOT deferred to the returned refinement level, and that is a measured decision
# rather than an oversight. 85% of MagnusConvergenceWarning firings describe an
# intermediate grid the ladder went on to refine away (46 of 66 single-point calls had
# some level over pi; only 7 had the RETURNED level over), which looks like pure noise
# -- so the deferral was built. Re-measuring over 168 configurations killed it: firings
# fell 70 -> 53, but TRUE positives fell 17 -> 4 while false positives fell only
# 53 -> 49. "The ladder started far from convergence" turns out to predict a bad answer
# better than "the final grid is coarse" does, so suppressing the early levels throws
# away most of the signal to remove a twelfth of the noise. Nothing became silent
# either way (2 of 168 in both), because the cases it stopped flagging are covered by
# ToleranceNotAchievedWarning. The mechanism and the full measurement are kept in
# magnus._deferred_slab_norm.
# The slab chain here spans the whole of [t_ini, t_fin], so a profile declared
# symmetric over that interval really is symmetric over what is about to be built. The
# declaration is passed as the interval rather than as a flag so that the Magnus layer
# can check the two agree, which is what keeps a blocked sub-range (see the cumulative
# scan below) off the mirrored path even if a flag reached it by mistake.
# ``not kwargs`` is a correctness condition, not an optimization. Anything left in
# kwargs is the Magnus core's to accept or reject, and the fast path below never calls
# it -- so a misspelled keyword would be silently swallowed here rather than raising,
# which test_only_the_labelling_keys_are_rejected exists to prevent. Falling through
# keeps the error contract exactly as it was; a normal call carries no extras and
# still takes the fast path.
if (H_constant is not None) and (not kwargs):
# A constant Hamiltonian needs none of the machinery below it. Omega_1 =
# -i H (t_fin - t_ini) is the whole expansion -- every higher term is built from
# commutators of A with itself, which vanish -- so the single slab is exact and
# the answer is one matrix exponential.
#
# Going through compute_evolution_operator_multiple_slabs instead means sampling a
# closure that ignores its argument, broadcasting the result to (1, k, d, d),
# building slab edges with column_stack, and running the Gauss-Legendre quadrature
# of a constant -- all to arrive back at -i H Delta. Skipping it is not an
# approximation; it is declining to derive a known quantity.
#
# warn_wide is left off deliberately, matching what the general path does here: it
# passes A_is_const=True, which suppresses the slab-width warning for exactly this
# reason -- the series terminates, so a wide slab means nothing.
U_chain = magnus._expm_stack(
(-1j*(t_fin - t_ini))*H_constant)[None, ...]
else:
U_chain = compute_evolution_operator_multiple_slabs(H_func, t_slab_edges,
n_tpts_per_slab, magnus_exp_order, integration_method=integration_method,
A_eval_mode=A_eval_mode,
symmetric_over=symmetric_over, **kwargs)
# Now compute the time-ordered product of all evolution operators across all slabs. The
# neutrino crosses the slabs in the order in which they appear in U_chain (earliest first),
# so the total operator is U_tot = U_chain[-1] @ ... @ U_chain[1] @ U_chain[0]: the operator
# of the *last* slab is the leftmost factor. (functools.reduce is used instead of
# np.linalg.multi_dot because all factors are square matrices of the same size, for which
# multi_dot wastes time computing an optimal parenthesization that does not exist.)
Utot = magnus.ordered_product(U_chain)
# Using Utot, compute all the survival and transition probabilities in a probability matrix
# P = (|Utot|^2).T and return that matrix, so that P[nu_i][nu_f] = |Utot[nu_f][nu_i]|^2.
P = np.transpose(Utot.real**2 + Utot.imag**2)
# Record the refinement parameters of this (latest) computation, so that callers (e.g.,
# osc_prob_energy_baseline) can warm-start neighboring points
if convergence_info is not None:
convergence_info['n_slabs'] = n_slabs
convergence_info['n_tpts_per_slab'] = n_tpts_per_slab
# Facts about the refinement, never an estimate of the error. See the
# convergence_info entry in the docstring for why the distinction is kept so
# strictly: every route from these numbers to an error bound was measured and
# under-reported the true error in the dangerous direction.
convergence_info['n_slab_edges'] = len(t_slab_edges)
convergence_info['n_slabs_previous'] = n_slabs_prev_level
convergence_info['n_tpts_per_slab_previous'] = n_tpts_prev_level
convergence_info['n_slab_edges_previous'] = n_edges_prev_level
convergence_info['last_gap'] = last_gap
convergence_info['n_agreements'] = n_agreements
convergence_info['tolerance_achieved'] = (
None if ((rtol is None) and (atol is None)) else False)
# If no target relative tolerance (rtol) or absolute tolerance (atol) of the probability is
# requested, then return the result obtained already. If, instead, a target tolerance is
# requested, then increase the number of points per slab approximately by the factor
# growth_factor_n_tpts_per_slab, and repeat the probability calculation until the desired
# tolerance is achieved.
if ((rtol is None) and (atol is None)): # No target tolerance requested: return right away
if save_log and close_file_log_upon_exit: file_log.close()
return (P, Utot) if return_evolution_operator else P
else: # Target tolerance requested: iterate until tolerance is achieved
if (verbose > 1):
for f in [None, file_log] if save_log else [None]:
if (loop_count == 1):
print("\nRunning loops until requested rtol and atol are achieved:", file=f)
print(" Loop #" + str(loop_count) + ":", file=f)
print(" magnus_exp_order = " + str(magnus_exp_order), file=f)
print(" n_slabs = " + str(n_slabs), file=f)
print(" n_tpts_per_slab = " + str(n_tpts_per_slab), file=f)
if P_old is not None:
# Compare the new and old probability matrices element-wise. A run of agreements
# is tracked rather than a single one: a disagreement resets it, so with
# strict_convergence the two agreements must be genuinely consecutive.
# Kept so the tolerance-not-achieved warnings below can say how far from
# converged the refinement stopped, rather than only that it stopped. The
# comparison is being made anyway; this is the number it is made on.
last_gap = float(np.max(np.abs(Utot - Utot_old)) if return_evolution_operator
else np.max(np.abs(P - P_old)))
# An agreement only counts if the two levels compared were genuinely
# different grids. With breakpoints re-inserted at every level, a nominal
# 2 -> 3 slab step can be a 16 -> 17 edge one, and two grids that differ by
# 6% agree for reasons unrelated to convergence. See
# MIN_EFFECTIVE_REFINEMENT for the population this was measured on.
effective_refinement = (
len(t_slab_edges)/n_edges_prev_level
if n_edges_prev_level else float('inf'))
# With the operator requested, the operator is what has to have converged:
# its moduli can agree between two levels while its phases still move.
levels_agree = (np.allclose(Utot, Utot_old, rtol=rtol, atol=atol)
if return_evolution_operator
else np.allclose(P, P_old, rtol=rtol, atol=atol))
if levels_agree and effective_refinement >= MIN_EFFECTIVE_REFINEMENT:
n_agreements += 1
else:
n_agreements = 0
if n_agreements >= agreements_required:
if convergence_info is not None:
convergence_info['last_gap'] = last_gap
convergence_info['n_agreements'] = n_agreements
convergence_info['tolerance_achieved'] = True
if (verbose > 0):
for f in [None, file_log] if save_log else [None]:
tol_msg = gd.TOL_MSG_IN_COLOR if f is None else gd.TOL_MSG_NO_COLOR
print(" " + tol_msg + " (for fixed magnus_exp_order "+ \
"= " + str(magnus_exp_order) + "): rtol = " + str(rtol) + \
", atol = " + str(atol) + ".\n", file=f)
# Outside the verbosity branch. The other three return paths close
# unconditionally; honoring close_file_log_upon_exit only when the caller
# also asked for output left the file open on the ordinary converged path.
if save_log and close_file_log_upon_exit: file_log.close()
return (P, Utot) if return_evolution_operator else P
P_old = np.ndarray.copy(P)
if return_evolution_operator:
Utot_old = np.ndarray.copy(Utot)
n_slabs_prev_level = n_slabs
n_tpts_prev_level = n_tpts_per_slab
n_edges_prev_level = len(t_slab_edges)
# Increase the number of slabs approximately by growth_factor_n_slabs. Do it only
# if the slab edges have *not* been explicitly provided by the user in t_slab_edges.
if t_slab_edges_original is None:
ran_with_max_n_slabs = False if n_slabs < max_n_slabs else True
n_slabs_old = n_slabs
n_slabs = min(round(growth_factor_n_slabs*n_slabs), max_n_slabs)
# Occasionally, the new number of slabs could be equal to the old number (i.e., if
# growth_factor_n_slabs is too small or if n_slabs = 1). If this happens, increase
# the new number of slabs by 1.
if ((growth_factor_n_slabs > 1.0) and (n_slabs < max_n_slabs) and \
(n_slabs == n_slabs_old)): n_slabs += 1
# Increase the number of points per slab approximately by growth_factor_n_tpts_per_slab
ran_with_max_n_tpts_per_slab = False if n_tpts_per_slab < max_n_tpts_per_slab else True
n_tpts_per_slab_old = n_tpts_per_slab
n_tpts_per_slab = min(int(growth_factor_n_tpts_per_slab*n_tpts_per_slab),
max_n_tpts_per_slab)
if ((growth_factor_n_tpts_per_slab > 1.0) and \
(n_tpts_per_slab < max_n_tpts_per_slab) and \
(n_tpts_per_slab == n_tpts_per_slab_old)): n_tpts_per_slab += 1
loop_count += 1
_PHASE_SLOPE_STEP = 1.0e-3
r"""float: Module-level constant
Step in :math:`\ln E` of the central difference :func:`_avg_prob_dispatch` takes of the
Hamiltonian for the slopes of the phase average. Truncation goes as its square (1e-7 relative
for a Hamiltonian linear in 1/E), round-off as its inverse; a slope that round-off could explain
is replaced from its phase in :mod:`magnus.avgprob`, which is told this step.
.. versionadded:: 1.1.1
"""
_PHASE_AVERAGE_GATE = 1.0e-4
r"""float: Module-level constant
Largest change in any probability for which :func:`_avg_prob_dispatch` returns the decohered
value it computed first, bit for bit, instead of the phase average (issue #64). A tenth of the
default tolerance: where every phase has decohered the two agree far below it, and a result
that was right before stays exactly what it was.
.. versionadded:: 1.1.1
"""
def _avg_prob_dispatch(
htot: Callable,
htot_is_function_only_of_energy: bool,
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Union[int, float],
nu_i: Optional[int],
nu_f: Optional[int],
average: bool,
source_func_name: str,
smooth_profile: Optional[bool] = True,
engine_kwargs: Optional[dict] = None,
average_spread: Optional[float] = None,
energy_dependent: Optional[bool] = True
):
r"""Phase-averaged probabilities: in closed form, by adiabatic transport, or over an energy window.
Returns ``NotImplemented`` when ``average`` is falsy, so a caller can place this ahead of
its ordinary dispatch chain and fall through untouched in the default case.
The first two routes return the phase average of :mod:`magnus.avgprob` (issue #64): every
interference term kept with its phase and weighted by the spread of that phase across a
relative energy spread ``average_spread``. With the Hamiltonian independent of position it
is a closed form, one eigendecomposition per energy. Each point is computed as the decohered
limit first, and recomputed only where some interference can survive -- on a profile, only
where the limit's own search found a non-adiabatic window, since adiabatic transport of a
decohered start carries none; the limit is returned, bit for bit, wherever the two agree
within ``_PHASE_AVERAGE_GATE``, or, on a profile, within the tighter of ``rtol`` and ``atol``
in ``engine_kwargs`` when that is tighter still. ``PhaseAveragingWarning`` then says that the result depends
on the spread. A Hamiltonian without energy dependence keeps the limit, and the warning
keeps its original meaning for it: some pair has neither decohered nor stayed coherent.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Returns the phase average; takes ``average_spread`` and ``energy_dependent``.
Parameters
----------
htot : Callable
Total Hamiltonian [eV]: ``htot(energy)`` when it does not depend on position,
``htot(energy, l)`` otherwise.
htot_is_function_only_of_energy : bool
Whether ``htot`` is independent of position. A position-dependent Hamiltonian is
averaged by adiabatic transport when ``smooth_profile`` is True, and across an energy
window, by real propagation, when it is not.
energy : int, float, list, or np.ndarray
Neutrino energy/energies [eV].
L : int, float, list, or np.ndarray
Baseline(s) [:math:`\text{eV}^{-1}`], used to decide which eigenvalue pairs have
decohered.
L0 : int or float
Start of the trajectory [:math:`\text{eV}^{-1}`], the position the
position-dependent routes integrate from.
nu_i, nu_f : int or None
Initial and final flavor; if both are given, the single probability is returned.
average : bool
Whether the caller asked for the averaged probability.
source_func_name : str
Name of the calling function, for error messages.
smooth_profile : bool
Whether the caller declared the profile smooth. True selects adiabatic
transport; False selects the energy-window average.
engine_kwargs : dict or None
Engine settings forwarded to the energy-window route. Required there: the
non-smooth branch raises ``ValueError`` without them. On a smooth profile, the tighter
of their ``rtol`` and ``atol`` is the tolerance of the phase average's window patches
and stretch phases (issue #65); without either, those take the defaults of
:func:`magnus.avgprob.phase_averaged_probabilities_adiabatic`.
average_spread : float or None
Relative energy spread of the phase average. None means
:data:`magnus.avgprob.AVG_PHASE_SPREAD`.
energy_dependent : bool
Whether ``htot`` depends on the energy it is given. A Hamiltonian that does not -- a
fixed matrix, or a function of position alone, on the direct route -- has no slope for
an energy spread to act on, so it keeps the decohered limit.
Returns
-------
np.ndarray, float, or NotImplemented
The averaged probabilities, shaped as the caller's ordinary return value, or
``NotImplemented`` if ``average`` is falsy.
"""
if not average:
return NotImplemented
spread = avgprob.AVG_PHASE_SPREAD if average_spread is None else average_spread
if (isinstance(spread, bool) or not isinstance(spread, (int, float, np.integer, np.floating))
or not np.isfinite(spread) or spread < 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": average_spread "
"is the relative energy spread of the phase average and must be a non-negative "
"number, not " + repr(average_spread) + ".")
spread = float(spread)
phase_average = bool(energy_dependent)
sample_numerically = (not htot_is_function_only_of_energy) and (not smooth_profile)
if sample_numerically and (engine_kwargs is None):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": average=True "
"needs a Hamiltonian that is either constant along the trajectory or smooth enough "
"to have an instantaneous eigenbasis, and this caller cannot fall back to sampling.")
energy_arr, L_arr, return_float, ok = _normalize_energy_L(energy, L)
if not ok:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": the energy "
"and L arrays must have the same length, or one of them must be a single value.")
if len(energy_arr) == 1 and len(L_arr) > 1:
energy_arr = np.repeat(energy_arr, len(L_arr))
if len(L_arr) == 1 and len(energy_arr) > 1:
L_arr = np.repeat(L_arr, len(energy_arr))
n_pts = len(energy_arr)
undecided_points = 0
uncertified_points = 0
unresolved_points = 0
recomputed_points = 0
spread_sensitive_points = 0
unaveraged_points = 0
largest_sensitivity = 0.0
h = _PHASE_SLOPE_STEP
# The caller's tolerance, for the profile route: the phase average converges its patches and
# phases to the tighter of rtol and atol, and the limit stands only where it agrees within
# that, if it is tighter than the gate (issue #65).
tols = [float(t) for t in ((engine_kwargs or {}).get('rtol'), (engine_kwargs or {}).get('atol'))
if t is not None]
tol = min(tols) if tols else None
gate = _PHASE_AVERAGE_GATE if tol is None else min(_PHASE_AVERAGE_GATE, tol)
def keep_or_replace(i, P_new, sensitivity):
# Today's value, bit for bit, unless the phase average moves it by more than the gate.
nonlocal recomputed_points, spread_sensitive_points, largest_sensitivity
if np.max(np.abs(P_new - P_out[i])) >= gate:
P_out[i] = P_new
recomputed_points += 1
largest_sensitivity = max(largest_sensitivity, float(sensitivity))
if sensitivity > avgprob.PHASE_SPREAD_SENSITIVITY_THRESHOLD:
spread_sensitive_points += 1
if htot_is_function_only_of_energy:
# Constant along the trajectory: the limit is closed-form, one
# eigendecomposition per energy.
H = np.stack([np.asarray(htot(float(enu)), dtype=complex) for enu in energy_arr])
eigenvalues, eigenvectors = np.linalg.eigh(H)
d = H.shape[-1]
P_out = np.empty((n_pts, d, d))
# Points where every pair has decohered have singleton blocks and nothing undecided --
# what coherence_report finds for them one at a time -- and their formula is batched.
# Bit for bit: a singleton block is a product, exact however many are formed at once.
gaps = (np.abs(eigenvalues[:, :, None] - eigenvalues[:, None, :])
* np.abs(np.asarray(L_arr, dtype=float))[:, None, None])
decohered = np.all(gaps[:, ~np.eye(d, dtype=bool)] > avgprob.DECOHERENCE_PHASE_THRESHOLD,
axis=1)
if np.any(decohered):
P_out[decohered] = avgprob.averaged_probabilities_from_eigenbasis(
eigenvectors[decohered])
for i in np.flatnonzero(~decohered):
blocks, undecided = avgprob.coherence_report(eigenvalues[i], float(L_arr[i]))
if undecided and not phase_average: undecided_points += 1
P_out[i] = avgprob.averaged_probabilities_from_eigenbasis(eigenvectors[i],
blocks=blocks)
if phase_average:
# The derivative in ln E by a central difference: two more Hamiltonians per energy
# and no eigendecomposition, the slopes coming from Hellmann-Feynman.
D = np.array([(np.asarray(htot(float(enu)*np.exp(h)), dtype=complex)
- np.asarray(htot(float(enu)*np.exp(-h)), dtype=complex))/(2.0*h)
for enu in energy_arr])
P_new, sens = avgprob.phase_averaged_probabilities_constant_hamiltonian(H, D,
np.asarray(L_arr, dtype=float) - float(L0), spread=spread, dH_dlnE_step=h)
moved = np.max(np.abs(P_new - P_out), axis=(1, 2)) >= _PHASE_AVERAGE_GATE
P_out[moved] = P_new[moved]
recomputed_points += int(np.count_nonzero(moved))
spread_sensitive_points += int(np.count_nonzero(
sens > avgprob.PHASE_SPREAD_SENSITIVITY_THRESHOLD))
largest_sensitivity = max(largest_sensitivity, float(np.max(sens, initial=0.0)))
elif sample_numerically:
# No closed form: the profile steps through discontinuities (PREM layer boundaries),
# so there is no instantaneous eigenbasis to decohere in. The probability is instead
# propagated for real across an energy window and averaged over it.
#
# This is a different quantity from the two branches above. They return the exact
# L/E -> infinity limit, which needs no window; this returns the average over one
# particular window, and the answer depends on its width. That is why it is warned
# about below, and why the width is a named constant rather than a literal.
d = np.asarray(htot(float(energy_arr[0]), float(L0)), dtype=complex).shape[-1]
P_out = np.empty((n_pts, d, d))
eng = dict(engine_kwargs)
extra = eng.pop('kwargs', None) or {}
worst_sem = 0.0
for i in range(n_pts):
L_i = float(L_arr[i])
def prob_of_energy(enu, L_i=L_i):
return osc_prob_energy_baseline(htot, enu, L_i, L0, None, None,
htot_is_function_only_of_energy, **eng, **extra)
P_out[i], sem = avgprob.averaged_probabilities_numerically(prob_of_energy,
float(energy_arr[i]))
worst_sem = max(worst_sem, sem)
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": average=True "
"on a profile with discontinuities has no closed form, so the probability was "
"propagated across an energy window of +/-" +
str(100.0*avgprob.AVG_DEFAULT_ENERGY_SPREAD) + "% and averaged over " +
str(avgprob.AVG_DEFAULT_N_SAMPLES) + " samples. This is the average over that "
"window, not the L/E -> infinity limit, and it depends on the window: the largest "
"standard error of the mean here is " + format(worst_sem, '.2e') + ". Pass an "
"explicit width via magnus.avgprob.averaged_probabilities_numerically if the "
"measurement has a known resolution. Shown once per session.",
PhaseAveragingWarning, stacklevel=3)
else:
# Position-dependent and smooth: decohere in the eigenbasis at production, transport
# along the levels of the instantaneous Hamiltonian (with the exact crossing
# probabilities wherever the evolution stops being adiabatic), and read out in the
# eigenbasis at detection. See magnus.avgprob.averaged_probabilities_adiabatic.
d = np.asarray(htot(float(energy_arr[0]), float(L0)), dtype=complex).shape[-1]
P_out = np.empty((n_pts, d, d))
for i in range(n_pts):
enu = float(energy_arr[i])
def H_of_l(l, enu=enu):
return htot(enu, l)
P_out[i], report = avgprob.averaged_probabilities_adiabatic(H_of_l, float(L0),
float(L_arr[i]))
if (report['undecided'] or report['undecided_between_crossings']) and not phase_average:
undecided_points += 1
if report.get('resolved') is False:
unresolved_points += 1
elif (not report['patches_converged']) or report.get('certified') is False:
uncertified_points += 1
if phase_average and report['windows']:
# Without a window a decohered start, carried adiabatically, has no interference
# to keep: the value above is already the phase average. With one, recompute.
def D_of_l(l, enu=enu):
return (np.asarray(htot(enu*np.exp(h), l), dtype=complex)
- np.asarray(htot(enu*np.exp(-h), l), dtype=complex))/(2.0*h)
try:
P_new, pa_report = avgprob.phase_averaged_probabilities_adiabatic(H_of_l, D_of_l,
float(L0), float(L_arr[i]), spread=spread, dH_dlnE_step=h,
patch_atol=tol, phase_tol=tol)
except RuntimeError:
unaveraged_points += 1
continue
keep_or_replace(i, P_new, pa_report['sigma_sensitivity'])
if not (pa_report['patches_converged'] and pa_report['phases_converged']):
uncertified_points += 1
if unresolved_points > 0:
# Issue #60. The profile has a feature narrower than the averaging engine's probe
# grid, able to move probability between levels, and no refinement resolves it: a
# discontinuity. The engine could only treat it as smooth, which is what it did
# silently before 1.1.1 -- measured wrong by up to 0.56 on a supernova shock ray.
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": average=True "
"on a profile with a discontinuity that was not declared, at " +
str(unresolved_points) + " of " + str(n_pts) + " (energy, L) point(s). The "
"adiabatic route cannot resolve it and treated it as smooth, so the averaged "
"probability there ignores whatever the jump does and may be far off. Pass "
"t_breakpoints at the discontinuity: the call then averages over an energy window "
"instead, measured within two standard errors of a decohered reference on a "
"supernova shock. Shown once per session.",
UnmarkedDiscontinuityWarning, stacklevel=3)
if uncertified_points > 0:
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": the "
"level-crossing probabilities could not be certified at " +
str(uncertified_points) + " of " + str(n_pts) + " (energy, L) point(s) -- a local "
"Magnus patch across a crossing did not converge, or the refinement that located "
"the crossings did not certify them -- so they are not trustworthy there. Shown "
"once per session.", HybridCertificationWarning, stacklevel=3)
if spread_sensitive_points > 0:
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": the phase-averaged "
"probability depends on the energy spread at " + str(spread_sensitive_points) + " of "
+ str(n_pts) + " (energy, L) point(s): some interference has partly survived the "
"spread average_spread=" + format(spread, 'g') + ", so the result changes by more than "
+ format(avgprob.PHASE_SPREAD_SENSITIVITY_THRESHOLD, 'g') + " per e-fold of it "
"(largest " + format(largest_sensitivity, '.1e') + "). It is the average over that "
"spread; pass average_spread to match the resolution of the measurement. Shown once "
"per session.", PhaseAveragingWarning, stacklevel=3)
if unaveraged_points > 0:
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": the phase "
"average could not be formed at " + str(unaveraged_points) + " of " + str(n_pts) +
" (energy, L) point(s), where too many interference terms survive across the "
"non-adiabatic windows; the decohered limit was returned there instead. Shown once "
"per session.", PhaseAveragingWarning, stacklevel=3)
if undecided_points > 0:
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": the averaged "
"probability was requested at " + str(undecided_points) + " of " +
str(len(energy_arr)) + " (energy, L) point(s) where at least one pair of eigenvalues "
"has neither decohered nor stayed coherent, so no averaged expression describes it. "
"The oscillation probability itself (average=False) is the meaningful quantity there. "
"Shown once per session.", PhaseAveragingWarning, stacklevel=3)
_note_engine('average', average_spread=spread, recomputed=recomputed_points,
sigma_sensitivity=largest_sensitivity)
if (nu_i is not None) and (nu_f is not None):
P_out = P_out[:, nu_i, nu_f]
return P_out.__getitem__(0 if return_float else slice(None))
r"""tuple: Module-level constant
Keys carried by the entries of :data:`magnus.globaldefs.OSC_PARAMS_PREDEFINED` that
label the parameter set rather than parameterize the physics.
They are the reason ``**OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']`` cannot be
splatted into a probability function: the two strings travel down the shared
``**kwargs`` chain and are rejected at the far end by
``magnus_expansion_multislab``, whose complaint names neither the caller nor the
parameter set it came from. :func:`magnus.globaldefs.load_nufit_params` returns
the same numbers without them.
.. versionadded:: 1.0.0
"""
[docs]
PASSTHROUGH_KWARGS_DOCUMENTED = (
'atol', 'cumulative', 'expm_backend', 'integration_method', 'magnus_exp_order',
'max_n_slabs', 'n_slabs', 'n_tpts_per_slab', 'rtol', 't_breakpoints', 't_slab_edges')
r"""tuple of str: Module-level constant
The engine keywords the probability wrappers forward through ``**kwargs``, in the order
they are listed to a caller who has just misspelled one.
**These are the parameters that do not appear in any wrapper signature**, which is the
problem this constant exists to mitigate: ``t_breakpoints``, ``n_slabs`` and ``cumulative``
are load-bearing on a hard profile -- declaring a shock front changes the answer by parts
in :math:`10^6` on a scan that otherwise measures straddled slabs -- and a reader working
from the signature alone will never find them.
``expm_backend`` was accepted and absent from this list, which is the worst of both: it
works, so it is worth knowing about, and misspelling it produced an error naming ten
keywords that did not include the one wanted. Validation is not affected either way --
that is derived from the signatures by ``_passthrough_kwarg_names`` -- so this tuple
is what a caller is *told*, and the whole point of it is to leave nothing out.
Deliberately a *subset* of what ``_passthrough_kwarg_names`` accepts. That function is
derived from signatures and includes engine internals no caller of a wrapper should reach
for; this is the curated list worth printing. ``test_documented_passthrough_kwargs_are_all_accepted``
keeps the two from drifting apart.
.. versionadded:: 1.0.0
"""
@functools.lru_cache(maxsize=1)
def _passthrough_kwarg_names() -> frozenset:
r"""Every keyword the ``**kwargs`` chain can absorb, read off the signatures.
Derived rather than listed, because a hand-written allowlist is a second copy of the
signatures and would drift from them silently -- the same duplication that let a
matter-potential defect survive in five places at once. The union is taken over the
functions ``**kwargs`` actually reaches: the dispatcher, the core, and the Magnus
engine at the far end.
Verified against the call: every keyword this returns is accepted by
:func:`osc_prob_matter_std_potential`, and the ones a caller is likely to misspell --
``t_breakpoint`` for ``t_breakpoints``, ``nslabs`` for ``n_slabs`` -- are not in it.
Cached, because this ran on every call that validates keywords and cost about
113 us each time -- a tenth of the fixed overhead of a single-point call, spent
re-reading three signatures that cannot change: they are module-level
definitions, and nothing in the package rebinds or wraps them. What is cached
is the reading, not a second copy of the names, so the derived-not-listed
property above is untouched. ``.cache_clear()`` is available for a caller that
patches one of those signatures and wants the union recomputed.
.. versionadded:: 1.0.0
"""
import inspect
import magnus.magnus as _magnus
names = set()
for func in (osc_prob, osc_prob_energy_baseline, _magnus.magnus_expansion_multislab):
names |= {p for p in inspect.signature(func).parameters if p != 'kwargs'}
return frozenset(names)
def _check_passthrough_kwargs(kwargs: dict, source_func_name: str) -> None:
r"""Rejects an unknown keyword here, where the caller and the near match can be named.
**The keywords that matter most on a hard profile are the ones least visible.**
``t_breakpoints``, ``n_slabs`` and ``cumulative`` reach these entry points only through
``**kwargs`` and appear in none of their signatures, while ``t_slab_edges`` -- the full
explicit edge set, which is rarely what a caller wants -- *is* declared. So the
discoverable keyword is the specialized one and the everyday one is invisible.
Left to itself a typo surfaces as ``TypeError: magnus_expansion_multislab() got an
unexpected keyword argument 't_breakpoint'``, naming a function the caller never
invoked and offering no correction. Since these keywords are load-bearing -- declaring
a shock front moves the answer by parts in 10^6 on a scan that otherwise measures
straddled slabs -- a silent typo is a wrong number rather than an error.
Raises
------
ValueError
Naming the entry point that was called, the keyword that is not recognized, and
the closest keyword that is.
.. versionadded:: 1.0.0
"""
valid = _passthrough_kwarg_names()
unknown = [key for key in kwargs if key not in valid]
if not unknown:
return
import difflib
# Suggestions are drawn from the full accepted set, but only the useful subset is
# *listed*: the derived set includes engine internals (`A`, `H_func`,
# `close_file_log_upon_exit`) that no caller of a wrapper should be reaching for, and
# printing forty names buries the one that matters.
lines = []
for key in sorted(unknown):
close = difflib.get_close_matches(key, sorted(valid), n=1, cutoff=0.6)
lines.append("'" + key + "'" + (", did you mean '" + close[0] + "'?"
if close else ''))
raise ValueError(
gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": unrecognized keyword "
"argument(s): " + '; '.join(lines) + ". The engine keywords these wrappers forward "
"are: " + ', '.join(PASSTHROUGH_KWARGS_DOCUMENTED) + ". Note that t_breakpoints "
"(positions at which to place slab edges, filling in between) and t_slab_edges (the "
"complete set of edges) are different parameters; t_breakpoints is the one wanted "
"for a density jump or a shock front.")
def _reject_parameter_set_metadata(kwargs: dict, source_func_name: str) -> None:
r"""Rejects the label keys of a predefined parameter set, with the remedy.
Splatting a whole ``OSC_PARAMS_PREDEFINED`` entry is the natural thing to
write and it does not work: ``name`` and ``description`` are not oscillation
parameters, so they flow through every ``**kwargs`` hop until the Magnus core
raises ``TypeError: magnus_expansion_multislab() got an unexpected keyword
argument 'name'`` -- a message that points at the one function in the chain
that has nothing to do with the mistake.
Caught here instead, where the caller and the fix can both be named.
.. versionadded:: 1.0.0
"""
found = [key for key in PARAMETER_SET_METADATA_KEYS if key in kwargs]
if not found:
return
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": received " +
str(found) + ", which label a predefined parameter set rather than parameterize the "
"physics. This happens when an entry of globaldefs.OSC_PARAMS_PREDEFINED is passed "
"whole, as **OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']. Use "
"globaldefs.load_nufit_params(...), which returns the same numbers without the labels, "
"or drop the labeling keys.")
def _normalize_energy_L(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray]
) -> Tuple[np.ndarray, np.ndarray, bool, bool]:
r"""Normalize energy and L to same-length 1D arrays.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
Returns
-------
(np.ndarray, np.ndarray, bool, bool)
``(energy, L, return_float, ok)``: ``energy`` and ``L`` broadcast to the same length;
``return_float`` records whether both inputs were scalars (so that the caller returns a
scalar-like result); ``ok`` records whether the input lengths were compatible (equal, or
one of them of length 1).
"""
# Two scalars is the single most common call shape, and it reached here as four numpy
# constructions (two np.array, two np.full) to build a pair of length-1 arrays. Building
# them directly skips the isinstance ladder and the broadcast branch below.
if type(energy) is float and type(L) is float:
return np.array([energy]), np.array([L]), True, True
energy = float(energy) if isinstance(energy, int) else energy
L = float(L) if isinstance(L, int) else L
return_float = isinstance(energy, float) and isinstance(L, float)
energy = np.array([energy]) if isinstance(energy, float) else np.array(energy)
L = np.array([L]) if isinstance(L, float) else np.array(L)
ok = ((len(energy) == len(L)) or (len(energy) == 1 and len(L) > 1) or
(len(energy) > 1 and len(L) == 1))
if ok:
energy = np.full(len(L), energy[0]) if (len(energy) == 1) else energy
L = np.full(len(energy), L[0]) if (len(L) == 1) else L
return energy, L, return_float, ok
#-----------------------------------------------------------------------
# Which engine answered: the shared instrument behind strategy_info and
# cross_check_strategies
#-----------------------------------------------------------------------
[docs]
ENGINE_FAMILIES = {
'hybrid': 'adiabatic',
'ip_exp': 'interaction-picture',
'magnus': 'magnus-ladder',
'cumulative': 'magnus-ladder',
'separable': 'magnus-ladder',
'average': 'phase-average',
'expm': 'exact',
# 'constant' shares a family with 'expm' rather than standing alone, even though the two use
# different exponential implementations (magnus._expm_stack against scipy's Pade). What they
# share is the *assumption* -- that H does not depend on position -- and that assumption is
# the thing that could be wrong, so their agreement must not be read as independent
# confirmation of it.
'constant': 'exact',
}
r"""dict: Module-level constant
Which engines share machinery, and therefore which pairwise comparisons in
:func:`cross_check_strategies` carry information. Two engines in the **same** family can be
wrong in the same way at the same time, so their agreement is not evidence; two in different
families fail for different reasons.
* ``'adiabatic'`` -- :func:`magnus.adiabatic.hybrid_propagator`: transport in the instantaneous
eigenbasis, with Magnus patches only inside non-adiabatic windows. Its blind spots are the
detector's (a feature narrower than the probe grid, a profile the resolution test rejects).
* ``'magnus-ladder'`` -- the general per-point path, the cumulative baseline scan, and the
energy-batched separable scan. All three walk slabs with
:func:`magnus.magnus.magnus_expansion_multislab`, and the cumulative scan additionally *sizes*
its grid from an ordinary adaptive :func:`osc_prob` probe, so it inherits that path's stopping
rule as well. Grouping them is deliberate: the accuracy step at
:data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS` shows they are not interchangeable, but a
quadrature that cannot see a feature will not see it on any of the three grids.
* ``'interaction-picture'`` -- the 2-flavor exponential-profile fast path. It uses the same
Magnus core, but factors the fast vacuum phase out analytically first, so what it must resolve
is a different function; it is kept separate for that reason and not because the core differs.
* ``'exact'`` -- ``scipy.linalg.expm``, used only where it is the exact answer rather than an
approximation (see :func:`cross_check_strategies`).
* ``'phase-average'`` -- :mod:`magnus.avgprob`'s closed form, which answers a different question
and is never compared against the others.
**A known limit of the ``'magnus-ladder'`` grouping.** It is right about *blind spots* -- all
three walk slabs, and all three miss a feature narrower than their grids -- but it understates
how differently they perform. Measured over 28 shared workloads, the cumulative scan's worst
error is 1.13e-07 against the general ladder's 9.28e-03
(``docs/dev/adversarial_batteries/fallback_quality.py``), which is what moved
:data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS` from 25 to 8. So a disagreement *between* them
carries real information even though this grouping tells :func:`cross_check_strategies` to
discount it. Erring this way is deliberate: treating them as independent would let two engines
that share a blind spot vouch for each other, which is the failure this constant exists to
prevent.
.. versionadded:: 1.0.0
"""
_ENGINE_TRACE = None
r"""list or None: set to a list by ``_engine_probe`` while a diagnostic is watching; ``None``
(and therefore free) on every ordinary call."""
_ENGINES_DISABLED = frozenset()
r"""frozenset: engine labels that the dispatchers must decline, set by ``_engine_probe``. Used
only by :func:`cross_check_strategies`, to reach an engine that a faster one would otherwise
answer for -- there is no user-facing way to ask for the general ladder specifically when the
interaction-picture path applies, and a cross-check that silently compared the same engine with
itself would be exactly the failure it exists to detect."""
def _suggest_breakpoints(profile, scan, n_local=2048, hold=0.90, n_edges=7) -> list:
r"""Build the ``t_breakpoints`` that :class:`HiddenFeatureWarning` tells the caller to pass.
The scan localizes a hidden feature only to a reference-interval pair, about
:math:`3\times10^{-4}` of the trajectory -- 10 to 100 widths of the features in the band
the detector is calibrated on. Edges that far apart do not cure: measured through the
public path on the ``FINDINGS_ADVERSARIAL_VALIDATION.md`` §3.3 cases, a pair-scale
padded bracket (which an earlier version of the warning suggested) left a single point at
3.0e-02 and moved a 60-point scan from 3.0e-02 to 5.8e-02 -- silently, because supplying
``t_breakpoints`` also switches the scan off. So the flagged pair is re-sampled here at
``n_local`` points, the smallest sub-interval holding ``hold`` of the pair's variation is
padded by its own width on each side, and ``n_edges`` uniform edges are laid across it.
The inner slabs come out near two feature widths, which is also what the verified manual
cure (edges at :math:`\pm 2` and :math:`\pm 8` widths) produces.
Verified through the public path at every detected centre tried, five per width: at a
width of 3e-5 of the trajectory, bare errors of 3.2e-03 to 3.0e-02 go to 2.9e-07 to
1.5e-04; at 1e-5, 4.1e-03 to 1.0e-02 go to 3.9e-05 to 9.4e-05; the flagship case's
60-point scan lands at 1.0e-04. On a feature so narrow it was harmless (width 1e-6 with a
bare error of 2.8e-11), the breakpoints route the call to the general slab ladder and the
answer lands near the requested tolerance instead -- the warning says so, making that
trade the caller's.
Cost is ``n_local`` scalar evaluations of ``profile``, paid only on the warn path, which
fires on none of the 67 smooth calibration profiles.
.. versionadded:: 1.0.12
Parameters
----------
profile : Callable
The scalar profile the scan ran on; must accept an array of positions.
scan : dict
A result of :func:`magnus.adiabatic.find_hidden_features` whose ``'hidden'`` is True.
n_local : int
Points of the local re-sample across the flagged pair.
hold : float
Fraction of the pair's variation the tight sub-interval must contain.
n_edges : int
Edges laid uniformly across the padded sub-interval.
Returns
-------
list
Positions to pass as ``t_breakpoints``. Falls back to the pair-scale 3-point bracket
if the profile cannot be re-sampled or shows no variation here -- nearly unreachable,
since the scan just sampled it and found plenty.
"""
lo0, hi0 = float(scan['l_lo']), float(scan['l_hi'])
fallback = [lo0 - 3.0*(hi0 - lo0), float(scan['l_centre']), hi0 + 3.0*(hi0 - lo0)]
try:
xs = np.linspace(lo0, hi0, n_local + 1)
steps = np.abs(np.diff(np.asarray(profile(xs), dtype=float)))
except Exception: # noqa: BLE001 -- same rule as the scan: never break the call
return fallback
if (steps.ndim != 1) or (steps.shape[0] != n_local) or (not np.all(np.isfinite(steps))):
return fallback
total = float(steps.sum())
if total <= 0.0:
return fallback
order = np.argsort(steps)[::-1]
kept = np.sort(order[:int(np.searchsorted(np.cumsum(steps[order]), hold*total)) + 1])
lo, hi = xs[kept[0]], xs[kept[-1] + 1]
width = hi - lo
return list(np.linspace(lo - width, hi + width, n_edges))
def _scan_for_hidden_features(profile, l0, L, t_breakpoints=None) -> Optional[Dict]:
r"""Run the sub-probe feature scan once for a whole call, and warn if it finds something.
Placed at the entry points rather than inside any one engine because the blind spot is not
one engine's: the hybrid probe grid, the general ladder's slabs and the cumulative scan's
accuracy grid all miss the same feature. See :class:`HiddenFeatureWarning`.
Skipped when the caller supplied ``t_breakpoints`` (they have already said where the
structure is) and when the profile is not a function of position (nothing to hide in).
.. versionadded:: 1.0.0
.. versionchanged:: 1.0.12
The warning's suggested ``t_breakpoints`` now come from ``_suggest_breakpoints``,
which localizes the feature by re-sampling the flagged interval; the pair-scale bracket
it replaces was measured not to cure the band the detector exists for.
Returns
-------
dict or None
The scan result from :func:`magnus.adiabatic.find_hidden_features`, or None if the scan
did not apply.
"""
if not callable(profile):
return None
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
return None
L_arr = np.atleast_1d(np.asarray(L, dtype=float))
l1 = float(np.max(L_arr))
if l1 == float(l0):
return None
# Sample more finely when the request is already expensive. The scan runs ONCE per call
# whatever the point count, so its share of the work falls as the request grows -- and what
# extra sub-steps buy is reach at the narrow end, where detection is limited by whether any
# sample lands inside the feature at all. Measured: 0.37 ms at 8 sub-steps, 2.85 ms at 32,
# against a ~16 ms single point and a ~40 ms eight-point scan, so this holds the scan under
# about 7% of the call at every size instead of spending 20% of the cheapest one.
#
# A single point therefore keeps the cheapest scan by design. That is the honest trade: the
# widths this buys (3e-6 of the trajectory and below, where detection goes 0.82 -> 0.97) are
# narrower than anything physically plausible in a density profile, so paying 20% on every
# single-point call to reach them would be the wrong way round.
n_points = int(L_arr.size)
n_sub = 8 if n_points < 4 else (16 if n_points < 16 else 32)
scan = adiabatic.find_hidden_features(profile, float(l0), l1, n_sub=n_sub)
if not scan['hidden']:
return scan
# The position IS the actionable content here, so it goes in the message even though that
# costs the static-message dedup other warnings keep. It does not cost much: the scan and
# the suggested edges depend on the profile and the interval only, so every call over the
# same profile produces the same string and Python's default filter still collapses them.
edges = _suggest_breakpoints(profile, scan)
warnings.warn(
"osc_prob: the density profile has structure too narrow for any grid this package "
"samples on, near l = " + format(scan['l_centre'], '.6e') + ". Every engine misses it "
"-- the adiabatic probe grid, the general Magnus slabs and the cumulative scan alike -- "
"so they agree with each other and may be wrong together, and no choice of strategy or "
"tolerance helps. Measured on such a profile: wrong by 2.9e-02 against a requested "
"1e-3. Pass t_breakpoints=["
+ ", ".join(format(e, '.6e') for e in edges) + "] to put slab edges across it "
"(verified on the case above: 3.0e-02 -> 1.0e-04, at a point and over a 60-point "
"scan). Passing t_breakpoints routes the call to the general slab ladder and switches "
"this scan off, so expect accuracy near the requested tolerance rather than below it. "
"If the narrow structure is an artifact of how the profile function was written rather "
"than physics, this is safe to ignore. Shown once per profile per session.",
HiddenFeatureWarning, stacklevel=3)
return scan
def _sampling_report(htot, htot_is_function_only_of_energy, energy, L, L0) -> Optional[Dict]:
r"""How coarsely does this request sample the oscillation it is computing?
Reported through ``strategy_info`` and never warned about. A Nyquist criterion is
objectively correct and fires on essentially every realistic scan -- 4400 points would be
needed on a solar trajectory and 73 000 on a supernova ray, so 44 of 45 measured scan sizes
are formally aliased -- and a warning firing on 98 % of calls is noise however right each
firing is. See :func:`magnus.adiabatic.oscillation_sampling` and
``docs/dev/adversarial_batteries/alias_fp.py``.
Evaluated at the **lowest** energy in the request, which is where the oscillation is
fastest: the vacuum term goes as ``1/E`` while the matter term does not, so the spectral
spread is largest there.
Called only when ``strategy_info`` was supplied, so a caller who does not ask for a report
pays nothing. A caller who does pays 5.5 % on the cheapest scan measured and under 0.1 % on
a substantial one.
.. versionadded:: 1.0.0
Returns
-------
dict or None
The report from :func:`magnus.adiabatic.oscillation_sampling`, or None if it does not
apply. A diagnostic must never break the call it was inspecting, so every failure here
is a quiet None.
"""
try:
e_min = float(np.min(np.atleast_1d(np.asarray(energy, dtype=float))))
L_arr = np.atleast_1d(np.asarray(L, dtype=float))
l1 = float(np.max(L_arr))
if htot_is_function_only_of_energy:
def H_of_l(l, e=e_min):
return np.asarray(htot(e))
else:
def H_of_l(l, e=e_min):
return np.asarray(htot(e, l))
report = adiabatic.oscillation_sampling(H_of_l, float(L0), l1, baselines=L_arr)
except Exception: # noqa: BLE001 -- never break the call being inspected
return None
return report or None
def _note_engine(label: str, answered: bool = True, **detail) -> None:
r"""Record that ``label`` answered (or declined), if anything is watching.
One dict per dispatch decision, in the order the decisions were taken, so the trace reads as
the route the request took: ``hybrid declined (uncertified) -> magnus answered``.
.. versionadded:: 1.0.0
"""
if _ENGINE_TRACE is not None:
_ENGINE_TRACE.append(dict(engine=label, answered=answered, **detail))
@contextmanager
def _engine_probe(disabled=(), info=None, extra=None):
r"""Watch which engine answers, and optionally forbid some of them.
Restores both globals on the way out, including on an exception, so a raising call (which
``cumulative=True`` does by design when it cannot serve a request) cannot leave a dispatcher
disabled for the rest of the session. ``info``, if given, is the caller's ``strategy_info``
dict and is filled on the way out -- also on an exception, since "which engine was I in when
this raised" is exactly what a caller debugging one wants.
.. versionadded:: 1.0.0
"""
global _ENGINE_TRACE, _ENGINES_DISABLED
prev_trace, prev_disabled = _ENGINE_TRACE, _ENGINES_DISABLED
# Nested probes SHARE one trace, and a nested one can only add to the disabled set. Both
# matter because nesting is the normal case, not an edge case: cross_check_strategies
# watches from outside the wrapper, and the wrapper opens its own probe for strategy_info.
# A fresh list at the inner level collected every note and left the outer one empty, so the
# cross-check reported that no engine had answered at all -- and re-assigning rather than
# unioning the disabled set would have let the inner probe re-enable an engine the outer one
# had switched off, which is how the cross-check reaches an engine a faster one shadows.
trace = prev_trace if prev_trace is not None else []
start = len(trace)
_ENGINE_TRACE = trace
_ENGINES_DISABLED = prev_disabled | frozenset(disabled)
try:
yield trace
finally:
_ENGINE_TRACE, _ENGINES_DISABLED = prev_trace, prev_disabled
if info is not None:
info.update(_summarize_engine_trace(trace[start:]))
if extra is not None:
info.update(extra)
def _summarize_engine_trace(trace) -> Dict:
r"""Turn a raw trace into the ``strategy_info`` an ordinary caller wants.
.. versionadded:: 1.0.0
"""
answered = [e for e in trace if e['answered']]
used = answered[-1] if answered else None
return {
'engine': used['engine'] if used else None,
'family': ENGINE_FAMILIES.get(used['engine']) if used else None,
'certified': used.get('certified') if used else None,
'declined': [(e['engine'], e.get('reason', 'does not apply'))
for e in trace if not e['answered']],
'trace': [{k: v for k, v in e.items() if not k.startswith('_')} for e in trace],
}
def _osc_prob_scan_separable(
H_E: np.ndarray,
VCC_func: Callable,
h_matt: np.ndarray,
L0: float,
L_val: float,
t_breakpoints: Optional[np.ndarray],
magnus_exp_order: int,
integration_method: str,
rtol: Optional[float],
atol: Optional[float],
growth_factor_n_slabs: float,
growth_factor_n_tpts_per_slab: float,
max_num_loops: int,
min_n_slabs: int,
max_n_slabs: int,
min_n_tpts_per_slab: int,
max_n_tpts_per_slab: int,
n_slabs: int,
n_tpts_per_slab: int
) -> np.ndarray:
r"""Energy-batched probability scan for separable Hamiltonians.
Computes the probabilities of many neutrino energies that share the same
baseline [``L0``, ``L_val``] in one batched pipeline, for Hamiltonians of
the separable form
H(E, l) = H_E(E) + M(l) , M(l) = VCC(l) * h_matt(l) ,
where ``H_E`` (shape (nE, d, d)) collects all the position-independent,
energy-dependent terms (vacuum, LIV, ...), ``VCC_func`` is the scalar
matter potential along the trajectory, and ``h_matt`` is the matter
matrix it multiplies -- constant (shape (d, d)) in the ordinary case, or
a callable of position when the sterile projector follows the
composition. What this engine's batching actually requires of the
matter term is that ``M(l)`` be *energy*-independent, not that it be
constant in ``l``: the position samples of ``M`` are computed once per
refinement level and shared by all energies either way, and the Magnus
kernel (quadrature, commutators, exponentials, slab products) runs with
the energy axis batched in front of the slab axis.
The adaptive refinement mirrors :func:`osc_prob`: the slab count (and,
for the quadrature methods, the points per slab) grows geometrically
until the probabilities of each energy agree between successive levels
within (rtol, atol); converged energies drop out of the batch. Energies
are processed in chunks to bound the memory of the sample array.
.. versionadded:: 1.0.0
Parameters
----------
H_E : np.ndarray
Position-independent, energy-dependent part of the Hamiltonian for each energy, shape
(nE, d, d) (vacuum, LIV, ...).
VCC_func : Callable
Scalar matter potential along the trajectory, as a function of position (accepts an array).
h_matt : np.ndarray or Callable
Matrix multiplying ``VCC_func(l)``: a constant of shape (d, d), or a callable of
position returning one -- shape (n, d, d) for an array of n positions, position
axis leading (see :func:`magnus.matter.matter_potential_projector`).
L0 : float
Initial position.
L_val : float
Final position (baseline).
t_breakpoints : np.ndarray, optional
Mandatory slab edges (e.g., PREM layer boundaries) inserted into the grid at every
refinement level.
magnus_exp_order : int
Highest order of the Magnus expansion.
integration_method : str
'gl', 'trapezoid', or 'simpson'.
rtol, atol : float, optional
Target relative/absolute tolerance between successive refinement levels. If both None,
run once with the given fixed ``n_slabs``/``n_tpts_per_slab``.
growth_factor_n_slabs, growth_factor_n_tpts_per_slab : float
Factors by which ``n_slabs``/``n_tpts_per_slab`` are multiplied on each refinement loop.
max_num_loops : int
Maximum number of refinement loops.
min_n_slabs, max_n_slabs : int
Bounds on the number of slabs. ``max_n_slabs=None`` selects the per-method cap;
see :data:`MAX_N_SLABS_DEFAULT`.
min_n_tpts_per_slab, max_n_tpts_per_slab : int
Bounds on the number of time points per slab.
n_slabs, n_tpts_per_slab : int
Starting number of slabs/time points per slab. Under a tolerance, ``n_slabs`` is a
floor on the refinement ladder rather than a discarded argument; see :func:`osc_prob`.
Returns
-------
np.ndarray
Stacked probability matrices, shape (nE, d, d).
"""
# None means 'use the cap appropriate to this integration method'
# (see MAX_N_SLABS_DEFAULT); an explicit value always wins.
max_n_slabs = _resolve_max_n_slabs(max_n_slabs, integration_method)
nE, dim = H_E.shape[0], H_E.shape[-1]
tol_requested = ((rtol is not None) and (atol is not None))
if integration_method == 'gl':
# The accuracy of the GL method is controlled by n_slabs only
growth_factor_n_tpts_per_slab = 1.0
min_n_tpts_per_slab = max_n_tpts_per_slab = n_tpts_per_slab = 2
s_nodes = magnus.gl_nodes(magnus_exp_order)
if tol_requested:
# The caller's n_slabs is a floor on the refinement ladder, not something to discard; see
# the corresponding note in osc_prob.
min_n_slabs = int(min(max(min_n_slabs, n_slabs), max_n_slabs))
n_tpts_per_slab = min_n_tpts_per_slab
# Physics-informed starting number of slabs (see magnus.suggest_n_slabs):
# integral of the traceless Hamiltonian over the trajectory, maximized
# over the energies of the scan
if integration_method == 'gl':
ts = np.linspace(L0, L_val, 17)
V17 = np.asarray(VCC_func(ts))
if callable(h_matt):
# Same trapezoid, on samples of the full matter matrix M(l) = VCC(l)*P(l):
# with a position-resolved projector the integral no longer factorizes into
# I_V times one constant matrix, but M(l) is still energy-independent, so
# the seed still costs one 17-point sweep shared by every energy.
M17 = V17[:, None, None]*np.asarray(h_matt(ts))
I_M = (np.sum(M17, axis=0) - 0.5*(M17[0] + M17[-1]))*(L_val - L0)/16.0
M = (L_val - L0)*H_E + I_M
else:
I_V = (np.sum(V17) - 0.5*(V17[0] + V17[-1]))*(L_val - L0)/16.0
M = (L_val - L0)*H_E + I_V*h_matt
M = M - (np.trace(M, axis1=-2, axis2=-1)/dim)[:, None, None]*np.eye(dim)
try:
phase = np.max(np.linalg.svd(M, compute_uv=False))
except np.linalg.LinAlgError:
phase = 0.0
n_slabs = int(np.clip(max(min_n_slabs,
np.ceil(phase/(2.0*np.pi))), 1, max_n_slabs))
else:
n_slabs = min_n_slabs
P_prev = np.full((nE, dim, dim), np.nan)
P_out = np.empty((nE, dim, dim))
active = np.arange(nE)
# For a constant projector the -i factor is folded once, here; a position-resolved one
# is sampled per refinement level below, on the same grid as the potential.
mA = None if callable(h_matt) else -1j*h_matt.astype(complex)
HE_c = -1j*H_E.astype(complex)
loop_count = 1
while True:
# Slab grid shared by all energies (PREM-layer breakpoints included)
grid = np.linspace(L0, L_val, n_slabs + 1)
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
bp = np.atleast_1d(np.asarray(t_breakpoints, dtype=float))
bp = bp[(bp > L0) & (bp < L_val)]
grid = np.unique(np.concatenate([grid, bp]))
edges = np.column_stack([grid[:-1], grid[1:]])
widths = edges[:, 1] - edges[:, 0]
if integration_method == 'gl':
s = s_nodes
else:
s = np.linspace(0.0, 1.0, n_tpts_per_slab)
tgrid = edges[:, :1] + widths[:, None]*s # (n_slabs, m)
V = np.asarray(VCC_func(tgrid.ravel())).reshape(tgrid.shape)
if callable(h_matt):
# Samples of M(l) = VCC(l)*P(l) on the same grid, shared across energies: the
# cross-energy reuse this engine exists for only ever needed M(l) to be
# energy-independent, never constant in l. On the Earth P is one matrix per
# slab (Y_e changes only at breakpoints, which are always slab edges), but
# nothing here assumes that.
Pm = np.asarray(h_matt(tgrid.ravel())).reshape(tgrid.shape + (dim, dim))
Vmat = (-1j)*V[:, :, None, None]*Pm # (n_slabs, m, d, d)
else:
Vmat = V[:, :, None, None]*mA # (n_slabs, m, d, d)
# Batched kernel over the active energies, chunked so that each sample
# array At holds at most BATCH_WORKING_ENTRIES complex entries -- 65,536,
# about 1 MB. That figure is a cache-residency target, not a memory cap;
# see its own definition for what it was tuned against.
chunk, _ = _tile_for_working_set(len(active), 1, tgrid.size*dim*dim)
P_new = np.empty((len(active), dim, dim))
for i0 in range(0, len(active), chunk):
sel = active[i0:i0+chunk]
At = HE_c[sel][:, None, None, :, :] + Vmat[None, :, :, :, :]
U = magnus.evolution_operators_from_samples(At, widths,
magnus_exp_order, integration_method, validate_input=False)
Utot = magnus._ordered_product_batched(U)
P_new[i0:i0+chunk] = np.swapaxes(
Utot.real**2 + Utot.imag**2, -1, -2)
if not tol_requested:
P_out[active] = P_new
return P_out
prev = P_prev[active]
have_prev = ~np.isnan(prev[:, 0, 0])
conv = have_prev & np.all(np.abs(P_new - prev) <= atol + rtol*np.abs(prev),
axis=(-1, -2))
P_out[active[conv]] = P_new[conv]
P_prev[active] = P_new
active = active[~conv]
if active.size == 0:
return P_out
at_caps = ((n_slabs >= max_n_slabs) and
(n_tpts_per_slab >= max_n_tpts_per_slab))
if (loop_count >= max_num_loops) or at_caps:
warnings.warn("osc_prob (energy-batched scan): requested tolerance "
"not achieved for some energies (refinement caps reached); the "
"returned probabilities may be inaccurate. Try increasing "
"max_n_slabs, max_n_tpts_per_slab, or max_num_loops. Shown "
"once per session.", ToleranceNotAchievedWarning, stacklevel=2)
P_out[active] = P_new[~conv]
return P_out
n_slabs_old = n_slabs
n_slabs = min(round(growth_factor_n_slabs*n_slabs), max_n_slabs)
if ((growth_factor_n_slabs > 1.0) and (n_slabs < max_n_slabs) and
(n_slabs == n_slabs_old)):
n_slabs += 1
n_tpts_old = n_tpts_per_slab
n_tpts_per_slab = min(int(growth_factor_n_tpts_per_slab*n_tpts_per_slab),
max_n_tpts_per_slab)
if ((growth_factor_n_tpts_per_slab > 1.0) and
(n_tpts_per_slab < max_n_tpts_per_slab) and
(n_tpts_per_slab == n_tpts_old)):
n_tpts_per_slab += 1
loop_count += 1
def _osc_prob_scan_constant_h(
H_tot: np.ndarray,
widths: np.ndarray
) -> np.ndarray:
r"""Probabilities for a position-independent Hamiltonian, in one batched exponential.
When the matter potential is constant, :math:`H` does not depend on position, and the
Magnus series *terminates at its first term*: :math:`\Omega_1 = -iH\Delta` and every
higher :math:`\Omega_k` is a nested commutator of :math:`H` with itself, hence zero. So
.. math::
U = \exp(-i H \Delta)
is not an approximation to be refined but the exact answer, and a whole energy scan is one
call to ``magnus._expm_stack`` on an ``(nE, d, d)`` stack.
**This is faster and more accurate than the path it replaces.** Before, a constant
potential was explicitly turned away by :func:`_osc_prob_scan_separable_dispatch` -- its
docstring said "a constant potential falls back to the generic path" -- and the scan then
ran ``osc_prob`` once per energy, each rediscovering the same constancy and each paying the
full wrapper and refinement-ladder overhead. Measured on 60 energies at 1300 km and
2.848 g/cm^3, that was 18,000 ``osc_prob`` calls per 300 repetitions; the loop cost about
20 us per energy against about 1 us here.
``n_slabs``, ``n_tpts_per_slab``, ``t_breakpoints`` and ``rtol``/``atol`` are accepted and
ignored *when valid*, because they can only ask for a refinement of something already
exact: subdividing a constant profile yields the same product of identical exponentials, to
rounding. Values :func:`osc_prob` would reject are declined rather than ignored -- see
:func:`_refinement_params_rejected`, since "ignored" must not stretch to "unvalidated". A
caller who wants to *see* that equivalence can force the generic route with
``engines=('constant',)`` disabled, and the tests do exactly that to compare the two.
.. versionadded:: 1.0.0
Parameters
----------
H_tot : np.ndarray
The full, position-independent Hamiltonian per energy, shape (nE, d, d).
widths : np.ndarray
Propagation distance per energy, shape (nE,) -- ``L - L0``. Per-point baselines are
allowed and cost nothing, since each still needs only its own exponential.
Returns
-------
np.ndarray
Probabilities, shape (nE, d, d), indexed ``[energy, initial, final]``.
"""
Om = (-1j*np.asarray(widths, dtype=float)[:, None, None])*np.asarray(H_tot)
# A_is_const: the series terminates exactly here, so the slab-width convergence warning
# would be reporting a condition that cannot apply -- a 20 GeV-to-0.6 GeV scan carries a
# large accumulated phase and would otherwise warn on every call.
U = magnus._expm_stack(Om, warn_wide=False, A_is_const=True)
# Same convention as _osc_prob_scan_separable: P[i, j] = |U[j, i]|^2.
return np.swapaxes(U.real**2 + U.imag**2, -1, -2)
def _refinement_params_rejected(scan_kwargs: Dict) -> bool:
r"""Whether :func:`osc_prob` would raise on this request's refinement parameters.
The batched engines answer *before* :func:`osc_prob` is reached, and therefore before its
validation. A parameter it would have rejected has to be rejected here too, or the same
bad value raises on a position-dependent profile and is silently accepted on a constant
one -- the caller's mistake going unreported because of a property of their density.
Declining rather than raising, so the request falls through to the per-point path and the
error the caller sees is the one :func:`osc_prob` has always produced, worded and numbered
the same way. Nothing here is re-implemented as a message.
These conditions mirror :func:`osc_prob`'s validation block and have to be read against
it: diverging in either direction reopens the inconsistency this closes, so they are
changed together or not at all. The rule they encode is that each ceiling clears its own
floor -- ``min_n_slabs`` defaults to 1 and ``min_n_tpts_per_slab`` to 2.
.. versionadded:: 1.0.0
Parameters
----------
scan_kwargs : dict
The dispatcher's forwarded call parameters.
Returns
-------
bool
True when :func:`osc_prob` would raise, and so when the engine must decline.
"""
rtol, atol = scan_kwargs.get('rtol'), scan_kwargs.get('atol')
if (rtol is not None) and (rtol <= 0.0):
return True
if (atol is not None) and (atol <= 0.0):
return True
if (rtol is None) or (atol is None):
# Every check below is gated on a tolerance having been requested, exactly as in
# osc_prob: without one the refinement ladder never runs and its bounds are unused.
return False
g_slabs = scan_kwargs.get('growth_factor_n_slabs')
g_tpts = scan_kwargs.get('growth_factor_n_tpts_per_slab')
if (g_slabs is not None) and (g_slabs < 1.0):
return True
if (g_tpts is not None) and (g_tpts < 1.0):
return True
if (g_slabs == 1.0) and (g_tpts == 1.0):
return True
max_num_loops = scan_kwargs.get('max_num_loops')
if (max_num_loops is not None) and (max_num_loops <= 1):
return True
max_n_slabs = scan_kwargs.get('max_n_slabs')
if (max_n_slabs is not None) and (max_n_slabs <= 1):
return True
max_n_tpts = scan_kwargs.get('max_n_tpts_per_slab')
if (max_n_tpts is not None) and (max_n_tpts <= 2):
return True
# The floors, and each floor against its own ceiling. osc_prob rejects all six; without
# them here the same request was answered by the batched engine and refused by the
# per-point one, so whether a caller saw an error depended on which engine applied.
n_slabs = scan_kwargs.get('n_slabs')
if (n_slabs is not None) and (n_slabs < 1):
return True
min_n_slabs = scan_kwargs.get('min_n_slabs')
if (min_n_slabs is not None) and (min_n_slabs < 1):
return True
n_tpts = scan_kwargs.get('n_tpts_per_slab')
if (n_tpts is not None) and (n_tpts < 2):
return True
min_n_tpts = scan_kwargs.get('min_n_tpts_per_slab')
if (min_n_tpts is not None) and (min_n_tpts < 2):
return True
if ((min_n_slabs is not None) and (max_n_slabs is not None)
and (min_n_slabs > max_n_slabs)):
return True
if ((min_n_tpts is not None) and (max_n_tpts is not None)
and (min_n_tpts > max_n_tpts)):
return True
return False
def _osc_prob_scan_separable_dispatch(
h_vac_energy_indep: np.ndarray,
VCC_func: Union[Callable, float],
h_matt: np.ndarray,
h_liv_energy_indep: Optional[np.ndarray],
n_liv: Optional[Union[int, float]],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Union[int, float],
nu_i: Optional[int],
nu_f: Optional[int],
scan_kwargs: Dict
):
r"""Decide whether the energy-batched scan engine applies; run it if so.
Returns NotImplemented when the request does not fit the engine (single
point, per-point baselines, user-provided slab edges, parallel or logged
runs, iteration over the expansion order, or unknown extra arguments), in
which case the caller falls back to the generic per-point path.
.. versionadded:: 1.0.0
Parameters
----------
h_vac_energy_indep : np.ndarray
Energy-independent part of the vacuum Hamiltonian.
VCC_func : Callable or float
Matter potential, as a function of position (required for the batched engine to apply;
a constant potential falls back to the generic path).
h_matt : np.ndarray or Callable
Constant matrix multiplying ``VCC_func(l)``, or a callable of position returning that
matrix (the position-resolved sterile projector). The callable form is served by the
separable engine, which samples it on the potential's own grids; over a *constant*
potential it declines instead, the constant engine's one exact exponential having no
position axis to put it on.
h_liv_energy_indep : np.ndarray, optional
Energy-independent part of the LIV Hamiltonian, if any.
n_liv : int or float, optional
Power of the energy dependence of the LIV operator, if ``h_liv_energy_indep`` is given.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s); the batched engine applies only when all requested baselines are equal.
L0 : int or float
Initial position.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned.
nu_f : int, optional
Final flavor index; see ``nu_i``.
scan_kwargs : dict
The refinement/logging keyword arguments of :func:`osc_prob_energy_baseline` (rtol, atol,
magnus_exp_order, integration_method, growth factors, loop/slab/time-point bounds,
t_slab_edges, n_jobs, save_log, file_log) plus a nested
'kwargs' dict of any remaining, unrecognized keyword arguments.
Returns
-------
np.ndarray or NotImplemented
The oscillation probability (or single channel), computed via the batched engine; or the
``NotImplemented`` singleton if the request does not fit it.
"""
if scan_kwargs.get('cumulative') is True:
return NotImplemented
# A constant potential is not a degenerate case of the separable engine but a different
# (exact, unrefined) one, so it gets its own name and its own disable switch. It used to
# be turned away here outright, which sent the easiest Hamiltonian there is down the
# slowest route available; see _osc_prob_scan_constant_h.
vcc_is_constant = not callable(VCC_func)
# A callable h_matt (the position-resolved sterile projector) over a constant potential
# still makes the Hamiltonian position-dependent -- exactly what the constant engine's
# single exact exponential cannot represent -- so that combination declines to the
# general ladder, whose closure evaluates both factors. The Earth path never builds
# it (its density is always a function); it takes a direct call with constant density
# plus a callable ratio.
if vcc_is_constant and callable(h_matt):
return NotImplemented
engine = 'constant' if vcc_is_constant else 'separable'
if engine in _ENGINES_DISABLED:
return NotImplemented
kwargs = dict(scan_kwargs.get('kwargs', {}))
t_breakpoints = kwargs.pop('t_breakpoints', None)
n_slabs = kwargs.pop('n_slabs', 1)
n_tpts_per_slab = kwargs.pop('n_tpts_per_slab', 100)
if len(kwargs) > 0:
return NotImplemented
if scan_kwargs['t_slab_edges'] is not None:
return NotImplemented
if (scan_kwargs['n_jobs'] != 1) or scan_kwargs['save_log'] or \
(scan_kwargs['file_log'] is not None):
return NotImplemented
# A verbose run is asking to be shown the route, and the banner plus run-parameter dump it
# expects (magnus_exp_order, slab counts, tolerances) is emitted by the per-point path and
# describes quantities these batched engines do not have. Printing them from here would mean
# reporting a refinement ladder that never ran, so a verbose request takes the route whose
# parameters the dump actually describes. Debugging output, so its cost does not matter.
#
# >= 1, not >= 2: level 1 also prints (the time-independent-Hamiltonian notice), and gating
# at 2 silently deleted it -- measured 210 characters to 0 on a constant-density call.
if scan_kwargs.get('verbose', 0) >= 1:
return NotImplemented
energy_arr, L_arr, return_float, ok = _normalize_energy_L(energy, L)
if not ok:
return NotImplemented
# Answering before the per-point path means answering before its validation, so anything
# the ladder would have rejected has to be rejected here or it is silently accepted.
#
# A backward or negative propagation distance is the sharp case: `widths` below is used
# unsigned, and exp(-iH(L-L0)) for L < L0 is the *transpose* of the forward answer -- row
# sums still exactly 1, survival probabilities still right, channels swapped, and 29% off
# on P(nu_mu -> nu_e). osc_prob raises ValueError for this; declining here routes the
# request there so it still does.
# ``not (L >= L0)`` rather than ``L < L0``, so that a NaN baseline is declined too:
# NaN fails every comparison, so ``L < L0`` is False for it and the engine would have
# answered with NaN where the per-point path raises.
if np.any(~(L_arr >= float(L0))):
return NotImplemented
# Likewise the refinement parameters. They do not change this engine's answer -- one
# exponential per point is exact either way -- but accepting a value the ladder rejects
# returns a different calculation from the one asked for without saying so, which is the
# thing tests/test_validation.py exists to prevent.
order = scan_kwargs.get('magnus_exp_order')
method = scan_kwargs.get('integration_method')
if (order is not None) and (method is not None):
try:
magnus._validate(order, method)
except ValueError:
return NotImplemented
# `magnus._validate` covers the order and the quadrature and nothing else, which left the
# ladder's own bounds unchecked: max_n_slabs=0, rtol=-1.0 and max_num_loops=0 were all
# answered here while osc_prob raised for each of them, so whether a bad parameter was
# reported depended on whether the density happened to be constant.
if _refinement_params_rejected(scan_kwargs):
return NotImplemented
# The separable engine shares one grid of potential samples across energies, so it needs a
# single common baseline and at least two energies to be worth entering. The constant
# engine shares nothing and refines nothing: every point is its own exponential, so
# per-point baselines and a single point are both served, and a single point served here
# skips the per-point wrapper stack entirely.
if not vcc_is_constant:
if (len(energy_arr) < 2) or (not np.all(L_arr == L_arr[0])):
return NotImplemented
rtol, atol = scan_kwargs['rtol'], scan_kwargs['atol']
if (rtol is None) != (atol is None):
rtol = 0.0 if rtol is None else rtol
atol = 0.0 if atol is None else atol
# All the position-independent, energy-dependent terms of the Hamiltonian
H_E = (1.0/energy_arr)[:, None, None]*np.asarray(h_vac_energy_indep)
if h_liv_energy_indep is not None:
H_E = H_E + (energy_arr**n_liv)[:, None, None]*np.asarray(h_liv_energy_indep)
if vcc_is_constant:
# Refuse a request whose result cannot fit before allocating anything, exactly as
# osc_prob_energy_baseline does. That guard lives in the per-point path, which this
# engine short-circuits, so without this call an oversized scan died as a raw
# MemoryError naming a 49.3 GiB allocation instead of the package's own message -- and
# this engine's peak is complex128, twice the float64 size the guard is sized against.
_check_output_fits(len(energy_arr), np.asarray(h_vac_energy_indep).shape[-1],
'_osc_prob_scan_constant_h')
# H is position-independent, so the Magnus series terminates at Omega_1 and one
# exponential per energy is exact. h_matt is absent on the paths that have no matter
# at all, where the constant potential to add is simply nothing.
H_tot = H_E
if h_matt is not None:
H_tot = H_tot + float(VCC_func)*np.asarray(h_matt)
P = _osc_prob_scan_constant_h(H_tot, L_arr - float(L0))
else:
# A callable h_matt goes through as the function it is; only the constant form is
# coerced to an array.
P = _osc_prob_scan_separable(H_E, VCC_func,
h_matt if callable(h_matt) else np.asarray(h_matt), float(L0),
float(L_arr[0]), t_breakpoints, scan_kwargs['magnus_exp_order'],
scan_kwargs['integration_method'], rtol, atol,
scan_kwargs['growth_factor_n_slabs'],
scan_kwargs['growth_factor_n_tpts_per_slab'],
scan_kwargs['max_num_loops'], scan_kwargs['min_n_slabs'],
scan_kwargs['max_n_slabs'], scan_kwargs['min_n_tpts_per_slab'],
scan_kwargs['max_n_tpts_per_slab'], n_slabs, n_tpts_per_slab)
_note_engine(engine)
if (nu_i is not None) and (nu_f is not None):
P = P[:, nu_i, nu_f]
return P.__getitem__(0 if return_float else slice(None))
def _osc_prob_ip_exp_core(
H_E: np.ndarray,
l_scale: float,
VCC_func: Callable,
h_matt: np.ndarray,
L0: float,
L_val: float,
rtol: Optional[float],
atol: Optional[float],
growth_factor_n_slabs: float,
max_num_loops: int,
min_n_slabs: int,
max_n_slabs: int,
n_slabs: int
) -> Tuple[np.ndarray, bool]:
r"""Interaction-picture Magnus integrator for an exponential matter profile.
Computes the evolution operator for Hamiltonians of the separable form
``H(E, l) = H_E(E) + VCC_func(l) * h_matt``, with ``VCC_func(l) = VCC_func(0) * exp(-l/l_scale)``
a genuine exponential profile, WITHOUT resolving the (possibly huge, at low energy) fast phase of
``H_E`` slab by slab. ``H_E`` is diagonalized once (it does not depend on position); in that
eigenbasis, the free ("vacuum") evolution ``exp(-i H_E s)`` is an exactly known diagonal phase for
any ``s``, so it is factored out analytically instead of being resolved by narrow slabs, leaving
only the matter-potential envelope to be integrated. Within each slab ``[l0, l0+h]``, the
first-order Magnus term of the resulting interaction-picture generator,
.. math::
\Omega_1(h) = -i \int_0^h e^{i H_E s}\, V_\text{CC}(l_0+s)\,
h_\text{matt}\, e^{-i H_E s}\, ds ,
has a closed form because ``VCC_func(l0+s) = VCC_func(l0) exp(-s/l_scale)`` is itself an
exponential: in the eigenbasis of ``H_E`` (eigenvalues :math:`\lambda_j`, so
:math:`\Delta_{jk} = \lambda_j - \lambda_k`),
.. math::
\left(\tilde{\Omega}_1\right)_{jk}(h) = -i \left(\tilde{h}_\text{matt}\right)_{jk}
V_\text{CC}(l_0)\,
\frac{e^{\left(i \Delta_{jk} - 1/l_\text{scale}\right) h} - 1}
{i \Delta_{jk} - 1/l_\text{scale}} ,
valid uniformly for :math:`j = k` too (the :math:`j = k` denominator,
:math:`-1/l_\text{scale}`, is never zero for finite
``l_scale``). This is exact in the envelope (no local-constant or local-linear approximation of
``VCC_func`` is made); the only approximation is truncating the interaction-picture Magnus series
at first order, which is accurate away from an MSW resonance (where the matter term becomes
comparable to the *vacuum* splitting :math:`\Delta_{jk}`, rather than merely small) and improves,
rather than worsens, at lower neutrino energy (:math:`\Delta_{jk}` grows as :math:`1/E`). Both
factors (``exp(-i H_E h)`` and :math:`\exp(\Omega_1)`) are exactly unitary by construction (the
former is a diagonal phase, the latter is exponentiated via ``_expm_stack`` from an
anti-Hermitian generator), so the returned probabilities remain exactly unitary regardless of how
good the approximation is. Accuracy is controlled the usual way: growing ``n_slabs`` shrinks the
per-slab truncation error (which vanishes faster than the slab width itself), so successive
refinements converge to the exact solution; the loop mirrors :func:`osc_prob`'s own
successive-refinement comparison, batched over the leading energy axis of ``H_E``.
.. versionadded:: 1.0.0
Parameters
----------
H_E : np.ndarray
Position-independent, energy-dependent part of the Hamiltonian, shape (nE, d, d).
l_scale : float
Length scale of the exponential density decrease.
VCC_func : Callable
Matter potential along the trajectory (accepts an array of positions), satisfying
``VCC_func(l) = VCC_func(0)*exp(-l/l_scale)``.
h_matt : np.ndarray
Constant matrix multiplying ``VCC_func(l)``, shape (d, d).
L0 : float
Initial position.
L_val : float
Final position (baseline).
rtol, atol : float, optional
Target relative/absolute tolerance between successive refinement levels. If both None, run
once with the given fixed ``n_slabs``.
growth_factor_n_slabs : float
Factor by which ``n_slabs`` is multiplied on each refinement loop.
max_num_loops : int
Maximum number of refinement loops.
min_n_slabs, max_n_slabs : int
Bounds on the number of slabs. ``max_n_slabs=None`` selects the 'gl' entry of
:data:`MAX_N_SLABS_DEFAULT`, this integrator being closed-form per slab and so
comparably cheap.
n_slabs : int
Starting number of slabs (or fixed count, if no tolerance is requested).
Returns
-------
(np.ndarray, bool)
Stacked probability matrices, shape (nE, d, d), and whether the requested tolerance was
achieved (always True if no tolerance was requested). If False, the result should be
discarded in favor of the general (slower, but unconditionally convergent) slab-refinement
method; this happens when the matter term is not a small perturbation on the vacuum
splitting anywhere along the trajectory (e.g., at an MSW resonance).
"""
# This integrator has no `integration_method`: within each slab it is closed-form, with
# no quadrature at all, so its cost per slab is comparable to 'gl' rather than to the
# cumulative-quadrature methods. It therefore takes the 'gl' cap when none is given.
if max_n_slabs is None:
max_n_slabs = MAX_N_SLABS_DEFAULT['gl']
tol_requested = (rtol is not None) and (atol is not None)
# Diagonalize the position-independent part of H once; this eigenbasis is shared by every slab,
# so the fast phase of H_E is handled exactly, analytically, regardless of slab width.
Lam, W = np.linalg.eigh(H_E) # (nE, d), (nE, d, d)
Wd = np.conj(np.swapaxes(W, -1, -2))
Mt = Wd @ h_matt.astype(complex)[None, :, :] @ W # h_matt in the H_E eigenbasis
Delta = Lam[:, :, None] - Lam[:, None, :] # (nE, d, d)
denom = 1j*Delta - (1.0/l_scale) # never zero (l_scale finite)
if tol_requested:
# The caller's n_slabs is a floor on the refinement ladder, not something to discard; see
# the corresponding note in osc_prob. Clipped at this method's own ceiling, not at
# max_n_slabs: the slab budget here is decoupled from the caller's cap (see the note
# below), so max_n_slabs is not the bound the growth step will respect.
n_slabs = int(min(max(min_n_slabs, n_slabs), IP_EXP_N_SLABS_CAP))
# The part of h_matt that is diagonal in the H_E eigenbasis commutes with H_E and does not
# oscillate: it accumulates an ordinary, unsuppressed phase (proportional to the matter
# potential integrated over the *whole* slab) that first-order Omega_1 does not shrink just
# because Delta_jk is large. So max||Omega_1|| does not fall smoothly slab by slab the way it
# would for a pure Magnus quadrature error -- for a slab still wide compared to the scale at
# which this diagonal phase becomes O(1), successive refinements can land on essentially
# uncorrelated (not just slowly converging) probabilities. The two safeguards below keep this
# method from ever reporting a false convergence in that pre-asymptotic regime: (a) the
# successive-refinement comparison is trusted only once max||Omega_1|| itself has dropped
# below a conservative threshold (comfortably inside the regime where the neglected Omega_2 ~
# O(||Omega_1||^2) term is genuinely small), and (b) even then, agreement is required twice in
# a row. The slab budget is also decoupled from the caller's max_n_slabs/growth_factor_n_slabs
# (calibrated for the much more expensive quadrature-based slabs of the general method): each
# slab here costs one small eigendecomposition, so pushing to hundreds of thousands of slabs
# when needed still completes in a couple of seconds.
# The neglected Omega_2 ~ O(||Omega_1||^2) term sets the probability error at ~C*omega^2 for
# some O(1) constant C (empirically ~0.005 for the sole off-diagonal pair of a genuine 2-level
# H_E, which is all _osc_prob_ip_exp_dispatch admits here); tie the trust threshold to the
# requested tolerance (with a safety factor) instead of a fixed value, so tighter requests
# correctly demand more slabs rather than risking a plausible-looking but insufficiently
# accurate "convergence". The slab budget is a fixed ceiling, independent of the caller's
# max_n_slabs/growth_factor_n_slabs (calibrated for the much more expensive quadrature-based
# slabs of the general method): each slab here costs one 2x2 eigendecomposition, so even the
# full ceiling completes in a couple of seconds.
omega_trust_threshold = min(0.1, np.sqrt((atol + rtol)/2.0)) if tol_requested else 0.1
n_slabs_cap = IP_EXP_N_SLABS_CAP
loop_cap = IP_EXP_LOOP_CAP
growth = 2.0
nE, dim = H_E.shape[0], H_E.shape[-1]
# Can the trust gate above ever open, at any slab count this method is allowed?
#
# Certification requires max|Omega_t| < omega_trust_threshold, and that maximum is bounded
# below by the largest *diagonal* entry, which has a closed form here. On the diagonal
# Delta_jj = 0, so denom_jj = -1/l_scale and the slab integral collapses to
# l_scale*(1 - exp(-w/l_scale)) for slab width w, giving
#
# max|Omega_jj| = max_ej|Mt[e,j,j]| * max_s|V(l_s)| * l_scale*(1 - exp(-w/l_scale)),
#
# decreasing in the slab count and independent of it otherwise. If that alone still
# exceeds the threshold at the ceiling, then max|Omega_t| does too at every reachable slab
# count, no comparison is ever trusted, and the ladder is guaranteed to climb to the cap
# and refuse. Detecting it costs two evaluations of VCC_func and no allocation.
#
# This is a bound, not an estimate: it can only ever say "certification is impossible",
# never "certification will succeed", so it cannot abandon a case that would have worked.
# The pass below still runs once, at the starting slab count, because an uncertified result
# is still required to be a genuine unitary probability matrix -- see the tests on this
# function's give-up exits.
certifiable = True
if tol_requested:
v_max = max(abs(float(np.real(np.asarray(VCC_func(L0))))),
abs(float(np.real(np.asarray(VCC_func(L_val))))))
mt_diag_max = float(np.max(np.abs(np.diagonal(Mt, axis1=-2, axis2=-1))))
scale = mt_diag_max*v_max
if scale > 0.0:
c = omega_trust_threshold/scale
if c < l_scale: # otherwise the bound is below threshold at any w
w_max = -l_scale*np.log1p(-c/l_scale)
certifiable = np.ceil((L_val - L0)/w_max) <= n_slabs_cap
P_prev = None
n_slabs_prev = None
consecutive_agreements = 0
loop_count = 1
while True:
# The full edge grid is O(n_slabs) floats and independent of the energy count, so it
# is affordable at any slab cap; it is built whole rather than per tile because
# np.linspace's endpoint handling is not reproduced by arithmetic on a sub-range,
# and the tiling below must not perturb a single slab edge.
grid = np.linspace(L0, L_val, n_slabs + 1)
# Everything below is tiled over (energy, slab). The arrays this replaces --
# (nE, n_slabs, d, d) complex, several live at once -- are the whole of the memory
# bug: at the slab ceiling they reached ~1.3 GB per energy, so a batched solar call
# could exhaust the machine. See docs/dev/BUG_IP_EXP_MEMORY.md.
#
# The tiling is exact, not approximate. Within a tile the arithmetic is elementwise,
# so slicing changes no value; and the product is folded slab-by-slab in the same
# descending order as before, with the accumulator on the left, so the parenthesis
# nesting -- the only thing that could move a floating-point result -- is unchanged.
# Blocks are therefore walked from the *last* slab backwards. A test pins the output
# of a tiled run against an untiled one at exact equality.
# live_arrays: arg, the exp() temporary, I, Omega_t, _expm_stack's eigenvectors and
# its workspace, U_slab, and the accumulator's operand -- eight of this shape at the
# peak, which is why the budget has to be divided rather than applied per array.
e_chunk, blk = _tile_for_working_set(nE, n_slabs, dim*dim, live_arrays=8)
Utot = np.empty((nE, dim, dim), dtype=complex)
max_omega = 0.0
for e0 in range(0, nE, e_chunk):
esel = slice(e0, min(e0 + e_chunk, nE))
acc = None
for b1 in range(n_slabs, 0, -blk): # descending slab blocks
b0 = max(0, b1 - blk)
edges0 = grid[b0:b1] # (nb,) slab starts
widths = grid[b0 + 1:b1 + 1] - edges0 # (nb,)
V0 = np.asarray(VCC_func(edges0), dtype=complex) # VCC at each slab's start
arg = denom[esel, None, :, :]*widths[None, :, None, None]
I = (np.exp(arg) - 1.0)/denom[esel, None, :, :]
Omega_t = -1j*Mt[esel, None, :, :]*V0[None, :, None, None]*I
max_omega = max(max_omega, float(np.max(np.abs(Omega_t))))
U_free_diag = np.exp(-1j*Lam[esel, None, :]*widths[None, :, None])
U_slab = U_free_diag[..., :, None]*magnus._expm_stack(
Omega_t, warn_wide=False)
# The fold runs compiled (magnus._ordered_product_into), accumulator on
# the left and k descending -- the association of the Python loop this
# replaces (`acc = acc @ U_slab[:, k]`). The accumulator is passed *in*
# rather than each block being reduced alone and multiplied afterwards,
# precisely so the cross-block parenthesis nesting stays the one the
# tiling comment above requires. The first block seeds the accumulator
# with its last slab (a copy: the kernel writes in place, and U_slab is
# about to be freed) and folds the rest.
if acc is None:
acc = np.ascontiguousarray(U_slab[:, -1])
magnus._ordered_product_into(acc, U_slab[:, :-1])
else:
magnus._ordered_product_into(acc, U_slab)
del arg, I, Omega_t, U_free_diag, U_slab
Utot[esel] = acc
Utot = W @ Utot @ Wd # back to the flavor basis
P_new = np.swapaxes(Utot.real**2 + Utot.imag**2, -1, -2)
if not tol_requested:
return P_new, True
# Certification is provably out of reach (see the bound above): refuse now rather
# than doubling the slab count twenty more times to arrive at the same refusal. The
# caller's dispatcher discards this result and falls back to the general method, as
# it would have anyway -- identically, and roughly a thousand times sooner.
if not certifiable:
return P_new, False
at_cap = (n_slabs >= n_slabs_cap)
# Once n_slabs is pinned at the cap, growth is a no-op: a further "refinement" would just
# repeat this identical computation, which trivially agrees with itself and would falsely
# look converged. So growing past the cap gives at most one genuine comparison (against
# the last, truly smaller, n_slabs); if that one comparison does not already satisfy both
# safeguards, there is no more evidence to be had, and the fast method must give up.
is_repeat = (n_slabs == n_slabs_prev)
if is_repeat: # pragma: no cover - unreachable; see below
# Unreachable as the loop currently stands, and kept as a guard rather than
# deleted. Below the cap the slab count strictly increases (the growth factor
# is 2, and the clause at the foot of the loop forces progress even if it were
# not); at the cap every branch below returns within the same iteration. So
# the loop never survives a pass at the cap to make a repeated comparison, and
# n_slabs never equals n_slabs_prev. It becomes live again the moment the
# growth factor, the cap, or the returns below change.
return P_new, False
if (P_prev is not None) and (max_omega < omega_trust_threshold):
if np.all(np.abs(P_new - P_prev) <= atol + rtol*np.abs(P_prev)):
consecutive_agreements += 1
if at_cap or (consecutive_agreements >= 2):
return P_new, True
else:
consecutive_agreements = 0
if at_cap:
return P_new, False
else:
consecutive_agreements = 0
if at_cap:
return P_new, False
P_prev = P_new
n_slabs_prev = n_slabs
if loop_count >= loop_cap:
return P_new, False
n_slabs_old = n_slabs
n_slabs = min(round(growth*n_slabs), n_slabs_cap)
if (n_slabs == n_slabs_old) and (n_slabs < n_slabs_cap): # pragma: no cover
# The no-progress guard, and unreachable while the growth factor is 2:
# round(2n) == n has no solution for n >= 1. It exists so that a smaller
# growth factor -- 1.1, say, which rounds to no change at small n -- cannot
# turn this into an infinite loop, which is precisely when it stops being
# dead code.
n_slabs += 1
loop_count += 1
def _osc_prob_ip_exp_dispatch(
h_vac_energy_indep: np.ndarray,
VCC_func: Union[Callable, float],
h_matt: np.ndarray,
h_liv_energy_indep: Optional[np.ndarray],
n_liv: Optional[Union[int, float]],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Union[int, float],
nu_i: Optional[int],
nu_f: Optional[int],
scan_kwargs: Dict
):
r"""Decide whether the fast interaction-picture integrator applies; run it if so.
Returns ``NotImplemented`` when the request does not fit the fast method (``VCC_func`` is not a
genuine exponential profile built via :func:`magnus.matter.exp_density_profile`, user-provided
slab edges or breakpoints, iteration over the expansion order, or logging requested) or when it
fails to converge within the requested tolerance (signaling that the matter term is not a small
perturbation on the vacuum splitting somewhere along the trajectory, e.g., an MSW resonance); the
caller falls back to the general per-point path in either case. Unlike
``_osc_prob_scan_separable_dispatch``, this applies equally to a single (energy, L) point (the
common case for :func:`osc_prob_sun`-family calls) and to a multi-energy scan at a shared baseline.
.. versionadded:: 1.0.0
Parameters
----------
h_vac_energy_indep : np.ndarray
Energy-independent part of the vacuum (and, if present, LIV) Hamiltonian.
VCC_func : Callable or float
Matter potential, as a function of position (required for the fast method to apply; a
constant potential, or one not tagged as exponential, falls back to the generic path).
h_matt : np.ndarray
Constant matrix multiplying ``VCC_func(l)``.
h_liv_energy_indep : np.ndarray, optional
Energy-independent part of the LIV Hamiltonian, if any.
n_liv : int or float, optional
Power of the energy dependence of the LIV operator, if ``h_liv_energy_indep`` is given.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s); the fast method applies only when all requested baselines are equal.
L0 : int or float
Initial position.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned.
nu_f : int, optional
Final flavor index; see ``nu_i``.
scan_kwargs : dict
The refinement/logging keyword arguments of :func:`osc_prob_energy_baseline` (rtol, atol,
growth_factor_n_slabs, max_num_loops, min_n_slabs, max_n_slabs, t_slab_edges,
save_log, file_log) plus a nested 'kwargs' dict of any
remaining, unrecognized keyword arguments.
Returns
-------
np.ndarray or NotImplemented
The oscillation probability (or single channel), computed via the fast method; or the
``NotImplemented`` singleton if the request does not fit it or it failed to converge.
"""
if ('ip_exp' in _ENGINES_DISABLED) or (scan_kwargs.get('cumulative') is True):
return NotImplemented
kwargs = dict(scan_kwargs.get('kwargs', {}))
t_breakpoints = kwargs.pop('t_breakpoints', None)
n_slabs0 = kwargs.pop('n_slabs', 1)
kwargs.pop('n_tpts_per_slab', None)
if len(kwargs) > 0:
return NotImplemented
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
return NotImplemented
if not callable(VCC_func):
return NotImplemented
if not getattr(VCC_func, 'is_exp_density_profile', False):
return NotImplemented
if np.asarray(h_vac_energy_indep).shape[-1] != 2:
# The neglected second-order term Omega_2 involves a sum over every off-diagonal pair in
# the H_E eigenbasis; empirically (see the validation grid in the module docstring... see
# tests/test_oscprob.py), its coefficient grows by three orders of magnitude already from
# 2 to 3 flavors (one pair vs three, with sizable diagonal mixing-angle contributions in
# each), which pushes the slab count needed for a certified answer far past what stays
# fast. The fast path is therefore restricted to genuinely 2-level Hamiltonians for now,
# where it has been validated against solve_ivp across the realistic solar-neutrino
# energy range; 3+ flavor (and LIV, which adds further energy-dependent terms to H_E) fall
# back to the general method unconditionally.
return NotImplemented
if scan_kwargs['t_slab_edges'] is not None:
return NotImplemented
if scan_kwargs['save_log'] or (scan_kwargs['file_log'] is not None):
return NotImplemented
l_scale = VCC_func.l_scale
energy_arr, L_arr, return_float, ok = _normalize_energy_L(energy, L)
if (not ok) or (not np.all(L_arr == L_arr[0])):
return NotImplemented
rtol, atol = scan_kwargs['rtol'], scan_kwargs['atol']
if (rtol is None) != (atol is None):
rtol = 0.0 if rtol is None else rtol
atol = 0.0 if atol is None else atol
H_E = (1.0/energy_arr)[:, None, None]*np.asarray(h_vac_energy_indep, dtype=complex)
if h_liv_energy_indep is not None:
H_E = H_E + (energy_arr**n_liv)[:, None, None]*np.asarray(h_liv_energy_indep, dtype=complex)
P, converged = _osc_prob_ip_exp_core(H_E, l_scale, VCC_func, np.asarray(h_matt), float(L0),
float(L_arr[0]), rtol, atol, scan_kwargs['growth_factor_n_slabs'],
scan_kwargs['max_num_loops'], scan_kwargs['min_n_slabs'], scan_kwargs['max_n_slabs'], n_slabs0)
if not converged:
_note_engine('ip_exp', answered=False, reason='did not converge')
return NotImplemented
_note_engine('ip_exp')
if (nu_i is not None) and (nu_f is not None):
P = P[:, nu_i, nu_f]
return P.__getitem__(0 if return_float else slice(None))
def _cumulative_scan_would_serve(energy_arr, L_arr, L0, min_points):
r"""Whether the cumulative scan applies to this set of requested points.
Used in two places with different ``min_points``, because the two guard different trades:
``osc_prob_energy_baseline`` resolves ``cumulative='auto'`` with
:data:`CUMULATIVE_AUTO_MIN_POINTS`, while ``_osc_prob_hybrid_dispatch`` decides whether to
stand aside with the larger :data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS`. Sharing the
rest of the predicate keeps them from drifting apart on the conditions that are genuinely
the same.
That the dispatcher's threshold is the larger one is what makes the fall-through safe: when
the hybrid dispatcher declines on this count, ``'auto'`` is guaranteed to accept, so a scan
can never be declined by both and land on the general per-point path -- slower and less
accurate than either, and silently so.
The remaining ``'auto'`` conditions (a position-dependent Hamiltonian, no ``t_slab_edges``)
are already guaranteed where the dispatcher calls this: it has returned ``NotImplemented``
for a non-callable ``VCC_func`` and for user-supplied slab edges or breakpoints above.
.. versionadded:: 1.0.0
Parameters
----------
energy_arr : np.ndarray
Requested energies, one per point.
L_arr : np.ndarray
Requested baselines, one per point.
L0 : int or float
Initial position.
min_points : int
Fewest points at which the cumulative scan is worth taking, for this caller.
Returns
-------
bool
True when the cumulative scan applies to this set of points.
"""
return bool(len(L_arr) >= min_points
and np.all(energy_arr == energy_arr[0])
and np.all(np.asarray(L_arr, dtype=float) >= L0))
def _resolve_cumulative_kwarg(kwargs, strategy):
r"""Pops a caller-supplied ``cumulative`` out of ``kwargs`` and decides what to forward.
The three scenario wrappers (:func:`osc_prob_matter_std_potential`,
:func:`osc_prob_matter_nsi`, :func:`osc_prob_liv`) each set ``cumulative`` themselves when
calling :func:`osc_prob_energy_baseline`, so a caller who also passed it in ``**kwargs``
used to get ``TypeError: got multiple values for keyword argument 'cumulative'`` --
which made ``cumulative=False`` unreachable from the entire wrapper layer, and that is the
one mitigation ``docs/dev/DECISION_CUMULATIVE_DEFAULT.md`` names for a caller who needs
bit-reproducibility against pre-1.0.0 results.
**An explicit value from the caller always wins**, including over the ``strategy='magnus'``
opt-out below: naming ``cumulative`` is a specific request about the scan engine, and
``cumulative=True`` is documented to *raise* rather than fall back when it cannot be served,
so honoring it cannot silently do the wrong thing.
Otherwise ``strategy='magnus'`` resolves to ``False`` and everything else to ``'auto'``.
That strategy promises the behavior Mag(nu)s had before the adiabatic strategy existed,
*unconditionally*; the cumulative scan is Magnus machinery but postdates the promise and
builds a different grid, so the escape hatch would quietly stop being one for exactly the
case -- a single-energy baseline scan -- where someone reproducing older numbers reaches
for it.
.. versionadded:: 1.0.0
Parameters
----------
kwargs : dict
The wrapper's ``**kwargs``, modified in place: ``'cumulative'`` is removed if present.
strategy : str
'auto', 'hybrid' or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential`.
Returns
-------
bool or str
The value to forward to :func:`osc_prob_energy_baseline` as ``cumulative``.
"""
if 'cumulative' in kwargs:
return kwargs.pop('cumulative')
return 'auto' if strategy != 'magnus' else False
class _PreferLadder:
r"""What the hybrid dispatchers return when ``strategy='auto'`` hands a request to the ladder.
Distinct from ``NotImplemented`` because the caller has to act on it (issue #70): the
remaining engines run at the tolerances and slab floor of :meth:`request`, and the
interaction-picture fast path is skipped.
.. versionadded:: 1.1.1
Attributes
----------
min_n_slabs : int
The fewest slabs over the longest baseline on which every slab meets the sufficient
condition :class:`magnus.magnus.MagnusConvergenceWarning` checks; see
:func:`_estimated_phase`.
"""
__slots__ = ('min_n_slabs',)
def __init__(self, min_n_slabs: int):
self.min_n_slabs = int(min_n_slabs)
def __repr__(self):
return '_PreferLadder(min_n_slabs=%d)' % self.min_n_slabs
def request(self, rtol, atol, min_n_slabs, max_n_slabs, integration_method):
r"""The ``(rtol, atol, min_n_slabs)`` the ladder runs at.
Each tolerance divided by :data:`AUTO_LADDER_TOLERANCE_MARGIN` (a ``None`` left as it
is), and the caller's ``min_n_slabs`` raised to :attr:`min_n_slabs`, capped at the
resolved ``max_n_slabs``. The floor keeps the refinement from starting on slabs wider
than the Magnus series is guaranteed to converge over: the seed of
:func:`magnus.magnus.suggest_n_slabs` allows twice that width on purpose, and on this
route every request then warned about slabs the ladder went on to refine away.
.. versionadded:: 1.1.1
"""
m = AUTO_LADDER_TOLERANCE_MARGIN
rtol = None if rtol is None else rtol/m
atol = None if atol is None else atol/m
cap = _resolve_max_n_slabs(max_n_slabs, integration_method)
floor = min(max(int(min_n_slabs or 1), self.min_n_slabs), int(cap))
return rtol, atol, floor
def _estimated_phase(H_at_energy: Callable, energy_arr: np.ndarray, L_arr: np.ndarray,
L0: float, n_probe: int = 17, n_energies: int = 5) -> Tuple[float, int]:
r"""The accumulated phase of a request, and the slab count that keeps each slab convergent.
The phase is the integral of :math:`\lambda_{\max} - \lambda_{\min}`, the spread of the
Hamiltonian's eigenvalues, from ``L0`` to the longest requested baseline: the phase of the
fastest oscillation, which is what the ladder has to resolve (see
:data:`AUTO_LADDER_MAX_PHASE` for why not the norm of the integrated Hamiltonian). The slab
count is the fewest uniform slabs over that span on which
:math:`\int_{\rm slab} \lVert H \rVert_2 \, dl \le \pi`, the sufficient condition
:class:`magnus.magnus.MagnusConvergenceWarning` checks, bounded with the largest spectral
radius met. Both come from ``n_probe`` samples at up to ``n_energies`` of the requested
energies spread over their range, about 85 evaluations of the Hamiltonian whatever the size
of the request.
.. versionadded:: 1.1.1
Returns
-------
tuple of (float, int)
The largest phase over the sampled energies, in radians, and the slab count.
"""
L0 = float(L0)
L1 = float(np.max(np.asarray(L_arr, dtype=float)))
if not L1 > L0:
return 0.0, 1
energies = np.unique(np.asarray(energy_arr, dtype=float))
if len(energies) > n_energies:
energies = energies[np.unique(np.round(
np.linspace(0, len(energies) - 1, n_energies)).astype(int))]
ls = np.linspace(L0, L1, n_probe)
phase = radius = 0.0
for enu in energies:
H_of_l = H_at_energy(enu)
eigs = np.linalg.eigvalsh(np.array([np.asarray(H_of_l(l), dtype=complex) for l in ls]))
spread = eigs[:, -1] - eigs[:, 0]
phase = max(phase, float(np.sum(0.5*(spread[1:] + spread[:-1])*np.diff(ls))))
# Trace included, because the norm the warning checks includes it.
radius = max(radius, float(np.max(np.abs(eigs))))
return phase, max(1, int(np.ceil((L1 - L0)*radius/np.pi)))
def _auto_prefers_ladder(H_at_energy: Callable, energy_arr: np.ndarray, L_arr: np.ndarray,
L0: float, rtol: float, atol: float,
max_n_slabs: int) -> Optional[_PreferLadder]:
r"""Whether ``strategy='auto'`` should hand a smooth-profile request to the ladder (issue #70).
Yes when the tolerance is no tighter than :data:`AUTO_LADDER_MIN_TOLERANCE`, the estimated
accumulated phase is at most :data:`AUTO_LADDER_MAX_PHASE`, and the ladder's starting slab
count is at most :data:`AUTO_LADDER_MAX_FLOOR_FRACTION` of ``max_n_slabs``, the resolved cap.
``rtol`` and ``atol`` are the dispatcher's, with a ``None`` already made 0.0; the tighter of
the nonzero ones is the tolerance. Records the decision in ``strategy_info`` when it is
taken.
Handing the request over skips the hybrid strategy's resolution test, which is also what
warns about a density jump nobody declared (:class:`UnmarkedDiscontinuityWarning`). So the
test runs here too, on the same probe grids, at the lowest requested energy: a profile that
fails it still goes to the ladder, which is where the hybrid strategy would have sent it, but
with the reason and the warning the hybrid strategy gives.
.. versionadded:: 1.1.1
Returns
-------
_PreferLadder or None
The marker the dispatcher returns, or None to run the hybrid strategy.
"""
tols = [t for t in (rtol, atol) if t > 0.0]
if not tols or min(tols) < AUTO_LADDER_MIN_TOLERANCE:
return None
phase, n_floor = _estimated_phase(H_at_energy, energy_arr, L_arr, L0)
if (phase > AUTO_LADDER_MAX_PHASE) or (n_floor > AUTO_LADDER_MAX_FLOOR_FRACTION*max_n_slabs):
return None
H_lo = H_at_energy(float(np.min(np.asarray(energy_arr, dtype=float))))
l0, l1 = float(L0), float(np.max(np.asarray(L_arr, dtype=float)))
resolved = (adiabatic._profile_is_resolved(H_lo, l0, l1, 200)
or adiabatic._profile_is_resolved(H_lo, l0, l1, 6400))
detail = dict(estimated_phase=phase, min_n_slabs=n_floor,
tolerance_margin=AUTO_LADDER_TOLERANCE_MARGIN)
if resolved:
_note_engine('hybrid', answered=False, reason='auto prefers the ladder', **detail)
else:
_note_engine('hybrid', answered=False, certified=False,
reason='the profile is not resolved at the probe scale', **detail)
_warn_hybrid_unresolved()
return _PreferLadder(n_floor)
def _osc_prob_hybrid_dispatch(
h_vac_energy_indep: np.ndarray,
VCC_func: Union[Callable, float],
h_matt: np.ndarray,
h_liv_energy_indep: Optional[np.ndarray],
n_liv: Optional[Union[int, float]],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Union[int, float],
nu_i: Optional[int],
nu_f: Optional[int],
scan_kwargs: Dict,
strategy: str
):
r"""Decide whether the adiabatic-transport-plus-Magnus-patch ("hybrid") strategy applies; run
it if so.
Unlike ``_osc_prob_ip_exp_dispatch``, this is not restricted to a genuine exponential
profile or to two flavors: :func:`magnus.adiabatic.hybrid_propagator` locates non-adiabatic
windows via an exact Hellmann-Feynman diagnostic that makes no assumption about the profile's
functional form or the Hamiltonian's dimension (see :doc:`/adiabatic_strategy`). It is still
restricted to a smooth ``VCC_func`` (no user-supplied slab edges or breakpoints -- a
piecewise-discontinuous profile such as PREM breaks the finite-difference diagnostics this
method relies on) and, since the method is fundamentally adaptive, to a genuinely requested
tolerance (``rtol``/``atol`` not both ``None``).
Each requested (energy, L) point is handled by an independent call to
:func:`magnus.adiabatic.hybrid_propagator`, since the position of a resonance (if any) is
generally energy-dependent -- unlike ``_osc_prob_ip_exp_dispatch``, this applies equally
to a scan with per-point baselines, not only a shared one. If any requested point fails to
self-certify, the whole batch is treated as not fitting this method with ``strategy='auto'``
(returns ``NotImplemented``, so the caller falls back to the general per-point path);
with ``strategy='hybrid'``, the best-effort result is returned together with
``HybridCertificationWarning``.
.. versionadded:: 1.0.0
Parameters
----------
h_vac_energy_indep : np.ndarray
Energy-independent part of the vacuum Hamiltonian.
VCC_func : Callable or float
Matter potential, as a function of position (required for this method to apply; a
constant potential falls back to the generic path, since there is then no position
dependence for a resonance to hide in).
h_matt : np.ndarray or Callable
Constant matrix multiplying ``VCC_func(l)``, or a callable of position returning it
(the position-resolved sterile projector); the callable is sampled at the same
positions as the potential.
h_liv_energy_indep : np.ndarray, optional
Energy-independent part of the LIV Hamiltonian, if any.
n_liv : int or float, optional
Power of the energy dependence of the LIV operator, if ``h_liv_energy_indep`` is given.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned.
nu_f : int, optional
Final flavor index; see ``nu_i``.
scan_kwargs : dict
The refinement/logging keyword arguments of :func:`osc_prob_energy_baseline` (rtol, atol,
magnus_exp_order, integration_method, t_slab_edges,
save_log, file_log) plus a nested 'kwargs' dict of any remaining, unrecognized keyword
arguments.
strategy : str
'auto', 'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential`. If 'magnus', this function always returns
``NotImplemented`` without doing any work.
Returns
-------
np.ndarray or NotImplemented
The oscillation probability (or single channel), computed via the hybrid strategy; or the
``NotImplemented`` singleton if the request does not fit it, ``strategy == 'magnus'``, or
(only with ``strategy == 'auto'``) it failed to self-certify for at least one point.
"""
if (strategy == 'magnus') or ('hybrid' in _ENGINES_DISABLED):
return NotImplemented
if scan_kwargs.get('cumulative') is True:
# An explicit cumulative=True is a request for one engine in particular, documented to
# raise rather than fall back if it cannot be served. Answering it here would be a
# silent substitution -- the exact thing that flag exists to rule out.
return NotImplemented
kwargs = dict(scan_kwargs.get('kwargs', {}))
t_breakpoints = kwargs.pop('t_breakpoints', None)
kwargs.pop('n_slabs', None)
kwargs.pop('n_tpts_per_slab', None)
if len(kwargs) > 0:
return NotImplemented
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
return NotImplemented
if not callable(VCC_func):
return NotImplemented
if scan_kwargs['t_slab_edges'] is not None:
return NotImplemented
if scan_kwargs['save_log'] or (scan_kwargs['file_log'] is not None):
return NotImplemented
rtol, atol = scan_kwargs['rtol'], scan_kwargs['atol']
if (rtol is None) and (atol is None):
# The hybrid method is fundamentally adaptive (self-certifying); "run once with a fixed,
# non-adaptive number of slabs" has no hybrid analogue, so fall back to the general path,
# which does support it.
return NotImplemented
rtol = 0.0 if rtol is None else rtol
atol = 0.0 if atol is None else atol
energy_arr, L_arr, return_float, ok = _normalize_energy_L(energy, L)
if not ok:
return NotImplemented
# Stand aside for a *large enough* baseline scan at a single energy: the cumulative scan
# answers all of those baselines from one traversal, where this method calls
# hybrid_propagator once per point at its ~20 ms floor. Measured through osc_prob_2nu_sun
# on a solar profile at N = 400: 7.5 s -> 0.29 s, with the error improving from ~1e-5 to
# ~1e-6 as well.
#
# The threshold is not 2. This method is accurate and cheap per point, so below
# HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS the cumulative scan's near-constant cost -- its
# strict probe -- is not yet amortized, and yielding would make a small scan several times
# slower (7.6x at N = 2) to buy accuracy that was already two orders inside what the caller
# asked for. See that constant for the measurements.
#
# Only under strategy='auto', which promises the best available answer rather than this
# method in particular; strategy='hybrid' is an explicit request and still gets hybrid.
# Declining here is enough to reach the cumulative path: ip_exp needs every baseline equal
# and the separable engine needs a single shared baseline, so both decline a scan too, and
# the caller falls through to osc_prob_energy_baseline, where cumulative='auto' engages --
# guaranteed, since its threshold is the smaller one.
#
# Guaranteed only while the cumulative scan is actually available, which is why
# cumulative=False is excluded here. That flag switches the cumulative scan off without
# touching this one, so standing aside for it made the hybrid path yield to an engine the
# caller had disabled: the request then fell past ip_exp and separable (both of which
# decline a scan) onto the general per-point ladder -- the outcome the fall-through argument
# above exists to rule out. Measured on a tagged exponential at d = 2, 10 MeV with
# cumulative=False, crossing the seam took the error from 1.16e-05 at N = 7 to 2.97e-03 at
# N = 8, a factor of 256 and outside the requested 1e-3, on the strength of one extra
# baseline. Pinned by test_engines.test_hybrid_does_not_stand_aside_for_a_disabled_engine.
if (strategy == 'auto') and (scan_kwargs.get('cumulative') is not False) \
and _cumulative_scan_would_serve(
energy_arr, L_arr, L0, HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS):
return NotImplemented
magnus_exp_order = scan_kwargs['magnus_exp_order']
integration_method = scan_kwargs['integration_method']
h_vac_energy_indep = np.asarray(h_vac_energy_indep, dtype=complex)
# A position-resolved projector stays the function it is; the closure below samples it
# at the same l as the potential. (Earth chords decline above on t_breakpoints, so
# this arm is exercised by smooth profiles -- e.g. a solar caller passing a callable
# ratio.) Only the constant form is coerced to a complex array.
h_matt_is_of_l = callable(h_matt)
if not h_matt_is_of_l:
h_matt = np.asarray(h_matt, dtype=complex)
if h_liv_energy_indep is not None:
h_liv_energy_indep = np.asarray(h_liv_energy_indep, dtype=complex)
d = h_vac_energy_indep.shape[-1]
if h_matt_is_of_l:
def H_at_energy(enu):
def H_of_l(l, enu=enu):
vcc = np.asarray(VCC_func(l))
H = (1.0/enu)*h_vac_energy_indep + \
vcc[..., None, None]*np.asarray(h_matt(l), dtype=complex)
if h_liv_energy_indep is not None:
H = H + (enu**n_liv)*h_liv_energy_indep
return H
return H_of_l
else:
def H_at_energy(enu):
def H_of_l(l, enu=enu):
vcc = np.asarray(VCC_func(l))
H = (1.0/enu)*h_vac_energy_indep + vcc[..., None, None]*h_matt
if h_liv_energy_indep is not None:
H = H + (enu**n_liv)*h_liv_energy_indep
return H
return H_of_l
# Under strategy='auto', a moderate phase at a loose tolerance goes to the ladder instead: the
# hybrid strategy's cost does not follow the tolerance, and there it is the slower route by
# one to two orders of magnitude (issue #70; see AUTO_LADDER_MAX_PHASE).
prefer = (_auto_prefers_ladder(H_at_energy, energy_arr, L_arr, L0, rtol, atol,
_resolve_max_n_slabs(scan_kwargs.get('max_n_slabs'), integration_method))
if strategy == 'auto' else None)
if prefer is not None:
return prefer
P_out = _hybrid_propagator_scan(H_at_energy, energy_arr, L_arr, L0, rtol, atol,
magnus_exp_order, integration_method, strategy, d)
if P_out is NotImplemented:
return NotImplemented
if (nu_i is not None) and (nu_f is not None):
P_out = P_out[:, nu_i, nu_f]
return P_out.__getitem__(0 if return_float else slice(None))
def _warn_hybrid_unresolved() -> None:
r"""The hybrid strategy declined because ``H_func`` is not resolved at the probe scale.
One function rather than two call sites with the same string: this fires both when
``strategy='auto'`` declines (and the general path answers) and when ``strategy='hybrid'``
was forced, and the two must not drift apart.
.. versionadded:: 1.0.0
"""
warnings.warn(
"osc_prob (hybrid strategy): the Hamiltonian is not resolved at the scale this method "
"samples it on -- a density jump, or a feature narrower than the probe grid can see -- "
"and no t_breakpoints were given. The adiabatic strategy is built on finite differences "
"of H between probe points, which mean nothing across a jump, so it declined; the "
"answer comes from the general Magnus path instead, which is correct there but slower, "
"and a slab straddling the same feature still limits its accuracy. Pass t_breakpoints "
"at the feature: on a BASELINE SCAN it is the cure, measured at 2.03e-01 -> 8.75e-06 on "
"a supernova shock front. On a SINGLE POINT it is not established and may make the "
"answer worse -- over 18 shock configurations it improved 7, worsened 11, and pushed 2 "
"from inside the requested tolerance to outside it, because declaring breakpoints there "
"also moves the request onto the general Magnus path. For a single point, compare "
"against strategy='magnus' or cumulative=True before trusting either answer. Measured "
"on an unmarked density step, the adiabatic answer was wrong by 0.54 in probability "
"while reporting itself certified. Shown once per session.",
UnmarkedDiscontinuityWarning, stacklevel=4)
def _hybrid_propagator_scan(
H_at_energy: Callable,
energy_arr: np.ndarray,
L_arr: np.ndarray,
L0: Union[int, float],
rtol: float,
atol: float,
magnus_exp_order: int,
integration_method: str,
strategy: str,
d: int
):
r"""Shared per-(energy, L)-point hybrid-propagator loop, used by both
``_osc_prob_hybrid_dispatch`` (separable vacuum + matter potential Hamiltonians) and
``_osc_prob_hybrid_dispatch_generic`` (an arbitrary user-supplied Hamiltonian, as accepted
by :func:`osc_prob_earth`/:func:`osc_prob_sun`).
.. versionadded:: 1.0.0
Parameters
----------
H_at_energy : Callable
Given a neutrino energy, returns ``H_of_l(l)``, the Hamiltonian at that energy as a
function of position only, suitable for :func:`magnus.adiabatic.hybrid_propagator`.
energy_arr : np.ndarray
Neutrino energies, one per requested point.
L_arr : np.ndarray
Baselines, one per requested point (same length as ``energy_arr``).
L0 : int or float
Initial position (shared by every point).
rtol, atol : float
Target relative/absolute tolerance, already coerced to non-``None`` floats by the caller.
magnus_exp_order : int
Magnus expansion order used for the local patch inside each non-adiabatic window.
integration_method : str
Integration method used for the local patch.
strategy : str
'auto' or 'hybrid' (never 'magnus'; the caller already handles that case). If 'auto', any
point that fails to self-certify aborts the whole scan (returns ``NotImplemented``); if
'hybrid', the best-effort result is kept and ``HybridCertificationWarning`` is raised once
at the end if at least one point was uncertified.
d : int
Hamiltonian dimension (number of flavors).
Returns
-------
np.ndarray or NotImplemented
Stacked probability matrices, shape ``(len(energy_arr), d, d)``; or ``NotImplemented`` if
``strategy == 'auto'`` and at least one point failed to self-certify.
"""
n_pts = len(energy_arr)
P_out = np.empty((n_pts, d, d))
any_uncertified = False
unresolved = False
for i in range(n_pts):
H_of_l = H_at_energy(energy_arr[i])
info = {}
U, _, certified = adiabatic.hybrid_propagator(H_of_l, float(L0), float(L_arr[i]),
rtol=rtol, atol=atol, magnus_exp_order=magnus_exp_order,
integration_method=integration_method, info=info)
unresolved = unresolved or (not info.get('resolved', True))
if not certified:
if strategy == 'auto':
_note_engine('hybrid', answered=False, certified=False,
reason=('the profile is not resolved at the probe scale'
if unresolved
else 'did not self-certify at the requested tolerance'))
if unresolved:
_warn_hybrid_unresolved()
return NotImplemented
any_uncertified = True
P_out[i] = np.swapaxes(U.real**2 + U.imag**2, -1, -2)
_note_engine('hybrid', certified=not any_uncertified)
if unresolved:
# A new instance of an existing kind. adiabatic._profile_is_resolved already runs
# inside hybrid_propagator, where failing it causes a decline rather than a message --
# so on an undeclared density jump the caller heard about slab widths from whichever
# engine answered instead, which is true but points at the wrong knob. Measured before
# shipping, over the axis the earlier measurement did not have -- one call per baseline
# means one call per sub-interval, not one per profile: 0 false positives over 1440
# smooth configurations, and every sub-interval containing a jump caught. Getting
# there needed a repair to the detector; see adiabatic.LOCAL_JUMP_RATIO.
_warn_hybrid_unresolved()
if any_uncertified:
warnings.warn("osc_prob (hybrid strategy): requested tolerance not achieved for at "
"least one (energy, L) point; the returned probabilities remain exactly unitary "
"but their accuracy is not certified -- unverified, which is not the same as "
"wrong. To get a certified answer: use strategy='auto', which falls back to the "
"general Magnus path for exactly these points; or, if the profile has known "
"structure (a density jump, a kink, a feature narrower than 1/200 of the "
"trajectory), pass t_breakpoints there, which is the one cure for a feature the "
"probe grid cannot resolve; or request a looser rtol/atol, if the accuracy you "
"need is less than the accuracy you asked for. Shown once per session.",
HybridCertificationWarning, stacklevel=3)
return P_out
def _osc_prob_hybrid_dispatch_generic(
htot: Callable,
VCC_func: Union[Callable, float],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Union[int, float],
nu_i: Optional[int],
nu_f: Optional[int],
t_breakpoints: Optional[np.ndarray],
rtol: Optional[Union[int, float]],
atol: Optional[Union[int, float]],
magnus_exp_order: int,
integration_method: str,
strategy: str,
kwargs: Dict
):
r"""Decide whether the hybrid strategy applies to an arbitrary, user-supplied Hamiltonian (as
accepted by :func:`osc_prob_earth`/:func:`osc_prob_sun`); run it if so.
Same method and gating philosophy as ``_osc_prob_hybrid_dispatch`` (see its docstring and
:doc:`/adiabatic_strategy`), adapted to ``htot(energy, l)`` -- the Hamiltonian already unified
into a single two-argument function by ``_osc_prob_with_potential``, regardless of
whether the user's own ``H_func`` takes ``(energy, l, VCC)`` or ``(energy, l)`` -- instead of
a separable ``h_vac_energy_indep``/``VCC_func``/``h_matt`` decomposition, since no such
decomposition is available (or needed: the resonance detector and adiabatic propagator make
no assumption about the Hamiltonian's internal structure) for a fully generic ``H_func``.
In practice, this means :func:`osc_prob_earth` almost always falls back to the ``'magnus'``
strategies regardless of what ``strategy`` is requested, since ``t_breakpoints`` (the PREM
layer-boundary crossings) is essentially always non-empty for a real Earth-crossing
trajectory; :func:`osc_prob_sun` has no such restriction, since its density profile has no
breakpoints.
.. versionadded:: 1.0.0
Parameters
----------
htot : Callable
The Hamiltonian, as a function of ``(energy, l)`` -- already unified by
``_osc_prob_with_potential`` from the user's own ``H_func(energy, l, VCC)`` or
``H_func(energy, l)``.
VCC_func : Callable or float
The environment's matter potential, as a function of position (required for this method
to apply; a constant potential falls back to the generic path).
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned.
nu_f : int, optional
Final flavor index; see ``nu_i``.
t_breakpoints : np.ndarray, optional
Mandatory slab edges (e.g., PREM layer boundaries); a non-empty array disables the hybrid
dispatch (see above).
rtol, atol : int or float, optional
Target relative/absolute tolerance requested by the caller.
magnus_exp_order : int
Magnus expansion order used for the local patch inside each non-adiabatic window.
integration_method : str
Integration method used for the local patch.
strategy : str
'auto', 'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential`. If 'magnus', this function always returns
``NotImplemented`` without doing any work.
kwargs : dict
Any additional, unrecognized keyword arguments the caller received (e.g., forwarded from
:func:`osc_prob_earth`/:func:`osc_prob_sun`'s own ``**kwargs``); any entry other than
``n_slabs``/``n_tpts_per_slab`` disables the hybrid dispatch, signaling that the caller
wants low-level control of the general slab-refinement method specifically.
Returns
-------
np.ndarray or NotImplemented
The oscillation probability (or single channel), computed via the hybrid strategy; or the
``NotImplemented`` singleton if the request does not fit it, ``strategy == 'magnus'``, or
(only with ``strategy == 'auto'``) it failed to self-certify for at least one point.
"""
if (strategy == 'magnus') or ('hybrid' in _ENGINES_DISABLED):
return NotImplemented
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
return NotImplemented
if not callable(VCC_func):
return NotImplemented
if len(set(kwargs) - {'n_slabs', 'n_tpts_per_slab'}) > 0:
return NotImplemented
if (rtol is None) and (atol is None):
return NotImplemented
rtol = 0.0 if rtol is None else rtol
atol = 0.0 if atol is None else atol
energy_arr, L_arr, return_float, ok = _normalize_energy_L(energy, L)
if not ok:
return NotImplemented
def H_at_energy(enu):
def H_of_l(l, enu=enu):
return htot(enu, l)
return H_of_l
d = np.asarray(htot(energy_arr[0], L0)).shape[-1]
# Under strategy='auto', a moderate phase at a loose tolerance goes to the ladder instead: the
# hybrid strategy's cost does not follow the tolerance, and there it is the slower route by
# one to two orders of magnitude (issue #70; see AUTO_LADDER_MAX_PHASE).
prefer = (_auto_prefers_ladder(H_at_energy, energy_arr, L_arr, L0, rtol, atol,
_resolve_max_n_slabs(None, integration_method))
if strategy == 'auto' else None)
if prefer is not None:
return prefer
P_out = _hybrid_propagator_scan(H_at_energy, energy_arr, L_arr, L0, rtol, atol,
magnus_exp_order, integration_method, strategy, d)
if P_out is NotImplemented:
return NotImplemented
if (nu_i is not None) and (nu_f is not None):
P_out = P_out[:, nu_i, nu_f]
return P_out.__getitem__(0 if return_float else slice(None))
def _cumulative_scan_grid(L_out, L0, n_acc, t_breakpoints):
"""Slab edges for a cumulative baseline scan, and where each output sits in them.
The grid is the union of three things, and each is there for a different reason:
* the **requested baselines**, so that every answer lands exactly on a slab edge and is
read off the running product rather than interpolated;
* a **uniform accuracy grid** of ``n_acc`` slabs, because the requested baselines are
typically logarithmically spaced and so are dense where accuracy is cheap and sparse
where it is expensive -- the opposite of what the integration needs;
* the **breakpoints**, for the usual reason: a slab straddling a density discontinuity
degrades the quadrature no matter how high ``magnus_exp_order`` is.
Returns
-------
(np.ndarray, np.ndarray)
Sorted, unique edges spanning ``[L0, max(L_out)]``, and the index into them of each
entry of ``L_out``.
.. versionadded:: 1.0.0
"""
L_out = np.asarray(L_out, dtype=float)
parts = [np.linspace(L0, float(L_out[-1]), int(n_acc) + 1), L_out, np.array([L0])]
if (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0):
bp = np.atleast_1d(np.asarray(t_breakpoints, dtype=float))
parts.append(bp[(bp > L0) & (bp < L_out[-1])])
edges = np.unique(np.concatenate(parts))
return edges, np.searchsorted(edges, L_out)
def _osc_prob_cumulative_scan(H_func, L_out, L0, n_acc, magnus_exp_order,
n_tpts_per_slab, integration_method, t_breakpoints,
A_eval_mode, **kwargs):
r"""Every baseline in ``L_out`` from a single traversal of the profile.
The evolution operator is a time-ordered product, so each requested answer is a *prefix*
of the next: :math:`U(0 \to L_2) = U(L_1 \to L_2)\,U(0 \to L_1)`. Computing N baselines
independently therefore re-walks the profile N times over. This walks it once and
records the running product wherever an answer was asked for -- the ``reduce`` in
:func:`osc_prob` with its intermediates kept rather than discarded.
Two properties are requirements rather than optimizations, and both are about memory:
* the traversal is **chunked**, so the slab operators are never all live at once;
* each snapshot is converted to a probability **immediately**, so the recorded term
collapses into the result the caller asked for instead of sitting beside it as N
complex unitaries.
Together they make peak memory ``O(block) + O(result)``, which above the block size is
less than the per-point path uses for the same scan.
Parameters
----------
H_func : Callable
Hamiltonian as a function of position alone; the energy is already bound.
L_out : np.ndarray
Requested baselines, non-decreasing and all at or beyond ``L0``. One equal to
``L0`` is the identity and is filled in as such.
L0 : int or float
Start of the trajectory [:math:`\text{eV}^{-1}`].
n_acc : int
Slabs the accuracy grid would use over the whole path on its own; see
:func:`_cumulative_scan_grid`.
magnus_exp_order : int
Order at which the Magnus expansion is truncated.
n_tpts_per_slab : int
Quadrature points per slab, read only by the cumulative methods.
integration_method : str
``'gl'``, ``'trapezoid'`` or ``'simpson'``.
t_breakpoints : list, np.ndarray, or None
Positions at which to force slab edges.
A_eval_mode : str or None
How the Hamiltonian may be evaluated; see
:func:`magnus.magnus.probe_eval_mode`. Forced to ``'vector'`` for a
non-callable ``H_func``, whatever the caller passed.
Returns
-------
np.ndarray
Probability matrices, shape ``(len(L_out), d, d)``.
.. versionadded:: 1.0.0
"""
edges, out_idx = _cumulative_scan_grid(L_out, L0, n_acc, t_breakpoints)
n_slabs = len(edges) - 1
# A position-independent Hamiltonian arrives here as a bare array, as it does in
# osc_prob; wrap it in the array-capable function the slab kernel expects. A single slab
# would be exact for it, but the caller asked for a scan and the extra slabs cost little
# -- and keeping one code path avoids a second place where the time ordering could differ.
if not callable(H_func):
H_const = np.asarray(H_func)
def H_func(l, _H=H_const):
return np.broadcast_to(_H, np.shape(l) + _H.shape) if np.ndim(l) else _H
A_eval_mode = 'vector'
dim = np.asarray(H_func(L0)).shape[-1]
P = np.empty((len(L_out), dim, dim))
running = np.eye(dim, dtype=complex)
# A baseline equal to L0 is the identity: no slab precedes it.
for j in np.flatnonzero(out_idx == 0):
P[j] = np.transpose(running.real**2 + running.imag**2)
_, block = _tile_for_working_set(1, n_slabs, dim*dim, live_arrays=8)
for start in range(0, n_slabs, block):
stop = min(start + block, n_slabs)
U = compute_evolution_operator_multiple_slabs(
H_func, np.column_stack([edges[start:stop], edges[start + 1:stop + 1]]),
n_tpts_per_slab, magnus_exp_order, integration_method=integration_method,
A_eval_mode=A_eval_mode, **kwargs)
# Outputs landing inside this block, in edge order, so the running product is
# snapshotted at the right moment without a second pass. The fold itself runs
# compiled (numba permitting): its Python form cost ~1.2 us/slab in numpy dispatch,
# which by v1.0.11 was several times the marginal cost of everything else in the
# traversal put together. `out_idx[order] - start - 1` is each snapshot's local
# slab index -- taken after that slab is applied, which is the
# `out_idx == start + k + 1` moment the loop this replaced used.
here = np.flatnonzero((out_idx > start) & (out_idx <= stop))
order = here[np.argsort(out_idx[here], kind='stable')]
magnus._running_product_snapshots(running, U, out_idx[order] - start - 1, order, P)
del U
return P
[docs]
def osc_prob_energy_baseline(
H_func: Union[Callable, np.ndarray],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Optional[Union[int, float]]=0.0,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
H_func_is_function_only_of_energy: Optional[bool]=False,
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
new_recursion_limit: Optional[int]=5000,
verbose: Optional[int]=0,
cumulative: Optional[Union[bool, str]]='auto',
symmetric_over: Optional[tuple]=None,
return_evolution_operator: Optional[bool]=False,
average: Optional[bool]=False,
average_spread: Optional[float]=None,
**kwargs
) -> Union[int, float, np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Compute and return oscillation probabilities for given arrays of
neutrino energy and baseline, and an arbitrary Hamiltonian.
Sits directly above :func:`osc_prob` in the primordial layer (see
:doc:`/architecture`): given arrays of ``energy`` and ``L``, builds the
right energy-dependent closure over ``H_func``, decides whether to parallelize over
(energy, L) points or hand a single call straight to :func:`osc_prob`, and carries the warm
start logic that seeds each point's refinement from the previous point's converged
(``n_slabs``, ``n_tpts_per_slab``). Called directly by :func:`osc_prob_vacuum`,
:func:`osc_prob_matter_std_potential`, :func:`osc_prob_matter_nsi`, and :func:`osc_prob_liv`.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator`` and ``average``.
Parameters
----------
H_func : Callable or np.ndarray
The Hamiltonian: a function of energy only, of position only, of both (in that
parameter order), or a constant matrix.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s). Must have the same length as ``energy``, or either may be a single value
broadcast against the other.
L0 : int or float, optional
Initial position. Default: 0.0.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
H_func_is_function_only_of_energy : bool, optional
If True and ``H_func`` accepts a single argument, treat it as energy-only (returning a
constant matrix per energy) rather than position-only. Default: False.
t_slab_edges : list or np.ndarray, optional
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
magnus_exp_order : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
n_jobs : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
integration_method : str
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
rtol : int or float, optional
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring. With
``average=True`` on a smooth position-dependent Hamiltonian, the tighter of ``rtol`` and
``atol`` is instead the tolerance of the phase average: of its window patches, of its
stretch phases, and of the agreement below which the decohered limit is returned (see
:func:`magnus.avgprob.phase_averaged_probabilities_adiabatic`).
atol : int or float, optional
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring, and
``rtol`` for ``average=True``.
growth_factor_n_slabs : int or float
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
**Provenance.** Swept over 1.2, 1.5, 2.0, 3.0 across 18 workloads spanning single
points, baseline scans and energy scans
(``docs/dev/adversarial_batteries/constants_audit2.py``): worst error 4.49e-04 at every
value. It sets how coarsely the ladder is sampled, not where it stops, so it trades
wasted refinement against overshoot without moving the accepted answer.
growth_factor_n_tpts_per_slab : int or float
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
max_num_loops : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
min_n_slabs : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
max_n_slabs : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
min_n_tpts_per_slab : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
**Provenance.** Swept over 2, 4, 8 on the same 18 workloads: worst error 4.49e-04 at
every value. With the default ``integration_method='gl'`` this is expected --
Gauss-Legendre pins the node count per slab and ignores it -- so the sweep confirms the
documented behavior rather than calibrating anything.
max_n_tpts_per_slab : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
validate_input : bool
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
save_log : bool
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
filename_log : str
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
file_log : TextIOWrapper, optional
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
close_file_log_upon_exit : bool
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
new_recursion_limit : int, optional
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
verbose : int
Forwarded to :func:`osc_prob` for each (energy, L) point; see its docstring.
cumulative : bool or str, optional
Compute a whole baseline scan from **one** traversal of the profile instead of one
traversal per baseline. The evolution operator is a time-ordered product, so
:math:`U(0 \to L_2) = U(L_1 \to L_2)\,U(0 \to L_1)`: each requested answer is a
prefix of the next, and recording the running product yields all of them at once.
One of:
* ``'auto'`` (default) -- use the cumulative scan whenever the request fits it, and
the ordinary per-point path otherwise. The request fits when the Hamiltonian varies
with position, all the energies are equal, no ``t_slab_edges`` were given, every
baseline is at or beyond ``L0``, and there are at least
``CUMULATIVE_AUTO_MIN_POINTS`` of them. A position-independent Hamiltonian
(vacuum, constant density) is excluded because :func:`osc_prob` integrates it
exactly on a single slab, leaving no traversal to share.
* ``True`` -- require it, and **raise** if the request does not fit. Use this when
the cumulative scan is what you want and silently getting the per-point path
instead would be a problem.
* ``False`` -- never use it.
Applies to a **baseline scan at a single energy**. The nesting it exploits belongs to
the baseline axis alone -- :math:`P(E_1)` shares nothing with :math:`P(E_2)`, since
each energy needs its own propagation through the whole profile -- so there is no
energy-axis counterpart to this.
Not compatible with ``t_slab_edges``, which it would have to override: the scan
builds a grid that is the union of the requested baselines, an accuracy grid, and
any ``t_breakpoints``. The accuracy grid is sized by one ordinary adaptive
:func:`osc_prob` call at the longest baseline, so the usual tolerance machinery and
its warnings apply unchanged.
The default became ``'auto'`` in 1.0.0, having been ``False``, because the cumulative
scan measured **more accurate at every scan size tested**, not merely faster. Against
``solve_ivp`` on a 5 MeV solar scan to one solar radius, the per-point path it replaces
returns answers outside the requested 1e-3 at several sizes -- 9.7e-3 at N = 10, 5.6e-3
at N = 25, 2.6e-3 at N = 100 -- where the cumulative scan stays near 5e-6 throughout.
Speed follows from N ~ 25 upward (2.65x there, 84x at N = 1000); below it the
cumulative scan can be ~1.3x slower in wall time, which is a few milliseconds.
Because the two paths build different grids, results move -- within the requested
tolerance, and generally toward the truth. Pass ``strategy='magnus'`` to reproduce
pre-1.0.0 numbers exactly: it opts out of the adiabatic strategy and already implies
``cumulative=False``.
``cumulative=False`` is the narrower flag and guarantees only that **the cumulative scan
is not used**; the hybrid path is still free to answer, at any point count. It was
briefly equivalent to ``strategy='magnus'`` above the seam, but only because the hybrid
dispatcher stood aside there for an engine the caller had switched off -- which sent the
request to the general ladder and cost 256x accuracy (1.157e-05 -> 2.966e-03 on a tagged
exponential at d = 2, 10 MeV, crossing N = 7 to N = 8). Below the seam the two flags
already differed. See
``tests/test_engines.test_hybrid_does_not_stand_aside_for_a_disabled_engine``.
\**kwargs
Additional arguments forwarded to :func:`osc_prob`.
symmetric_over : tuple, optional
Caller's declaration that ``A(t) == A(lo + hi - t)`` on ``(lo, hi)``, which lets
the Hamiltonian be evaluated on half the slabs. A declaration, not a test: it
is not checked. See :func:`magnus.magnus.magnus_expansion_multislab`.
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone: ``U`` is the evolution
operator over the same interval, in the flavor basis, a complex square array with
``U[final, initial]`` the amplitude from the initial to the final state, so that
``P == abs(U)**2.T``. The refinement ladder then compares the operator itself
between levels, with the same ``rtol`` and ``atol``, so the returned operator is
converged in its own right (phases included) and not only in its moduli. The
cumulative traversal is bypassed, since it walks a fixed grid with no ladder: every
point takes the per-point path. Every other setting keeps its meaning. Default: False.
average : bool, optional
If True, return the phase average of the probability over a relative energy spread (see
:mod:`magnus.avgprob`) instead of the oscillating one, as the wrappers do with the
same keyword. Which route answers follows from the Hamiltonian: a matrix, or a
function of the energy alone, is averaged in closed form, one eigendecomposition per
energy; a function of position is averaged by adiabatic transport along its
instantaneous eigenstates, with a Magnus patch across every non-adiabatic crossing,
when the profile is smooth; and across an energy window, with a warning, when
``t_breakpoints`` or ``t_slab_edges`` declare discontinuities. ``strategy``,
``n_jobs`` and the cumulative traversal play no role on this route, and on a smooth
profile the tighter of ``rtol`` and ``atol`` is its tolerance. A matrix, or a
function of position alone, does not depend on the energy, so an energy spread has
nothing to act on and it keeps the decohered limit. Cannot be combined with
``return_evolution_operator``. Default: False.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
Returns
-------
int, float, np.ndarray, or tuple
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each (energy, L) point; a single value/matrix if both ``energy`` and ``L`` were floats.
With ``return_evolution_operator=True``, the pair ``(P, U)``: ``P`` as above and ``U``
the operators, one ``(d, d)`` array per point, ``(n, d, d)`` for arrays of points.
"""
if (callable(H_func) and (_n_required_params(H_func) > 2)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline:"+\
" H_func can be energy- and position-dependent, only energy-dependent, or only" + \
" position-dependent. H_func cannot depend on more than two parameters. To vary" + \
" the third parameter, call osc_prob_energy_baseline within a loop where it is" + \
" varied.")
# Turn int into float
energy = float(energy) if isinstance(energy, int) else energy
L = float(L) if isinstance(L, int) else L
# As given, before the broadcasting below: the averaged route broadcasts for itself and
# reads the scalar-or-array shape of its answer off the caller's values.
energy_in, L_in = energy, L
# Flag return_float remembers if energy and L were both floats. If True,
# osc_prob_energy_baseline returns a float, too.
return_float = isinstance(energy, float) and isinstance(L, float)
# If there is a single value of energy, make an array out of it. Same for L. This will allow
# us to zip them later.
energy = np.array([energy]) if isinstance(energy, float) else np.array(energy)
L = np.array([L]) if isinstance(L, float) else np.array(L)
# Either energy and L are both lists (or NumPy arrays) of the same length; or one is a float and
# the other is a list (or NumPy array). Any other possibility will generate an exception. This
# exception may be raised earlier in routines that call osc_prob_energy_baseline if they are
# called wih validate_input == True, but we check below in case it osc_prob_energy_baseline was
# set to False.
if not ((len(energy) == len(L)) or (len(energy) == 1 and len(L) > 1) or \
(len(energy) > 1 and len(L) == 1)):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_energy_baseline: energy and L must be both " + \
"int or float; or, if lists (or NumPy arrays), they must have the same length;" + \
" or, if one is a float or single-entry list, the other must be a list with " + \
"multiple entries.")
# If energy is a single value, then transform it into an array containing the value energy
# repeated a number of times equal to the length of the L, and vice versa, in order to zip them.
energy = np.full(len(L), energy[0]) if (len(energy) == 1) else energy
L = np.full(len(energy), L[0]) if (len(L) == 1) else L
n_points = len(energy)
# When there are multiple (energy, L) points and n_jobs != 1, parallelize over the points, and
# run each individual osc_prob call serially. The per-point tasks are large enough for
# process-based parallelism to pay off, unlike the much smaller per-slab tasks inside osc_prob.
parallelize_over_points = (n_jobs != 1) and (n_points > 1)
# Keyword arguments common to all the calls to osc_prob below. Additional keyword arguments
# received in **kwargs are passed through to osc_prob as well (e.g., n_slabs,
# n_tpts_per_slab).
osc_prob_kwargs = dict(
t_slab_edges=t_slab_edges, magnus_exp_order=magnus_exp_order,
n_jobs=1 if parallelize_over_points else n_jobs,
integration_method=integration_method, rtol=rtol, atol=atol,
growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
validate_input=validate_input, save_log=save_log, filename_log=filename_log,
file_log=file_log, close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=new_recursion_limit, verbose=verbose,
symmetric_over=symmetric_over,
return_evolution_operator=return_evolution_operator, **kwargs)
# Build, for a given neutrino energy, the Hamiltonian to be passed to osc_prob: either a
# one-parameter function of position or, if position-independent, a constant matrix (which
# osc_prob detects and handles with internal speed-ups).
if not callable(H_func):
# H_func is position- and energy-independent
def H_at_energy(enu: float) -> np.ndarray:
return H_func
elif (_n_required_params(H_func) == 2):
# H_func is a function of two parameters; it is assumed that the first parameter is the
# energy and the second one is the position
def H_at_energy(enu: float) -> Callable:
return lambda l: H_func(enu, l)
elif H_func_is_function_only_of_energy:
# H_func is a function only of energy: at fixed energy, it is a constant matrix
def H_at_energy(enu: float) -> np.ndarray:
return H_func(enu)
else:
# H_func is a function only of position
def H_at_energy(enu: float) -> Callable:
return H_func
# Probe once how the Hamiltonian can be evaluated (vectorized over an array of positions,
# constant, or scalar-only): the verdict is structural and holds for every (energy, L) point,
# so probing here avoids re-probing inside every osc_prob call.
H_first = H_at_energy(energy[0])
# Phase average, requested with average=True: the same dispatch the wrappers place
# ahead of their engines, reached here on the direct route. Answered before the
# evaluation-mode probe below, which the averaged routes never use. The size check that
# the ordinary path runs further down is run here first, since this route allocates its
# result the same way.
if average:
if return_evolution_operator:
_check_operator_request(True, None, 'osc_prob_energy_baseline')
_check_output_fits(
n_points,
np.asarray(H_first(L0) if callable(H_first) else H_first).shape[-1],
'osc_prob_energy_baseline')
# The dispatch takes the Hamiltonian as a function of the energy alone when it does
# not depend on position, and of (energy, position) otherwise; the four forms H_func
# can take are wrapped into those two.
# A fixed matrix, or a function of position alone, does not depend on the energy, so
# an energy spread has no slope to act on and the decohered limit stands for it.
if not callable(H_func):
def htot(enu):
return H_func
only_energy, energy_dependent = True, False
elif _n_required_params(H_func) == 2:
htot, only_energy, energy_dependent = H_func, False, True
elif H_func_is_function_only_of_energy:
htot, only_energy, energy_dependent = H_func, True, True
else:
def htot(enu, l):
return H_func(l)
only_energy, energy_dependent = False, False
breakpoints = kwargs.get('t_breakpoints')
smooth = ((breakpoints is None or len(np.atleast_1d(breakpoints)) == 0)
and (t_slab_edges is None))
# The refinement and logging keywords assembled above, for the energy-window route,
# which propagates for real; the two averaging keywords stay out of it.
engine = dict(osc_prob_kwargs, cumulative=cumulative)
engine.pop('return_evolution_operator', None)
return _avg_prob_dispatch(htot, only_energy, energy_in, L_in, L0, nu_i, nu_f, True,
'osc_prob_energy_baseline', smooth_profile=smooth, engine_kwargs=engine,
average_spread=average_spread, energy_dependent=energy_dependent)
if callable(H_first):
osc_prob_kwargs['A_eval_mode'] = magnus.probe_eval_mode(
lambda t: -1j*H_first(t), L0, np.max(L))
# Refuse a request whose *result* cannot fit, before the scan allocates anything. Every
# batched engine either runs from here or falls back to here, and their working sets are
# now tiled to a fixed budget, which leaves the result array as the only quantity still
# free to grow without bound. Placed here because this is the first point at which the
# flavor count is known without evaluating the Hamiltonian specially for it: H_first has
# been built already, and probe_eval_mode has just called it if it is a function of
# position. Costs one multiply for ordinary requests; see _check_output_fits.
_check_output_fits(
n_points,
np.asarray(H_first(L0) if callable(H_first) else H_first).shape[-1],
'osc_prob_energy_baseline')
# Cumulative baseline scan: one traversal of the profile for every requested baseline,
# instead of one traversal per baseline. Applicable only when the Hamiltonian is the same
# for every requested point -- i.e. a single energy, scanned over baselines -- because the
# nesting it exploits is a property of the baseline axis alone: P(0->L1) is a prefix of
# P(0->L2), while P(E1) shares nothing with P(E2).
#
# cumulative='auto' (the default) resolves to True exactly when the request fits, and to
# False otherwise, so that the explicit cumulative=True can keep *raising* on a request it
# cannot serve -- a caller who asked for it by name should hear that it did not happen.
#
# 'auto' adds two requirements beyond what cumulative=True checks:
#
# - at least two baselines. A single point has no prefix to reuse and would pay the
# inherited-grid probe for nothing, which matters because every single-point call
# through the wrapper layer is served from here.
# - a position-*dependent* Hamiltonian. When H does not vary along the trajectory
# (vacuum, constant density), osc_prob integrates it exactly on one slab, so there is
# no traversal to share and the cumulative scan is strictly worse: it sizes a grid from
# an adaptive probe and then walks it. H_first is the Hamiltonian at the first energy,
# already built above; it is a plain matrix exactly when the profile is constant.
if isinstance(cumulative, str):
if cumulative != 'auto':
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline: "
"cumulative must be True, False, or 'auto'; got " + repr(cumulative) + ".")
cumulative = bool(
callable(H_first)
and (t_slab_edges is None)
and _cumulative_scan_would_serve(np.asarray(energy), np.asarray(L), L0,
CUMULATIVE_AUTO_MIN_POINTS))
if return_evolution_operator:
# The cumulative traversal walks a fixed grid with no ladder, and the operator is only
# offered from the ladder: every point takes the per-point path below instead.
cumulative = False
if cumulative:
if t_slab_edges is not None:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline: "
"cumulative=True builds its own slab grid and cannot also honor "
"t_slab_edges.")
if not np.all(energy == energy[0]):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline: "
"cumulative=True scans baselines at one energy; the given energies differ. "
"Baselines nest and energies do not, so there is nothing to reuse across "
"energies.")
if np.any(np.asarray(L, dtype=float) < L0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline: "
"cumulative=True requires every baseline to be at or beyond L0.")
H_fixed = H_at_energy(energy[0])
order = np.argsort(np.asarray(L, dtype=float), kind='stable')
L_sorted = np.asarray(L, dtype=float)[order]
# The accuracy grid is inherited, not invented. One ordinary adaptive osc_prob call
# at the longest baseline reports the slab count at which *it* converged -- which is
# precisely "slabs needed for a uniform grid over the whole path", the definition of
# n_acc -- and brings with it every safeguard that path already has, including the
# n_slabs floor and the tolerance-not-achieved warning. Getting this wrong is the
# one way a cumulative scan goes silently wrong: on a solar profile an n_acc of 2000
# is off by 1.6e-2 where 14883 is right, and nothing in the traversal itself notices.
n_acc_from_ceiling = False
if (rtol is None) and (atol is None):
n_acc = kwargs.get('n_slabs', 1)
else:
probe_info = {}
probe_kwargs = dict(osc_prob_kwargs)
probe_kwargs['convergence_info'] = probe_info
probe_kwargs.pop('A_eval_mode', None)
# The probe is always strict, whatever the caller asked for their own points. It is
# the one call whose convergence decides the grid for the *whole* scan, so its
# failure mode is not one bad point but N of them -- and the ladder's ordinary stop
# rule can end on a coincidental agreement between two levels that are both wrong
# (see the strict_convergence entry in osc_prob's docstring). Measured on the solar
# profile at 10 MeV, where that is exactly what the ordinary ladder does: the scan
# came out at 5.2e-3 against a requested 1e-3 with a loose probe, and 1.0e-6 with a
# strict one. It costs one extra refinement level on a single call, amortized over
# every baseline in the scan.
probe_kwargs['strict_convergence'] = True
# The probe runs a uniform-density Hamiltonian over the longest baseline, not the
# caller's profile over the caller's interval, so the caller's symmetry declaration
# does not describe it. A constant Hamiltonian is trivially symmetric and mirroring
# it would be harmless, but the declaration is dropped rather than reasoned about:
# only its slab count is kept, so there is nothing to gain by carrying it.
probe_kwargs['symmetric_over'] = None
# The probe's *probabilities* are discarded -- only its slab count is kept -- so a
# MagnusConvergenceWarning about its intermediate refinement levels describes a
# result nobody receives, and would be actively misleading: the grid this call
# sizes produces no such warning when it is actually traversed. Suppressed here
# rather than globally, and only this one category: anything reporting that the
# count itself is unreliable (ToleranceNotAchievedWarning) still reaches the caller,
# because that does bear on the answer.
with warnings.catch_warnings():
warnings.simplefilter('ignore', magnus.MagnusConvergenceWarning)
osc_prob(H_fixed, L0, float(L_sorted[-1]),
A_eval_mode=osc_prob_kwargs.get('A_eval_mode'), **probe_kwargs)
# Scaled up because that count is what the *longest* baseline needed, and the
# same uniform density is thinner than the shorter baselines in the scan would
# have chosen for themselves; see CUMULATIVE_N_ACC_SAFETY for the measurement.
n_acc = probe_info['n_slabs']*CUMULATIVE_N_ACC_SAFETY
# Whether that count is a converged requirement or merely where the probe ran out
# of room. The probe's own ToleranceNotAchievedWarning says the *probe* did not
# converge; it says nothing about the consequence that matters here, which is that
# the grid for every baseline in the scan is now sized by a cap. See the warning
# a few lines below.
n_acc_from_ceiling = bool(
probe_info['n_slabs'] >= _resolve_max_n_slabs(max_n_slabs, integration_method))
if n_acc_from_ceiling:
# The probe raises ToleranceNotAchievedWarning of its own here, and that is a
# statement about the probe -- one call whose probabilities are discarded. The
# consequence a caller needs is different and larger: the accuracy grid for
# EVERY baseline in the scan is now sized by a cap rather than by a converged
# requirement, so one capped call becomes N inaccurate answers. Measured on a
# solar profile, an n_acc of 2000 is off by 1.6e-2 where 14883 is right, and
# nothing in the traversal itself notices.
warnings.warn(
"osc_prob_energy_baseline (cumulative scan): the accuracy grid was sized "
"from max_n_slabs rather than from a converged requirement -- the probe "
"that sizes it ran out of slabs before it converged. Every baseline in "
"this scan inherits that grid, so the whole scan is affected, not one "
"point. Raise max_n_slabs (currently "
+ str(_resolve_max_n_slabs(max_n_slabs, integration_method))
+ "), or shorten the longest baseline, which is what sets the grid. Shown "
"once per session.",
ToleranceNotAchievedWarning, stacklevel=2)
# A jump the caller did not declare is the one way this grid goes wrong that adding
# slabs cannot fix: a slab straddling the discontinuity degrades the quadrature
# regardless of magnus_exp_order, and refining only narrows the straddling slab. Say
# so rather than returning a quietly-wrong scan -- measured at 1.4e-03 and 2.1e-03,
# silently, on two of 150 random piecewise profiles. Skipped when the caller supplied
# breakpoints, because then the grid already has edges on the discontinuities.
# Only for a position-*dependent* Hamiltonian. cumulative='auto' excludes a constant
# one, but an explicit cumulative=True accepts it, and it then arrives here as a bare
# matrix rather than a callable -- there is nothing to sample and nothing to be
# discontinuous. (Calling it anyway is a TypeError, which three existing
# constant-Hamiltonian tests caught immediately.)
if callable(H_fixed) and (
(kwargs.get('t_breakpoints') is None)
or (len(np.atleast_1d(kwargs.get('t_breakpoints'))) == 0)):
if not (adiabatic._profile_is_resolved(H_fixed, float(L0), float(L_sorted[-1]), 200)
or adiabatic._profile_is_resolved(H_fixed, float(L0),
float(L_sorted[-1]), 6400)):
warnings.warn(
"osc_prob_energy_baseline (cumulative scan): the Hamiltonian is "
"discontinuous at the scale of the grid this scan builds, and no "
"t_breakpoints were given. A slab straddling a density jump degrades the "
"quadrature no matter how many slabs are used; pass t_breakpoints at the "
"discontinuities. Shown once per session.",
UnmarkedDiscontinuityWarning, stacklevel=2)
P_sorted = _osc_prob_cumulative_scan(
H_fixed, L_sorted, L0, n_acc, magnus_exp_order,
kwargs.get('n_tpts_per_slab', 100), integration_method,
kwargs.get('t_breakpoints'), osc_prob_kwargs.get('A_eval_mode'),
# strict_convergence is dropped rather than forwarded: the traversal walks a fixed
# grid and runs no refinement ladder, so the flag has nothing to act on here, and
# the engine below rejects unknown keywords. The one adaptive step in a cumulative
# scan is the probe above, which is unconditionally strict already. Without this a
# user who passes the flag to a baseline scan gets a TypeError out of
# magnus_expansion_multislab.
# `convergence_info` is dropped for the same reason as `strict_convergence`
# directly above: it is the per-point refinement ladder's report, and this traversal
# walks a fixed grid with no ladder to report on. Forwarding it reached
# `magnus_expansion_multislab`, which rejects unknown keywords, so
# `cumulative=True` together with `convergence_info=...` raised
# `TypeError: magnus_expansion_multislab() got an unexpected keyword argument`
# rather than returning a probability. Reachable on the shipped tree, and no test
# covered the combination.
**{k: v for k, v in kwargs.items()
if k not in ('n_slabs', 'n_tpts_per_slab', 't_breakpoints',
'strict_convergence', 'convergence_info')})
_note_engine('cumulative', n_acc=int(n_acc), n_acc_from_ceiling=n_acc_from_ceiling)
P_all = np.empty_like(P_sorted)
P_all[order] = P_sorted
if (nu_i is not None) and (nu_f is not None):
P_all = P_all[:, nu_i, nu_f]
# Squeeze the singleton point axis when both inputs were scalars, exactly as every
# other return site in this function does. This branch omitted it, which was a latent
# shape bug rather than a harmless one: `osc_prob_3nu_earth(E, ..., cumulative=True)`
# with scalar energy and L returned (1, d, d) instead of (d, d), so `P[nu_i][nu_f]`
# silently selected a row of the wrong array. It went unnoticed because
# CUMULATIVE_AUTO_MIN_POINTS was 2: `return_float` is True only when energy and L are
# both scalars, which is one point, which never reached this branch by default. Only
# an explicit `cumulative=True` could get here, and no test covered that combination.
return P_all.__getitem__(0 if return_float else slice(None))
# Warm starts: osc_prob reports the refinement parameters at which each point converged
# (conv_info), and the next point starts its refinement from there (divided by one growth
# factor, so that the comparison between successive refinements is still performed).
# Neighboring points typically converge at (nearly) the same parameters, so this skips most
# of the refinement ladder.
conv_info = osc_prob_kwargs.get('convergence_info')
if conv_info is None:
conv_info = {}
osc_prob_kwargs['convergence_info'] = conv_info
warm_start = (t_slab_edges is None) and \
((rtol is not None) or (atol is not None))
def apply_warm_start():
# Seed the next point TWO growth steps below the last converged values. One step below
# reproduces exactly the pair of refinements at which the previous point was accepted;
# starting one step lower than that lets the refinement scale decay geometrically across
# points when the previous point was harder than the next ones (e.g., the lowest energy
# of a scan), at the price of at most one extra refinement when it was not.
if warm_start and conv_info:
g1 = max(growth_factor_n_slabs, 1.0)**2
g2 = max(growth_factor_n_tpts_per_slab, 1.0)**2
osc_prob_kwargs['min_n_slabs'] = max(min_n_slabs,
int(np.ceil(conv_info['n_slabs']/g1)))
osc_prob_kwargs['min_n_tpts_per_slab'] = max(min_n_tpts_per_slab,
int(np.ceil(conv_info['n_tpts_per_slab']/g2)))
def compute_single_point(enu: float, baseline: float):
out = osc_prob(H_at_energy(enu), L0, baseline, **osc_prob_kwargs)
P, U = out if return_evolution_operator else (out, None)
# Select one oscillation channel if requested; otherwise keep the full matrix
if ((nu_i is not None) and (nu_f is not None)):
P = P[nu_i][nu_f]
return (P, U) if return_evolution_operator else P
if parallelize_over_points:
# Compute the first point serially to learn the refinement parameters, then distribute
# the remaining points over the workers, warm-started from the first point. (The shared
# conv_info dict cannot be updated across processes, so it is dropped from the parallel
# calls.)
probs = [compute_single_point(energy[0], L[0])]
apply_warm_start()
osc_prob_kwargs.pop('convergence_info', None)
# A module global does not cross a process boundary: loky re-imports magnus in each
# worker, where EXPM_BACKEND is back at its default. Since the wrappers expose no
# expm_backend parameter, that global is the *only* backend control a caller of this
# function has, so leaving it behind meant an explicit request was silently ignored for
# every point but the first -- worst for the one use the switch is documented for,
# comparing the two backends, which would have compared 'auto' against itself. The
# answers agree to ~1e-15 either way, so this is about honoring the request, not about
# the numbers. Carried by value and re-applied inside the worker.
def compute_single_point_in_worker(enu: float, baseline: float,
_backend: str = magnus.EXPM_BACKEND):
magnus.EXPM_BACKEND = _backend
return compute_single_point(enu, baseline)
probs += Parallel(n_jobs=n_jobs)(delayed(compute_single_point_in_worker)(enu, baseline)
for enu, baseline in zip(energy[1:], L[1:]))
else:
probs = []
for enu, baseline in zip(energy, L):
apply_warm_start()
probs.append(compute_single_point(enu, baseline))
# The private '_hamiltonian' payload is what lets cross_check_strategies build the 'expm'
# reference without rebuilding any wrapper's physics: this is the one place every entry
# point's Hamiltonian arrives already assembled. Stripped from strategy_info.
_note_engine('magnus', n_points=n_points, _hamiltonian=dict(
H_at_energy=H_at_energy, L0=L0, energy=energy, L=L, nu_i=nu_i, nu_f=nu_f,
t_breakpoints=kwargs.get('t_breakpoints')))
# The call to __getitem__ below is a way to return a single float (or single probability
# matrix) if both energy and L were given as floats.
sel = 0 if return_float else slice(None)
if return_evolution_operator:
return np.array([p for p, _ in probs])[sel], np.array([u for _, u in probs])[sel]
return np.array(probs).__getitem__(sel)
#-----------------------------------------------------------------------
# Cross-method agreement
#-----------------------------------------------------------------------
# Engines that answer with probabilities only. A request for the evolution operator disables
# them for the call, so that the general Magnus ladder -- the one engine that forms the
# operator -- is the engine that answers. Same set the cross-check uses to force the ladder.
_OPERATOR_ONLY_FROM_LADDER = ('hybrid', 'ip_exp', 'separable', 'constant')
def _check_operator_request(average: bool, strategy: Optional[str], source_func_name: str) -> None:
r"""Rejects a request for the evolution operator that no engine could serve.
``average=True`` answers through the phase-averaged routes, and ``strategy='hybrid'``
insists on the adiabatic engine; neither forms an evolution operator, so combining either
with ``return_evolution_operator=True`` raises here, naming the entry point, rather than
being silently ignored.
.. versionadded:: 1.1.1
"""
if average:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name +
": return_evolution_operator=True cannot be combined with average=True: the "
"phase-averaged routes never form an evolution operator.")
if strategy == 'hybrid':
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name +
": return_evolution_operator=True cannot be combined with strategy='hybrid': that "
"engine answers with probabilities only. Use strategy='auto' or 'magnus'.")
_CROSS_CHECK_FORCING = {
# label: (strategy, cumulative, engines to forbid so that this one is reached)
#
# 'constant' has to appear in every other row's forbid list, not only in its own. It answers
# before osc_prob_energy_baseline is reached, and osc_prob_energy_baseline is what records the
# `_hamiltonian` payload that _expm_reference needs -- so leaving it enabled silenced the expm
# reference entirely, which is the one member of this table that is an independent oracle
# rather than another of this package's engines.
'hybrid': ('hybrid', False, ('ip_exp', 'separable', 'constant')),
'ip_exp': ('magnus', False, ('hybrid', 'separable', 'constant')),
'separable': ('magnus', False, ('hybrid', 'ip_exp', 'constant')),
'constant': ('magnus', False, ('hybrid', 'ip_exp', 'separable')),
'cumulative': ('magnus', True, ('hybrid', 'ip_exp', 'separable', 'constant')),
'magnus': ('magnus', False, ('hybrid', 'ip_exp', 'separable', 'constant')),
}
def _expm_reference(payload: Dict) -> Tuple[Optional[np.ndarray], str]:
r"""Oscillation probabilities from ``scipy.linalg.expm``, **only where it is exact**.
:math:`U = \exp(-iH\,\Delta l)` solves :math:`dU/dl = -iH U` exactly when :math:`H` does not
depend on :math:`l`; across a piecewise-constant profile whose edges are known, the
time-ordered product of one exponential per piece is likewise exact. Anywhere else it is a
first-order approximation, which is worse than every engine it would be checking, so this
declines instead of supplying a bad reference dressed as a good one.
Constancy is *measured*, not assumed: ``H`` is sampled inside each piece and required to be
constant to a relative :math:`10^{-12}`. Sampling cannot prove constancy, but a profile that
varies below that on 33 samples per piece and not between them is not one this test is
protecting anybody from.
.. versionadded:: 1.0.0
Parameters
----------
payload : dict
The ``_hamiltonian`` payload recorded by :func:`osc_prob_energy_baseline`:
``H_at_energy``, ``L0``, ``energy``, ``L``, ``nu_i``, ``nu_f``, ``t_breakpoints``.
Returns
-------
(np.ndarray or None, str)
Probabilities of shape ``(n_points, d, d)`` (or ``(n_points,)`` if a single channel was
requested), and an empty reason; or ``None`` and the reason it declined.
"""
from scipy.linalg import expm
H_at_energy = payload['H_at_energy']
L0 = float(payload['L0'])
energy = np.asarray(payload['energy'], dtype=float)
L = np.asarray(payload['L'], dtype=float)
bp = payload['t_breakpoints']
bp = (np.atleast_1d(np.asarray(bp, dtype=float)) if bp is not None
else np.array([], dtype=float))
P_out = []
for enu, baseline in zip(energy, L):
H_of_l = H_at_energy(float(enu))
edges = np.unique(np.concatenate(
[[L0, float(baseline)], bp[(bp > min(L0, baseline)) & (bp < max(L0, baseline))]]))
U = None
for a, b in zip(edges[:-1], edges[1:]):
if callable(H_of_l):
# Sample the open interval: the endpoints are exactly where a piecewise profile
# is ambiguous, and a jump sitting on an edge is not a reason to decline.
xs = np.linspace(a, b, 35)[1:-1]
Hs = adiabatic._H_on_grid(H_of_l, xs)
scale = np.max(np.abs(Hs))
if scale > 0.0 and np.max(np.abs(Hs - Hs[0])) > 1.0e-12*scale:
return None, ('H varies with position on [%g, %g]; expm is exact only for '
'a piecewise-constant H with declared edges' % (a, b))
H_piece = Hs[len(Hs)//2]
else:
H_piece = np.asarray(H_of_l, dtype=complex)
U_piece = expm(-1j*np.asarray(H_piece, dtype=complex)*(b - a))
U = U_piece if U is None else U_piece @ U
P_out.append(np.transpose(U.real**2 + U.imag**2))
P_out = np.array(P_out)
if (payload['nu_i'] is not None) and (payload['nu_f'] is not None):
P_out = P_out[:, payload['nu_i'], payload['nu_f']]
return P_out, ''
[docs]
def cross_check_strategies(entry_point: Callable, *args, engines=None, **kwargs) -> Dict:
r"""Answer the same request with every engine that applies, and report how far apart they are.
**Why this exists.** Every silently-wrong result found in
``docs/dev/FINDINGS_ADVERSARIAL_VALIDATION.md`` came from a method certifying itself by
comparing itself with itself. :func:`magnus.adiabatic.hybrid_propagator` refines its own
knobs and checks the two answers agree; :func:`osc_prob`'s slab ladder does the same. When
the method has a blind spot, both sides of the comparison share it and the agreement carries
no information -- that is not a bug in either comparison, it is a limit of the *shape* of the
check. This package contains genuinely different engines (see :data:`ENGINE_FAMILIES`), and
running two of them needs **no oracle at all** while detecting exactly the class that
self-certification cannot.
**This is a diagnostic, not a safety net.** It is never on by default: it multiplies the cost
of a call by the number of engines that apply. A large spread is *reported*, never raised --
what it means depends on the request, and deciding that is the caller's job.
**Acceptance.** Measured by running the same comparison against the *pre-fix* package (a
worktree at ``978663a``), on every construction of ``FINDINGS`` §3 that was silently wrong
and reported ``certified=True`` there -- because a diagnostic validated only against code
with no known defects has not been validated. Reproduce with
``docs/dev/adversarial_batteries/crosscheck_acceptance.py``:
============================================ ============== ==============================
construction silent error max cross-family spread
============================================ ============== ==============================
step function, unmarked edge (§3.1) 5.395e-01 **5.399e-01**
ten crossings (§3.2, worst found anywhere) 3.907e-02 **3.913e-02**
sinusoid at span/7 (§3.2) 1.672e-02 **1.687e-02**
kink, :math:`C^0` but not :math:`C^1` 1.448e-02 **1.448e-02**
singularity approached, not reached 8.625e-03 **8.613e-03**
sub-threshold bump, w = 1e-2 span (§3.2) 7.701e-03 **7.768e-03**
sub-threshold bump, w = 3e-2 span (§3.2) 4.388e-03 **4.594e-03**
narrow bump, w = 3e-5 span (§3.3) 2.907e-02 3.5e-14 -- **not detected**
============================================ ============== ==============================
Seven of eight, each at least four times the requested 1e-3. The last row is the honest
limit, and is why this table is here rather than a claim of coverage: a feature narrower than
the probe spacing is invisible to *every* engine that samples the profile on a grid, so they
agree -- correctly, given what they can see -- and are wrong together. No cross-check
between grid-based methods can find that; the cure is ``t_breakpoints`` at the feature (see
:doc:`/adiabatic_strategy`). Stated as a rule: **this sees a wrong engine exactly when some
other engine got it right.**
.. versionadded:: 1.0.0
Parameters
----------
entry_point : Callable
The function to cross-check: :func:`osc_prob_matter_std_potential`,
:func:`osc_prob_matter_nsi`, :func:`osc_prob_liv`, :func:`osc_prob_sun`,
:func:`osc_prob_earth`, one of their fixed-flavor wrappers, or
:func:`osc_prob_energy_baseline`. ``strategy`` and ``cumulative`` are supplied by this
function and dropped from ``kwargs`` if the caller passed them, since forcing them is
how each engine is reached.
\*args
Positional arguments for ``entry_point``, exactly as in an ordinary call.
engines : sequence of str, optional
Restrict the check to these engines; see :data:`ENGINE_FAMILIES` for the labels. Default:
every engine that applies.
\**kwargs
Keyword arguments for ``entry_point``, exactly as in an ordinary call.
Returns
-------
dict
``'answers'``
``{label: probabilities}`` for each engine that answered.
``'ran'``
The labels that answered, in the order tried.
``'declined'``
``{label: reason}`` for each engine that did not. Most engines decline on most
requests, which is expected and not a finding.
``'spread'``
``{(label_a, label_b): max |P_a - P_b|}`` over every pair that ran.
``'max_spread'``, ``'max_spread_pair'``
The largest spread and where it was.
``'max_spread_independent'``, ``'max_spread_independent_pair'``
The same, restricted to pairs from **different** families (see
:data:`ENGINE_FAMILIES`). This is the number to read: two engines from the same
family can be wrong in the same way, so their disagreement is informative but their
*agreement* is not.
``'families'``
``{label: family}`` for the engines that ran.
``'warnings'``
``{label: [warning class names]}`` raised while that engine answered.
``'certified'``
``{label: bool}``, currently only for ``'hybrid'``.
Examples
--------
.. jupyter-execute::
import magnus.globaldefs as gd
import magnus.matter as matter
import magnus.oscprob as oscprob
ne = matter.exp_density_profile(gd.NUM_DENSITY_E_SUN_CENTRAL,
gd.L_SCALE_SUN)
params = gd.OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']
out = oscprob.cross_check_strategies(
oscprob.osc_prob_matter_std_potential, 2, ne, 10.0e6,
0.5*gd.SUN_RADIUS*gd.UNIT_KM,
{'sth': params['s12'], 'Dm2': params['D21']}, L0=0.0,
density_is_of_number_of_electrons=True)
print('engines that ran :', sorted(out['ran']))
print('worst independent gap : %.2e' % out['max_spread_independent'])
See Also
--------
ENGINE_FAMILIES : which engines share machinery, and therefore which pairs carry information.
"""
wanted = tuple(_CROSS_CHECK_FORCING) + ('expm',) if engines is None else tuple(engines)
unknown = set(wanted) - set(_CROSS_CHECK_FORCING) - {'expm'}
if unknown:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.cross_check_strategies: unknown "
"engine label(s) " + repr(sorted(unknown)) + "; known labels are "
+ repr(sorted(set(_CROSS_CHECK_FORCING) | {'expm'})) + ".")
call_kwargs = {k: v for k, v in kwargs.items() if k not in ('strategy', 'cumulative')}
takes_strategy = 'strategy' in signature(entry_point).parameters
answers, declined, warns, certified = {}, {}, {}, {}
hamiltonian = None
for label in wanted:
if label == 'expm':
continue
strategy, cumulative, forbid = _CROSS_CHECK_FORCING[label]
forced = dict(call_kwargs, cumulative=cumulative)
if takes_strategy:
forced['strategy'] = strategy
elif strategy == 'hybrid':
declined[label] = "entry point has no 'strategy' parameter"
continue
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
try:
with _engine_probe(forbid) as trace:
P = entry_point(*args, **forced)
except Exception as exc: # noqa: BLE001 -- a decline, not a failure
declined[label] = type(exc).__name__ + ': ' + str(exc).strip().split('\n')[0]
continue
note = next((e for e in trace if e['engine'] == label and e['answered']), None)
if note is None:
other = next((e['engine'] for e in trace if e['answered']), 'nothing')
refused = next((e.get('reason') for e in trace
if e['engine'] == label and not e['answered']), None)
declined[label] = refused or ('does not apply to this request (answered by '
+ other + ')')
continue
answers[label] = np.asarray(P)
warns[label] = sorted({w.category.__name__ for w in caught})
if 'certified' in note:
certified[label] = note['certified']
if hamiltonian is None:
hamiltonian = next((e['_hamiltonian'] for e in trace if '_hamiltonian' in e), None)
if 'expm' in wanted:
if hamiltonian is None:
declined['expm'] = ('the Hamiltonian was not observed; run the general Magnus '
'engine as well (it is where the Hamiltonian is assembled)')
else:
P_expm, reason = _expm_reference(hamiltonian)
if P_expm is None:
declined['expm'] = reason
else:
answers['expm'] = P_expm
warns['expm'] = []
ran = [lab for lab in wanted if lab in answers]
spread, best, best_pair, best_ind, best_ind_pair = {}, 0.0, None, 0.0, None
for i, a in enumerate(ran):
for b in ran[i + 1:]:
Pa, Pb = np.ravel(answers[a]), np.ravel(answers[b])
if Pa.shape != Pb.shape:
# Different shapes mean the engines answered different questions; that is a
# defect in this diagnostic's forcing, not a physics finding, so say so rather
# than broadcasting them into a number.
spread[(a, b)] = np.nan
continue
s = float(np.max(np.abs(Pa - Pb)))
spread[(a, b)] = s
if s > best:
best, best_pair = s, (a, b)
if ENGINE_FAMILIES[a] != ENGINE_FAMILIES[b] and s > best_ind:
best_ind, best_ind_pair = s, (a, b)
# A spread of zero means "no disagreement was found", which is not the same statement as "the
# engines agree" -- and when nothing was compared, it is the wrong one. Say so, because the
# numbers themselves cannot: 0.0 is 0.0 either way.
if len(ran) < 2:
if not ran:
detail = ('no engine ran at all. ' + '; '.join(
'%s: %s' % (lab, why) for lab, why in declined.items()) if declined
else 'no engine ran at all, and none reported a reason.')
else:
detail = ('only one engine ran (%s), so there was no second answer to compare it '
'with.' % ran[0])
warnings.warn(gd.WARNING_MSG_NO_COLOR + " cross_check_strategies: " + detail +
" max_spread and max_spread_independent are therefore 0.0 because nothing was "
"compared, not because anything agreed. If the entry point has no 'strategy' "
"parameter -- osc_prob itself does not -- pass a wrapper such as "
"osc_prob_matter_std_potential instead, and check out['ran'] before reading any "
"spread.", CrossCheckInconclusiveWarning, stacklevel=2)
elif best_ind_pair is None:
warnings.warn(gd.WARNING_MSG_NO_COLOR + " cross_check_strategies: the " +
format(len(ran), 'd') + " engines that ran (" + ', '.join(ran) + ") all belong to "
"the '" + ENGINE_FAMILIES[ran[0]] + "' family, so max_spread_independent is 0.0 "
"because no cross-family pair exists, not because independent methods agreed. "
"Engines within a family share machinery and can share a blind spot; only a "
"cross-family comparison carries information (see ENGINE_FAMILIES).",
CrossCheckInconclusiveWarning, stacklevel=2)
return {
'answers': answers,
'ran': tuple(ran),
'declined': declined,
'spread': spread,
'max_spread': best,
'max_spread_pair': best_pair,
'max_spread_independent': best_ind,
'max_spread_independent_pair': best_ind_pair,
'families': {lab: ENGINE_FAMILIES[lab] for lab in ran},
'warnings': warns,
'certified': certified,
}
#-----------------------------------------------------------------------
# General functions for vacuum, standard matter, NSI, LIV
#-----------------------------------------------------------------------
[docs]
def osc_prob_vacuum(
num_flavors: int,
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
osc_params: Dict,
h_vac_energy_indep: Union[list, np.ndarray]=None,
average: Optional[bool]=False,
average_spread: Optional[float]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
return_evolution_operator: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Computes and returns neutrino oscillation probabilities for
oscillations in vacuum
Middle (scenario) layer for the vacuum case, generic in ``num_flavors``: unpacks
``osc_params``, builds the energy-independent vacuum Hamiltonian via
``hamiltonians.hamiltonian_{num_flavors}nu_vacuum_energy_independent``, and calls
:func:`osc_prob_energy_baseline`. Called by :func:`osc_prob_2nu_vacuum`,
:func:`osc_prob_3nu_vacuum`, :func:`osc_prob_4nu_vacuum`, and :func:`osc_prob_5nu_vacuum`.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator``.
Parameters
----------
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_vac_energy_indep`` is given).
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
osc_params : dict
Oscillation parameters; see :func:`unpack_oscillation_params_from_dict` for the required
keys for each ``num_flavors``.
h_vac_energy_indep : list or np.ndarray, optional
Precomputed energy-independent vacuum Hamiltonian, used instead of ``osc_params`` when
``num_flavors`` exceeds ``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
average : bool, optional
If True, return the phase-averaged probability rather than the oscillating one.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any parameter left as
None in ``osc_params``. Default: 'OSC_PARAMS_DEFAULT'.
t_slab_edges : list or np.ndarray, optional
Accepted for signature parity with the matter routes and **ignored**: a vacuum
Hamiltonian is constant in position, so every point is computed exactly with a single
slab and there is nothing left to refine.
magnus_exp_order : int
Accepted and **ignored**, as ``t_slab_edges``.
n_jobs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
integration_method : str
Accepted and **ignored**, as ``t_slab_edges``.
rtol : int or float, optional
Accepted and **ignored**, as ``t_slab_edges``.
atol : int or float, optional
Accepted and **ignored**, as ``t_slab_edges``.
growth_factor_n_slabs : int or float
Accepted and **ignored**, as ``t_slab_edges``.
growth_factor_n_tpts_per_slab : int or float
Accepted and **ignored**, as ``t_slab_edges``.
max_num_loops : int
Accepted and **ignored**, as ``t_slab_edges``.
min_n_slabs : int
Accepted and **ignored**, as ``t_slab_edges``.
max_n_slabs : int
Accepted and **ignored**, as ``t_slab_edges``.
min_n_tpts_per_slab : int
Accepted and **ignored**, as ``t_slab_edges``.
max_n_tpts_per_slab : int
Accepted and **ignored**, as ``t_slab_edges``.
validate_input : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
save_log : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
filename_log : str
Accepted and **ignored**; this route logs through ``save_log`` and ``file_log`` only,
so pass an already-open file object rather than a name.
file_log : TextIOWrapper, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
close_file_log_upon_exit : bool
Accepted and **ignored**; see ``filename_log``.
verbose : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`.
**The engine keywords travel this way and so appear in no signature above.** The
ones worth knowing are listed in :data:`magnus.oscprob.PASSTHROUGH_KWARGS_DOCUMENTED`; three of
them decide whether an answer on a hard profile is right at all:
``t_breakpoints``
Positions at which to place slab edges, filling in between. **This is the
parameter for a density jump, a kink or a shock front** -- no number of slabs
fixes a slab that straddles one. Not to be confused with ``t_slab_edges``
above, which is the *complete* set of edges and is rarely what is wanted.
``n_slabs``
An explicit slab count. On a single request over a large accumulated phase,
``rtol`` is not the dial that moves the answer; the refinement ladder can run
to its ceiling and warn, and the slab count is what to set instead.
``cumulative``
Reuse one propagation across a scan of baselines rather than re-propagating
for each. ``'auto'`` by default, which engages it on scans large enough to
benefit.
A misspelling is rejected here, naming the near match, rather than several hops
away by a function the caller never invoked.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone: ``U`` is the evolution
operator over the same interval, in the flavor basis, a complex square array with
``U[final, initial]`` the amplitude from the initial to the final state, so that
``P == abs(U)**2.T``. The refinement ladder then compares the operator itself
between levels, with the same ``rtol`` and ``atol``, so the returned operator is
converged in its own right (phases included) and not only in its moduli. Every
other setting keeps its meaning. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each (energy, L) point.
"""
# Unpack oscillation parameters from the osc_params dict, check if all values are available
# The function name is sys._getframe().f_code.co_name
osc_params_list = unpack_oscillation_params_from_dict(sys._getframe().f_code.co_name,
num_flavors, osc_params, h_vac_energy_indep)
if num_flavors == 2:
sth, Dm2 = osc_params_list
elif num_flavors == 3:
s12, s23, s13, dCP, D21, D31 = osc_params_list
elif num_flavors == 4:
s12, s23, s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41 = osc_params_list
elif num_flavors == 5:
s12, s23, s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51 = \
osc_params_list
if validate_input:
validate_input_battery(sys._getframe().f_code.co_name, energy=energy, L=L, L0=0.0,
num_flavors=num_flavors, nu_i=nu_i, nu_f=nu_f, osc_params=osc_params_list,
validate_energy_and_L=True, validate_flavor_indices=True, validate_osc_params=True,
validate_initial_position=False, validate_density=False)
# If any of the standard oscillation parameters has not been given a value, assign to it the
# value from the specified parameter set with name default_osc_params_set_name. Only the values
# of the parameters passed as None are assigned from the predefined set; others are not
# modified.
# Bounded above as well as below: past MAGNUS_MAX_PREDEFINED_NUM_FLAVORS there are no
# standard parameters to fill in -- the caller's h_vac_energy_indep is the Hamiltonian --
# and s12 and its neighbours were never assigned, so an unbounded test raised
# UnboundLocalError on the path the unpacking warning says is supported.
if 2 < num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS:
s12, s23, s13, dCP, D21, D31 = values_to_unspecified_osc_params(s12, s23, s13, dCP, D21,
D31, default_osc_params_set_name, verbose, angles=angles)
# Compute the energy-independent part of the vacuum Hamiltonian, i.e., everything but the 1/E
# prefactor, only once, to save time. Multiply by the 1/E factor later when calling osc_prob.
# If num_flavors > MAGNUS_MAX_PREDEFINED_NUM_FLAVORS, we use the h_vac_energy_indep that was
# passed to the function.
if num_flavors == 2:
h_vac_energy_indep = hamiltonians.hamiltonian_2nu_vacuum_energy_independent(sth, Dm2, angles=angles)
elif num_flavors == 3:
h_vac_energy_indep = hamiltonians.hamiltonian_3nu_vacuum_energy_independent(s12, s23,
s13, dCP, D21, D31, nubar=nubar, angles=angles)
elif num_flavors == 4:
h_vac_energy_indep = hamiltonians.hamiltonian_4nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41, nubar=nubar, angles=angles)
elif num_flavors == 5:
h_vac_energy_indep = hamiltonians.hamiltonian_5nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51,
nubar=nubar, angles=angles)
def htot(enu: Union[int, float]) -> np.ndarray:
return (1/enu)*h_vac_energy_indep
htot_is_function_only_of_energy = True
# Checked here, rather than only in osc_prob: the averaged path returns before anything
# forwards **kwargs onwards, so a check further down would see these keys on the ordinary
# path and silently ignore them on the averaged one.
_reject_parameter_set_metadata(kwargs, 'osc_prob_vacuum')
_check_passthrough_kwargs(kwargs, 'osc_prob_vacuum')
# Phase average, requested with average=True: closed-form whenever the
# Hamiltonian does not depend on position, so it is tried before any of the propagation
# machinery below, all of which would resolve phases that the average discards (see
# _avg_prob_dispatch and :mod:`magnus.avgprob`).
if return_evolution_operator:
# The phase-averaged and scan engines below answer with probabilities only, so the
# request goes straight to the ladder, which is the one engine that forms the operator.
_check_operator_request(average, None, 'osc_prob_vacuum')
return osc_prob_energy_baseline(htot, energy, L, 0.0, nu_i, nu_f,
htot_is_function_only_of_energy, n_jobs=n_jobs, validate_input=validate_input,
verbose=verbose, return_evolution_operator=True, **kwargs)
P_avg = _avg_prob_dispatch(htot, htot_is_function_only_of_energy, energy, L, 0.0, nu_i, nu_f,
average, 'osc_prob_vacuum', average_spread=average_spread)
if P_avg is not NotImplemented:
return P_avg
# Batched exact path. The Hamiltonian is position-independent -- which the loop below
# already relied on, one point at a time -- so the entire request is a single stacked
# exponential: U(E) = exp(-i h_vac(E) (L - 0)). Vacuum is the most trivially constant case
# there is, and it was taking the slowest route available: measured at 18-28 us per energy
# against ~1 us here. A constant (indeed zero) potential is what selects the 'constant'
# engine inside the dispatcher, and h_matt is None because there is no matter term at all.
P_scan = _osc_prob_scan_separable_dispatch(
h_vac_energy_indep, 0.0, None, None, None, energy, L, 0.0, nu_i, nu_f,
dict(t_slab_edges=None, n_jobs=n_jobs, save_log=save_log, file_log=file_log,
rtol=None, atol=None, cumulative=None, verbose=verbose, kwargs=kwargs))
if P_scan is not NotImplemented:
return P_scan
# Generate the probabilities for all pairs of energy and baseline in zip(energy, L). (The
# Hamiltonian is constant in position, so osc_prob computes each point exactly with a single
# slab; the tolerance and refinement parameters play no role and are not forwarded.)
return osc_prob_energy_baseline(htot, energy, L, 0.0, nu_i, nu_f,
htot_is_function_only_of_energy, n_jobs=n_jobs, validate_input=validate_input,
verbose=verbose, save_log=save_log, filename_log=filename_log, file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit, **kwargs)
[docs]
def osc_prob_matter_std_potential(
num_flavors: int,
rho_func: Union[Callable, int, float],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
osc_params: Dict,
L0: Optional[Union[int, float]]=0.0,
h_vac_energy_indep: Union[list, np.ndarray]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
average: Optional[bool]=False,
average_spread: Optional[float]=None,
strategy: Optional[str]='auto',
strategy_info: Optional[Dict]=None,
return_evolution_operator: Optional[bool]=False,
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
new_recursion_limit: Optional[int]=5000,
symmetric_over: Optional[tuple]=None,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Computes and returns neutrino oscillation probabilities for
standard oscillations in matter, i.e., the matter potential is only
due to the coherent forward scattering of nu_e on electrons.
Middle (scenario) layer for the standard-matter case, generic in ``num_flavors``: unpacks
``osc_params``, builds the vacuum + matter Hamiltonian (via
``hamiltonians.hamiltonian_{num_flavors}nu_vacuum_energy_independent`` and
:func:`magnus.matter.matter_potential_projector`, with the potential from
:func:`magnus.matter.vcc_func_from_rho_func`), and calls
:func:`osc_prob_energy_baseline`. Called by every
``osc_prob_{2,3,4,5}nu_matter_{constant,exp}_density`` and
``osc_prob_{2,3,4,5}nu_earth``/``osc_prob_{2,3,4,5}nu_sun`` wrapper.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator``.
Parameters
----------
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_vac_energy_indep`` is given).
rho_func : Callable, int, or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is
True), either as a function of position or as a constant.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
osc_params : dict
Oscillation parameters; see :func:`unpack_oscillation_params_from_dict`.
L0 : int or float, optional
Initial position. Default: 0.0.
h_vac_energy_indep : list or np.ndarray, optional
Precomputed energy-independent vacuum Hamiltonian, used instead of ``osc_params`` when
``num_flavors`` exceeds ``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
Ratio of the number of neutrons to protons in matter. Scales the sterile
states' entry in the matter term (see
:func:`magnus.matter.matter_potential_projector`); a callable is read as
:math:`r(l)`, a function of the same position ``rho_func`` takes, and makes
that entry -- and, when a matter density is being converted, the average
nucleon mass -- follow the local composition. This is how the Earth wrappers
feed their layered :math:`Y_e` through. A callable with structure away from
``t_breakpoints`` is subject to the same sampling limits as ``rho_func``.
Default: 1.0.
.. versionchanged:: 1.1.0
A callable is accepted; it used to have to be a scalar.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos (flips the sign of the matter
potential). Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
density_matter_is_in_g_per_cm3 : bool, optional
If True, ``rho_func`` returns the density in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, ``rho_func`` directly returns the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any parameter left as
None in ``osc_params``. Default: 'OSC_PARAMS_DEFAULT'.
average : bool, optional
If True, return the phase-averaged probability rather than the oscillating one. On a
smooth profile, the tighter of ``rtol`` and ``atol`` is its tolerance.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid',
or 'magnus'.
* ``'magnus'`` uses only the traditional Magnus-expansion machinery (the closed-form
two-flavor interaction-picture integrator when it applies, the energy-batched scan
engine, or the general adaptive slab-refinement method) -- this reproduces the exact
behavior of Magνs as it was before the adiabatic strategy was added,
unconditionally. It therefore also opts out of the cumulative baseline scan, which
postdates that behavior: pass ``strategy='magnus'`` to reproduce older numbers
exactly, on a baseline scan as well as at a single point.
* ``'hybrid'`` additionally tries :func:`magnus.adiabatic.hybrid_propagator` (adiabatic
transport, with a Magnus patch at any non-adiabatic window; see
:doc:`/adiabatic_strategy`) for any requested (energy, L) point where ``rho_func`` is
position-dependent and no ``t_slab_edges``/breakpoints are given, and a target
tolerance (``rtol``/``atol``) is requested. If it fails to self-certify for at least
one point, the best-effort result is still returned, together with
:class:`HybridCertificationWarning`.
* ``'auto'`` tries the hybrid strategy first, under the same conditions, but falls back
silently to the ``'magnus'`` strategies above (no warning about the hybrid attempt
itself) for any point where it does not apply or fails to self-certify. It also
stands aside for a **baseline scan at a single energy** of at least
``HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS`` points, which the cumulative scan
(see ``cumulative`` in :func:`osc_prob_energy_baseline`) answers from one traversal
instead of one hybrid call per point -- measured on solar profiles as tens of times
faster at equal or better accuracy. Ahead of the hybrid, it hands a request to the
Magnus ladder when the tolerance is no tighter than :data:`AUTO_LADDER_MIN_TOLERANCE`,
the estimated accumulated phase is at most :data:`AUTO_LADDER_MAX_PHASE`, and the
ladder can start well below its slab cap (:data:`AUTO_LADDER_MAX_FLOOR_FRACTION`):
there the hybrid is the slower route by one to two orders of magnitude, because its
window search costs the same at any tolerance. The ladder then runs at a tenth of the
requested ``rtol`` and ``atol`` (:data:`AUTO_LADDER_TOLERANCE_MARGIN`), without the
interaction-picture integrator, on slabs narrow enough from its first rung for the
Magnus series to converge. ``strategy_info`` reports the handoff as the hybrid
declining, with the reason ``'auto prefers the ladder'``; the hybrid's test for a
density jump nobody declared still runs, with its reason and its warning.
.. versionchanged:: 1.1.1
``'auto'`` hands a moderate phase at a loose tolerance to the ladder (issue #70).
The hybrid strategy is the natural tool exactly where the plain Magnus refinement needs
very many slabs (an extreme accumulated phase, e.g., low-energy solar neutrinos crossing
an MSW resonance), and applies to any number of flavors and to genuinely complex
Hamiltonians; see :doc:`/adiabatic_strategy` for the full derivation, validation, and
performance comparison. Default: 'auto'.
.. warning::
**If your profile has a feature much narrower than the trajectory, say where it is.**
The hybrid strategy locates resonances by sampling ``n_probe`` points (200, refined to
at most 6400), and the general Magnus path seeds its grid from an integral along the
path; neither can see a feature that falls between samples, and no refinement of
either finds it, because refinement never puts a point inside it. Measured on a
Gaussian resonance of width :math:`10^{-5}` of the trajectory, the returned
probability was wrong by **2.9e-02 against a requested 1e-3** -- on the hybrid path,
the general path, and the cumulative scan alike.
It is **no longer silent**: :func:`magnus.adiabatic.find_hidden_features` scans the
profile itself, once per call, and raises :class:`HiddenFeatureWarning` naming the
position and the ``t_breakpoints`` to pass. It reaches this class precisely because
it looks at the profile rather than at the answers, which is what no comparison
between engines can do when they are all wrong together. Detection is 68-90 % over
the unresolvable band with 0 false positives on 67 smooth profiles, so it is a
report -- not a guarantee, and not a cure.
Passing ``t_breakpoints`` at the feature fixes it, and is tested: with edges placed
by hand at the feature's own width the same case goes to 8.8e-04 at a single point
and 8.9e-04 over a 60-point scan, and with the set the warning itself prints it goes
to 1.0e-04 (verified end to end: warn, pass the printed edges back, re-run). It is
the right tool twice over, since an edge placed *on* a sharp feature also stops a
slab straddling it from degrading the quadrature. This is the one exposure the adversarial validation
(``docs/dev/FINDINGS_ADVERSARIAL_VALIDATION.md``) could not close in the library
itself: what a fixed grid never samples, it cannot report.
.. versionadded:: 1.0.0
strategy_info : dict, optional
If given, filled in place with which engine actually answered, following the same
out-parameter convention as ``convergence_info`` in :func:`osc_prob`. Under
``strategy='auto'`` the fallbacks are silent by design -- that is right for ordinary
calls, and wrong for anyone asking why a result moved or why a call got slow -- so this
is how to see them without turning the fallbacks into warnings. Keys:
* ``'engine'`` -- ``'hybrid'``, ``'ip_exp'``, ``'separable'``, ``'constant'``,
``'cumulative'``, ``'magnus'`` or ``'average'``.
* ``'family'`` -- the engine's family; see :data:`ENGINE_FAMILIES`.
* ``'certified'`` -- for ``'hybrid'``, whether
:func:`magnus.adiabatic.hybrid_propagator` self-certified. ``None`` for engines
that do not certify. Under ``'auto'`` an uncertified hybrid result is never
returned, so this is ``True`` whenever the engine is ``'hybrid'``; under
``'hybrid'`` it can be ``False``, and then it means the accuracy is **unverified**,
not that the answer is wrong.
* ``'declined'`` -- ``[(engine, reason)]`` for the engines that stood aside first.
Most requests decline most engines, which is ordinary and not a finding.
* ``'trace'`` -- every dispatch decision in order, with per-engine detail (for the
cumulative scan, ``'n_acc'`` and whether it came from a ceiling).
Costs nothing when omitted. Default: None.
.. versionadded:: 1.0.0
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone: ``U`` is the evolution
operator over the same interval, in the flavor basis, a complex square array with
``U[final, initial]`` the amplitude from the initial to the final state, so that
``P == abs(U)**2.T``. The refinement ladder then compares the operator itself
between levels, with the same ``rtol`` and ``atol``, so the returned operator is
converged in its own right (phases included) and not only in its moduli. Every
other setting keeps its meaning. Default: False.
t_slab_edges : list or np.ndarray, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
magnus_exp_order : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
n_jobs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
integration_method : str
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
rtol : int or float, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
atol : int or float, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
growth_factor_n_slabs : int or float
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
growth_factor_n_tpts_per_slab : int or float
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_num_loops : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
min_n_slabs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_n_slabs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
min_n_tpts_per_slab : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_n_tpts_per_slab : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
validate_input : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
save_log : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
filename_log : str
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
file_log : TextIOWrapper, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
close_file_log_upon_exit : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
verbose : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
new_recursion_limit : int, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`.
**The engine keywords travel this way and so appear in no signature above.** The
ones worth knowing are listed in :data:`magnus.oscprob.PASSTHROUGH_KWARGS_DOCUMENTED`; three of
them decide whether an answer on a hard profile is right at all:
``t_breakpoints``
Positions at which to place slab edges, filling in between. **This is the
parameter for a density jump, a kink or a shock front** -- no number of slabs
fixes a slab that straddles one. Not to be confused with ``t_slab_edges``
above, which is the *complete* set of edges and is rarely what is wanted.
``n_slabs``
An explicit slab count. On a single request over a large accumulated phase,
``rtol`` is not the dial that moves the answer; the refinement ladder can run
to its ceiling and warn, and the slab count is what to set instead.
``cumulative``
Reuse one propagation across a scan of baselines rather than re-propagating
for each. ``'auto'`` by default, which engages it on scans large enough to
benefit.
A misspelling is rejected here, naming the near match, rather than several hops
away by a function the caller never invoked.
symmetric_over : tuple, optional
Caller's declaration that ``A(t) == A(lo + hi - t)`` on ``(lo, hi)``, which lets
the Hamiltonian be evaluated on half the slabs. A declaration, not a test: it
is not checked. See :func:`magnus.magnus.magnus_expansion_multislab`.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each (energy, L) point.
"""
if return_evolution_operator:
_check_operator_request(average, strategy, 'osc_prob_matter_std_potential')
if validate_input and (strategy not in ('auto', 'hybrid', 'magnus')):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_matter_std_potential:" + \
" strategy must be 'auto', 'hybrid', or 'magnus'.")
# Unpack oscillation parameters from the osc_params dict, check if all values are available
# The function name is sys._getframe().f_code.co_name
osc_params_list = unpack_oscillation_params_from_dict(sys._getframe().f_code.co_name,
num_flavors, osc_params, h_vac_energy_indep)
if num_flavors == 2:
sth, Dm2 = osc_params_list
elif num_flavors == 3:
s12, s23, s13, dCP, D21, D31 = osc_params_list
elif num_flavors == 4:
s12, s23, s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41 = osc_params_list
elif num_flavors == 5:
s12, s23, s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51 = \
osc_params_list
if validate_input:
validate_input_battery(sys._getframe().f_code.co_name, energy=energy, L=L, L0=L0,
num_flavors=num_flavors, nu_i=nu_i, nu_f=nu_f, osc_params=osc_params_list,
rho_func=rho_func, ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction, validate_energy_and_L=True,
validate_flavor_indices=True, validate_osc_params=True, validate_initial_position=True,
validate_density=True)
# If any of the standard oscillation parameters has not been given a value, assign to it the
# value from the specified parameter set with name default_osc_params_set_name. Only the values
# of the parameters passed as None are assigned from the predefined set; others are not
# modified.
# Bounded above as well as below: past MAGNUS_MAX_PREDEFINED_NUM_FLAVORS there are no
# standard parameters to fill in -- the caller's h_vac_energy_indep is the Hamiltonian --
# and s12 and its neighbours were never assigned, so an unbounded test raised
# UnboundLocalError on the path the unpacking warning says is supported.
if 2 < num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS:
s12, s23, s13, dCP, D21, D31 = values_to_unspecified_osc_params(s12, s23, s13, dCP, D21,
D31, default_osc_params_set_name, verbose, angles=angles)
# Compute the energy-independent part of the vacuum Hamiltonian, i.e., everything but the 1/E
# prefactor, only once, to save time. Multiply by the 1/E factor later when calling osc_prob.
# If num_flavors > MAGNUS_MAX_PREDEFINED_NUM_FLAVORS, we use the h_vac_energy_indep that was
# passed to the function.
if num_flavors == 2:
h_vac_energy_indep = hamiltonians.hamiltonian_2nu_vacuum_energy_independent(sth, Dm2, angles=angles)
elif num_flavors == 3:
h_vac_energy_indep = hamiltonians.hamiltonian_3nu_vacuum_energy_independent(s12, s23,
s13, dCP, D21, D31, nubar=nubar, angles=angles)
elif num_flavors == 4:
h_vac_energy_indep = hamiltonians.hamiltonian_4nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41, nubar=nubar, angles=angles)
elif num_flavors == 5:
h_vac_energy_indep = hamiltonians.hamiltonian_5nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51,
nubar=nubar, angles=angles)
# Build the coherent forward potential function, VCC_func, from the density function, rho_func.
# If the provided rho_func is the matter density (e.g., g cm^{-3}), convert rho_func to a
# function that returns the electron number density [eV^3].
VCC_func = matter.vcc_func_from_rho_func(rho_func, L0, ratio_number_neutrons_to_protons,
electron_fraction, nubar, density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons) # [eV]
# Projector onto the nu_e--nu_e entry, multiplied below by the potential VCC. Note that
# VCC_func already carries the antineutrino sign flip (applied inside
# matter.vcc_func_from_rho_func), so no extra sign is applied here. [Previously, the sign was
# applied twice, which gave the antineutrino matter potential the wrong (positive) sign.]
# Sterile states do not share the actives' neutral-current potential, so beyond three
# flavors this is not e_ee; see matter.matter_potential_projector for the physics and
# for what omitting it cost. With a callable ratio (the Earth wrappers' default beyond
# three flavors) the projector comes back as a function of position, so the sterile
# entries follow the composition the way the density already does.
h_matt_proj = matter.matter_potential_projector(
num_flavors, ratio_number_neutrons_to_protons)
# Cache repeated evaluations of the potential on identical position grids (see
# _PositionProfileCache)
if callable(VCC_func):
VCC_func = _PositionProfileCache(VCC_func)
# A position-resolved projector is sampled on exactly the grids the potential is, so it
# gets the identical treatment.
if callable(h_matt_proj):
h_matt_proj = _PositionProfileCache(h_matt_proj)
# Matter Hamiltonian function: diagonal matrix with VCC in the top-left (ee) entry
if callable(h_matt_proj):
# The projector follows the composition, so the Hamiltonian depends on position
# regardless of whether the potential does (a constant VCC with a callable ratio is
# a legal, if unusual, direct call). Both factors are evaluated on the same l; for
# an array l the result is a stack with the position axis leading, as below.
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
vcc = np.asarray(VCC_func(l) if callable(VCC_func) else VCC_func)
return (1/enu)*h_vac_energy_indep + \
vcc[..., None, None]*np.asarray(h_matt_proj(l))
htot_is_function_only_of_energy = False
elif callable(VCC_func):
# VCC_func is a function of position, so the Hamiltonian is, too. If l is an array, the
# result is a stack of Hamiltonians with the position axis leading; this lets the Magnus
# routines evaluate the Hamiltonian at all time points in a single vectorized call.
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
vcc = np.asarray(VCC_func(l))
return (1/enu)*h_vac_energy_indep + vcc[..., None, None]*h_matt_proj
htot_is_function_only_of_energy = False
else:
# VCC_func is a constant in position, so the Hamiltonian is, too. When VCC_func is passed to
# osc_prob below, osc_prob will detect that VCC_func is constant and set parameters
# internally for speed-up.
h_matt = VCC_func*h_matt_proj
def htot(enu: Union[int, float]) -> np.ndarray:
return (1/enu)*h_vac_energy_indep+h_matt
htot_is_function_only_of_energy = True
# Resolved -- and, crucially, POPPED out of kwargs -- before scan_kwargs is built, because
# every dispatcher below declines outright on an unrecognized entry in kwargs. Left in, an
# explicitly-passed cumulative silently disabled the hybrid, interaction-picture and
# separable engines: passing cumulative='auto', which is the documented default, changed
# which engine answered (hybrid -> general ladder) and moved a 10 MeV solar single point by
# 9.3e-06. See _resolve_cumulative_kwarg.
cumulative_resolved = _resolve_cumulative_kwarg(kwargs, strategy)
scan_kwargs = dict(t_slab_edges=t_slab_edges, verbose=verbose, magnus_exp_order=magnus_exp_order, n_jobs=n_jobs,
integration_method=integration_method, rtol=rtol, atol=atol,
growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
save_log=save_log, file_log=file_log, cumulative=cumulative_resolved,
kwargs=kwargs)
# Checked here, rather than only in osc_prob: the averaged path returns before anything
# forwards **kwargs onwards, so a check further down would see these keys on the ordinary
# path and silently ignore them on the averaged one.
_reject_parameter_set_metadata(kwargs, 'osc_prob_matter_std_potential')
_check_passthrough_kwargs(kwargs, 'osc_prob_matter_std_potential')
# Phase average, requested with average=True: closed-form whenever the
# Hamiltonian does not depend on position, so it is tried before any of the propagation
# machinery below, all of which would resolve phases that the average discards (see
# _avg_prob_dispatch and :mod:`magnus.avgprob`).
# `or` on an array would ask for its truth value, so the emptiness test is explicit.
_breakpoints = kwargs.get('t_breakpoints')
_profile_is_smooth = ((_breakpoints is None or len(np.atleast_1d(_breakpoints)) == 0)
and (t_slab_edges is None))
# One scan of the profile for the whole call, before any engine sees it: a feature
# narrower than every grid here is the one exposure none of them can detect for itself,
# because they all miss it together. Depends on the profile and the interval, never on
# energy, so it is not repeated per point. See _scan_for_hidden_features.
_hidden = _scan_for_hidden_features(VCC_func, L0, L, kwargs.get('t_breakpoints'))
# How coarsely does this request sample the oscillation it computes? Reported, never
# warned: see _sampling_report. Computed ONLY when the caller passes strategy_info, so the
# default path pays nothing for it.
_osc = (_sampling_report(htot, htot_is_function_only_of_energy, energy, L, L0)
if strategy_info is not None else None)
# Everything below is dispatch: which engine gets the request. Watched as a unit so
# that strategy_info can report which one answered and, for the hybrid strategy,
# whether it certified -- see _engine_probe. Costs one list allocation per call when
# nobody is watching.
with _engine_probe(disabled=_OPERATOR_ONLY_FROM_LADDER if return_evolution_operator else (),
info=strategy_info, extra={'hidden_feature': _hidden, 'sampling': _osc}):
P_avg = _avg_prob_dispatch(htot, htot_is_function_only_of_energy, energy, L, L0, nu_i, nu_f,
average, 'osc_prob_matter_std_potential', smooth_profile=_profile_is_smooth, engine_kwargs=scan_kwargs,
average_spread=average_spread)
if P_avg is not NotImplemented:
return P_avg
# Hybrid strategy (adiabatic transport + Magnus patch at any non-adiabatic window; see
# _osc_prob_hybrid_dispatch and :doc:`/adiabatic_strategy`): broader than the interaction-
# picture fast path below (any number of flavors, any smooth position-dependent profile), and
# tried first unless strategy == 'magnus'. Falls back transparently (returns NotImplemented)
# if it does not apply or (with strategy == 'auto' only) fails to self-certify.
#
# Tried *before* the fast path because that is what strategy='auto' has always been
# documented to mean ("tries the hybrid strategy first ... but falls back silently to the
# 'magnus' strategies", of which the interaction-picture integrator is one) -- and because
# measurement says the documented order is also the better one. On solar configurations,
# across 50 (energy, baseline) points spanning the standard, NSI and LIV families and
# 0.5-100 MeV, scored against solve_ivp/DOP853: the hybrid strategy certified 50/50 with a
# worst error of 1.8e-04 against a requested 1e-3 and no warnings, while the fast path
# certified 22/50 and took a mean of 13.2 s to decline the other 28. Where both answer,
# hybrid is 28-594x faster (median 397x). The fast path is more accurate only at 40-100 MeV
# and only at the default tolerance -- at rtol/atol <= 1e-5 it declines outright at every
# energy measured. See docs/dev/DECISION_DISPATCH_ORDER.md.
P_hybrid = _osc_prob_hybrid_dispatch(h_vac_energy_indep, VCC_func, h_matt_proj, None, None,
energy, L, L0, nu_i, nu_f, scan_kwargs, strategy)
# strategy='auto' handed the request to the ladder (issue #70): the engines below run at a
# tenth of the tolerance, and the interaction-picture fast path is skipped.
prefer_ladder = isinstance(P_hybrid, _PreferLadder)
if prefer_ladder:
rtol, atol, min_n_slabs = P_hybrid.request(rtol, atol, min_n_slabs, max_n_slabs,
integration_method)
scan_kwargs = dict(scan_kwargs, rtol=rtol, atol=atol, min_n_slabs=min_n_slabs)
elif P_hybrid is not NotImplemented:
return P_hybrid
# Fast path for a genuine exponential density profile (e.g., the Sun): factor out the
# (possibly huge, at low energy) fast vacuum phase analytically in the interaction picture,
# instead of resolving it slab by slab (see _osc_prob_ip_exp_dispatch). Applies to a single
# (energy, L) point as well as to a scan, and falls back transparently (returns
# NotImplemented) if the profile is not exponential or if it fails to converge (e.g., near an
# MSW resonance), in which case the general methods below are used instead. Reached only
# where the hybrid strategy declined, or with strategy == 'magnus'.
P_ip = NotImplemented if prefer_ladder else _osc_prob_ip_exp_dispatch(
h_vac_energy_indep, VCC_func, h_matt_proj, None, None,
energy, L, L0, nu_i, nu_f, scan_kwargs)
if P_ip is not NotImplemented:
return P_ip
# Energy-batched fast path: when many energies share a single baseline and the Hamiltonian
# is position-dependent, compute the whole scan in one batched pipeline, with the potential
# samples shared across energies (see _osc_prob_scan_separable). If the request does not fit
# the engine, fall back to the generic per-point path below.
P_scan = _osc_prob_scan_separable_dispatch(h_vac_energy_indep, VCC_func, h_matt_proj, None, None,
energy, L, L0, nu_i, nu_f, scan_kwargs)
if P_scan is not NotImplemented:
return P_scan
# Generate the probabilities for all pairs of energy and baseline in zip(energy, L).
return osc_prob_energy_baseline(htot, energy, L, L0, nu_i, nu_f,
htot_is_function_only_of_energy, t_slab_edges=t_slab_edges,
return_evolution_operator=return_evolution_operator,
magnus_exp_order=magnus_exp_order, n_jobs=n_jobs, integration_method=integration_method,
rtol=rtol, atol=atol, growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
validate_input=validate_input, save_log=save_log, filename_log=filename_log,
file_log=file_log, close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=new_recursion_limit, verbose=verbose,
# An explicit cumulative= from the caller wins; otherwise strategy='magnus' opts out
# of the cumulative scan and everything else takes 'auto'. Resolved near the top of
# this function, which is also where it is removed from kwargs.
cumulative=cumulative_resolved, symmetric_over=symmetric_over, **kwargs)
def _standard_plus_constant(proj: Union[np.ndarray, Callable],
h_extra: np.ndarray) -> Union[np.ndarray, Callable]:
r"""The (standard + extra) matter matrix, as a constant or as a function of position.
``proj`` is :func:`magnus.matter.matter_potential_projector`'s output: a constant
matrix or, for a callable neutron-to-proton ratio, a callable of position. The extra
couplings (the NSI eps matrix) are constant either way, so the sum is a plain matrix
sum in the first case and a closure evaluating the projector per position in the
second -- which keeps the constant case's arithmetic, and therefore its results,
exactly what they were.
.. versionadded:: 1.1.0
Parameters
----------
proj : np.ndarray or Callable
The standard matter projector, constant or position-resolved.
h_extra : np.ndarray
Constant matrix added to it (e.g., the NSI couplings at VCC = 1).
Returns
-------
np.ndarray or Callable
``proj + h_extra``, in whichever of the two forms ``proj`` has.
"""
if not callable(proj):
return proj + h_extra
def h_matt_of_l(l: Union[int, float, np.ndarray]) -> np.ndarray:
return proj(l) + h_extra
return h_matt_of_l
[docs]
def osc_prob_matter_nsi(
num_flavors: int,
rho_func: Union[Callable, int, float],
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
osc_params: Dict,
nsi_params: Dict,
L0: Optional[Union[int, float]]=0.0,
h_vac_energy_indep: Union[list, np.ndarray]=None,
h_nsi: Union[list, np.ndarray]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
average: Optional[bool]=False,
average_spread: Optional[float]=None,
strategy: Optional[str]='auto',
strategy_info: Optional[Dict]=None,
return_evolution_operator: Optional[bool]=False,
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
new_recursion_limit: Optional[int]=5000,
symmetric_over: Optional[tuple]=None,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Computes and returns neutrino oscillation probabilities for
oscillations in matter with non-standard interactions (NSI), i.e., the matter potential
includes both the standard coherent-forward-scattering term and the NSI epsilon couplings.
Middle (scenario) layer for the NSI case, generic in ``num_flavors``: unpacks ``osc_params``
and ``nsi_params``, builds the vacuum + matter + NSI Hamiltonian (via
``hamiltonians.hamiltonian_{num_flavors}nu_vacuum_energy_independent``,
:func:`magnus.matter.matter_potential_projector`, and
``hamiltonian_{num_flavors}nu_nsi``, with
the potential from :func:`magnus.matter.vcc_func_from_rho_func`), and calls
:func:`osc_prob_energy_baseline`. Called by every
``osc_prob_{2,3,4,5}nu_matter_nsi_{constant,exp}_density`` and
``osc_prob_{2,3,4,5}nu_earth_nsi``/``osc_prob_{2,3,4,5}nu_sun_nsi`` wrapper.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator``.
Parameters
----------
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_vac_energy_indep``/``h_nsi``
are given).
rho_func : Callable, int, or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is
True), either as a function of position or as a constant.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
osc_params : dict
Oscillation parameters; see :func:`unpack_oscillation_params_from_dict`.
nsi_params : dict
NSI epsilon parameters; see :func:`unpack_nsi_params_from_dict`.
L0 : int or float, optional
Initial position. Default: 0.0.
h_vac_energy_indep : list or np.ndarray, optional
Precomputed energy-independent vacuum Hamiltonian, used instead of ``osc_params`` when
``num_flavors`` exceeds ``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
h_nsi : list or np.ndarray, optional
Precomputed NSI Hamiltonian, used instead of ``nsi_params`` when ``num_flavors`` exceeds
``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
Ratio of the number of neutrons to protons in matter. Scales the sterile
states' entry in the matter term (see
:func:`magnus.matter.matter_potential_projector`); a callable is read as
:math:`r(l)`, a function of the same position ``rho_func`` takes, and makes
that entry -- and, when a matter density is being converted, the average
nucleon mass -- follow the local composition. This is how the Earth wrappers
feed their layered :math:`Y_e` through. A callable with structure away from
``t_breakpoints`` is subject to the same sampling limits as ``rho_func``.
Default: 1.0.
.. versionchanged:: 1.1.0
A callable is accepted; it used to have to be a scalar.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos (flips the sign of the matter
potential). Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
density_matter_is_in_g_per_cm3 : bool, optional
If True, ``rho_func`` returns the density in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, ``rho_func`` directly returns the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any parameter left as
None in ``osc_params``. Default: 'OSC_PARAMS_DEFAULT'.
average : bool, optional
If True, return the phase-averaged probability rather than the oscillating one. On a
smooth profile, the tighter of ``rtol`` and ``atol`` is its tolerance.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid',
or 'magnus'; see the ``strategy`` parameter of :func:`osc_prob_matter_std_potential` for
the full description and :doc:`/adiabatic_strategy` for the derivation and validation of
the ``'hybrid'``/``'auto'`` strategies (adiabatic transport with a Magnus patch at any
non-adiabatic window, applicable to any number of flavors). Default: 'auto'.
.. versionadded:: 1.0.0
strategy_info : dict, optional
If given, filled in place with which engine actually answered, following the same
out-parameter convention as ``convergence_info`` in :func:`osc_prob`. Under
``strategy='auto'`` the fallbacks are silent by design -- that is right for ordinary
calls, and wrong for anyone asking why a result moved or why a call got slow -- so this
is how to see them without turning the fallbacks into warnings. Keys:
* ``'engine'`` -- ``'hybrid'``, ``'ip_exp'``, ``'separable'``, ``'constant'``,
``'cumulative'``, ``'magnus'`` or ``'average'``.
* ``'family'`` -- the engine's family; see :data:`ENGINE_FAMILIES`.
* ``'certified'`` -- for ``'hybrid'``, whether
:func:`magnus.adiabatic.hybrid_propagator` self-certified. ``None`` for engines
that do not certify. Under ``'auto'`` an uncertified hybrid result is never
returned, so this is ``True`` whenever the engine is ``'hybrid'``; under
``'hybrid'`` it can be ``False``, and then it means the accuracy is **unverified**,
not that the answer is wrong.
* ``'declined'`` -- ``[(engine, reason)]`` for the engines that stood aside first.
Most requests decline most engines, which is ordinary and not a finding.
* ``'trace'`` -- every dispatch decision in order, with per-engine detail (for the
cumulative scan, ``'n_acc'`` and whether it came from a ceiling).
Costs nothing when omitted. Default: None.
.. versionadded:: 1.0.0
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone: ``U`` is the evolution
operator over the same interval, in the flavor basis, a complex square array with
``U[final, initial]`` the amplitude from the initial to the final state, so that
``P == abs(U)**2.T``. The refinement ladder then compares the operator itself
between levels, with the same ``rtol`` and ``atol``, so the returned operator is
converged in its own right (phases included) and not only in its moduli. Every
other setting keeps its meaning. Default: False.
t_slab_edges : list or np.ndarray, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
magnus_exp_order : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
n_jobs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
integration_method : str
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
rtol : int or float, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
atol : int or float, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
growth_factor_n_slabs : int or float
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
growth_factor_n_tpts_per_slab : int or float
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_num_loops : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
min_n_slabs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_n_slabs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
min_n_tpts_per_slab : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_n_tpts_per_slab : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
validate_input : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
save_log : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
filename_log : str
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
file_log : TextIOWrapper, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
close_file_log_upon_exit : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
verbose : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
new_recursion_limit : int, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`.
**The engine keywords travel this way and so appear in no signature above.** The
ones worth knowing are listed in :data:`magnus.oscprob.PASSTHROUGH_KWARGS_DOCUMENTED`; three of
them decide whether an answer on a hard profile is right at all:
``t_breakpoints``
Positions at which to place slab edges, filling in between. **This is the
parameter for a density jump, a kink or a shock front** -- no number of slabs
fixes a slab that straddles one. Not to be confused with ``t_slab_edges``
above, which is the *complete* set of edges and is rarely what is wanted.
``n_slabs``
An explicit slab count. On a single request over a large accumulated phase,
``rtol`` is not the dial that moves the answer; the refinement ladder can run
to its ceiling and warn, and the slab count is what to set instead.
``cumulative``
Reuse one propagation across a scan of baselines rather than re-propagating
for each. ``'auto'`` by default, which engages it on scans large enough to
benefit.
A misspelling is rejected here, naming the near match, rather than several hops
away by a function the caller never invoked.
symmetric_over : tuple, optional
Caller's declaration that ``A(t) == A(lo + hi - t)`` on ``(lo, hi)``, which lets
the Hamiltonian be evaluated on half the slabs. A declaration, not a test: it
is not checked. See :func:`magnus.magnus.magnus_expansion_multislab`.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each (energy, L) point.
"""
if return_evolution_operator:
_check_operator_request(average, strategy, 'osc_prob_matter_nsi')
if validate_input and (strategy not in ('auto', 'hybrid', 'magnus')):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_matter_nsi: strategy" + \
" must be 'auto', 'hybrid', or 'magnus'.")
# Unpack oscillation parameters from the osc_params dict, check if all values are available
# The function name is sys._getframe().f_code.co_name
osc_params_list = unpack_oscillation_params_from_dict(sys._getframe().f_code.co_name,
num_flavors, osc_params, h_vac_energy_indep)
nsi_params_list = unpack_nsi_params_from_dict(sys._getframe().f_code.co_name,
num_flavors, nsi_params, h_nsi)
if num_flavors == 2:
sth, Dm2 = osc_params_list
eps_aa, eps_ab = nsi_params_list
elif num_flavors == 3:
s12, s23, s13, dCP, D21, D31 = osc_params_list
eps_ee, eps_em, eps_et, eps_mm, eps_mt, eps_tt = nsi_params_list
elif num_flavors == 4:
s12, s23, s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41 = osc_params_list
eps_ee, eps_em, eps_et, eps_es, eps_mm, eps_mt, eps_ms, eps_tt, eps_ts, eps_ss = \
nsi_params_list
elif num_flavors == 5:
s12, s23, s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51 = \
osc_params_list
eps_ee, eps_em, eps_et, eps_es1, eps_es2, eps_mm, eps_mt, eps_ms1, eps_ms2, eps_tt, \
eps_ts1, eps_ts2, eps_s1s1, eps_s1s2, eps_s2s2 = nsi_params_list
if validate_input:
validate_input_battery(sys._getframe().f_code.co_name, energy=energy, L=L, L0=L0,
num_flavors=num_flavors, nu_i=nu_i, nu_f=nu_f, osc_params=osc_params_list,
rho_func=rho_func, ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction, validate_energy_and_L=True,
validate_flavor_indices=True, validate_osc_params=True, validate_initial_position=True,
validate_density=True)
# If any of the standard oscillation parameters has not been given a value, assign to it the
# value from the specified parameter set with name default_osc_params_set_name. Only the values
# of the parameters passed as None are assigned from the predefined set; others are not
# modified.
# Bounded above as well as below: past MAGNUS_MAX_PREDEFINED_NUM_FLAVORS there are no
# standard parameters to fill in -- the caller's h_vac_energy_indep is the Hamiltonian --
# and s12 and its neighbours were never assigned, so an unbounded test raised
# UnboundLocalError on the path the unpacking warning says is supported.
if 2 < num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS:
s12, s23, s13, dCP, D21, D31 = values_to_unspecified_osc_params(s12, s23, s13, dCP, D21,
D31, default_osc_params_set_name, verbose, angles=angles)
# Compute the energy-independent part of the vacuum Hamiltonian, i.e., everything but the 1/E
# prefactor, only once, to save time. Multiply by the 1/E factor later when calling osc_prob.
# If num_flavors > MAGNUS_MAX_PREDEFINED_NUM_FLAVORS, we use the h_vac_energy_indep that was
# passed to the function.
if num_flavors == 2:
h_vac_energy_indep = hamiltonians.hamiltonian_2nu_vacuum_energy_independent(sth, Dm2, angles=angles)
elif num_flavors == 3:
h_vac_energy_indep = hamiltonians.hamiltonian_3nu_vacuum_energy_independent(s12, s23,
s13, dCP, D21, D31, nubar=nubar, angles=angles)
elif num_flavors == 4:
h_vac_energy_indep = hamiltonians.hamiltonian_4nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41, nubar=nubar, angles=angles)
elif num_flavors == 5:
h_vac_energy_indep = hamiltonians.hamiltonian_5nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51,
nubar=nubar, angles=angles)
# Compute the standard + NSI matter Hamiltonian *without* the multiplicative prefactor of VCC.
# To do this we call the functions hamiltonians_Xnu_nsi(VCC, ...) with VCC = 1.0. We add the
# standard matter contribution to the NSI matter contribution by adding 1.0 to the eps_ee entry.
# The overall antineutrino sign flip is carried by VCC_func (see
# matter.vcc_func_from_rho_func); for antineutrinos, the NSI couplings are additionally
# conjugated (H_matt -> -H_matt^* relative to neutrinos).
# `matter.matter_potential_projector` for the standard piece, NOT a hand-written
# diagonal: beyond three flavors the standard matter term is not e_ee. The sterile
# states carry -V_NC = (r/2) V_CC once the actives' common V_NC is removed, and a
# literal [1, 0, 0, 0] gives them zero instead -- which is the same omission that was
# found and fixed in the standard-potential path, and it survived here. With every
# eps set to zero this route has to reproduce that path exactly, and did not: it
# differed by 5.2e-02 at four flavors and 5.1e-02 at five.
if num_flavors == 2:
h_matt = matter.matter_potential_projector(2) + \
hamiltonians.hamiltonian_2nu_nsi(1.0, eps_aa, eps_ab) # VCC = 1.0
elif num_flavors == 3:
h_matt = matter.matter_potential_projector(3) + \
hamiltonians.hamiltonian_3nu_nsi(1.0, eps_ee, eps_em, eps_et, eps_mm, eps_mt, eps_tt)
elif num_flavors == 4:
# Combined via _standard_plus_constant rather than "+": with a callable ratio the
# standard projector is position-resolved while the eps couplings stay constant,
# so the sum becomes a closure over position.
h_matt = _standard_plus_constant(
matter.matter_potential_projector(4, ratio_number_neutrons_to_protons),
hamiltonians.hamiltonian_4nu_nsi(1.0, eps_ee, eps_em, eps_et, eps_es, eps_mm, eps_mt,
eps_ms, eps_tt, eps_ts, eps_ss))
elif num_flavors == 5:
h_matt = _standard_plus_constant(
matter.matter_potential_projector(5, ratio_number_neutrons_to_protons),
hamiltonians.hamiltonian_5nu_nsi(1.0, eps_ee, eps_em, eps_et, eps_es1, eps_es2,
eps_mm, eps_mt, eps_ms1, eps_ms2, eps_tt, eps_ts1, eps_ts2, eps_s1s1, eps_s1s2,
eps_s2s2))
if nubar:
if callable(h_matt):
# Conjugation distributes over the sum and the projector part is real, so
# conjugating the evaluated matrix per position states exactly what the
# constant branch states.
_h_matt_nu = h_matt
def h_matt(l):
return np.conj(_h_matt_nu(l))
else:
h_matt = np.conj(h_matt)
# Build the coherent forward potential function, VCC_func, from the density function, rho_func.
# If the provided rho_func is the matter density (e.g., g cm^{-3}), convert rho_func to a
# function that returns the electron number density [eV^3].
VCC_func = matter.vcc_func_from_rho_func(rho_func, L0, ratio_number_neutrons_to_protons,
electron_fraction, nubar, density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons) # [eV]
# Cache repeated evaluations of the potential on identical position grids (see
# _PositionProfileCache)
if callable(VCC_func):
VCC_func = _PositionProfileCache(VCC_func)
# A position-resolved matter matrix is sampled on exactly the grids the potential is,
# so it gets the identical treatment.
if callable(h_matt):
h_matt = _PositionProfileCache(h_matt)
# Matter Hamiltonian function: (standard + NSI) matter matrix scaled by VCC
if callable(h_matt):
# The standard projector inside h_matt follows the composition, so the Hamiltonian
# depends on position regardless of whether the potential does. Both factors are
# evaluated on the same l; for an array l the result is a stack with the position
# axis leading, as below.
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
vcc = np.asarray(VCC_func(l) if callable(VCC_func) else VCC_func)
return (1/enu)*h_vac_energy_indep + \
vcc[..., None, None]*np.asarray(h_matt(l))
htot_is_function_only_of_energy = False
elif callable(VCC_func):
# VCC_func is a function of position, so the Hamiltonian is, too. If l is an array, the
# result is a stack of Hamiltonians with the position axis leading; this lets the Magnus
# routines evaluate the Hamiltonian at all time points in a single vectorized call.
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
vcc = np.asarray(VCC_func(l))
return (1/enu)*h_vac_energy_indep + vcc[..., None, None]*h_matt
htot_is_function_only_of_energy = False
else:
# VCC_func is a constant in position, so the Hamiltonian is, too. When VCC_func is passed to
# osc_prob below, osc_prob will detect that VCC_func is constant and set parameters
# internally for speed-up.
#
# Bound to a NEW name rather than rebinding h_matt: every dispatcher below is documented
# to take h_matt as "the constant matrix multiplying VCC_func(l)", and folding VCC into
# that name broke the contract for whichever dispatcher went on to multiply by VCC
# itself -- silently, since VCC^2 is both tiny and sign-independent, so the matter term
# all but vanished and the antineutrino sign canceled with it. The separable engine
# never noticed because it declined the constant case that reaches this line.
h_matt_scaled = VCC_func*h_matt
def htot(enu: Union[int, float]) -> np.ndarray:
return (1/enu)*h_vac_energy_indep+h_matt_scaled
htot_is_function_only_of_energy = True
# Resolved -- and, crucially, POPPED out of kwargs -- before scan_kwargs is built, because
# every dispatcher below declines outright on an unrecognized entry in kwargs. Left in, an
# explicitly-passed cumulative silently disabled the hybrid, interaction-picture and
# separable engines: passing cumulative='auto', which is the documented default, changed
# which engine answered (hybrid -> general ladder) and moved a 10 MeV solar single point by
# 9.3e-06. See _resolve_cumulative_kwarg.
cumulative_resolved = _resolve_cumulative_kwarg(kwargs, strategy)
scan_kwargs = dict(t_slab_edges=t_slab_edges, verbose=verbose, magnus_exp_order=magnus_exp_order, n_jobs=n_jobs,
integration_method=integration_method, rtol=rtol, atol=atol,
growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
save_log=save_log, file_log=file_log, cumulative=cumulative_resolved,
kwargs=kwargs)
# Checked here, rather than only in osc_prob: the averaged path returns before anything
# forwards **kwargs onwards, so a check further down would see these keys on the ordinary
# path and silently ignore them on the averaged one.
_reject_parameter_set_metadata(kwargs, 'osc_prob_matter_nsi')
_check_passthrough_kwargs(kwargs, 'osc_prob_matter_nsi')
# Phase average, requested with average=True: closed-form whenever the
# Hamiltonian does not depend on position, so it is tried before any of the propagation
# machinery below, all of which would resolve phases that the average discards (see
# _avg_prob_dispatch and :mod:`magnus.avgprob`).
# `or` on an array would ask for its truth value, so the emptiness test is explicit.
_breakpoints = kwargs.get('t_breakpoints')
_profile_is_smooth = ((_breakpoints is None or len(np.atleast_1d(_breakpoints)) == 0)
and (t_slab_edges is None))
# One scan of the profile for the whole call, before any engine sees it: a feature
# narrower than every grid here is the one exposure none of them can detect for itself,
# because they all miss it together. Depends on the profile and the interval, never on
# energy, so it is not repeated per point. See _scan_for_hidden_features.
_hidden = _scan_for_hidden_features(VCC_func, L0, L, kwargs.get('t_breakpoints'))
# How coarsely does this request sample the oscillation it computes? Reported, never
# warned: see _sampling_report. Computed ONLY when the caller passes strategy_info, so the
# default path pays nothing for it.
_osc = (_sampling_report(htot, htot_is_function_only_of_energy, energy, L, L0)
if strategy_info is not None else None)
# Everything below is dispatch: which engine gets the request. Watched as a unit so
# that strategy_info can report which one answered and, for the hybrid strategy,
# whether it certified -- see _engine_probe. Costs one list allocation per call when
# nobody is watching.
with _engine_probe(disabled=_OPERATOR_ONLY_FROM_LADDER if return_evolution_operator else (),
info=strategy_info, extra={'hidden_feature': _hidden, 'sampling': _osc}):
P_avg = _avg_prob_dispatch(htot, htot_is_function_only_of_energy, energy, L, L0, nu_i, nu_f,
average, 'osc_prob_matter_nsi', smooth_profile=_profile_is_smooth, engine_kwargs=scan_kwargs,
average_spread=average_spread)
if P_avg is not NotImplemented:
return P_avg
# Hybrid strategy, tried first: see _osc_prob_hybrid_dispatch and the matching comment in
# osc_prob_matter_std_potential for why it precedes the interaction-picture fast path.
P_hybrid = _osc_prob_hybrid_dispatch(h_vac_energy_indep, VCC_func, h_matt, None, None,
energy, L, L0, nu_i, nu_f, scan_kwargs, strategy)
# See the matching comment in osc_prob_matter_std_potential (issue #70).
prefer_ladder = isinstance(P_hybrid, _PreferLadder)
if prefer_ladder:
rtol, atol, min_n_slabs = P_hybrid.request(rtol, atol, min_n_slabs, max_n_slabs,
integration_method)
scan_kwargs = dict(scan_kwargs, rtol=rtol, atol=atol, min_n_slabs=min_n_slabs)
elif P_hybrid is not NotImplemented:
return P_hybrid
# Fast path for a genuine exponential density profile (e.g., the Sun), reached only where the
# hybrid strategy declined: see _osc_prob_ip_exp_dispatch and the matching comment in
# osc_prob_matter_std_potential.
P_ip = NotImplemented if prefer_ladder else _osc_prob_ip_exp_dispatch(
h_vac_energy_indep, VCC_func, h_matt, None, None,
energy, L, L0, nu_i, nu_f, scan_kwargs)
if P_ip is not NotImplemented:
return P_ip
# Energy-batched fast path: when many energies share a single baseline and the Hamiltonian
# is position-dependent, compute the whole scan in one batched pipeline, with the potential
# samples shared across energies (see _osc_prob_scan_separable). If the request does not fit
# the engine, fall back to the generic per-point path below.
P_scan = _osc_prob_scan_separable_dispatch(h_vac_energy_indep, VCC_func, h_matt, None, None,
energy, L, L0, nu_i, nu_f, scan_kwargs)
if P_scan is not NotImplemented:
return P_scan
# Generate the probabilities for all pairs of energy and baseline in zip(energy, L).
return osc_prob_energy_baseline(htot, energy, L, L0, nu_i, nu_f,
htot_is_function_only_of_energy, t_slab_edges=t_slab_edges,
return_evolution_operator=return_evolution_operator,
magnus_exp_order=magnus_exp_order, n_jobs=n_jobs, integration_method=integration_method,
rtol=rtol, atol=atol, growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
validate_input=validate_input, save_log=save_log, filename_log=filename_log,
file_log=file_log, close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=new_recursion_limit, verbose=verbose,
# An explicit cumulative= from the caller wins; otherwise strategy='magnus' opts out
# of the cumulative scan and everything else takes 'auto'. Resolved near the top of
# this function, which is also where it is removed from kwargs.
cumulative=cumulative_resolved, symmetric_over=symmetric_over, **kwargs)
[docs]
def osc_prob_liv(
num_flavors: int,
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
osc_params: Dict,
liv_params: Dict,
rho_func: Optional[Union[Callable, int, float]]=0.0,
L0: Optional[Union[int, float]]=0.0,
h_vac_energy_indep: Union[list, np.ndarray]=None,
h_liv_energy_indep: Union[list, np.ndarray]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
average: Optional[bool]=False,
average_spread: Optional[float]=None,
strategy: Optional[str]='auto',
strategy_info: Optional[Dict]=None,
return_evolution_operator: Optional[bool]=False,
t_slab_edges: Optional[Union[list, np.ndarray]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
growth_factor_n_slabs: Optional[Union[int, float]]=1.5,
growth_factor_n_tpts_per_slab: Optional[Union[int, float]]=1.5,
max_num_loops: Optional[int]=50,
min_n_slabs: Optional[int]=1,
max_n_slabs: Optional[int]=None,
min_n_tpts_per_slab: Optional[int]=2,
max_n_tpts_per_slab: Optional[int]=500,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
new_recursion_limit: Optional[int]=5000,
symmetric_over: Optional[tuple]=None,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Computes and returns neutrino oscillation probabilities for
oscillations under (one form of) Lorentz-invariance violation, in
vacuum or in matter.
Middle (scenario) layer for the LIV case, generic in ``num_flavors``: unpacks ``osc_params``
and ``liv_params``, builds the vacuum (+ matter, if ``rho_func`` is nonzero) + LIV
Hamiltonian (via ``hamiltonians.hamiltonian_{num_flavors}nu_vacuum_energy_independent``,
optionally :func:`magnus.matter.matter_potential_projector`, and
``hamiltonian_{num_flavors}nu_liv_energy_independent``), and calls
:func:`osc_prob_energy_baseline`. Called by every ``osc_prob_{2,3,4,5}nu_vacuum_liv``,
``osc_prob_{2,3,4,5}nu_matter_liv_{constant,exp}_density``, and
``osc_prob_{2,3,4,5}nu_earth_liv``/``osc_prob_{2,3,4,5}nu_sun_liv`` wrapper.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator``.
Parameters
----------
num_flavors : int
Number of neutrino flavors (2, 3, 4, or 5; or higher, if ``h_vac_energy_indep``/
``h_liv_energy_indep`` are given).
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
osc_params : dict
Oscillation parameters; see :func:`unpack_oscillation_params_from_dict`.
liv_params : dict
LIV parameters; see :func:`unpack_liv_params_from_dict`.
rho_func : Callable, int, or float, optional
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is
True). If 0.0 (default), the probability is for vacuum + LIV only, with no matter term.
L0 : int or float, optional
Initial position. Default: 0.0.
h_vac_energy_indep : list or np.ndarray, optional
Precomputed energy-independent vacuum Hamiltonian, used instead of ``osc_params`` when
``num_flavors`` exceeds ``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
h_liv_energy_indep : list or np.ndarray, optional
Precomputed energy-independent LIV Hamiltonian, used instead of ``liv_params`` when
``num_flavors`` exceeds ``globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS``.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
Ratio of the number of neutrons to protons in matter. Scales the sterile
states' entry in the matter term (see
:func:`magnus.matter.matter_potential_projector`); a callable is read as
:math:`r(l)`, a function of the same position ``rho_func`` takes, and makes
that entry -- and, when a matter density is being converted, the average
nucleon mass -- follow the local composition. This is how the Earth wrappers
feed their layered :math:`Y_e` through. A callable with structure away from
``t_breakpoints`` is subject to the same sampling limits as ``rho_func``.
Default: 1.0.
.. versionchanged:: 1.1.0
A callable is accepted; it used to have to be a scalar.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
density_matter_is_in_g_per_cm3 : bool, optional
If True, ``rho_func`` returns the density in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, ``rho_func`` directly returns the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any parameter left as
None in ``osc_params``. Default: 'OSC_PARAMS_DEFAULT'.
average : bool, optional
If True, return the phase-averaged probability rather than the oscillating one. On a
smooth profile, the tighter of ``rtol`` and ``atol`` is its tolerance.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid',
or 'magnus'; see the ``strategy`` parameter of :func:`osc_prob_matter_std_potential` for
the full description and :doc:`/adiabatic_strategy` for the derivation and validation of
the ``'hybrid'``/``'auto'`` strategies (adiabatic transport with a Magnus patch at any
non-adiabatic window, applicable to any number of flavors). Only relevant when
``rho_func`` is nonzero (there is no position dependence, hence no resonance, in pure
vacuum + LIV). Default: 'auto'.
.. versionadded:: 1.0.0
strategy_info : dict, optional
If given, filled in place with which engine actually answered, following the same
out-parameter convention as ``convergence_info`` in :func:`osc_prob`. Under
``strategy='auto'`` the fallbacks are silent by design -- that is right for ordinary
calls, and wrong for anyone asking why a result moved or why a call got slow -- so this
is how to see them without turning the fallbacks into warnings. Keys:
* ``'engine'`` -- ``'hybrid'``, ``'ip_exp'``, ``'separable'``, ``'constant'``,
``'cumulative'``, ``'magnus'`` or ``'average'``.
* ``'family'`` -- the engine's family; see :data:`ENGINE_FAMILIES`.
* ``'certified'`` -- for ``'hybrid'``, whether
:func:`magnus.adiabatic.hybrid_propagator` self-certified. ``None`` for engines
that do not certify. Under ``'auto'`` an uncertified hybrid result is never
returned, so this is ``True`` whenever the engine is ``'hybrid'``; under
``'hybrid'`` it can be ``False``, and then it means the accuracy is **unverified**,
not that the answer is wrong.
* ``'declined'`` -- ``[(engine, reason)]`` for the engines that stood aside first.
Most requests decline most engines, which is ordinary and not a finding.
* ``'trace'`` -- every dispatch decision in order, with per-engine detail (for the
cumulative scan, ``'n_acc'`` and whether it came from a ceiling).
Costs nothing when omitted. Default: None.
.. versionadded:: 1.0.0
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone: ``U`` is the evolution
operator over the same interval, in the flavor basis, a complex square array with
``U[final, initial]`` the amplitude from the initial to the final state, so that
``P == abs(U)**2.T``. The refinement ladder then compares the operator itself
between levels, with the same ``rtol`` and ``atol``, so the returned operator is
converged in its own right (phases included) and not only in its moduli. Every
other setting keeps its meaning. Default: False.
t_slab_edges : list or np.ndarray, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
magnus_exp_order : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
n_jobs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
integration_method : str
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
rtol : int or float, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
atol : int or float, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
growth_factor_n_slabs : int or float
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
growth_factor_n_tpts_per_slab : int or float
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_num_loops : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
min_n_slabs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_n_slabs : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
min_n_tpts_per_slab : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
max_n_tpts_per_slab : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
validate_input : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
save_log : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
filename_log : str
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
file_log : TextIOWrapper, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
close_file_log_upon_exit : bool
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
verbose : int
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
new_recursion_limit : int, optional
Forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`; see their docstrings.
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`.
**The engine keywords travel this way and so appear in no signature above.** The
ones worth knowing are listed in :data:`magnus.oscprob.PASSTHROUGH_KWARGS_DOCUMENTED`; three of
them decide whether an answer on a hard profile is right at all:
``t_breakpoints``
Positions at which to place slab edges, filling in between. **This is the
parameter for a density jump, a kink or a shock front** -- no number of slabs
fixes a slab that straddles one. Not to be confused with ``t_slab_edges``
above, which is the *complete* set of edges and is rarely what is wanted.
``n_slabs``
An explicit slab count. On a single request over a large accumulated phase,
``rtol`` is not the dial that moves the answer; the refinement ladder can run
to its ceiling and warn, and the slab count is what to set instead.
``cumulative``
Reuse one propagation across a scan of baselines rather than re-propagating
for each. ``'auto'`` by default, which engages it on scans large enough to
benefit.
A misspelling is rejected here, naming the near match, rather than several hops
away by a function the caller never invoked.
symmetric_over : tuple, optional
Caller's declaration that ``A(t) == A(lo + hi - t)`` on ``(lo, hi)``, which lets
the Hamiltonian be evaluated on half the slabs. A declaration, not a test: it
is not checked. See :func:`magnus.magnus.magnus_expansion_multislab`.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each (energy, L) point.
"""
if return_evolution_operator:
_check_operator_request(average, strategy, 'osc_prob_liv')
if validate_input and (strategy not in ('auto', 'hybrid', 'magnus')):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_liv: strategy must be" + \
" 'auto', 'hybrid', or 'magnus'.")
# Unpack oscillation parameters from the osc_params dict, check if all values are available
# The function name is sys._getframe().f_code.co_name
osc_params_list = unpack_oscillation_params_from_dict(sys._getframe().f_code.co_name,
num_flavors, osc_params, h_vac_energy_indep)
liv_params_list = unpack_liv_params_from_dict(sys._getframe().f_code.co_name,
num_flavors, liv_params, h_liv_energy_indep)
if num_flavors == 2:
sth, Dm2 = osc_params_list
sxi, b1, b2, Lambda, n_liv = liv_params_list
elif num_flavors == 3:
s12, s23, s13, dCP, D21, D31 = osc_params_list
sxi12, sxi23, sxi13, dxiCP, b1, b2, b3, Lambda, n_liv = liv_params_list
elif num_flavors == 4:
s12, s23, s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41 = osc_params_list
sxi12, sxi23, sxi13, dxi13, sxi14, dxi14, sxi24, dxi24, sxi34, b1, b2, b3, b4, Lambda, \
n_liv = liv_params_list
elif num_flavors == 5:
s12, s23, s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51 = \
osc_params_list
sxi12, sxi23, sxi13, dxi13, sxi14, dxi14, sxi15, dxi15, sxi24, dxi24, sxi25, sxi34, sxi35, \
dxi35, b1, b2, b3, b4, b5, Lambda, n_liv = liv_params_list
if validate_input:
validate_input_battery(sys._getframe().f_code.co_name, energy=energy, L=L, L0=L0,
num_flavors=num_flavors, nu_i=nu_i, nu_f=nu_f, osc_params=osc_params_list,
rho_func=rho_func, ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction, validate_energy_and_L=True,
validate_flavor_indices=True, validate_osc_params=True, validate_initial_position=True,
validate_density=True)
# If any of the standard oscillation parameters has not been given a value, assign to it the
# value from the specified parameter set with name default_osc_params_set_name. Only the values
# of the parameters passed as None are assigned from the predefined set; others are not
# modified.
# Bounded above as well as below: past MAGNUS_MAX_PREDEFINED_NUM_FLAVORS there are no
# standard parameters to fill in -- the caller's h_vac_energy_indep is the Hamiltonian --
# and s12 and its neighbours were never assigned, so an unbounded test raised
# UnboundLocalError on the path the unpacking warning says is supported.
if 2 < num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS:
s12, s23, s13, dCP, D21, D31 = values_to_unspecified_osc_params(s12, s23, s13, dCP, D21,
D31, default_osc_params_set_name, verbose, angles=angles)
# Compute the energy-independent part of the vacuum Hamiltonian, i.e., everything but the 1/E
# prefactor, only once, to save time. Multiply by the 1/E factor later when calling osc_prob.
# If num_flavors > MAGNUS_MAX_PREDEFINED_NUM_FLAVORS, we use the h_vac_energy_indep that was
# passed to the function.
if num_flavors == 2:
h_vac_energy_indep = hamiltonians.hamiltonian_2nu_vacuum_energy_independent(sth, Dm2, angles=angles)
elif num_flavors == 3:
h_vac_energy_indep = hamiltonians.hamiltonian_3nu_vacuum_energy_independent(s12, s23,
s13, dCP, D21, D31, nubar=nubar, angles=angles)
elif num_flavors == 4:
h_vac_energy_indep = hamiltonians.hamiltonian_4nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s24, d24, s34, D21, D31, D41, nubar=nubar, angles=angles)
elif num_flavors == 5:
h_vac_energy_indep = hamiltonians.hamiltonian_5nu_vacuum_energy_independent(s12, s23,
s13, dCP, s14, d14, s15, d15, s24, d24, s25, s34, s35, d35, D21, D31, D41, D51,
nubar=nubar, angles=angles)
# Compute the energy-independent part of the LIV Hamiltonian, i.e., everything but the 1/E
# prefactor, only once, to save time. Multiply by the 1/E factor later when calling osc_prob.
# If num_flavors > MAGNUS_MAX_PREDEFINED_NUM_FLAVORS, we use the h_liv_energy_indep that was
# passed to the function.
if num_flavors == 2:
h_liv_energy_indep = hamiltonians.hamiltonian_2nu_liv_energy_independent(sxi, b1, b2,
Lambda, n_liv, angles=angles)
elif num_flavors == 3:
h_liv_energy_indep = hamiltonians.hamiltonian_3nu_liv_energy_independent(sxi12, sxi23,
sxi13, dxiCP, b1, b2, b3, Lambda, n_liv, nubar=nubar, angles=angles)
elif num_flavors == 4:
h_liv_energy_indep = hamiltonians.hamiltonian_4nu_liv_energy_independent(sxi12, sxi23,
sxi13, dxi13, sxi14, dxi14, sxi24, dxi24, sxi34, b1, b2, b3, b4, Lambda, n_liv,
nubar=nubar, angles=angles)
elif num_flavors == 5:
h_liv_energy_indep = hamiltonians.hamiltonian_5nu_liv_energy_independent(sxi12, sxi23,
sxi13, dxi13, sxi14, dxi14, sxi15, dxi15, sxi24, dxi24, sxi25, sxi34, sxi35, dxi35, b1,
b2, b3, b4, b5, Lambda, n_liv, nubar=nubar, angles=angles)
if (rho_func != 0.0): # Matter density is nonzero, include the matter term in the Hamiltonian
# Projector onto the nu_e--nu_e entry, multiplied below by the potential VCC. Note that
# VCC_func already carries the antineutrino sign flip (applied inside
# matter.vcc_func_from_rho_func), so no extra sign is applied here.
# See matter.matter_potential_projector: beyond three flavors this is not e_ee, and
# with a callable ratio (the Earth wrappers' default beyond three flavors) it comes
# back as a function of position, handled by the callable branch below.
h_matt = matter.matter_potential_projector(
num_flavors, ratio_number_neutrons_to_protons)
# Build the coherent forward potential function, VCC_func, from the density function,
# rho_func. If the provided rho_func is the matter density (e.g., g cm^{-3}), convert
# rho_func to a function that returns the electron number density [eV^3].
VCC_func = matter.vcc_func_from_rho_func(rho_func, L0, ratio_number_neutrons_to_protons,
electron_fraction, nubar, density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons) # [eV]
# Cache repeated evaluations of the potential on identical position grids (see
# _PositionProfileCache)
if callable(VCC_func):
VCC_func = _PositionProfileCache(VCC_func)
# A position-resolved projector is sampled on exactly the grids the potential is,
# so it gets the identical treatment.
if callable(h_matt):
h_matt = _PositionProfileCache(h_matt)
# Matter Hamiltonian function: diagonal matrix with VCC in the top-left (ee) entry
if callable(h_matt):
# The projector follows the composition, so the Hamiltonian depends on position
# regardless of whether the potential does. Both factors are evaluated on the
# same l; an array l returns a stack with the position axis leading, as below.
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
vcc = np.asarray(VCC_func(l) if callable(VCC_func) else VCC_func)
return (1/enu)*h_vac_energy_indep + \
vcc[..., None, None]*np.asarray(h_matt(l)) + \
pow(enu,n_liv)*h_liv_energy_indep
htot_is_function_only_of_energy = False
elif callable(VCC_func):
# VCC_func is a function of position, so the Hamiltonian is, too. If l is an array,
# the result is a stack of Hamiltonians with the position axis leading; this lets the
# Magnus routines evaluate the Hamiltonian at all time points in a single call.
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
vcc = np.asarray(VCC_func(l))
return (1/enu)*h_vac_energy_indep + vcc[..., None, None]*h_matt + \
pow(enu,n_liv)*h_liv_energy_indep
htot_is_function_only_of_energy = False
else:
# VCC_func is a constant in position, so the Hamiltonian is, too. When VCC_func is
# passed to osc_prob below, osc_prob will detect that VCC_func is constant and set
# parameters internally for speed-up.
#
# A new name, not a rebinding of h_matt: see the matching comment in
# osc_prob_matter_nsi for what folding VCC into that name costs.
h_matt_scaled = VCC_func*h_matt
def htot(enu: Union[int, float]) -> np.ndarray:
return (1/enu)*h_vac_energy_indep + h_matt_scaled \
+ pow(enu,n_liv)*h_liv_energy_indep
htot_is_function_only_of_energy = True
else: # Matter density is zero; the only terms in the Hamiltonian are vacuum and LIV
# Bound on this branch too, so that everything below can refer to it unconditionally.
# Unlike the other two scenario wrappers, this one builds VCC_func only when the
# density is nonzero -- and a LIV *vacuum* call therefore reached the profile scan with
# the name unbound. 0.0 is the right value as well as a safe one: there is no
# potential, so there is no position dependence for anything to hide in.
VCC_func = 0.0
def htot(enu: Union[int, float]) -> np.ndarray:
return (1/enu)*h_vac_energy_indep + pow(enu,n_liv)*h_liv_energy_indep
htot_is_function_only_of_energy = True
# Built unconditionally: every entry is a parameter of this function, and the averaging
# dispatch below needs them on the zero-density path too, where the rest of this block
# does not apply.
# Resolved -- and, crucially, POPPED out of kwargs -- before scan_kwargs is built, because
# every dispatcher below declines outright on an unrecognized entry in kwargs. Left in, an
# explicitly-passed cumulative silently disabled the hybrid, interaction-picture and
# separable engines: passing cumulative='auto', which is the documented default, changed
# which engine answered (hybrid -> general ladder) and moved a 10 MeV solar single point by
# 9.3e-06. See _resolve_cumulative_kwarg.
cumulative_resolved = _resolve_cumulative_kwarg(kwargs, strategy)
scan_kwargs = dict(t_slab_edges=t_slab_edges, verbose=verbose, magnus_exp_order=magnus_exp_order,
n_jobs=n_jobs, integration_method=integration_method, rtol=rtol, atol=atol,
growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
save_log=save_log, file_log=file_log, cumulative=cumulative_resolved,
kwargs=kwargs)
# Checked here, rather than only in osc_prob: the averaged path returns before anything
# forwards **kwargs onwards, so a check further down would see these keys on the ordinary
# path and silently ignore them on the averaged one.
_reject_parameter_set_metadata(kwargs, 'osc_prob_liv')
_check_passthrough_kwargs(kwargs, 'osc_prob_liv')
# Phase average, requested with average=True: closed-form whenever the
# Hamiltonian does not depend on position, so it is tried before any of the propagation
# machinery below, all of which would resolve phases that the average discards (see
# _avg_prob_dispatch and :mod:`magnus.avgprob`).
# `or` on an array would ask for its truth value, so the emptiness test is explicit.
_breakpoints = kwargs.get('t_breakpoints')
_profile_is_smooth = ((_breakpoints is None or len(np.atleast_1d(_breakpoints)) == 0)
and (t_slab_edges is None))
# One scan of the profile for the whole call, before any engine sees it: a feature
# narrower than every grid here is the one exposure none of them can detect for itself,
# because they all miss it together. Depends on the profile and the interval, never on
# energy, so it is not repeated per point. See _scan_for_hidden_features.
_hidden = _scan_for_hidden_features(VCC_func, L0, L, kwargs.get('t_breakpoints'))
# How coarsely does this request sample the oscillation it computes? Reported, never
# warned: see _sampling_report. Computed ONLY when the caller passes strategy_info, so the
# default path pays nothing for it.
_osc = (_sampling_report(htot, htot_is_function_only_of_energy, energy, L, L0)
if strategy_info is not None else None)
# Everything below is dispatch: which engine gets the request. Watched as a unit so
# that strategy_info can report which one answered and, for the hybrid strategy,
# whether it certified -- see _engine_probe. Costs one list allocation per call when
# nobody is watching.
with _engine_probe(disabled=_OPERATOR_ONLY_FROM_LADDER if return_evolution_operator else (),
info=strategy_info, extra={'hidden_feature': _hidden, 'sampling': _osc}):
P_avg = _avg_prob_dispatch(htot, htot_is_function_only_of_energy, energy, L, L0, nu_i, nu_f,
average, 'osc_prob_liv', smooth_profile=_profile_is_smooth, engine_kwargs=scan_kwargs,
average_spread=average_spread)
if P_avg is not NotImplemented:
return P_avg
# Energy-batched fast path: when many energies share a single baseline and the Hamiltonian
# is position-dependent, compute the whole scan in one batched pipeline, with the potential
# samples shared across energies (see _osc_prob_scan_separable). If the request does not fit
# the engine, fall back to the generic per-point path below.
P_scan = NotImplemented
if (rho_func != 0.0): # VCC_func and h_matt exist only when there is matter
# Hybrid strategy, tried first: see _osc_prob_hybrid_dispatch and the matching comment in
# osc_prob_matter_std_potential for why it precedes the interaction-picture fast path.
P_scan = _osc_prob_hybrid_dispatch(h_vac_energy_indep, VCC_func, h_matt,
h_liv_energy_indep, n_liv, energy, L, L0, nu_i, nu_f, scan_kwargs, strategy)
# See the matching comment in osc_prob_matter_std_potential (issue #70).
prefer_ladder = isinstance(P_scan, _PreferLadder)
if prefer_ladder:
rtol, atol, min_n_slabs = P_scan.request(rtol, atol, min_n_slabs, max_n_slabs,
integration_method)
scan_kwargs = dict(scan_kwargs, rtol=rtol, atol=atol, min_n_slabs=min_n_slabs)
P_scan = NotImplemented
elif P_scan is NotImplemented:
# Fast path for a genuine exponential density profile (e.g., the Sun), reached only
# where the hybrid strategy declined: see _osc_prob_ip_exp_dispatch and the matching
# comment in osc_prob_matter_std_potential.
P_scan = _osc_prob_ip_exp_dispatch(h_vac_energy_indep, VCC_func, h_matt,
h_liv_energy_indep, n_liv, energy, L, L0, nu_i, nu_f, scan_kwargs)
if P_scan is NotImplemented:
P_scan = _osc_prob_scan_separable_dispatch(h_vac_energy_indep, VCC_func, h_matt,
h_liv_energy_indep, n_liv, energy, L, L0, nu_i, nu_f, scan_kwargs)
if P_scan is not NotImplemented:
return P_scan
# Generate the probabilities for all pairs of energy and baseline in zip(energy, L).
return osc_prob_energy_baseline(htot, energy, L, L0, nu_i, nu_f,
htot_is_function_only_of_energy, t_slab_edges=t_slab_edges,
return_evolution_operator=return_evolution_operator,
magnus_exp_order=magnus_exp_order, n_jobs=n_jobs, integration_method=integration_method,
rtol=rtol, atol=atol, growth_factor_n_slabs=growth_factor_n_slabs,
growth_factor_n_tpts_per_slab=growth_factor_n_tpts_per_slab,
max_num_loops=max_num_loops, min_n_slabs=min_n_slabs, max_n_slabs=max_n_slabs,
min_n_tpts_per_slab=min_n_tpts_per_slab, max_n_tpts_per_slab=max_n_tpts_per_slab,
validate_input=validate_input, save_log=save_log, filename_log=filename_log,
file_log=file_log, close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=new_recursion_limit, verbose=verbose,
# An explicit cumulative= from the caller wins; otherwise strategy='magnus' opts out
# of the cumulative scan and everything else takes 'auto'. Resolved near the top of
# this function, which is also where it is removed from kwargs.
cumulative=cumulative_resolved, symmetric_over=symmetric_over, **kwargs)
#-----------------------------------------------------------------------
# In vacuum
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_vacuum(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
sth: Union[int, float],
Dm2: Union[int, float],
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
vacuum.
By default, returns :math:`2 \times 2` probability matrices for all
the oscillation channels. Each matrix has shape ``np.ndarray([[Pab,
Pba],[Pba,Pab]])``. The matrix is symmetric, i.e., ``Pba == Pab``.
If a single energy and baseline is given, the function returns a
single matrix. If multiple energies and baselines are given,
function returns an NumPy array of matrices. See examples below.
If the probability needs to be computed multiple times, it is
recommended to pass the array of energies and the array of baselines
to the function in a single call instead of calling the function
separately for each combination of energy and baseline. The reason
is that the function has an overhead that gets diluted when
computing when the input energies and baselines are many.
Unlike :func:`osc_prob_3nu_vacuum` (and also
:func:`osc_prob_4nu_vacuum` and :func:`osc_prob_5nu_vacuum`), the
oscillation parameters `sth` and `Dm2` are not optional, but must be
passed. Depending on the values passed, :func:`osc_prob_2nu_vacuum`
will return probabilities for different two-neutrino systems:
- :math:`\nu_e-\nu_\mu` if ``sth`` is :math:`\sin \theta_{12}` and
``Dm2`` is :math:`\Delta m_{21}^2`
- :math:`\nu_\mu-\nu_\tau` if ``sth`` is :math:`\sin \theta_{23}`
and ``Dm2`` is :math:`\Delta m_{32}^2`
- :math:`\nu_e-\nu_\tau` if ``sth`` is :math:`\sin \theta_{13}`
and ``Dm2`` is :math:`\Delta m_{31}^2`.
If the initial and final flavors, ``nu_i`` and ``nu_f``, are
specified (by setting them to ``NUE``, ``NUMU``, or ``NUTAU``
from the :py:mod:`magnus.globaldefs` module), the function returns
instead a one-dimensional array of the probabilities computed for
each value of energy and baseline requested. See examples below.
Because this is a two-neutrino system, the flavor indices can only
be 0 or 1. To prevent using other values, we convert the indices
like this:
- If ``nu_i == NUE`` (i.e., 0) and ``nu_f == NUTAU`` (i.e., 2), we
set ``nu_f = 1``
- If ``nu_i == NUTAU`` (i.e., 2) and ``nu_f == NUE`` (i.e., 0), we
set ``nu_i = 1``
- If ``nu_i == NUMU`` (i.e., 1) and ``nu_f == NUTAU`` (i.e., 2), we
set ``nu_i = 0`` and ``nu_f = 1``
- If ``nu_i == NUTAU`` (i.e., 2) and ``nu_f == NUMU`` (i.e., 1), we
set ``nu_i = 1`` and ``nu_f = 0``
If ``validate_input`` is set to True, the function validates the
input arguments before calculating the probability, by calling the
function :func:`validate_input_battery`.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy, single value or array.
L : int, float, list, or np.ndarray
Neutrino baseline, single value or array.
sth : int or float
Mixing angle :math:`\theta`, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2`.
nu_i : int, optional
Initial neutrino flavor, either ``NUE``, ``NUMU``, or ``NUTAU``
from the :py:mod:`magnus.globaldefs` module.
nu_f : int, optional
Final neutrino flavor, either ``NUE``, ``NUMU``, or ``NUTAU``
from the :py:mod:`magnus.globaldefs` module.
validate_input : bool, optional
True to validate input (default); False not to, which is faster
but riskier.
save_log : bool, optional
If True, also write all messages to the log file.
filename_log : str, optional
Name of the log file.
file_log : TextIOWrapper, optional
Open file handle to write the log to, if one is already open.
close_file_log_upon_exit : bool, optional
If True, close ``file_log`` before returning.
verbose : int, optional
0 not to print warnings and errors; 1 to print them; 2 to print
progress.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
Union[float, np.ndarray]
Neutrino oscillation probability matrix or probability for a
single oscillation channel, for the values of `energy` and `L`.
Examples
--------
Single energy and baseline (the code below runs when these docs are
built, so the output shown is always current):
.. jupyter-execute::
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
sth = gd.S12_NO_BF_NUFIT_6_0 # sin(theta) [adim]
Dm2 = gd.D21_NO_BF_NUFIT_6_0 # [eV^2]
baseline = 10.0 * gd.UNIT_KM # 10 km in natural units [eV^-1]
energy = 1.0 * gd.UNIT_MEV # [eV]
oscprob.osc_prob_2nu_vacuum(energy, baseline, sth, Dm2)
.. seealso::
:func:`osc_prob_3nu_vacuum`
Three-flavor oscillation probabilities in vacuum.
:func:`osc_prob_4nu_vacuum`
Four-flavor (3+1) oscillation probabilities in vacuum.
:func:`osc_prob_5nu_vacuum`
Four-flavor (3+2) oscillation probabilities in vacuum.
"""
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_vacuum(
num_flavors=2,
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
nu_i=nu_i,
nu_f=nu_f,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_vacuum(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
vacuum.
By default, returns :math:`3 \times 3` probability matrices for all
the oscillation channels. Each matrix has shape ``np.ndarray([[Pee,
Pem,Pet],[Pme,Pmm,Pmt],[Pte,Ptm,Ptt]])``. The matrix is symmetric
only when ``dCP`` is zero, where ``Pem == Pme``, ``Pet == Pte`` and
``Pmt == Ptm``. A non-zero ``dCP`` breaks it, and the shipped
parameter set carries one: at the example below the two sides differ
by 4.8e-02. ``P[i][j]`` is always the probability that flavor ``i``
arrives as flavor ``j``.
If a single energy and baseline is given, the function returns a
single matrix. If multiple energies and baselines are given,
function returns an NumPy array of matrices. See examples below.
If the probability needs to be computed multiple times, it is
recommended to pass the array of energies and the array of baselines
to the function in a single call instead of calling the function
separately for each combination of energy and baseline. The reason
is that the function has an overhead that gets diluted when
computing when the input energies and baselines are many.
If the initial and final flavors, ``nu_i`` and ``nu_f``, are
specified (by setting them to ``NUE``, ``NUMU``, or ``NUTAU``
from the :py:mod:`magnus.globaldefs` module), the function returns
instead a one-dimensional array of the probabilities computed for
each value of energy and baseline requested. See examples below.
If the function is called without specifying values of the standard
oscillation parameters (``s12``, ``s23``, ``s13``, ``dCP``, ``D21``,
``D31``), the unspecified parameters are assigned default values
taken from a predefined parameter set. The name of the default
parameter set can be changed by passing
``default_osc_params_set_name``.
The names of the predefined parameter sets included in
:math:`\text{Mag}\nu\text{s}` can be seen by printing
.. jupyter-execute::
import magnus.globaldefs as gd
list(gd.OSC_PARAMS_PREDEFINED.keys())
And the default parameter values are from the set with name
``'OSC_PARAMS_DEFAULT'``:
.. jupyter-execute::
gd.OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']
If ``validate_input`` is set to True, the function validates the
input arguments before calculating the probability, by calling the
function :func:`validate_input_battery`.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy, single value or array.
L : int, float, list, or np.ndarray
Neutrino baseline, single value or array.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine).
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine).
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine).
dCP : int or float, optional
CP-violation phase, :math:`\delta_\text{CP}`.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`.
nubar : bool, optional
False (default) for neutrinos; True for anti-neutrinos.
nu_i : int, optional
Initial neutrino flavor, either ``NUE``, ``NUMU``, or ``NUTAU``
from the :py:mod:`magnus.globaldefs` module.
nu_f : int, optional
Final neutrino flavor, either ``NUE``, ``NUMU``, or ``NUTAU``
from the :py:mod:`magnus.globaldefs` module.
default_osc_params_set_name : str, optional
Name of the predefined set of oscillation parameters to use when
assigning default values to unspecified parameters.
validate_input : bool, optional
True to validate input (default); False not to, which is faster
but riskier.
save_log : bool, optional
If True, also write all messages to the log file.
filename_log : str, optional
Name of the log file.
file_log : TextIOWrapper, optional
Open file handle to write the log to, if one is already open.
close_file_log_upon_exit : bool, optional
If True, close ``file_log`` before returning.
verbose : int, optional
0 not to print warnings and errors; 1 to print them; 2 to print
progress.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
Returns
-------
Union[float, np.ndarray]
Neutrino oscillation probability matrix or probability for a
single oscillation channel, for the values of `energy` and `L`.
Examples
--------
If both ``energy`` and ``L`` are single values, this function returns
the full :math:`3\times 3` probability matrix computed at those
values, using the NuFIT 6.1 (normal ordering) defaults for any
oscillation parameter not passed explicitly:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
baseline = 10.0 * gd.UNIT_KM # 10 km in natural units [eV^-1]
energy = 1.0 * gd.UNIT_MEV # [eV]
oscprob.osc_prob_3nu_vacuum(energy, baseline)
Pick one channel only, e.g., :math:`\nu_e \to \nu_\mu`, by passing an
initial flavor, ``nu_i``, and a final flavor, ``nu_f`` (the flavor
indices ``NUE``, ``NUMU``, ``NUTAU`` are defined in
:py:mod:`magnus.globaldefs`); pass ``nubar=True`` for the
antineutrino channel :math:`\bar\nu_e \to \bar\nu_\mu`:
.. jupyter-execute::
print(oscprob.osc_prob_3nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUMU))
print(oscprob.osc_prob_3nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUMU,
nubar=True))
Any standard oscillation parameter can be overridden; the rest keep
defaulting to the predefined set named by ``default_osc_params_set_name``
(``'OSC_PARAMS_DEFAULT'`` unless changed -- see
``globaldefs.OSC_PARAMS_PREDEFINED`` for the values it uses):
.. jupyter-execute::
oscprob.osc_prob_3nu_vacuum(energy, baseline, s12=0.0)
If a single energy and multiple baselines are passed, the result is
an array of probabilities, one per baseline (and, conversely, one
per energy for a single baseline and multiple energies; or a full
grid for arrays of both -- paired index-by-index, not an outer
product):
.. jupyter-execute::
baselines = gd.UNIT_KM * np.array([1.0, 10.0, 100.0])
energies = gd.UNIT_MEV * np.array([1.0, 5.0, 20.0])
print(oscprob.osc_prob_3nu_vacuum(energy, baselines, nu_i=gd.NUE, nu_f=gd.NUMU))
print(oscprob.osc_prob_3nu_vacuum(energies, baseline, nu_i=gd.NUE, nu_f=gd.NUMU))
print(oscprob.osc_prob_3nu_vacuum(energies, baselines, nu_i=gd.NUE, nu_f=gd.NUMU))
.. seealso::
:func:`osc_prob_2nu_vacuum`
Two-flavor oscillation probabilities in vacuum.
:func:`osc_prob_4nu_vacuum`
Four-flavor (3+1) oscillation probabilities in vacuum.
:func:`osc_prob_5nu_vacuum`
Four-flavor (3+2) oscillation probabilities in vacuum.
"""
return osc_prob_vacuum(
num_flavors=3,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_vacuum(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino (3+1) oscillation
probability in vacuum.
By default, returns :math:`4 \times 4` probability matrices for all
the oscillation channels. Each matrix has shape ``np.ndarray([[Pee,
Pem,Pet,Pes],[Pme,Pmm,Pmt,Pms],[Pte,Ptm,Ptt,Pts],
[Pse,Psm,Pst,Pss]])``. The matrix is symmetric only when every CP
phase is zero, where ``Pem == Pme``, ``Pet == Pte``, ``Pes == Pse``,
``Pmt == Ptm``, ``Pms == Psm`` and ``Pts == Pst``. A non-zero phase
breaks it, and the shipped parameter set carries one: at the example
below the two sides differ by 4.8e-02. ``P[i][j]`` is always the
probability that flavor ``i`` arrives as flavor ``j``.
If a single energy and baseline is given, the function returns a
single matrix. If multiple energies and baselines are given,
function returns an NumPy array of matrices. See examples below.
If the probability needs to be computed multiple times, it is
recommended to pass the array of energies and the array of baselines
to the function in a single call instead of calling the function
separately for each combination of energy and baseline. The reason
is that the function has an overhead that gets diluted when
computing when the input energies and baselines are many.
If the initial and final flavors, ``nu_i`` and ``nu_f``, are
specified (by setting them to ``NUE``, ``NUMU``, ``NUTAU``, or
``NUS`` from the :py:mod:`magnus.globaldefs` module), the function
returns instead a one-dimensional array of the probabilities
computed for each value of energy and baseline requested. See
examples below.
If the function is called without specifying values of the standard
oscillation parameters (``s12``, ``s23``, ``s13``, ``dCP``, ``D21``,
``D31``), the unspecified parameters are assigned default values
taken from a predefined parameter set. The name of the default
parameter set can be changed by passing
``default_osc_params_set_name``.
The names of the predefined parameter sets included in
:math:`\text{Mag}\nu\text{s}` can be seen by printing
.. jupyter-execute::
import magnus.globaldefs as gd
list(gd.OSC_PARAMS_PREDEFINED.keys())
And the default parameter values are from the set with name
``'OSC_PARAMS_DEFAULT'``:
.. jupyter-execute::
gd.OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']
If ``validate_input`` is set to True, the function validates the
input arguments before calculating the probability, by calling the
function :func:`validate_input_battery`.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy, single value or array.
L : int, float, list, or np.ndarray
Neutrino baseline, single value or array.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine).
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine).
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine).
d14 : int or float, optional
CP-violation phase, :math:`\delta_{14}`.
d24 : int or float, optional
CP-violation phase, :math:`\delta_{24}`.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine).
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine).
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine).
dCP : int or float, optional
CP-violation phase, :math:`\delta_\text{CP}`.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`.
nubar : bool, optional
False (default) for neutrinos; True for anti-neutrinos.
nu_i : int, optional
Initial neutrino flavor, either ``NUE``, ``NUMU``, ``NUTAU``,
or ``NUS`` from the :py:mod:`magnus.globaldefs` module.
nu_f : int, optional
Final neutrino flavor, either ``NUE``, ``NUMU``, ``NUTAU``,
or ``NUS`` from the :py:mod:`magnus.globaldefs` module.
default_osc_params_set_name : str, optional
Name of the predefined set of standard oscillation parameters to
use when assigning default values to unspecified parameters.
validate_input : bool, optional
True to validate input (default); False not to, which is faster
but riskier.
save_log : bool, optional
If True, also write all messages to the log file.
filename_log : str, optional
Name of the log file.
file_log : TextIOWrapper, optional
Open file handle to write the log to, if one is already open.
close_file_log_upon_exit : bool, optional
If True, close ``file_log`` before returning.
verbose : int, optional
0 not to print warnings and errors; 1 to print them; 2 to print
progress.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
Union[float, np.ndarray]
Neutrino oscillation probability matrix or probability for a
single oscillation channel, for the values of `energy` and `L`.
Examples
--------
With the sterile-sector angles/phases given explicitly and the
active-sector angles left at their NuFIT 6.1 defaults:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
baseline = 10.0 * gd.UNIT_KM # 10 km in natural units [eV^-1]
energy = 1.0 * gd.UNIT_MEV # [eV]
s14, s24, s34 = 0.1, 0.2, 0.3
d14, d24 = np.radians(10.0), np.radians(100.0)
D41 = 0.1 # [eV^2]
oscprob.osc_prob_4nu_vacuum(energy, baseline, s14=s14, s24=s24, s34=s34,
d14=d14, d24=d24, D41=D41)
Pick one channel only, e.g., :math:`\nu_e \to \nu_s`, and the
antineutrino channel :math:`\bar\nu_e \to \bar\nu_\mu` (the flavor
indices ``NUE``, ``NUMU``, ``NUTAU``, ``NUS`` are defined in
:py:mod:`magnus.globaldefs`):
.. jupyter-execute::
common = dict(s14=s14, s24=s24, s34=s34, d14=d14, d24=d24, D41=D41)
print(oscprob.osc_prob_4nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUS, **common))
print(oscprob.osc_prob_4nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUMU,
nubar=True, **common))
Any active-sector parameter can still be overridden on top of the
sterile-sector values:
.. jupyter-execute::
oscprob.osc_prob_4nu_vacuum(energy, baseline, s12=0.0, **common)
Arrays of energies and/or baselines work the same way as for
:func:`osc_prob_3nu_vacuum` (paired index-by-index for two arrays,
not an outer product):
.. jupyter-execute::
baselines = gd.UNIT_KM * np.array([1.0, 10.0, 100.0])
energies = gd.UNIT_MEV * np.array([1.0, 5.0, 20.0])
print(oscprob.osc_prob_4nu_vacuum(energy, baselines, nu_i=gd.NUE, nu_f=gd.NUMU, **common))
print(oscprob.osc_prob_4nu_vacuum(energies, baseline, nu_i=gd.NUE, nu_f=gd.NUMU, **common))
print(oscprob.osc_prob_4nu_vacuum(energies, baselines, nu_i=gd.NUE, nu_f=gd.NUMU, **common))
.. seealso::
:func:`osc_prob_2nu_vacuum`
Two-flavor oscillation probabilities in vacuum.
:func:`osc_prob_3nu_vacuum`
Three-flavor oscillation probabilities in vacuum.
:func:`osc_prob_5nu_vacuum`
Five-flavor (3+2) oscillation probabilities in vacuum.
"""
return osc_prob_vacuum(
num_flavors=4,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_vacuum(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino (3+2) oscillation
probability in vacuum.
By default, returns :math:`5 \times 5` probability matrices for all
the oscillation channels. Each matrix has shape ``np.ndarray([[Pee,
Pem,Pet,Pes1,Pes2],[Pme,Pmm,Pmt,Pms1,Pms2],[Pte,Ptm,Ptt,Pts1,Pts2],
[Ps1e,Ps1m,Ps1t,Ps1s1,Ps1s2],[Ps2e,Ps2m,Ps2t,Ps2s1,Ps2s2]])``. The
matrix is symmetric only when every CP phase is zero, where
``Pem == Pme``, ``Pet == Pte``, ``Pes1 == Ps1e``, ``Pes2 == Ps2e``,
``Pmt == Ptm``, ``Pms1 == Ps1m``, ``Pts1 == Ps1t``, ``Pts2 == Ps2t``
and ``Ps1s2 == Ps2s1``. A non-zero phase breaks it, and the shipped
parameter set carries one: at the example below the two sides differ
by 4.8e-02. ``P[i][j]`` is always the probability that flavor ``i``
arrives as flavor ``j``.
If a single energy and baseline is given, the function returns a
single matrix. If multiple energies and baselines are given,
function returns an NumPy array of matrices. See examples below.
If the probability needs to be computed multiple times, it is
recommended to pass the array of energies and the array of baselines
to the function in a single call instead of calling the function
separately for each combination of energy and baseline. The reason
is that the function has an overhead that gets diluted when
computing when the input energies and baselines are many.
If the initial and final flavors, ``nu_i`` and ``nu_f``, are
specified (by setting them to ``NUE``, ``NUMU``, ``NUTAU``,
``NUS1``, or ``NUS2`` from the :py:mod:`magnus.globaldefs` module),
the function returns instead a one-dimensional array of the
probabilities computed for each value of energy and baseline
requested. See examples below.
If the function is called without specifying values of the standard
oscillation parameters (``s12``, ``s23``, ``s13``, ``dCP``, ``D21``,
``D31``), the unspecified parameters are assigned default values
taken from a predefined parameter set. The name of the default
parameter set can be changed by passing
``default_osc_params_set_name``.
The names of the predefined parameter sets included in
:math:`\text{Mag}\nu\text{s}` can be seen by printing
.. jupyter-execute::
import magnus.globaldefs as gd
list(gd.OSC_PARAMS_PREDEFINED.keys())
And the default parameter values are from the set with name
``'OSC_PARAMS_DEFAULT'``:
.. jupyter-execute::
gd.OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']
If ``validate_input`` is set to True, the function validates the
input arguments before calculating the probability, by calling the
function :func:`validate_input_battery`.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy, single value or array.
L : int, float, list, or np.ndarray
Neutrino baseline, single value or array.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine).
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine).
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine).
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine).
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine).
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine).
d14 : int or float, optional
CP-violation phase, :math:`\delta_{14}`.
d15 : int or float, optional
CP-violation phase, :math:`\delta_{15}`.
d24 : int or float, optional
CP-violation phase, :math:`\delta_{24}`.
d35 : int or float, optional
CP-violation phase, :math:`\delta_{35}`.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine).
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine).
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine).
dCP : int or float, optional
CP-violation phase, :math:`\delta_\text{CP}`.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`.
nubar : bool, optional
False (default) for neutrinos; True for anti-neutrinos.
nu_i : int, optional
Initial neutrino flavor, either ``NUE``, ``NUMU``, ``NUTAU``,
``NUS1``, or ``NUS2`` from the :py:mod:`magnus.globaldefs`
module.
nu_f : int, optional
Final neutrino flavor, either ``NUE``, ``NUMU``, ``NUTAU``,
``NUS1``, or ``NUS2`` from the :py:mod:`magnus.globaldefs`
module.
default_osc_params_set_name : str, optional
Name of the predefined set of standard oscillation parameters to
use when assigning default values to unspecified parameters.
validate_input : bool, optional
True to validate input (default); False not to, which is faster
but riskier.
save_log : bool, optional
If True, also write all messages to the log file.
filename_log : str, optional
Name of the log file.
file_log : TextIOWrapper, optional
Open file handle to write the log to, if one is already open.
close_file_log_upon_exit : bool, optional
If True, close ``file_log`` before returning.
verbose : int, optional
0 not to print warnings and errors; 1 to print them; 2 to print
progress.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
Union[float, np.ndarray]
Neutrino oscillation probability matrix or probability for a
single oscillation channel, for the values of `energy` and `L`.
Examples
--------
With the sterile-sector angles/phases given explicitly and the
active-sector angles left at their NuFIT 6.1 defaults:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
baseline = 10.0 * gd.UNIT_KM # 10 km in natural units [eV^-1]
energy = 1.0 * gd.UNIT_MEV # [eV]
s14, s15, s24, s25, s34, s35 = 0.1, 0.1, 1.e-2, 1.e-2, 1.e-3, 1.e-3
d14, d15, d24, d35 = np.radians([10.0, 20.0, 30.0, 40.0])
D41, D51 = 0.1, 0.001 # [eV^2]
common = dict(s14=s14, s15=s15, s24=s24, s25=s25, s34=s34, s35=s35,
d14=d14, d15=d15, d24=d24, d35=d35, D41=D41, D51=D51)
oscprob.osc_prob_5nu_vacuum(energy, baseline, **common).shape
Pick one channel only -- :math:`\nu_e \to \nu_{s_1}`,
:math:`\nu_e \to \nu_{s_2}`, and :math:`\nu_{s_1} \to \nu_{s_2}` (the
flavor indices ``NUE``, ``NUMU``, ``NUTAU``, ``NUS1``, ``NUS2`` are
defined in :py:mod:`magnus.globaldefs`); and the antineutrino channel
:math:`\bar\nu_e \to \bar\nu_{s_1}`:
.. jupyter-execute::
print(oscprob.osc_prob_5nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUS1, **common))
print(oscprob.osc_prob_5nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUS2, **common))
print(oscprob.osc_prob_5nu_vacuum(energy, baseline, nu_i=gd.NUS1, nu_f=gd.NUS2, **common))
print(oscprob.osc_prob_5nu_vacuum(energy, baseline, nu_i=gd.NUE, nu_f=gd.NUS1,
nubar=True, **common))
Any active-sector parameter can still be overridden on top of the
sterile-sector values:
.. jupyter-execute::
oscprob.osc_prob_5nu_vacuum(energy, baseline, s12=0.0, **common)
Arrays of energies and/or baselines work the same way as for
:func:`osc_prob_3nu_vacuum` (paired index-by-index for two arrays,
not an outer product):
.. jupyter-execute::
baselines = gd.UNIT_KM * np.array([1.0, 10.0, 100.0])
energies = gd.UNIT_MEV * np.array([1.0, 5.0, 20.0])
print(oscprob.osc_prob_5nu_vacuum(energy, baselines, nu_i=gd.NUE, nu_f=gd.NUMU, **common))
print(oscprob.osc_prob_5nu_vacuum(energies, baseline, nu_i=gd.NUE, nu_f=gd.NUMU, **common))
print(oscprob.osc_prob_5nu_vacuum(energies, baselines, nu_i=gd.NUE, nu_f=gd.NUMU, **common))
.. seealso::
:func:`osc_prob_2nu_vacuum`
Two-flavor oscillation probabilities in vacuum.
:func:`osc_prob_3nu_vacuum`
Three-flavor oscillation probabilities in vacuum.
:func:`osc_prob_4nu_vacuum`
Four-flavor (3+1) oscillation probabilities in vacuum.
"""
return osc_prob_vacuum(
num_flavors=5,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, standard oscillations, constant density
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_matter_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
matter with a constant density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_matter_std_potential(
num_flavors=2,
rho_func=rho,
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=None,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_matter_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
matter with a constant density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_matter_std_potential(
num_flavors=3,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_matter_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability in
matter with a constant density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_matter_std_potential(
num_flavors=4,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_matter_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability in
matter with a constant density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_matter_std_potential(
num_flavors=5,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, standard oscillations, exponentially falling density
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_matter_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation
probability in matter with an exponentially falling density profile.
.. versionadded:: 1.0.0
.. note::
Dispatches to a fast, closed-form interaction-picture Magnus
integrator whenever the accumulated matter phase stays small enough
to certify (see ``_osc_prob_ip_exp_dispatch``), giving warning-free
results in a fraction of a second across the realistic solar-neutrino
energy range for baselines up to a few e-folds of ``l_scale``. Longer
baselines fall back transparently to the general slab-refinement
method.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_2nu_matter_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_matter_std_potential(
num_flavors=2,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_matter_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation
probability in matter with an exponentially falling density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_3nu_matter_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
return osc_prob_matter_std_potential(
num_flavors=3,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_matter_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino (3+1) oscillation
probability in matter with an exponentially falling density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_4nu_matter_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
return osc_prob_matter_std_potential(
num_flavors=4,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_matter_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino (3+2) oscillation
probability in matter with an exponentially falling density profile.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_5nu_matter_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
return osc_prob_matter_std_potential(
num_flavors=5,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, standard oscillations, in the Earth
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_earth(
energy: Union[int, float, list, np.ndarray],
sth: Union[int, float],
Dm2: Union[int, float],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\\nu_e` on
electrons.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Standard two-neutrino oscillations through the Earth, specified by
the cosine of the zenith angle:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
sth = gd.S12_NO_BF_NUFIT_6_0
Dm2 = gd.D21_NO_BF_NUFIT_6_0
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM # chord length for this costhz
energy = 1.0 * gd.UNIT_GEV
# The small solar mass splitting Dm2 combined with this Earth baseline
# means the adaptive refinement needs a few loops; this is the expected,
# informational MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_2nu_earth(energy, sth, Dm2, costhz=costhz, L=baseline)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth) # L in eV^{-1}
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# [g cm^{-3}] as a function of radial distance, r, using the Preliminary Reference Earth Model
# (PREM). The function matter.num_density_e_func converts the matter density into electron
# number density [eV^3].
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=2,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_std_potential(
num_flavors=2,
rho_func=rho_func,
energy=energy,
L=L, # [eV^{-1}]
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'sth': sth, 'Dm2': Dm2},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_earth(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\\nu_e` on
electrons.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Standard three-neutrino oscillations through the Earth, using the
NuFIT 6.1 defaults for the oscillation parameters:
.. jupyter-execute::
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_3nu_earth(energy, costhz=costhz, L=baseline)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=3,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_std_potential(
num_flavors=3,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_earth(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\\nu_e` on
electrons.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Four-neutrino (3+1 sterile) oscillations through the Earth, with a
modest sterile mixing on top of the NuFIT 6.1 active-sector defaults:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_4nu_earth(energy, costhz=costhz, L=baseline,
s14=0.1, s24=0.05, s34=0.02,
d14=np.radians(10.0), d24=np.radians(20.0), D41=0.1)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=4,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_std_potential(
num_flavors=4,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_earth(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\\nu_e` on
electrons.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Five-neutrino (3+2 sterile) oscillations through the Earth, with
modest sterile mixing on top of the NuFIT 6.1 active-sector defaults:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_5nu_earth(energy, costhz=costhz, L=baseline,
s14=0.1, s15=0.05, s24=0.05, s25=0.02, s34=0.02, s35=0.01,
d14=np.radians(10.0), d15=np.radians(15.0),
d24=np.radians(20.0), d35=np.radians(25.0),
D41=0.1, D51=0.05)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=5,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_std_potential(
num_flavors=5,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_earth(
H_func: Callable,
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
validate_input: Optional[bool]=True,
verbose: Optional[int]=0,
strategy: Optional[str]='auto',
strategy_info: Optional[Dict]=None,
average: Optional[bool]=False,
average_spread: Optional[float]=None,
**kwargs
) -> Union[float, np.ndarray]:
r"""Compute and return the neutrino oscillation probability inside
the Earth for a given arbitrary Hamiltonian.
Does **not** assume standard oscillations nor a given number of
neutrino flavors: the user supplies their own Hamiltonian function,
``H_func``, and this routine takes care of the geometry of the
trajectory through the Earth and of the matter density along it.
The trajectory runs between two points on the surface unless
``source_depth`` or ``detector_depth`` says otherwise. ``costhz`` is
the zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that leaves
the depths alone. A buried detector fixes where the trajectory ends,
so ``L`` is then computed rather than given.
``H_func`` must be a function of either three arguments,
``H_func(energy, l, VCC)``, or two arguments,
``H_func(energy, l)``, returning a square complex NumPy array (the
Hamiltonian in the flavor basis, in eV). In the three-argument
form, ``VCC`` is the charged-current matter potential
:math:`V_{\rm CC} = \sqrt{2} G_F N_e` [eV] at position ``l``
along the chord, computed from the Preliminary Reference Earth
Model; its sign is already flipped for antineutrinos
(``nubar=True``). The user is free to use it, scale it, or ignore
it (e.g., to add non-standard matter potentials that affect flavors
other than :math:`\nu_e`). For extra speed, ``H_func`` may accept
an array of positions ``l`` and return a stack of Hamiltonians with
the position axis leading; this is detected automatically.
The trajectory can be specified either by the cosine of the zenith
angle (``costhz``) together with the baseline ``L`` [:math:`\text{eV}^{-1}`], or
by an initial and a final location on the surface of the Earth
(``loc_ini``, ``loc_fin``), given as (degree, minute, second)
latitude/longitude tuples or as the names of predefined locations
(see ``earth.loc_coords_dms``); in the latter case the neutrino
travels the chord that joins the two locations.
The slab edges used internally are aligned with the crossings of
the PREM layer boundaries along the chord.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Added ``average``.
Parameters
----------
H_func : Callable
The Hamiltonian, as ``H_func(energy, l, VCC)`` or ``H_func(energy, l)``; see above.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative
to ``loc_ini``/``loc_fin``.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a
predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``.
L : float, list, or np.ndarray, optional
Baseline(s) [:math:`\text{eV}^{-1}`]. Used together with ``costhz``, as an alternative to
``loc_ini``/``loc_fin``.
nubar : bool, optional
If True, compute the probability for antineutrinos (flips the sign of the PREM-based
matter potential passed to ``H_func``). Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
ratio_number_neutrons_to_protons : int or float, optional
Accepted and **inert on this entry point**. The density derives its own ratio from
the layered :math:`Y_e`, and with a caller-supplied ``H_func`` there is no
package-built matter projector for the ratio to enter -- sterile entries, if any,
are ``H_func``'s own business. Changing it moves nothing: the largest difference
between ``r = 1.0`` and ``r = 0.1`` is exactly 0.0, at three flavors and at four.
The flavor-specific wrappers do use it, and there it is worth 0.29 in probability.
Default: 1.0.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed.
Cannot be combined with any ``electron_fraction_*`` argument. Default: None,
meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic).
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater).
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
Pair it with ``electron_fraction_ocean``, which sets the composition
of the same shell. Default: None, i.e. PREM's own ocean.
magnus_exp_order : int, optional
Highest order of the Magnus expansion. Default: 4.
n_jobs : int, optional
Number of parallel joblib workers. Default: 1.
integration_method : str, optional
'gl', 'trapezoid', or 'simpson'. Default: 'gl'.
rtol, atol : int or float, optional
Target relative/absolute tolerance for the adaptive slab refinement. With
``average=True`` on a smooth profile, the tighter of the two is the tolerance of the
phase average instead; see :func:`osc_prob_energy_baseline`. Default: 1e-3 each.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
verbose : int, optional
Verbosity level. Default: 0.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid',
or 'magnus'; see the ``strategy`` parameter of :func:`osc_prob_matter_std_potential` for
the full description and :doc:`/adiabatic_strategy` for the derivation and validation. In
practice, ``'hybrid'``/``'auto'`` rarely engage here: the PREM density profile has
layer-boundary discontinuities (``t_breakpoints``), which this strategy does not support
(see :doc:`/adiabatic_strategy`), so a real Earth-crossing trajectory almost always falls
back to the ``'magnus'`` strategies regardless of what is requested. Default: 'auto'.
.. versionadded:: 1.0.0
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`
(e.g., the refinement-loop bounds).
strategy_info : dict, optional
If given, filled in place with which engine actually answered, following the
same out-parameter convention as ``convergence_info`` in :func:`osc_prob`.
average : bool, optional
If True, return the phase average of the probability over a relative energy spread (see
:mod:`magnus.avgprob`) instead of the oscillating one, as the wrappers do with the
same keyword; ``strategy`` is then not consulted. Which route answers follows from
the Hamiltonian: a function of the energy alone is averaged in closed form, one
eigendecomposition per energy; a function of position is averaged by adiabatic
transport along its instantaneous eigenstates, with a Magnus patch across every
non-adiabatic crossing, when the profile is smooth; and across an energy window, with
a warning, when ``t_breakpoints`` declare discontinuities. ``n_jobs`` and the
cumulative traversal play no role on this route, and on a smooth profile the tighter
of ``rtol`` and ``atol`` is its tolerance. Cannot be combined with
``return_evolution_operator``. Default: False.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each energy.
Examples
--------
Standard three-neutrino oscillations, written by hand (the
dedicated wrapper :func:`osc_prob_3nu_earth` does this internally):
.. jupyter-execute::
import warnings
import numpy as np
import magnus.oscprob as oscprob
import magnus.hamiltonians as hamiltonians
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
p = gd.OSC_PARAMS_PREDEFINED['OSC_PARAMS_DEFAULT']
s12, s23, s13, dCP, D21, D31 = p['s12'], p['s23'], p['s13'], p['dCP'], p['D21'], p['D31']
h_vac = hamiltonians.hamiltonian_3nu_vacuum_energy_independent(
s12, s23, s13, dCP, D21, D31)
# Written so that it accepts an array of positions: VCC[..., None, None]
# turns one potential per position into a stack of matrices, which keeps
# osc_prob on its vectorized path (see ScalarHamiltonianWarning).
e00 = np.diag([1.0, 0.0, 0.0])
def H(energy, l, VCC):
return (1 / energy) * h_vac + np.asarray(VCC)[..., None, None] * e00
# At the default tolerance the first, coarsest level of the refinement
# trips the informational MagnusConvergenceWarning discussed in the
# package README; the ladder then refines past it.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_earth(H, energy=1.0 * gd.UNIT_GEV,
loc_ini='fermilab', loc_fin='homestake')
P
"""
source_func_name = sys._getframe().f_code.co_name
# If the location is given as a string, look it up among the predefined named locations
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# Resolve the trajectory: either the chord between two surface locations, or (costhz, L)
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# Charged-current potential along the chord from the PREM electron density; the antineutrino
# sign flip is applied inside matter.vcc_func_from_rho_func. The profile evaluations are
# cached on repeated position grids.
# _earth_composition resolves the density together with a projector ratio, but with a
# user-supplied H_func there is no package-built projector for the ratio to enter --
# sterile entries, if any, are H_func's own business -- so only the density is kept.
rho_func, _ = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=None,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
VCC_func = matter.vcc_func_from_rho_func(
rho_func=rho_func,
nubar=nubar,
density_is_of_number_of_electrons=True) # [eV]
VCC_func = _PositionProfileCache(VCC_func)
# A chord meets every radius twice; see _earth_chord_symmetry for why this may be declared
# here and not by a general caller, and why it declines unless the whole chord is traversed.
return _osc_prob_with_potential(source_func_name, H_func, VCC_func, energy, L, 0.0, nu_i,
nu_f, t_breakpoints, magnus_exp_order, n_jobs, integration_method, rtol, atol,
validate_input, verbose, strategy=strategy, strategy_info=strategy_info, average=average,
average_spread=average_spread,
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth), **kwargs)
def _osc_prob_with_potential(
source_func_name: str,
H_func: Callable,
VCC_func: Callable,
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
L0: Union[int, float],
nu_i: Optional[int],
nu_f: Optional[int],
t_breakpoints: Optional[np.ndarray],
magnus_exp_order: int,
n_jobs: int,
integration_method: str,
rtol: Optional[Union[int, float]],
atol: Optional[Union[int, float]],
validate_input: bool,
verbose: int,
strategy: Optional[str] = 'auto',
strategy_info: Optional[Dict] = None,
symmetric_over: Optional[tuple] = None,
return_evolution_operator: Optional[bool] = False,
average: Optional[bool] = False,
average_spread: Optional[float] = None,
**kwargs
) -> Union[float, np.ndarray, Tuple[np.ndarray, np.ndarray]]:
r"""Common machinery of :func:`osc_prob_earth` and
:func:`osc_prob_sun`: wire a user-supplied Hamiltonian function --
H_func(energy, l, VCC) or H_func(energy, l) -- to the environment
potential ``VCC_func`` and hand it to
:func:`osc_prob_energy_baseline`.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``return_evolution_operator`` and ``average``.
.. note::
With ``strategy='auto'`` (the default) or ``'hybrid'``, this also tries the
adiabatic-transport-plus-Magnus-patch hybrid strategy (see
``_osc_prob_hybrid_dispatch_generic`` and :doc:`/adiabatic_strategy`) whenever
``t_breakpoints`` is empty and a target tolerance is requested, before falling back to
the general slab-refinement method.
Parameters
----------
source_func_name : str
Name of the calling function (``osc_prob_earth`` or ``osc_prob_sun``), used to build more
informative error messages.
H_func : Callable
The Hamiltonian, as ``H_func(energy, l, VCC)`` or ``H_func(energy, l)``.
VCC_func : Callable
The environment's matter potential, as a function of position.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned.
nu_f : int, optional
Final flavor index; see ``nu_i``.
t_breakpoints : np.ndarray, optional
Mandatory slab edges (e.g., PREM layer boundaries).
magnus_exp_order : int
Highest order of the Magnus expansion.
n_jobs : int
Number of parallel joblib workers.
integration_method : str
'gl', 'trapezoid', or 'simpson'.
rtol, atol : int or float, optional
Target relative/absolute tolerance for the adaptive slab refinement. With
``average=True`` on a smooth profile, the tighter of the two is the tolerance of the
phase average instead; see :func:`osc_prob_energy_baseline`.
validate_input : bool
If True, validate that ``H_func`` has the expected signature.
verbose : int
Verbosity level.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid',
or 'magnus'; see the ``strategy`` parameter of :func:`osc_prob_matter_std_potential` for
the full description and :doc:`/adiabatic_strategy` for the derivation and validation.
Default: 'auto'.
.. versionadded:: 1.0.0
strategy_info : dict, optional
If given, filled in place with which engine actually answered, exactly as in
:func:`osc_prob_matter_std_potential` -- see that function for the keys. A
user-supplied Hamiltonian gets the same answer to "which engine answered, and what
stood aside" as a built-in scenario does. Default: None.
.. versionadded:: 1.0.0
symmetric_over : tuple, optional
Interval over which the caller declares the profile mirror-symmetric, forwarded to
:func:`osc_prob`. Set by :func:`osc_prob_earth`, whose chord is symmetric by geometry;
left None by :func:`osc_prob_sun`, whose profile is monotonic.
return_evolution_operator : bool, optional
If True, return the pair ``(P, U)`` instead of ``P`` alone, with ``U`` the
evolution operators, one ``(d, d)`` array per point. Cannot be combined
with ``average``. Default: False.
average : bool, optional
If True, return the phase average of the probability over a relative energy spread (see
:mod:`magnus.avgprob`) instead of the oscillating one, as the wrappers do with the
same keyword; ``strategy`` is then not consulted. Which route answers follows from
the Hamiltonian: a function of the energy alone is averaged in closed form, one
eigendecomposition per energy; a function of position is averaged by adiabatic
transport along its instantaneous eigenstates, with a Magnus patch across every
non-adiabatic crossing, when the profile is smooth; and across an energy window, with
a warning, when ``t_breakpoints`` declare discontinuities. ``n_jobs`` and the
cumulative traversal play no role on this route, and on a smooth profile the tighter
of ``rtol`` and ``atol`` is its tolerance. Cannot be combined with
``return_evolution_operator``. Default: False.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`.
**The engine keywords travel this way and so appear in no signature above.** The
ones worth knowing are listed in :data:`magnus.oscprob.PASSTHROUGH_KWARGS_DOCUMENTED`; three of
them decide whether an answer on a hard profile is right at all:
``t_breakpoints``
Positions at which to place slab edges, filling in between. **This is the
parameter for a density jump, a kink or a shock front** -- no number of slabs
fixes a slab that straddles one. Not to be confused with ``t_slab_edges``
above, which is the *complete* set of edges and is rarely what is wanted.
``n_slabs``
An explicit slab count. On a single request over a large accumulated phase,
``rtol`` is not the dial that moves the answer; the refinement ladder can run
to its ceiling and warn, and the slab count is what to set instead.
``cumulative``
Reuse one propagation across a scan of baselines rather than re-propagating
for each. ``'auto'`` by default, which engages it on scans large enough to
benefit.
A misspelling is rejected here, naming the near match, rather than several hops
away by a function the caller never invoked.
Returns
-------
float, np.ndarray, or tuple
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are
given) for each (energy, L) point. With
``return_evolution_operator=True``, the pair ``(P, U)``.
"""
if validate_input:
if not callable(H_func):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": H_func must be a function of (energy, l, VCC) or of (energy, l).")
n_params_H = _n_required_params(H_func)
if n_params_H not in (2, 3):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": H_func must be a function of either three arguments (energy, l, VCC) or" + \
" two arguments (energy, l); the provided H_func takes " + \
str(n_params_H) + " argument(s).")
if strategy not in ('auto', 'hybrid', 'magnus'):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \
": strategy must be 'auto', 'hybrid', or 'magnus'.")
if return_evolution_operator:
_check_operator_request(average, strategy, source_func_name)
n_params_H = _n_required_params(H_func)
if n_params_H == 3:
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
return H_func(enu, l, VCC_func(l))
else:
def htot(enu: Union[int, float], l: Union[int, float, np.ndarray]) -> np.ndarray:
return H_func(enu, l)
# Hybrid strategy (adiabatic transport + Magnus patch at any non-adiabatic window; see
# _osc_prob_hybrid_dispatch_generic and :doc:`/adiabatic_strategy`). Falls back transparently
# (returns NotImplemented) if it does not apply -- in particular, this is essentially always
# the case for osc_prob_earth, since t_breakpoints (the PREM layer crossings) is virtually
# never empty for a real trajectory -- or (with strategy == 'auto' only) fails to certify.
#
# cumulative is popped out of kwargs first, for the reason given in the three scenario
# wrappers: every dispatcher declines on an unrecognized entry in kwargs, so leaving it
# there made passing cumulative -- even the default 'auto' -- silently disable this
# strategy instead of configuring the scan. An explicit True still stands aside here,
# since it names one engine and is documented to raise rather than be substituted for.
cumulative = kwargs.pop('cumulative', 'auto')
# The same one-per-call profile scan the scenario wrappers run; the blind spot is a property
# of the grids, not of which entry point built the Hamiltonian.
_hidden = _scan_for_hidden_features(VCC_func, L0, L, t_breakpoints)
# As above: reported through strategy_info only, so the default path pays nothing. htot is
# a function of (energy, position) in both branches here, so it is never energy-only.
_osc = (_sampling_report(htot, False, energy, L, L0)
if strategy_info is not None else None)
# Watched as a unit, as in the three scenario wrappers, so that a user-supplied Hamiltonian
# gets the same answer to "which engine answered, and what stood aside" as a built-in one.
with _engine_probe(disabled=_OPERATOR_ONLY_FROM_LADDER if return_evolution_operator else (),
info=strategy_info, extra={'hidden_feature': _hidden, 'sampling': _osc}):
# Phase average, as in the three scenario wrappers: answered before any engine
# that would resolve the phases the average discards. Dispatched from here rather
# than through osc_prob_energy_baseline, so that its errors and warnings name the
# function the caller called.
P_avg = _avg_prob_dispatch(htot, False, energy, L, L0, nu_i, nu_f, average,
source_func_name,
smooth_profile=(t_breakpoints is None or len(np.atleast_1d(t_breakpoints)) == 0),
engine_kwargs=dict(t_breakpoints=t_breakpoints, magnus_exp_order=magnus_exp_order,
n_jobs=n_jobs, integration_method=integration_method, rtol=rtol, atol=atol,
validate_input=validate_input, verbose=verbose, cumulative=cumulative,
symmetric_over=symmetric_over, kwargs=kwargs),
average_spread=average_spread)
if P_avg is not NotImplemented:
return P_avg
P_hybrid = (NotImplemented if cumulative is True else
_osc_prob_hybrid_dispatch_generic(htot, VCC_func, energy, L, L0, nu_i, nu_f,
t_breakpoints, rtol, atol, magnus_exp_order, integration_method, strategy,
kwargs))
# strategy='auto' handed the request to the ladder, which runs at a tenth of the
# tolerance and above a slab floor (issue #70; see osc_prob_matter_std_potential).
if isinstance(P_hybrid, _PreferLadder):
rtol, atol, n_floor = P_hybrid.request(rtol, atol, kwargs.get('min_n_slabs'),
kwargs.get('max_n_slabs'), integration_method)
kwargs = dict(kwargs, min_n_slabs=n_floor)
elif P_hybrid is not NotImplemented:
return P_hybrid
return osc_prob_energy_baseline(htot, energy, L, L0, nu_i, nu_f, False,
return_evolution_operator=return_evolution_operator,
t_breakpoints=t_breakpoints, magnus_exp_order=magnus_exp_order, n_jobs=n_jobs,
integration_method=integration_method, rtol=rtol, atol=atol,
validate_input=validate_input, verbose=verbose, cumulative=cumulative,
symmetric_over=symmetric_over, **kwargs)
#-----------------------------------------------------------------------
# In matter, standard oscillations, in the Sun
#-----------------------------------------------------------------------
def _is_exponential(density_profile) -> bool:
return str(density_profile).strip().lower() == solarmodels.EXPONENTIAL
def _solar_profile(density_profile, ratio_number_neutrons_to_protons, source_func_name: str):
r"""The density profile and neutron-to-proton ratio a Sun wrapper passes on.
For the exponential fit, ``(None, ratio)``: the ``_exp_density`` wrapper then builds the
profile exactly as it always has, and a ratio left unset is 1.0, as it always was. For a
standard solar model, its tabulated electron density, and the ratio as given or, left unset,
the model's own composition (:mod:`magnus.solarmodels`).
.. versionadded:: 1.1.1
"""
if _is_exponential(density_profile):
return None, (1.0 if ratio_number_neutrons_to_protons is None
else ratio_number_neutrons_to_protons)
try:
name = solarmodels.canonical_name(density_profile)
except ValueError as e:
raise ValueError(str(e).replace(" solarmodels:", " oscprob." + source_func_name +
": density_profile:", 1)) from None
ratio = (solarmodels.neutron_to_proton_ratio_profile(name)
if ratio_number_neutrons_to_protons is None else ratio_number_neutrons_to_protons)
return solarmodels.electron_density_profile(name), ratio
def _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, source_func_name: str):
r"""Which baselines reach past a solar model's last tabulated radius, when that is refused.
Returns ``(L, None)`` unless ``stop_at_table_edge`` is set and some baseline does. Then each
such baseline is replaced by ``L0``, a path of zero length that costs nothing and keeps the
call's shape, and the second value marks the results :func:`_refuse_past_table_edge` must
blank; a warning says how many and where the edge is. A path that *starts* past the edge
has nothing inside the table to compute, and is refused outright.
.. versionadded:: 1.1.1
"""
if not stop_at_table_edge:
return L, None
if _is_exponential(density_profile):
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": "
"stop_at_table_edge applies to a tabulated standard solar model, and the exponential "
"fit has no last row to stop at. Name one through density_profile -- " +
", ".join(solarmodels.SOLAR_MODELS) + " -- or leave stop_at_table_edge at False.")
name = solarmodels.canonical_name(density_profile)
edge, r_edge = solarmodels.table_edge(name), solarmodels.solar_model_info(name)['r_max']
if float(L0) > edge:
raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": the path "
"starts past the last tabulated radius of the " + name + " solar model, " +
format(r_edge, '.4g') + " R_sun, so with stop_at_table_edge=True there is nothing "
"inside the table to compute. Start the path inside it, or leave "
"stop_at_table_edge at False to continue the profile past the table.")
L_arr = np.asarray(L, dtype=float)
beyond = L_arr > edge
if not np.any(beyond):
return L, None
warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": " +
str(int(np.sum(beyond))) + " of " + str(int(np.size(beyond))) + " baseline(s) reach past "
"the last tabulated radius of the " + name + " solar model, " + format(r_edge, '.4g') +
" R_sun, and stop_at_table_edge=True returns NaN there instead of a probability. Leave "
"it False to continue the profile past the table along the slope of its last interval, "
"or use a model tabulated to the surface (B16 and B23 are).",
SolarModelRangeWarning, stacklevel=3)
L_eff = np.where(beyond, float(L0), L_arr)
return (float(L_eff) if np.ndim(L_eff) == 0 else L_eff), beyond
def _refuse_past_table_edge(P, beyond):
r"""Blank the results :func:`_stop_at_table_edge` marked, leaving the rest as computed.
``P`` is whatever the wrapper returns -- a probability, a matrix, one per point, or the pair
``(P, U)`` -- and its first axis runs over the points when there is more than one.
.. versionadded:: 1.1.1
"""
if beyond is None:
return P
if isinstance(P, tuple):
return tuple(_refuse_past_table_edge(x, beyond) for x in P)
out = np.array(P, dtype=complex if np.iscomplexobj(P) else float)
b = np.asarray(beyond)
if b.size == 1 or out.ndim == 0:
if bool(np.any(b)):
out[...] = np.nan
elif out.shape[0] == b.size:
out[b.ravel()] = np.nan
else:
out[...] = np.nan
return out[()] if out.ndim == 0 else out
[docs]
def osc_prob_2nu_sun(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability
for neutrinos inside the Sun.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\nu_e` on
electrons.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Standard two-neutrino oscillations from the center of the Sun to
90% of its radius:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
sth = gd.S12_NO_BF_NUFIT_6_0
Dm2 = gd.D21_NO_BF_NUFIT_6_0
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_2nu_sun(energy, L, L0, sth, Dm2)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``.
.. note::
Dispatches to a fast, closed-form interaction-picture Magnus
integrator whenever the accumulated matter phase stays small enough
to certify (see ``_osc_prob_ip_exp_dispatch``), giving warning-free
results in a fraction of a second across the realistic solar-neutrino
energy range for baselines up to a few e-folds of ``l_scale``. Longer
baselines fall back transparently to the general slab-refinement
method.
.. note::
With the default ``strategy='auto'``, this also tries the more general
adiabatic-transport-plus-Magnus-patch hybrid strategy (see
:func:`magnus.adiabatic.hybrid_propagator` and :doc:`/adiabatic_strategy`) for baselines
beyond the interaction-picture integrator's reach (e.g., low-energy neutrinos over most
of the Sun's radius), before falling back to the general slab-refinement method.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid', or
'magnus'; see the ``strategy`` parameter of :func:`osc_prob_matter_std_potential` for the
full description and :doc:`/adiabatic_strategy` for the derivation and validation.
Default: 'auto'.
.. versionadded:: 1.0.0
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
_rho, _ = _solar_profile(density_profile, None, 'osc_prob_2nu_sun')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_2nu_sun')
P = osc_prob_2nu_matter_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
sth=sth,
Dm2=Dm2,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_3nu_sun(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability
for neutrinos inside the Sun.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\nu_e` on
electrons.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Standard three-neutrino oscillations through the Sun, using the
NuFIT 6.1 defaults for the oscillation parameters:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_3nu_sun(energy, L, L0)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, _ = _solar_profile(density_profile, None, 'osc_prob_3nu_sun')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_3nu_sun')
P = osc_prob_3nu_matter_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
default_osc_params_set_name=default_osc_params_set_name,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_4nu_sun(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino (3+1) oscillation
probability for neutrinos inside the Sun.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\nu_e` on
electrons.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Four-neutrino (3+1 sterile) oscillations through the Sun, with a
modest sterile mixing on top of the NuFIT 6.1 active-sector defaults:
.. jupyter-execute::
import warnings
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_4nu_sun(energy, L, L0, s14=0.1, s24=0.05, s34=0.02,
d14=np.radians(10.0), d24=np.radians(20.0), D41=0.1)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``. ``ratio_number_neutrons_to_protons``
defaults to None: 1.0 with the exponential profile, as before, and the model's own
composition with a standard solar model.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
ratio_number_neutrons_to_protons : int, float, Callable, or None, optional
:math:`r = n_n/n_p` of the medium, a number or a function of position. Scales the
sterile states' entry in the matter term; see
:func:`magnus.matter.matter_potential_projector`. Default: None, which means 1.0
(isoscalar matter, i.e. :math:`Y_e = 0.5`) with the exponential profile, as before,
and the model's own composition with a standard solar model:
:math:`r = (1 - X)/(1 + X)` at every radius, from its hydrogen mass fraction :math:`X`
(see :func:`magnus.solarmodels.neutron_to_proton_ratio_profile`). A value passed here
is used as given, with either profile.
**The Sun is not isoscalar.** It is hydrogen-rich, so :math:`Y_e = (1 + X)/2` runs
from about 0.68 at the center to 0.88 near the surface, and :math:`r` from about 0.47
down to 0.14 -- nowhere near 1.0, unlike the Earth where the isoscalar value at least
sits among the layers. The exponential profile is a fit to the electron *number*
density and carries no composition to derive :math:`r` from, so 1.0 is kept there as
before; naming a standard solar model through ``density_profile`` supplies one. Left
at 1.0 the averaged survival probability moves by about 4e-03 at
:math:`\sin\theta_{14} = 0.4`, above the default tolerance. Three flavors are
unaffected -- the projector's sterile block is empty.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). With the sterile states it also supplies the composition; see
``ratio_number_neutrons_to_protons``. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, ratio_number_neutrons_to_protons = _solar_profile(density_profile, ratio_number_neutrons_to_protons, 'osc_prob_4nu_sun')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_4nu_sun')
P = osc_prob_4nu_matter_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s14=s14,
s24=s24,
s34=s34,
d14=d14,
d24=d24,
D41=D41,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
default_osc_params_set_name=default_osc_params_set_name,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_5nu_sun(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino (3+2) oscillation
probability for neutrinos inside the Sun.
Assumes that the matter potential is due only to the standard
charged-current coherent forward scattering of :math:`\nu_e` on
electrons.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Five-neutrino (3+2 sterile) oscillations through the Sun, with
modest sterile mixing on top of the NuFIT 6.1 active-sector defaults:
.. jupyter-execute::
import warnings
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_5nu_sun(energy, L, L0,
s14=0.1, s15=0.05, s24=0.05, s25=0.02, s34=0.02, s35=0.01,
d14=np.radians(10.0), d15=np.radians(15.0),
d24=np.radians(20.0), d35=np.radians(25.0),
D41=0.1, D51=0.05)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``. ``ratio_number_neutrons_to_protons``
defaults to None: 1.0 with the exponential profile, as before, and the model's own
composition with a standard solar model.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
ratio_number_neutrons_to_protons : int, float, Callable, or None, optional
:math:`r = n_n/n_p` of the medium, a number or a function of position. Scales the
sterile states' entry in the matter term; see
:func:`magnus.matter.matter_potential_projector`. Default: None, which means 1.0
(isoscalar matter, i.e. :math:`Y_e = 0.5`) with the exponential profile, as before,
and the model's own composition with a standard solar model:
:math:`r = (1 - X)/(1 + X)` at every radius, from its hydrogen mass fraction :math:`X`
(see :func:`magnus.solarmodels.neutron_to_proton_ratio_profile`). A value passed here
is used as given, with either profile.
**The Sun is not isoscalar.** It is hydrogen-rich, so :math:`Y_e = (1 + X)/2` runs
from about 0.68 at the center to 0.88 near the surface, and :math:`r` from about 0.47
down to 0.14 -- nowhere near 1.0, unlike the Earth where the isoscalar value at least
sits among the layers. The exponential profile is a fit to the electron *number*
density and carries no composition to derive :math:`r` from, so 1.0 is kept there as
before; naming a standard solar model through ``density_profile`` supplies one. Left
at 1.0 the averaged survival probability moves by about 4e-03 at
:math:`\sin\theta_{14} = 0.4`, above the default tolerance. Three flavors are
unaffected -- the projector's sterile block is empty.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). With the sterile states it also supplies the composition; see
``ratio_number_neutrons_to_protons``. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, ratio_number_neutrons_to_protons = _solar_profile(density_profile, ratio_number_neutrons_to_protons, 'osc_prob_5nu_sun')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_5nu_sun')
P = osc_prob_5nu_matter_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s14=s14,
s15=s15,
s24=s24,
s25=s25,
s34=s34,
s35=s35,
d14=d14,
d15=d15,
d24=d24,
d35=d35,
D41=D41,
D51=D51,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
default_osc_params_set_name=default_osc_params_set_name,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_sun(
H_func: Callable,
energy: Union[int, float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
magnus_exp_order: Optional[int]=4,
n_jobs: Optional[int]=1,
integration_method: Optional[str]='gl',
rtol: Optional[Union[int, float]]=1.e-3,
atol: Optional[Union[int, float]]=1.e-3,
validate_input: Optional[bool]=True,
verbose: Optional[int]=0,
strategy: Optional[str]='auto',
strategy_info: Optional[Dict]=None,
average: Optional[bool]=False,
average_spread: Optional[float]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs
) -> Union[float, np.ndarray]:
r"""Compute and return the neutrino oscillation probability inside
the Sun for a given arbitrary Hamiltonian.
Does **not** assume standard oscillations nor a given number of
neutrino flavors: the user supplies their own Hamiltonian function,
``H_func``, and this routine provides the solar electron density
along the (radial) trajectory.
``H_func`` must be a function of either three arguments,
``H_func(energy, l, VCC)``, or two arguments,
``H_func(energy, l)``, returning a square complex NumPy array (the
Hamiltonian in the flavor basis, in eV). In the three-argument
form, ``VCC`` is the charged-current matter potential
:math:`V_{\rm CC} = \sqrt{2} G_F N_e` [eV] at radial position
``l``; its sign is already flipped for antineutrinos
(``nubar=True``). For extra speed, ``H_func`` may accept an array
of positions ``l`` and return a stack of Hamiltonians with the
position axis leading; this is detected automatically.
The neutrino travels radially outward from ``L0`` to ``L`` (both in
:math:`\text{eV}^{-1}`, measured from the center of the Sun).
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``average``, ``density_profile`` and ``stop_at_table_edge``.
Parameters
----------
H_func : Callable
The Hamiltonian, as ``H_func(energy, l, VCC)`` or ``H_func(energy, l)``; see above.
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Final radial position(s) [:math:`\text{eV}^{-1}`], measured from the center of the Sun.
L0 : int or float, optional
Initial radial position [:math:`\text{eV}^{-1}`]. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos (flips the sign of the solar matter
potential passed to ``H_func``). Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned
instead of the full probability matrix.
nu_f : int, optional
Final flavor index; see ``nu_i``.
magnus_exp_order : int, optional
Highest order of the Magnus expansion. Default: 4.
n_jobs : int, optional
Number of parallel joblib workers. Default: 1.
integration_method : str, optional
'gl', 'trapezoid', or 'simpson'. Default: 'gl'.
rtol, atol : int or float, optional
Target relative/absolute tolerance for the adaptive slab refinement. With
``average=True`` on a smooth profile, the tighter of the two is the tolerance of the
phase average instead; see :func:`osc_prob_energy_baseline`. Default: 1e-3 each.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
verbose : int, optional
Verbosity level. Default: 0.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default), 'hybrid',
or 'magnus'; see the ``strategy`` parameter of :func:`osc_prob_matter_std_potential` for
the full description and :doc:`/adiabatic_strategy` for the derivation and validation
(adiabatic transport with a Magnus patch at any non-adiabatic window, applicable to any
``H_func`` regardless of its internal structure). Default: 'auto'.
.. versionadded:: 1.0.0
\**kwargs
Additional arguments forwarded to :func:`osc_prob_energy_baseline`/:func:`osc_prob`
(e.g., the refinement-loop bounds).
strategy_info : dict, optional
If given, filled in place with which engine actually answered, following the
same out-parameter convention as ``convergence_info`` in :func:`osc_prob`.
average : bool, optional
If True, return the phase average of the probability over a relative energy spread (see
:mod:`magnus.avgprob`) instead of the oscillating one, as the wrappers do with the
same keyword; ``strategy`` is then not consulted. Which route answers follows from
the Hamiltonian: a function of the energy alone is averaged in closed form, one
eigendecomposition per energy; a function of position is averaged by adiabatic
transport along its instantaneous eigenstates, with a Magnus patch across every
non-adiabatic crossing, when the profile is smooth; and across an energy window, with
a warning, when ``t_breakpoints`` declare discontinuities. ``n_jobs`` and the
cumulative traversal play no role on this route, and on a smooth profile the tighter
of ``rtol`` and ``atol`` is its tolerance. Cannot be combined with
``return_evolution_operator``. Default: False.
average_spread : float, optional
Relative energy spread :math:`\sigma` of the phase average ``average=True`` returns:
every interference term keeps its phase and is weighted by
:math:`e^{-\sigma^2\phi'^2/2}`, :math:`\phi' = d\phi/d\ln E` (see
:data:`magnus.avgprob.AVG_PHASE_SPREAD`). Ignored without ``average``. Default:
None, meaning 0.1.
density_profile : str, optional
The Sun's electron density, which sets the ``VCC`` passed to ``H_func``. ``'exp'``,
the default, is the exponential fit described above. The name of a standard solar
model -- one of :data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any
case -- uses that model's tabulated profile instead: interpolated in the logarithm of
the density, held flat below the first tabulated radius, and continued past the last
along the slope of the last interval (see :mod:`magnus.solarmodels`). Only the
electron density reaches ``H_func``; a Hamiltonian with sterile states that wants the
model's composition as well can take it from
:func:`magnus.solarmodels.neutron_to_proton_ratio_profile`. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for
each energy.
Examples
--------
Standard two-neutrino oscillations, written by hand (the dedicated
wrapper :func:`osc_prob_2nu_sun` does this internally):
.. jupyter-execute::
import warnings
import numpy as np
import magnus.oscprob as oscprob
import magnus.hamiltonians as hamiltonians
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
sth = gd.S12_NO_BF_NUFIT_6_0
Dm2 = gd.D21_NO_BF_NUFIT_6_0
h_vac = hamiltonians.hamiltonian_2nu_vacuum_energy_independent(sth, Dm2)
# Array-capable: VCC[..., None, None] broadcasts one potential per
# position over a stack of matrices, keeping the vectorized path.
e00 = np.diag([1.0, 0.0])
def H(energy, l, VCC):
return (1 / energy) * h_vac + np.asarray(VCC)[..., None, None] * e00
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_sun(H, energy=1.0 * gd.UNIT_GEV,
L=0.9 * gd.SUN_RADIUS * gd.UNIT_KM)
P
"""
source_func_name = sys._getframe().f_code.co_name
_rho, _ = _solar_profile(density_profile, None, source_func_name)
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0,
source_func_name)
# Solar electron number density [eV^3] along the radial trajectory; the antineutrino sign
# flip of the potential is applied inside matter.vcc_func_from_rho_func. The profile
# evaluations are cached on repeated position grids.
VCC_func = matter.vcc_func_from_rho_func(
rho_func=((lambda l: matter.density_matter_func_exp(l, gd.NUM_DENSITY_E_SUN_CENTRAL,
gd.L_SCALE_SUN)) if _rho is None else _rho), # [eV^3] (l in eV^{-1})
L0=L0,
nubar=nubar,
density_is_of_number_of_electrons=True) # [eV]
VCC_func = _PositionProfileCache(VCC_func)
# Popped rather than left in kwargs: the positional slot below is t_breakpoints, so a
# caller who supplied one collided with the None passed here. osc_prob_earth already
# pops it; this path did not.
t_breakpoints = kwargs.pop('t_breakpoints', None)
P = _osc_prob_with_potential(source_func_name, H_func, VCC_func, energy, L, L0, nu_i,
nu_f, t_breakpoints, magnus_exp_order, n_jobs, integration_method, rtol, atol,
validate_input, verbose, strategy=strategy, strategy_info=strategy_info,
average=average, average_spread=average_spread, **kwargs)
return _refuse_past_table_edge(P, _beyond)
#-----------------------------------------------------------------------
# In matter, NSI, constant density
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_matter_nsi_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
eps_aa: Optional[Union[int, float]]=0.0,
eps_ab: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
matter with a constant density profile, including non-standard
interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
eps_aa : int or float, optional
Non-universal diagonal NSI coupling of nu_e (relative to nu_mu, whose diagonal coupling is fixed to 0 by convention); see ``hamiltonians.hamiltonian_2nu_nsi``. Default: 0.0.
eps_ab : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_matter_nsi(
num_flavors=2,
rho_func=rho,
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
nsi_params={'eps_aa': eps_aa, 'eps_ab': eps_ab},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=None,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_matter_nsi_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
matter with a constant density profile, including non-standard
interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_matter_nsi(
num_flavors=3,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_mm': eps_mm,
'eps_mt': eps_mt, 'eps_tt': eps_tt},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=None,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_matter_nsi_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts: Optional[Union[int, float]]=0.0,
eps_ss: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability in
matter with a constant density profile, including non-standard
interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es : int or float, optional
Flavor-off-diagonal (nu_e-nu_s) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s) NSI coupling. Default: 0.0.
eps_ss : int or float, optional
Diagonal NSI coupling of nu_s. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_matter_nsi(
num_flavors=4,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_es': eps_es,
'eps_mm': eps_mm, 'eps_mt': eps_mt, 'eps_ms': eps_ms, 'eps_tt': eps_tt,
'eps_ts': eps_ts, 'eps_ss': eps_ss},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=None,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_matter_nsi_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es1: Optional[Union[int, float]]=0.0,
eps_es2: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms1: Optional[Union[int, float]]=0.0,
eps_ms2: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts1: Optional[Union[int, float]]=0.0,
eps_ts2: Optional[Union[int, float]]=0.0,
eps_s1s1: Optional[Union[int, float]]=0.0,
eps_s1s2: Optional[Union[int, float]]=0.0,
eps_s2s2: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability in
matter with a constant density profile, including non-standard
interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es1 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s1) NSI coupling. Default: 0.0.
eps_es2 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s2) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms1 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s1) NSI coupling. Default: 0.0.
eps_ms2 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s2) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts1 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s1) NSI coupling. Default: 0.0.
eps_ts2 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s2) NSI coupling. Default: 0.0.
eps_s1s1 : int or float, optional
Diagonal NSI coupling of nu_s1. Default: 0.0.
eps_s1s2 : int or float, optional
Flavor-off-diagonal (nu_s1-nu_s2) NSI coupling. Default: 0.0.
eps_s2s2 : int or float, optional
Diagonal NSI coupling of nu_s2. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_matter_nsi(
num_flavors=5,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_es1': eps_es1,
'eps_es2': eps_es2, 'eps_mm': eps_mm, 'eps_mt': eps_mt, 'eps_ms1': eps_ms1,
'eps_ms2': eps_ms2, 'eps_tt': eps_tt, 'eps_ts1': eps_ts1, 'eps_ts2': eps_ts2,
'eps_s1s1': eps_s1s1, 'eps_s1s2': eps_s1s2, 'eps_s2s2': eps_s2s2},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
new_recursion_limit=None,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, NSI, exponentially falling density
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_matter_nsi_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
eps_aa: Optional[Union[int, float]]=0.0,
eps_ab: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
matter with an exponentially falling density profile, including
non-standard interactions (NSI).
.. versionadded:: 1.0.0
.. note::
Dispatches to a fast, closed-form interaction-picture Magnus
integrator whenever the accumulated matter phase stays small enough
to certify (see ``_osc_prob_ip_exp_dispatch``), giving warning-free
results in a fraction of a second across the realistic solar-neutrino
energy range for baselines up to a few e-folds of ``l_scale`` (the NSI
couplings are folded into the same fast path). Longer baselines fall
back transparently to the general slab-refinement method.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
eps_aa : int or float, optional
Non-universal diagonal NSI coupling of nu_e (relative to nu_mu, whose diagonal coupling is fixed to 0 by convention); see ``hamiltonians.hamiltonian_2nu_nsi``. Default: 0.0.
eps_ab : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_2nu_matter_nsi_exp_density: rho_central and l_scale must be " + \
"non-negative.")
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_matter_nsi(
num_flavors=2,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
nsi_params={'eps_aa': eps_aa, 'eps_ab': eps_ab},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_matter_nsi_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
matter with an exponentially falling density profile, including
non-standard interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_3nu_matter_nsi_exp_density: rho_central and l_scale must be " + \
"non-negative.")
return osc_prob_matter_nsi(
num_flavors=3,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_mm': eps_mm,
'eps_mt': eps_mt, 'eps_tt': eps_tt},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_matter_nsi_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts: Optional[Union[int, float]]=0.0,
eps_ss: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino (3+1) oscillation
probability in matter with an exponentially falling density profile,
including non-standard interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es : int or float, optional
Flavor-off-diagonal (nu_e-nu_s) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s) NSI coupling. Default: 0.0.
eps_ss : int or float, optional
Diagonal NSI coupling of nu_s. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_4nu_matter_nsi_exp_density: rho_central and l_scale must be " + \
"non-negative.")
return osc_prob_matter_nsi(
num_flavors=4,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_es': eps_es,
'eps_mm': eps_mm, 'eps_mt': eps_mt, 'eps_ms': eps_ms, 'eps_tt': eps_tt,
'eps_ts': eps_ts, 'eps_ss': eps_ss},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_matter_nsi_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es1: Optional[Union[int, float]]=0.0,
eps_es2: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms1: Optional[Union[int, float]]=0.0,
eps_ms2: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts1: Optional[Union[int, float]]=0.0,
eps_ts2: Optional[Union[int, float]]=0.0,
eps_s1s1: Optional[Union[int, float]]=0.0,
eps_s1s2: Optional[Union[int, float]]=0.0,
eps_s2s2: Optional[Union[int, float]]=0.0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino (3+2) oscillation
probability in matter with an exponentially falling density profile,
including non-standard interactions (NSI).
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es1 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s1) NSI coupling. Default: 0.0.
eps_es2 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s2) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms1 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s1) NSI coupling. Default: 0.0.
eps_ms2 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s2) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts1 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s1) NSI coupling. Default: 0.0.
eps_ts2 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s2) NSI coupling. Default: 0.0.
eps_s1s1 : int or float, optional
Diagonal NSI coupling of nu_s1. Default: 0.0.
eps_s1s2 : int or float, optional
Flavor-off-diagonal (nu_s1-nu_s2) NSI coupling. Default: 0.0.
eps_s2s2 : int or float, optional
Diagonal NSI coupling of nu_s2. Default: 0.0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_5nu_matter_nsi_exp_density: rho_central and l_scale must be " + \
"non-negative.")
return osc_prob_matter_nsi(
num_flavors=5,
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_es1': eps_es1,
'eps_es2': eps_es2, 'eps_mm': eps_mm, 'eps_mt': eps_mt, 'eps_ms1': eps_ms1,
'eps_ms2': eps_ms2, 'eps_tt': eps_tt, 'eps_ts1': eps_ts1, 'eps_ts2': eps_ts2,
'eps_s1s1': eps_s1s1, 'eps_s1s2': eps_s1s2, 'eps_s2s2': eps_s2s2},
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, NSI, in the Earth
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_earth_nsi(
energy: Union[int, float, list, np.ndarray],
sth: Union[int, float],
Dm2: Union[int, float],
eps_aa: Optional[Union[int, float]]=0.0,
eps_ab: Optional[Union[int, float]]=0.0,
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, including
non-standard interactions (NSI).
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Two-neutrino oscillations through the Earth with non-standard
interactions:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
sth = gd.S12_NO_BF_NUFIT_6_0
Dm2 = gd.D21_NO_BF_NUFIT_6_0
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The small solar mass splitting Dm2 combined with this Earth baseline
# means the adaptive refinement needs a few loops; this is the expected,
# informational MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_2nu_earth_nsi(energy, sth, Dm2, eps_aa=0.05, eps_ab=0.02,
costhz=costhz, L=baseline)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
eps_aa : int or float, optional
Non-universal diagonal NSI coupling of nu_e (relative to nu_mu, whose diagonal coupling is fixed to 0 by convention); see ``hamiltonians.hamiltonian_2nu_nsi``. Default: 0.0.
eps_ab : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=2,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_nsi(
num_flavors=2,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'sth': sth, 'Dm2': Dm2},
nsi_params={'eps_aa': eps_aa, 'eps_ab': eps_ab},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_earth_nsi(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, including
non-standard interactions (NSI).
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Three-neutrino oscillations through the Earth with non-standard
interactions, using the NuFIT 6.1 defaults for the standard
oscillation parameters:
.. jupyter-execute::
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_3nu_earth_nsi(energy, costhz=costhz, L=baseline,
eps_ee=0.05, eps_em=-0.03, eps_et=0.01)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=3,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_nsi(
num_flavors=3,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_mm': eps_mm,
'eps_mt': eps_mt, 'eps_tt': eps_tt},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_earth_nsi(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts: Optional[Union[int, float]]=0.0,
eps_ss: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, including
non-standard interactions (NSI).
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Four-neutrino (3+1 sterile) oscillations through the Earth with
non-standard interactions, including the sterile-flavor couplings:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_4nu_earth_nsi(
energy, costhz=costhz, L=baseline,
s14=0.1, s24=0.05, s34=0.02, d14=np.radians(10.0), d24=np.radians(20.0), D41=0.1,
eps_ee=0.05, eps_em=-0.03, eps_et=0.01, eps_es=0.02, eps_ms=0.01, eps_ts=0.01)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es : int or float, optional
Flavor-off-diagonal (nu_e-nu_s) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s) NSI coupling. Default: 0.0.
eps_ss : int or float, optional
Diagonal NSI coupling of nu_s. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=4,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_nsi(
num_flavors=4,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_es': eps_es,
'eps_mm': eps_mm, 'eps_mt': eps_mt, 'eps_ms': eps_ms, 'eps_tt': eps_tt,
'eps_ts': eps_ts, 'eps_ss': eps_ss},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_earth_nsi(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es1: Optional[Union[int, float]]=0.0,
eps_es2: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms1: Optional[Union[int, float]]=0.0,
eps_ms2: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts1: Optional[Union[int, float]]=0.0,
eps_ts2: Optional[Union[int, float]]=0.0,
eps_s1s1: Optional[Union[int, float]]=0.0,
eps_s1s2: Optional[Union[int, float]]=0.0,
eps_s2s2: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, including
non-standard interactions (NSI).
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Five-neutrino (3+2 sterile) oscillations through the Earth with
non-standard interactions, including the sterile-flavor couplings:
.. jupyter-execute::
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_5nu_earth_nsi(
energy, costhz=costhz, L=baseline,
s14=0.1, s15=0.05, s24=0.05, s25=0.02, s34=0.02, s35=0.01,
d14=np.radians(10.0), d15=np.radians(15.0), d24=np.radians(20.0), d35=np.radians(25.0),
D41=0.1, D51=0.05,
eps_ee=0.05, eps_em=-0.03, eps_et=0.01, eps_es1=0.02, eps_es2=0.01,
eps_ms1=0.01, eps_ms2=0.01, eps_ts1=0.01, eps_ts2=0.01)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es1 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s1) NSI coupling. Default: 0.0.
eps_es2 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s2) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms1 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s1) NSI coupling. Default: 0.0.
eps_ms2 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s2) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts1 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s1) NSI coupling. Default: 0.0.
eps_ts2 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s2) NSI coupling. Default: 0.0.
eps_s1s1 : int or float, optional
Diagonal NSI coupling of nu_s1. Default: 0.0.
eps_s1s2 : int or float, optional
Flavor-off-diagonal (nu_s1-nu_s2) NSI coupling. Default: 0.0.
eps_s2s2 : int or float, optional
Diagonal NSI coupling of nu_s2. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=5,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_matter_nsi(
num_flavors=5,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
nsi_params={'eps_ee': eps_ee, 'eps_em': eps_em, 'eps_et': eps_et, 'eps_es1': eps_es1,
'eps_es2': eps_es2, 'eps_mm': eps_mm, 'eps_mt': eps_mt, 'eps_ms1': eps_ms1,
'eps_ms2': eps_ms2, 'eps_tt': eps_tt, 'eps_ts1': eps_ts1, 'eps_ts2': eps_ts2,
'eps_s1s1': eps_s1s1, 'eps_s1s2': eps_s1s2, 'eps_s2s2': eps_s2s2},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, NSI, in the Sun
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_sun_nsi(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
eps_aa: Optional[Union[int, float]]=0.0,
eps_ab: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability
for neutrinos inside the Sun, including non-standard interactions
(NSI).
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Two-neutrino oscillations through the Sun with non-standard
interactions:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
sth = gd.S12_NO_BF_NUFIT_6_0
Dm2 = gd.D21_NO_BF_NUFIT_6_0
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_2nu_sun_nsi(energy, L, L0, sth, Dm2, eps_aa=0.05, eps_ab=0.02)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``.
.. note::
Dispatches to a fast, closed-form interaction-picture Magnus
integrator whenever the accumulated matter phase stays small enough
to certify (see ``_osc_prob_ip_exp_dispatch``), giving warning-free
results in a fraction of a second across the realistic solar-neutrino
energy range for baselines up to a few e-folds of ``l_scale`` (the NSI
couplings are folded into the same fast path). Longer baselines fall
back transparently to the general slab-refinement method.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
eps_aa : int or float, optional
Non-universal diagonal NSI coupling of nu_e (relative to nu_mu, whose diagonal coupling is fixed to 0 by convention); see ``hamiltonians.hamiltonian_2nu_nsi``. Default: 0.0.
eps_ab : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angle is stated: ``'sin'`` (default) its sine,
``'sin2'`` its sine *squared* -- which is what global fits report --
``'rad'`` the angle itself in radians, or ``'deg'`` in degrees. Any other
value raises.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, _ = _solar_profile(density_profile, None, 'osc_prob_2nu_sun_nsi')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_2nu_sun_nsi')
P = osc_prob_2nu_matter_nsi_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
sth=sth,
Dm2=Dm2,
eps_aa=eps_aa,
eps_ab=eps_ab,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_3nu_sun_nsi(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability
for neutrinos inside the Sun.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Three-neutrino oscillations through the Sun with non-standard
interactions, using the NuFIT 6.1 defaults for the standard
oscillation parameters:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_3nu_sun_nsi(energy, L, L0, eps_ee=0.05, eps_em=-0.03, eps_et=0.01)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phase is read as degrees too; under the other three
it stays in radians, a sine being no way to state a phase.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, _ = _solar_profile(density_profile, None, 'osc_prob_3nu_sun_nsi')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_3nu_sun_nsi')
P = osc_prob_3nu_matter_nsi_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
eps_ee=eps_ee,
eps_em=eps_em,
eps_et=eps_et,
eps_mm=eps_mm,
eps_mt=eps_mt,
eps_tt=eps_tt,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
default_osc_params_set_name=default_osc_params_set_name,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_4nu_sun_nsi(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts: Optional[Union[int, float]]=0.0,
eps_ss: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino (3+1) oscillation
probability for neutrinos inside the Sun.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Four-neutrino (3+1 sterile) oscillations through the Sun with
non-standard interactions, including the sterile-flavor couplings:
.. jupyter-execute::
import warnings
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_4nu_sun_nsi(
energy, L, L0,
s14=0.1, s24=0.05, s34=0.02, d14=np.radians(10.0), d24=np.radians(20.0), D41=0.1,
eps_ee=0.05, eps_em=-0.03, eps_et=0.01, eps_es=0.02, eps_ms=0.01, eps_ts=0.01)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``. ``ratio_number_neutrons_to_protons``
defaults to None: 1.0 with the exponential profile, as before, and the model's own
composition with a standard solar model.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es : int or float, optional
Flavor-off-diagonal (nu_e-nu_s) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s) NSI coupling. Default: 0.0.
eps_ss : int or float, optional
Diagonal NSI coupling of nu_s. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
ratio_number_neutrons_to_protons : int, float, Callable, or None, optional
:math:`r = n_n/n_p` of the medium, a number or a function of position. Scales the
sterile states' entry in the matter term; see
:func:`magnus.matter.matter_potential_projector`. Default: None, which means 1.0
(isoscalar matter, i.e. :math:`Y_e = 0.5`) with the exponential profile, as before,
and the model's own composition with a standard solar model:
:math:`r = (1 - X)/(1 + X)` at every radius, from its hydrogen mass fraction :math:`X`
(see :func:`magnus.solarmodels.neutron_to_proton_ratio_profile`). A value passed here
is used as given, with either profile.
**The Sun is not isoscalar.** It is hydrogen-rich, so :math:`Y_e = (1 + X)/2` runs
from about 0.68 at the center to 0.88 near the surface, and :math:`r` from about 0.47
down to 0.14 -- nowhere near 1.0, unlike the Earth where the isoscalar value at least
sits among the layers. The exponential profile is a fit to the electron *number*
density and carries no composition to derive :math:`r` from, so 1.0 is kept there as
before; naming a standard solar model through ``density_profile`` supplies one. Left
at 1.0 the averaged survival probability moves by about 4e-03 at
:math:`\sin\theta_{14} = 0.4`, above the default tolerance. Three flavors are
unaffected -- the projector's sterile block is empty.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). With the sterile states it also supplies the composition; see
``ratio_number_neutrons_to_protons``. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, ratio_number_neutrons_to_protons = _solar_profile(density_profile, ratio_number_neutrons_to_protons, 'osc_prob_4nu_sun_nsi')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_4nu_sun_nsi')
P = osc_prob_4nu_matter_nsi_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s14=s14,
s24=s24,
s34=s34,
d14=d14,
d24=d24,
D41=D41,
eps_ee=eps_ee,
eps_em=eps_em,
eps_et=eps_et,
eps_es=eps_es,
eps_mm=eps_mm,
eps_mt=eps_mt,
eps_ms=eps_ms,
eps_tt=eps_tt,
eps_ts=eps_ts,
eps_ss=eps_ss,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
default_osc_params_set_name=default_osc_params_set_name,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_5nu_sun_nsi(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
eps_ee: Optional[Union[int, float]]=0.0,
eps_em: Optional[Union[int, float]]=0.0,
eps_et: Optional[Union[int, float]]=0.0,
eps_es1: Optional[Union[int, float]]=0.0,
eps_es2: Optional[Union[int, float]]=0.0,
eps_mm: Optional[Union[int, float]]=0.0,
eps_mt: Optional[Union[int, float]]=0.0,
eps_ms1: Optional[Union[int, float]]=0.0,
eps_ms2: Optional[Union[int, float]]=0.0,
eps_tt: Optional[Union[int, float]]=0.0,
eps_ts1: Optional[Union[int, float]]=0.0,
eps_ts2: Optional[Union[int, float]]=0.0,
eps_s1s1: Optional[Union[int, float]]=0.0,
eps_s1s2: Optional[Union[int, float]]=0.0,
eps_s2s2: Optional[Union[int, float]]=0.0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino (3+2) oscillation
probability for neutrinos inside the Sun.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
Examples
--------
Five-neutrino (3+2 sterile) oscillations through the Sun with
non-standard interactions, including the sterile-flavor couplings:
.. jupyter-execute::
import warnings
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
energy = 15.0 * gd.UNIT_GEV # high enough energy for the adaptive refinement
L0 = 0.0 # to converge cleanly under the default tolerance
L = 0.9 * gd.SUN_RADIUS * gd.UNIT_KM
# A trajectory through most of the Sun accumulates a large phase, so the
# adaptive refinement needs a few loops to narrow the slabs; this is the
# expected, informational MagnusConvergenceWarning discussed in the
# package README, suppressed here to keep the example focused.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_5nu_sun_nsi(
energy, L, L0,
s14=0.1, s15=0.05, s24=0.05, s25=0.02, s34=0.02, s35=0.01,
d14=np.radians(10.0), d15=np.radians(15.0), d24=np.radians(20.0), d35=np.radians(25.0),
D41=0.1, D51=0.05,
eps_ee=0.05, eps_em=-0.03, eps_et=0.01, eps_es1=0.02, eps_es2=0.01,
eps_ms1=0.01, eps_ms2=0.01, eps_ts1=0.01, eps_ts2=0.01)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``. ``ratio_number_neutrons_to_protons``
defaults to None: 1.0 with the exponential profile, as before, and the model's own
composition with a standard solar model.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
eps_ee : int or float, optional
Diagonal NSI coupling of nu_e. Default: 0.0.
eps_em : int or float, optional
Flavor-off-diagonal (nu_e-nu_mu) NSI coupling. Default: 0.0.
eps_et : int or float, optional
Flavor-off-diagonal (nu_e-nu_tau) NSI coupling. Default: 0.0.
eps_es1 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s1) NSI coupling. Default: 0.0.
eps_es2 : int or float, optional
Flavor-off-diagonal (nu_e-nu_s2) NSI coupling. Default: 0.0.
eps_mm : int or float, optional
Diagonal NSI coupling of nu_mu. Default: 0.0.
eps_mt : int or float, optional
Flavor-off-diagonal (nu_mu-nu_tau) NSI coupling. Default: 0.0.
eps_ms1 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s1) NSI coupling. Default: 0.0.
eps_ms2 : int or float, optional
Flavor-off-diagonal (nu_mu-nu_s2) NSI coupling. Default: 0.0.
eps_tt : int or float, optional
Diagonal NSI coupling of nu_tau. Default: 0.0.
eps_ts1 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s1) NSI coupling. Default: 0.0.
eps_ts2 : int or float, optional
Flavor-off-diagonal (nu_tau-nu_s2) NSI coupling. Default: 0.0.
eps_s1s1 : int or float, optional
Diagonal NSI coupling of nu_s1. Default: 0.0.
eps_s1s2 : int or float, optional
Flavor-off-diagonal (nu_s1-nu_s2) NSI coupling. Default: 0.0.
eps_s2s2 : int or float, optional
Diagonal NSI coupling of nu_s2. Default: 0.0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
ratio_number_neutrons_to_protons : int, float, Callable, or None, optional
:math:`r = n_n/n_p` of the medium, a number or a function of position. Scales the
sterile states' entry in the matter term; see
:func:`magnus.matter.matter_potential_projector`. Default: None, which means 1.0
(isoscalar matter, i.e. :math:`Y_e = 0.5`) with the exponential profile, as before,
and the model's own composition with a standard solar model:
:math:`r = (1 - X)/(1 + X)` at every radius, from its hydrogen mass fraction :math:`X`
(see :func:`magnus.solarmodels.neutron_to_proton_ratio_profile`). A value passed here
is used as given, with either profile.
**The Sun is not isoscalar.** It is hydrogen-rich, so :math:`Y_e = (1 + X)/2` runs
from about 0.68 at the center to 0.88 near the surface, and :math:`r` from about 0.47
down to 0.14 -- nowhere near 1.0, unlike the Earth where the isoscalar value at least
sits among the layers. The exponential profile is a fit to the electron *number*
density and carries no composition to derive :math:`r` from, so 1.0 is kept there as
before; naming a standard solar model through ``density_profile`` supplies one. Left
at 1.0 the averaged survival probability moves by about 4e-03 at
:math:`\sin\theta_{14} = 0.4`, above the default tolerance. Three flavors are
unaffected -- the projector's sterile block is empty.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). With the sterile states it also supplies the composition; see
``ratio_number_neutrons_to_protons``. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, ratio_number_neutrons_to_protons = _solar_profile(density_profile, ratio_number_neutrons_to_protons, 'osc_prob_5nu_sun_nsi')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_5nu_sun_nsi')
P = osc_prob_5nu_matter_nsi_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s14=s14,
s15=s15,
s24=s24,
s25=s25,
s34=s34,
s35=s35,
d14=d14,
d15=d15,
d24=d24,
d35=d35,
D41=D41,
D51=D51,
eps_ee=eps_ee,
eps_em=eps_em,
eps_et=eps_et,
eps_es1=eps_es1,
eps_es2=eps_es2,
eps_mm=eps_mm,
eps_mt=eps_mt,
eps_ms1=eps_ms1,
eps_ms2=eps_ms2,
eps_tt=eps_tt,
eps_ts1=eps_ts1,
eps_ts2=eps_ts2,
eps_s1s1=eps_s1s1,
eps_s1s2=eps_s1s2,
eps_s2s2=eps_s2s2,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
default_osc_params_set_name=default_osc_params_set_name,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
#-----------------------------------------------------------------------
# In vacuum, LIV
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_vacuum_liv(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
sth: Union[int, float],
Dm2: Union[int, float],
sxi: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
vacuum under (one form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
sxi : int or float, optional
Rotation angle xi between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines,
``'sin2'`` their sines *squared* -- which is what global fits report --
``'rad'`` the angles themselves in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_liv(
num_flavors=2,
rho_func=0.0,
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
liv_params={'sxi': sxi, 'b1': b1, 'b2': b2, 'Lambda': Lambda, 'n_liv': n_liv},
nu_i=nu_i,
nu_f=nu_f,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_vacuum_liv(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxiCP: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
vacuum under (one form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxiCP : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_liv(
num_flavors=3,
rho_func=0.0,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxiCP': dxiCP, 'b1': b1,
'b2': b2, 'b3': b3, 'Lambda': Lambda, 'n_liv': n_liv},
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_vacuum_liv(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability in
vacuum under (one form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_liv(
num_flavors=4,
rho_func=0.0,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi24': sxi24, 'dxi24': dxi24, 'sxi34': sxi34, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'Lambda': Lambda, 'n_liv': n_liv},
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_vacuum_liv(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi15: Optional[Union[int, float]]=0.0,
dxi15: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi25: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
sxi35: Optional[Union[int, float]]=0.0,
dxi35: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
b5: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability in
vacuum under (one form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi15 : int or float, optional
Angle xi_15; see ``sxi12``. Default: 0.0.
dxi15 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi25 : int or float, optional
Angle xi_25; see ``sxi12``. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
sxi35 : int or float, optional
Angle xi_35; see ``sxi12``. Default: 0.0.
dxi35 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
b5 : int or float, optional
Eigenvalue b5 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_liv(
num_flavors=5,
rho_func=0.0,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi15': sxi15, 'dxi15': dxi15, 'sxi24': sxi24, 'dxi24': dxi24,
'sxi25': sxi25, 'sxi34': sxi34, 'sxi35': sxi35, 'dxi35': dxi35, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'b5': b5, 'Lambda': Lambda, 'n_liv': n_liv},
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, LIV, constant density
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_matter_liv_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
sxi: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
matter with a constant density profile, under (one form of)
Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
sxi : int or float, optional
Rotation angle xi between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines,
``'sin2'`` their sines *squared* -- which is what global fits report --
``'rad'`` the angles themselves in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_liv(
num_flavors=2,
rho_func=rho,
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
liv_params={'sxi': sxi, 'b1': b1, 'b2': b2, 'Lambda': Lambda, 'n_liv': n_liv},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_matter_liv_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Optional[Union[int, float]],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxiCP: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
matter with a constant density profile, under (one form of)
Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxiCP : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_liv(
num_flavors=3,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxiCP': dxiCP, 'b1': b1,
'b2': b2, 'b3': b3, 'Lambda': Lambda, 'n_liv': n_liv},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_matter_liv_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Optional[Union[int, float]],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability in
matter with a constant density profile, under (one form of)
Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_liv(
num_flavors=4,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi24': sxi24, 'dxi24': dxi24, 'sxi34': sxi34, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'Lambda': Lambda, 'n_liv': n_liv},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_matter_liv_constant_density(
energy: Union[int, float, list, np.ndarray],
L: Union[int, float, list, np.ndarray],
rho: Optional[Union[int, float]],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi15: Optional[Union[int, float]]=0.0,
dxi15: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi25: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
sxi35: Optional[Union[int, float]]=0.0,
dxi35: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
b5: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability in
matter with a constant density profile, under (one form of)
Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
L : int, float, list, or np.ndarray
Baseline(s).
rho : int or float
Matter density (or electron number density, if ``density_is_of_number_of_electrons`` is True).
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi15 : int or float, optional
Angle xi_15; see ``sxi12``. Default: 0.0.
dxi15 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi25 : int or float, optional
Angle xi_25; see ``sxi12``. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
sxi35 : int or float, optional
Angle xi_35; see ``sxi12``. Default: 0.0.
dxi35 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
b5 : int or float, optional
Eigenvalue b5 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
return osc_prob_liv(
num_flavors=5,
rho_func=rho,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi15': sxi15, 'dxi15': dxi15, 'sxi24': sxi24, 'dxi24': dxi24,
'sxi25': sxi25, 'sxi34': sxi34, 'sxi35': sxi35, 'dxi35': dxi35, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'b5': b5, 'Lambda': Lambda, 'n_liv': n_liv},
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, LIV, exponentially falling density
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_matter_liv_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
sxi: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability in
matter with an exponentially falling density profile, under (one
form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
sxi : int or float, optional
Rotation angle xi between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines,
``'sin2'`` their sines *squared* -- which is what global fits report --
``'rad'`` the angles themselves in radians, or ``'deg'`` in degrees. Any other
value raises.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_2nu_matter_liv_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
return osc_prob_liv(
num_flavors=2,
energy=energy,
L=L,
osc_params={'sth': sth, 'Dm2': Dm2},
liv_params={'sxi': sxi, 'b1': b1, 'b2': b2, 'Lambda': Lambda, 'n_liv': n_liv},
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_matter_liv_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxiCP: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability in
matter with an exponentially falling density profile, under (one
form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxiCP : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_3nu_matter_liv_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
return osc_prob_liv(
num_flavors=3,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxiCP': dxiCP, 'b1': b1,
'b2': b2, 'b3': b3, 'Lambda': Lambda, 'n_liv': n_liv},
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_matter_liv_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability in
matter with an exponentially falling density profile, under (one
form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_4nu_matter_liv_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
return osc_prob_liv(
num_flavors=4,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi24': sxi24, 'dxi24': dxi24, 'sxi34': sxi34, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'Lambda': Lambda, 'n_liv': n_liv},
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_matter_liv_exp_density(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
rho_central: Union[int, float],
l_scale: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi15: Optional[Union[int, float]]=0.0,
dxi15: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi25: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
sxi35: Optional[Union[int, float]]=0.0,
dxi35: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
b5: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
ratio_number_neutrons_to_protons: Optional[Union[int, float]]=1.0,
electron_fraction: Optional[Union[int, float]]=0.5,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
density_matter_is_in_g_per_cm3: Optional[bool]=False,
density_is_of_number_of_electrons: Optional[bool]=False,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
_rho_func: Optional[Callable]=None,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability in
matter with an exponentially falling density profile, under (one
form of) Lorentz-invariance violation.
.. versionadded:: 1.0.0
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
rho_central : int or float
Matter density (or electron number density) at the center of the exponential profile (l = 0).
l_scale : int or float
Length scale of the exponential density decrease.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi15 : int or float, optional
Angle xi_15; see ``sxi12``. Default: 0.0.
dxi15 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi25 : int or float, optional
Angle xi_25; see ``sxi12``. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
sxi35 : int or float, optional
Angle xi_35; see ``sxi12``. Default: 0.0.
dxi35 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
b5 : int or float, optional
Eigenvalue b5 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
ratio_number_neutrons_to_protons : int or float, optional
Ratio of the number of neutrons to protons in matter. Default: 1.0.
electron_fraction : int or float, optional
Electron fraction. Default: 0.5.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
density_matter_is_in_g_per_cm3 : bool, optional
If True, the density is given in :math:`\text{g cm}^{-3}`. Default: False.
density_is_of_number_of_electrons : bool, optional
If True, the density parameter directly gives the electron number density [:math:`\text{eV}^{3}`]. Default: False.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
_rho_func : Callable, optional
Internal: a density profile to use in place of the exponential one, which is how the
``osc_prob_*_sun*`` wrappers pass a tabulated standard solar model (see
:mod:`magnus.solarmodels`). ``rho_central`` and ``l_scale`` are then not used. Leave
it None. Default: None.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
if (rho_central < 0.0 or l_scale <= 0.0):
raise ValueError(gd.ERROR_MSG_NO_COLOR + \
" oscprob.osc_prob_5nu_matter_liv_exp_density: rho_central must be non-negative" + \
" and l_scale must be positive.")
return osc_prob_liv(
num_flavors=5,
energy=energy,
L=L,
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi15': sxi15, 'dxi15': dxi15, 'sxi24': sxi24, 'dxi24': dxi24,
'sxi25': sxi25, 'sxi34': sxi34, 'sxi35': sxi35, 'dxi35': dxi35, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'b5': b5, 'Lambda': Lambda, 'n_liv': n_liv},
rho_func=(matter.exp_density_profile(rho_central, l_scale) if _rho_func is None
else _rho_func),
L0=L0,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
electron_fraction=electron_fraction,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=density_matter_is_in_g_per_cm3,
density_is_of_number_of_electrons=density_is_of_number_of_electrons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, LIV, in the Earth
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_earth_liv(
energy: Union[int, float, list, np.ndarray],
sth: Union[int, float],
Dm2: Union[int, float],
sxi: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, under
(one form of) Lorentz-invariance violation.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Two-neutrino oscillations through the Earth under Lorentz-invariance
violation:
.. jupyter-execute::
import warnings
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.magnus import MagnusConvergenceWarning
sth = gd.S12_NO_BF_NUFIT_6_0
Dm2 = gd.D21_NO_BF_NUFIT_6_0
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The small solar mass splitting Dm2 combined with this Earth baseline
# means the adaptive refinement needs a few loops; this is the expected,
# informational MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_2nu_earth_liv(energy, sth, Dm2, sxi=0.1, b1=1.e-13, b2=2.e-13,
Lambda=1.e9, n_liv=1, costhz=costhz, L=baseline)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
sxi : int or float, optional
Rotation angle xi between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines,
``'sin2'`` their sines *squared* -- which is what global fits report --
``'rad'`` the angles themselves in radians, or ``'deg'`` in degrees. Any other
value raises.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# If any of the flavor indices is > 1, fix it (read the docstring above).
nu_i, nu_f = valid_flavor_indices_2nu(nu_i, nu_f)
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=2,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_liv(
num_flavors=2,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'sth': sth, 'Dm2': Dm2},
liv_params={'sxi': sxi, 'b1': b1, 'b2': b2, 'Lambda': Lambda, 'n_liv': n_liv},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_3nu_earth_liv(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxiCP: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, under
(one form of) Lorentz-invariance violation.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Three-neutrino oscillations through the Earth under Lorentz-invariance
violation, using the NuFIT 6.1 defaults for the standard oscillation
parameters:
.. jupyter-execute::
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_3nu_earth_liv(
energy, costhz=costhz, L=baseline,
sxi12=0.1, sxi23=0.05, sxi13=0.02, dxiCP=0.3,
b1=1.e-13, b2=2.e-13, b3=3.e-13, Lambda=1.e9, n_liv=1)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxiCP : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=3,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_liv(
num_flavors=3,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 'D21': D21, 'D31': D31},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxiCP': dxiCP, 'b1': b1,
'b2': b2, 'b3': b3, 'Lambda': Lambda, 'n_liv': n_liv},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_4nu_earth_liv(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, under
(one form of) Lorentz-invariance violation.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Four-neutrino (3+1 sterile) oscillations through the Earth under
Lorentz-invariance violation:
.. jupyter-execute::
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_4nu_earth_liv(
energy, costhz=costhz, L=baseline,
sxi12=0.1, sxi23=0.05, sxi13=0.02, dxi13=0.2,
sxi14=0.05, dxi14=0.4, sxi24=0.03, dxi24=0.5, sxi34=0.02,
b1=1.e-13, b2=2.e-13, b3=3.e-13, b4=1.e-13, Lambda=1.e9, n_liv=1)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=4,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_liv(
num_flavors=4,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's24': s24, 'd24': d24, 's34': s34, 'D21': D21, 'D31': D31, 'D41': D41},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi24': sxi24, 'dxi24': dxi24, 'sxi34': sxi34, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'Lambda': Lambda, 'n_liv': n_liv},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
[docs]
def osc_prob_5nu_earth_liv(
energy: Union[int, float, list, np.ndarray],
costhz: Optional[Union[int, float]]=None,
loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None,
L: Optional[Union[float, list, np.ndarray]]=None,
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi15: Optional[Union[int, float]]=0.0,
dxi15: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi25: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
sxi35: Optional[Union[int, float]]=0.0,
dxi35: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
b5: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
default_osc_params_set_name: Optional[str]='OSC_PARAMS_DEFAULT',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
electron_fraction: Optional[Union[int, float]]=None,
electron_fraction_core: Optional[Union[int, float]]=None,
electron_fraction_mantle: Optional[Union[int, float]]=None,
electron_fraction_crust: Optional[Union[int, float]]=None,
electron_fraction_ocean: Optional[Union[int, float]]=None,
source_depth: Optional[Union[int, float]]=0.0,
detector_depth: Optional[Union[int, float]]=0.0,
density_matter_ocean: Optional[Union[int, float]]=None,
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability
inside the Earth, either between two locations on the surface of the
Earth, or between the surface and a point in the interior, under
(one form of) Lorentz-invariance violation.
For the matter density inside the Earth, it uses the Preliminary
Reference Earth Model.
If the initial location (``loc_ini``) and final location
(``loc_fin``) on the surface of the Earth are given (i.e., if they
are not ``None``), then the neutrino travels the chord joining them
through the Earth, overriding any given value of costhz given, and
using the chord length as the baseline.
Both locations lie on the surface. To put either end of the
trajectory underground, give ``costhz`` instead and name
``source_depth``, ``detector_depth``, or both. ``costhz`` is the
zenith angle at the detector, which is the surface angle when the
detector is on the surface, so nothing changes for a call that
leaves the depths alone. A buried detector fixes where the
trajectory ends, so ``L`` is then computed rather than given.
The initial and final location can be given as a three-entry tuple
of coordinates in the (degree, minute, second) format. Alternatively,
any of the two locations can be given as a predefined named
location. The predefined locations are in the earth.loc_coords_dms
dictionary:
.. jupyter-execute::
import magnus.earth as earth
list(earth.loc_coords_dms.keys())
.. jupyter-execute::
print(earth.loc_coords_dms['fermilab'])
See the example below.
[If only a single location is given (i.e., if either ``loc_ini`` or
``loc_fin`` are ``None``), the function throws an exception.]
If neither of the two locations is given, the function uses the
given value of ``costhz`` as direction and of ``L`` as baseline.
(And ``L`` can be an array of baselines.)
Examples
--------
Five-neutrino (3+2 sterile) oscillations through the Earth under
Lorentz-invariance violation:
.. jupyter-execute::
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
import warnings
from magnus.magnus import MagnusConvergenceWarning
costhz = -0.8
baseline = 2.0 * gd.EARTH_RADIUS * 0.8 * gd.UNIT_KM
energy = 1.0 * gd.UNIT_GEV
# The chosen baseline/energy combination needs a few adaptive-refinement
# loops to converge; this is the expected, informational
# MagnusConvergenceWarning discussed in the package README.
with warnings.catch_warnings():
warnings.simplefilter('ignore', MagnusConvergenceWarning)
P = oscprob.osc_prob_5nu_earth_liv(
energy, costhz=costhz, L=baseline,
sxi12=0.1, sxi23=0.05, sxi13=0.02, dxi13=0.2,
sxi14=0.05, dxi14=0.4, sxi15=0.02, dxi15=0.6,
sxi24=0.03, dxi24=0.5, sxi25=0.01, sxi34=0.02, sxi35=0.01, dxi35=0.7,
b1=1.e-13, b2=2.e-13, b3=3.e-13, b4=1.e-13, b5=1.e-13, Lambda=1.e9, n_liv=1)
P
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Added ``source_depth``, ``detector_depth`` and ``density_matter_ocean``.
Their defaults leave the trajectory and the density profile exactly as
they were: both endpoints on the surface, and PREM's own ocean.
Parameters
----------
energy : int, float, list, or np.ndarray
Neutrino energy/energies.
costhz : int or float, optional
Cosine of the zenith angle of the neutrino. Used together with ``L``, as an alternative to ``loc_ini``/``loc_fin``. Default: None.
loc_ini : tuple, list, np.ndarray, or str, optional
Initial location on the surface of the Earth, as (latitude, longitude) coordinates or a predefined location name (see ``earth.loc_coords_dms``). Must be given with ``loc_fin``. Default: None.
loc_fin : tuple, list, np.ndarray, or str, optional
Final location, same format as ``loc_ini``. Must be given with ``loc_ini``. Default: None.
L : float, list, or np.ndarray, optional
Baseline(s). Default: None.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi15 : int or float, optional
Angle xi_15; see ``sxi12``. Default: 0.0.
dxi15 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi25 : int or float, optional
Angle xi_25; see ``sxi12``. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
sxi35 : int or float, optional
Angle xi_35; see ``sxi12``. Default: 0.0.
dxi35 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
b5 : int or float, optional
Eigenvalue b5 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
default_osc_params_set_name : str, optional
Name of the predefined oscillation-parameter set used to fill in any oscillation parameter left as None (see ``globaldefs.OSC_PARAMS_PREDEFINED``). Default: 'OSC_PARAMS_DEFAULT'.
ratio_number_neutrons_to_protons : int, float, or Callable, optional
:math:`r = n_n/n_p` of the medium. Scales the sterile states' entry in the
matter term; see :func:`magnus.matter.matter_potential_projector`. Default: None,
meaning the projector follows the same layer-by-layer :math:`Y_e` the density
uses, so the two always describe the same medium -- exactly, not approximately,
since :math:`Y_e` only changes at PREM boundaries, which are already slab edges.
Passing a *scalar* instead forces one medium onto the projector while the density
stays layered. No scalar describes a chord that crosses iron and rock: near the
sterile matter resonance on a core-crossing chord the mismatch is worth up to
~0.4 in probability at 3+1, and the best possible scalar still leaves ~7e-3, so a
scalar over layered composition raises
:class:`magnus.globaldefs.SterileMatterCompositionWarning`. A callable of
position [:math:`\text{eV}^{-1}`] is forwarded untouched and trusted, the way
``rho_func`` is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar
matter, and with it these wrappers reproduce the numbers from before composition
was layered. Three flavors are unaffected: the projector's sterile block is
empty.
.. versionchanged:: 1.1.0
Default changed from 1.0 (isoscalar, one matrix for the whole chord) to None
(follow the composition); a callable is accepted.
electron_fraction : int or float, optional
One :math:`Y_e` for the whole Earth, overriding the per-layer values below.
``0.5`` reproduces the uniform composition assumed before those existed, and is
the way to reproduce a result computed then. Cannot be combined with any
``electron_fraction_*`` argument; passing both is an error rather than one
silently winning. Default: None, meaning the layered values are used.
electron_fraction_core : int or float, optional
:math:`Y_e = \langle Z/A \rangle` for :math:`r \le 3480` km. Default:
:data:`magnus.earth.Y_E_CORE_PREM` (0.4656, iron).
electron_fraction_mantle : int or float, optional
:math:`Y_e` for :math:`3480 < r \le 6346.6` km. Default:
:data:`magnus.earth.Y_E_MANTLE_PREM` (0.4957, peridotite).
electron_fraction_crust : int or float, optional
:math:`Y_e` for :math:`6346.6 < r \le 6368` km. Default:
:data:`magnus.earth.Y_E_CRUST_PREM` (0.4952, granitic). Within 0.1% of the
mantle, so this exists for explicitness rather than for effect.
electron_fraction_ocean : int or float, optional
:math:`Y_e` for :math:`r > 6368` km. Default:
:data:`magnus.earth.Y_E_OCEAN_PREM` (0.5551, seawater -- above 0.5 because
hydrogen has :math:`Z/A = 1`). PREM's ocean is a global average that a
land-based baseline does not cross; pass
:data:`magnus.earth.Y_E_CRUST_PREM` for one.
source_depth : int or float, optional
Depth of the neutrino's entry point below the surface of the Earth
[:math:`\text{eV}^{-1}`]. Default: 0.0, i.e. the neutrino enters at
the surface, which is the geometry every earlier version assumed.
detector_depth : int or float, optional
Depth of the detector below the surface of the Earth
[:math:`\text{eV}^{-1}`]. The zenith angle ``costhz`` is measured at
the detector, so a buried detector also sees downward-going neutrinos
(``costhz > 0``) through its overburden. Naming this fixes where the
trajectory ends, so ``L`` must then be left as None and the baseline
is computed for you. Default: 0.0, i.e. a detector on the surface.
density_matter_ocean : int or float, optional
Density of PREM's outermost shell, :math:`r > 6368` km
[:math:`\text{g cm}^{-3}`]. PREM puts a global-average ocean there,
at 1.020; continental rock is about 2.6 and Antarctic ice about 0.92.
The shell is 3 km thick, so this matters for a trajectory close to
horizontal, which can spend its whole length inside it. Pair it with
``electron_fraction_ocean``, which sets the composition of the same
shell. Default: None, i.e. PREM's own ocean.
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
# If the location is given as a string, check if it is one of the predefined named locations in
# Magnus. The method sys._getframe().f_code.co_name returns the function name. If the name
# is one of the predefined ones, coordinates_of_named_location returns the coordinates as
# np.array([lat, lon]). The latitude and longitude are each returned in day-minute-second
# format, (dd, mm, ss)
source_func_name = sys._getframe().f_code.co_name
if isinstance(loc_ini, str):
loc_ini = earth.coordinates_of_named_location(source_func_name, loc_name=loc_ini)
if isinstance(loc_fin, str):
loc_fin = earth.coordinates_of_named_location(source_func_name, loc_name=loc_fin)
# If the initial and final locations are given (i.e., if they are not None), then the neutrino
# travels the chord joining them through the Earth, overriding any given value of costhz given,
# and using the chord length as the baseline. If only a single location is given, throw an
# exception. If neither of the two locations are given, use the given value of costhz and of
# baseline given (could be an array of baselines).
# Both depths are declared Optional, so None has to mean "no depth". Normalized here,
# where they first arrive, because the three uses below divide by gd.UNIT_KM and a None
# would surface as a TypeError rather than this package's descriptive ValueError.
source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth)
costhz, L = validate_input_osc_prob_earth(source_func_name, loc_ini, loc_fin, costhz, L,
verbose=verbose, source_depth=source_depth, detector_depth=detector_depth)
# Align the slab edges with the crossings of the PREM layer boundaries along the chord: the
# matter density is discontinuous there, and the high-order quadrature of the Magnus kernel
# converges at its nominal order only if the Hamiltonian is smooth inside each slab.
t_breakpoints = earth.prem_layer_edges_along_chord(
costhz, source_depth/gd.UNIT_KM,
detector_depth/gd.UNIT_KM)*gd.UNIT_KM # [eV^{-1}]
# A caller may have breakpoints of their own -- a feature in a custom H, say. These
# wrappers set t_breakpoints themselves, so an argument of the same name arrived in
# **kwargs and collided, and the caller got "got multiple values for keyword argument
# 't_breakpoints'" raised from two layers down. The keyword is listed as forwardable
# in this package's own unrecognized-keyword message, so it was reachable and broken.
# The two sets are merged rather than one replacing the other: the PREM crossings are
# required for the quadrature to be O(h^2) across a density jump, so dropping them
# silently would be the defect t_breakpoints exists to prevent. To place every edge
# yourself instead, pass t_slab_edges, which is the complete set.
_user_breakpoints = kwargs.pop('t_breakpoints', None)
if _user_breakpoints is not None:
t_breakpoints = np.unique(np.concatenate(
[np.atleast_1d(np.asarray(t_breakpoints, dtype=float)),
np.atleast_1d(np.asarray(_user_breakpoints, dtype=float))]))
# The function earth.density_matter_func_prem returns the internal matter density of the Earth
# as a function of radial distance, r, using the Preliminary Reference Earth Model (PREM). The
# function matter.num_density_e_func converts the matter density into electron number density.
# The function earth.earth_radial_distance_from_depth returns the radial distance, measured from
# the center of the Earth, given a neutrino direction (cosine of zenith angle, costhz) and the
# distance of the neutrino, or depth (l), measured from the surface of the Earth.
# The density and the projector's ratio come from one resolution, against the same
# Y_e, so the sterile entries follow the composition by default (None); see
# _earth_composition. The resolved ratio is rebound to the parameter's own name
# and forwarded below with everything else.
rho_func, ratio_number_neutrons_to_protons = _earth_composition(
costhz, electron_fraction, ratio_number_neutrons_to_protons,
electron_fraction_core, electron_fraction_mantle,
electron_fraction_crust, electron_fraction_ocean,
source_func_name, num_flavors=5,
source_depth=source_depth, detector_depth=detector_depth,
density_matter_ocean=density_matter_ocean)
return osc_prob_liv(
num_flavors=5,
rho_func=rho_func,
energy=energy,
L=L,
t_breakpoints=t_breakpoints,
# A chord meets every radius twice, so its profile reads the same from either
# end. Declared, not detected: see _earth_chord_symmetry. Returns None -- and
# so takes the ordinary path -- unless every requested baseline is the whole
# chord, because a chord is symmetric over no shorter prefix of itself.
symmetric_over=_earth_chord_symmetry(costhz, L, source_depth, detector_depth),
osc_params={'s12': s12, 's23': s23, 's13': s13, 'dCP': dCP, 's14': s14, 'd14': d14,
's15': s15, 'd15': d15, 's24': s24, 'd24': d24, 's25': s25, 's34': s34, 's35': s35,
'd35': d35, 'D21': D21, 'D31': D31, 'D41': D41, 'D51': D51},
liv_params={'sxi12': sxi12, 'sxi23': sxi23, 'sxi13': sxi13, 'dxi13': dxi13, 'sxi14': sxi14,
'dxi14': dxi14, 'sxi15': sxi15, 'dxi15': dxi15, 'sxi24': sxi24, 'dxi24': dxi24,
'sxi25': sxi25, 'sxi34': sxi34, 'sxi35': sxi35, 'dxi35': dxi35, 'b1': b1, 'b2': b2,
'b3': b3, 'b4': b4, 'b5': b5, 'Lambda': Lambda, 'n_liv': n_liv},
L0=0.0,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_is_of_number_of_electrons=True,
# Forwarded as resolved above: beyond three flavors the matter projector
# needs it for the sterile entries, and the default resolution hands it the
# same Y_e-derived r(l) the density uses, so the two cannot describe
# different media.
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
default_osc_params_set_name=default_osc_params_set_name,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
**kwargs
)
#-----------------------------------------------------------------------
# In matter, LIV, in the Sun
#-----------------------------------------------------------------------
[docs]
def osc_prob_2nu_sun_liv(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
sth: Union[int, float],
Dm2: Union[int, float],
sxi: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the two-neutrino oscillation probability
for neutrinos inside the Sun, under (one form of) Lorentz-invariance
violation.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
sth : int or float
Mixing angle :math:`\theta` of the two-flavor system, in the convention set by ``angles`` (default: its sine).
Dm2 : int or float
Mass-squared difference :math:`\Delta m^2` of the two-flavor system.
sxi : int or float, optional
Rotation angle xi between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines,
``'sin2'`` their sines *squared* -- which is what global fits report --
``'rad'`` the angles themselves in radians, or ``'deg'`` in degrees. Any other
value raises.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, _ = _solar_profile(density_profile, None, 'osc_prob_2nu_sun_liv')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_2nu_sun_liv')
P = osc_prob_2nu_matter_liv_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
sth=sth,
Dm2=Dm2,
sxi=sxi,
b1=b1,
b2=b2,
Lambda=Lambda,
n_liv=n_liv,
# The solar profile is an ELECTRON NUMBER density -- the standard exponential
# fit, in which Y_e is already folded in -- so the mass-density conversion
# these four describe never runs. They are fixed here rather than exposed,
# matching osc_prob_Nnu_sun and osc_prob_Nnu_sun_nsi, which never took them:
# a parameter the caller can set and the calculation ignores is worse than
# no parameter, because nothing says it was ignored.
ratio_number_neutrons_to_protons=1.0,
electron_fraction=0.5,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=False,
density_is_of_number_of_electrons=True,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_3nu_sun_liv(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxiCP: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the three-neutrino oscillation probability
for neutrinos inside the Sun, under (one form of) Lorentz-invariance
violation.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxiCP : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, _ = _solar_profile(density_profile, None, 'osc_prob_3nu_sun_liv')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_3nu_sun_liv')
P = osc_prob_3nu_matter_liv_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
sxi12=sxi12,
sxi23=sxi23,
sxi13=sxi13,
dxiCP=dxiCP,
b1=b1,
b2=b2,
b3=b3,
Lambda=Lambda,
n_liv=n_liv,
# The solar profile is an ELECTRON NUMBER density -- the standard exponential
# fit, in which Y_e is already folded in -- so the mass-density conversion
# these four describe never runs. They are fixed here rather than exposed,
# matching osc_prob_Nnu_sun and osc_prob_Nnu_sun_nsi, which never took them:
# a parameter the caller can set and the calculation ignores is worse than
# no parameter, because nothing says it was ignored.
ratio_number_neutrons_to_protons=1.0,
electron_fraction=0.5,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=False,
density_is_of_number_of_electrons=True,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_4nu_sun_liv(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the four-neutrino oscillation probability
for neutrinos inside the Sun, under (one form of) Lorentz-invariance
violation.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``. ``ratio_number_neutrons_to_protons``
defaults to None: 1.0 with the exponential profile, as before, and the model's own
composition with a standard solar model.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
ratio_number_neutrons_to_protons : int, float, Callable, or None, optional
:math:`r = n_n/n_p` of the medium, a number or a function of position. Scales the
sterile states' entry in the matter term; see
:func:`magnus.matter.matter_potential_projector`. Default: None, which means 1.0
(isoscalar matter, i.e. :math:`Y_e = 0.5`) with the exponential profile, as before,
and the model's own composition with a standard solar model:
:math:`r = (1 - X)/(1 + X)` at every radius, from its hydrogen mass fraction :math:`X`
(see :func:`magnus.solarmodels.neutron_to_proton_ratio_profile`). A value passed here
is used as given, with either profile.
**The Sun is not isoscalar.** It is hydrogen-rich, so :math:`Y_e = (1 + X)/2` runs
from about 0.68 at the center to 0.88 near the surface, and :math:`r` from about 0.47
down to 0.14 -- nowhere near 1.0, unlike the Earth where the isoscalar value at least
sits among the layers. The exponential profile is a fit to the electron *number*
density and carries no composition to derive :math:`r` from, so 1.0 is kept there as
before; naming a standard solar model through ``density_profile`` supplies one. Left
at 1.0 the averaged survival probability moves by about 4e-03 at
:math:`\sin\theta_{14} = 0.4`, above the default tolerance. Three flavors are
unaffected -- the projector's sterile block is empty.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). With the sterile states it also supplies the composition; see
``ratio_number_neutrons_to_protons``. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, ratio_number_neutrons_to_protons = _solar_profile(density_profile, ratio_number_neutrons_to_protons, 'osc_prob_4nu_sun_liv')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_4nu_sun_liv')
P = osc_prob_4nu_matter_liv_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
s14=s14,
s24=s24,
s34=s34,
d14=d14,
d24=d24,
D41=D41,
sxi12=sxi12,
sxi23=sxi23,
sxi13=sxi13,
dxi13=dxi13,
sxi14=sxi14,
dxi14=dxi14,
sxi24=sxi24,
dxi24=dxi24,
sxi34=sxi34,
b1=b1,
b2=b2,
b3=b3,
b4=b4,
Lambda=Lambda,
n_liv=n_liv,
# The solar profile is an ELECTRON NUMBER density -- the standard exponential
# fit, in which Y_e is already folded in -- so the mass-density conversion
# these four describe never runs. They are fixed here rather than exposed,
# matching osc_prob_Nnu_sun and osc_prob_Nnu_sun_nsi, which never took them:
# a parameter the caller can set and the calculation ignores is worse than
# no parameter, because nothing says it was ignored.
electron_fraction=0.5,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=False,
density_is_of_number_of_electrons=True,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
[docs]
def osc_prob_5nu_sun_liv(
energy: Union[float, list, np.ndarray],
L: Union[float, list, np.ndarray],
L0: Union[int, float],
s12: Optional[Union[int, float]]=None,
s23: Optional[Union[int, float]]=None,
s13: Optional[Union[int, float]]=None,
dCP: Optional[Union[int, float]]=None,
D21: Optional[Union[int, float]]=None,
D31: Optional[Union[int, float]]=None,
s14: Optional[Union[int, float]]=0.0,
s15: Optional[Union[int, float]]=0.0,
s24: Optional[Union[int, float]]=0.0,
s25: Optional[Union[int, float]]=0.0,
s34: Optional[Union[int, float]]=0.0,
s35: Optional[Union[int, float]]=0.0,
d14: Optional[Union[int, float]]=0.0,
d15: Optional[Union[int, float]]=0.0,
d24: Optional[Union[int, float]]=0.0,
d35: Optional[Union[int, float]]=0.0,
D41: Optional[Union[int, float]]=0.0,
D51: Optional[Union[int, float]]=0.0,
sxi12: Optional[Union[int, float]]=0.0,
sxi23: Optional[Union[int, float]]=0.0,
sxi13: Optional[Union[int, float]]=0.0,
dxi13: Optional[Union[int, float]]=0.0,
sxi14: Optional[Union[int, float]]=0.0,
dxi14: Optional[Union[int, float]]=0.0,
sxi15: Optional[Union[int, float]]=0.0,
dxi15: Optional[Union[int, float]]=0.0,
sxi24: Optional[Union[int, float]]=0.0,
dxi24: Optional[Union[int, float]]=0.0,
sxi25: Optional[Union[int, float]]=0.0,
sxi34: Optional[Union[int, float]]=0.0,
sxi35: Optional[Union[int, float]]=0.0,
dxi35: Optional[Union[int, float]]=0.0,
b1: Optional[Union[int, float]]=0.0,
b2: Optional[Union[int, float]]=0.0,
b3: Optional[Union[int, float]]=0.0,
b4: Optional[Union[int, float]]=0.0,
b5: Optional[Union[int, float]]=0.0,
Lambda: Optional[Union[int, float]]=1.0,
n_liv: Optional[int]=0,
nubar: Optional[bool]=False,
nu_i: Optional[int]=None,
nu_f: Optional[int]=None,
strategy: Optional[str]='auto',
validate_input: Optional[bool]=True,
save_log: Optional[bool]=False,
filename_log: Optional[str]='./out.log',
file_log: Optional[TextIOWrapper]=None,
close_file_log_upon_exit: Optional[bool]=True,
verbose: Optional[int]=0,
angles: Optional[str]='sin',
ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=None,
density_profile: Optional[str]='exp',
stop_at_table_edge: Optional[bool]=False,
**kwargs) -> Union[float, np.ndarray]:
r"""Compute and return the five-neutrino oscillation probability
for neutrinos inside the Sun, under (one form of) Lorentz-invariance
violation.
For the electron density inside the Sun, it assumes an exponentially
falling density profile: :math:`N_e(r) = N_e(0) \exp(-r/r_0)`,
with :math:`N_e(0) = 245 N_\text{Av}~\text{cm}^{-3}` and
:math:`r_0 = R_\odot/10.54`. See Eq. (10.62) in
`Fundamentals of Neutrino Physics and Astrophysics
<https://academic.oup.com/book/3490>`_ by Carlo Giunti and Chung
Wook Kim.
.. versionadded:: 1.0.0
.. versionchanged:: 1.1.1
Takes ``density_profile``, to use a tabulated standard solar model in place of the
exponential fit, and ``stop_at_table_edge``. ``ratio_number_neutrons_to_protons``
defaults to None: 1.0 with the exponential profile, as before, and the model's own
composition with a standard solar model.
Parameters
----------
energy : float, list, or np.ndarray
Neutrino energy/energies.
L : float, list, or np.ndarray
Baseline(s).
L0 : int or float
Initial position.
s12 : int or float, optional
Mixing angle :math:`\theta_{12}`, in the convention set by ``angles`` (default: its sine). Default: None.
s23 : int or float, optional
Mixing angle :math:`\theta_{23}`, in the convention set by ``angles`` (default: its sine). Default: None.
s13 : int or float, optional
Mixing angle :math:`\theta_{13}`, in the convention set by ``angles`` (default: its sine). Default: None.
dCP : int or float, optional
:math:`\delta_\text{CP}` [radian, or degree if ``angles='deg'``]. Default: None.
D21 : int or float, optional
Mass-squared difference :math:`\Delta m_{21}^2`. Default: None.
D31 : int or float, optional
Mass-squared difference :math:`\Delta m_{31}^2`. Default: None.
s14 : int or float, optional
Mixing angle :math:`\theta_{14}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s15 : int or float, optional
Mixing angle :math:`\theta_{15}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s24 : int or float, optional
Mixing angle :math:`\theta_{24}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s25 : int or float, optional
Mixing angle :math:`\theta_{25}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s34 : int or float, optional
Mixing angle :math:`\theta_{34}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
s35 : int or float, optional
Mixing angle :math:`\theta_{35}`, in the convention set by ``angles`` (default: its sine). Default: 0.0.
d14 : int or float, optional
:math:`\delta_{14}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d15 : int or float, optional
:math:`\delta_{15}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d24 : int or float, optional
:math:`\delta_{24}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
d35 : int or float, optional
:math:`\delta_{35}` [radian, or degree if ``angles='deg'``]. Default: 0.0.
D41 : int or float, optional
Mass-squared difference :math:`\Delta m_{41}^2`. Default: 0.0.
D51 : int or float, optional
Mass-squared difference :math:`\Delta m_{51}^2`. Default: 0.0.
sxi12 : int or float, optional
Mixing angle xi_12 between the space of the eigenvectors of the LIV operator and the flavor states, in the convention set by ``angles`` (default: its sine). Default: 0.0.
sxi23 : int or float, optional
Angle xi_23; see ``sxi12``. Default: 0.0.
sxi13 : int or float, optional
Angle xi_13; see ``sxi12``. Default: 0.0.
dxi13 : int or float, optional
CP-violation phase of the LIV operator [radian] (replaces ``dxiCP`` for 4/5-flavor systems). Default: 0.0.
sxi14 : int or float, optional
Angle xi_14; see ``sxi12``. Default: 0.0.
dxi14 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi15 : int or float, optional
Angle xi_15; see ``sxi12``. Default: 0.0.
dxi15 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi24 : int or float, optional
Angle xi_24; see ``sxi12``. Default: 0.0.
dxi24 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
sxi25 : int or float, optional
Angle xi_25; see ``sxi12``. Default: 0.0.
sxi34 : int or float, optional
Angle xi_34; see ``sxi12``. Default: 0.0.
sxi35 : int or float, optional
Angle xi_35; see ``sxi12``. Default: 0.0.
dxi35 : int or float, optional
CP-violation phase of the LIV operator [radian, or degree if ``angles='deg'``]. Default: 0.0.
b1 : int or float, optional
Eigenvalue b1 of the LIV operator. Default: 0.0.
b2 : int or float, optional
Eigenvalue b2 of the LIV operator. Default: 0.0.
b3 : int or float, optional
Eigenvalue b3 of the LIV operator. Default: 0.0.
b4 : int or float, optional
Eigenvalue b4 of the LIV operator. Default: 0.0.
b5 : int or float, optional
Eigenvalue b5 of the LIV operator. Default: 0.0.
Lambda : int or float, optional
Energy scale of the LIV operator. Default: 1.0.
n_liv : int, optional
Power of the energy dependence of the LIV operator (dimension of the operator minus 3). Default: 0.
nubar : bool, optional
If True, compute the probability for antineutrinos. Default: False.
nu_i : int, optional
Initial flavor index. If given together with ``nu_f``, a single channel is returned instead of the full probability matrix. Default: None.
nu_f : int, optional
Final flavor index; see ``nu_i``. Default: None.
strategy : str, optional
Numerical strategy used to compute the evolution operator: 'auto' (default),
'hybrid', or 'magnus'; see the ``strategy`` parameter of
:func:`osc_prob_matter_std_potential` for the full description and
:doc:`/adiabatic_strategy` for the derivation and validation. Default: 'auto'.
.. versionadded:: 1.0.0
validate_input : bool, optional
If True, validate the input parameters. Default: True.
save_log : bool, optional
If True, also write log messages to a file. Default: False.
filename_log : str, optional
Name of the log file (used if ``save_log`` is True and no ``file_log`` object is given). Default: './out.log'.
file_log : TextIOWrapper, optional
Optional file object to write log messages to. Default: None.
close_file_log_upon_exit : bool, optional
If True, close the log file before returning. Default: True.
verbose : int, optional
Verbosity level: 0 (silent), 1 (warnings), 2 (progress of the refinement loops). Default: 0.
\**kwargs
Additional arguments forwarded to the underlying middle-layer function, and
through it to :func:`osc_prob`, whose signature declares them. The refinement
keywords reached this way are ``n_slabs``, ``min_n_slabs``, ``max_n_slabs``,
``t_slab_edges``, ``t_breakpoints``, ``magnus_exp_order``,
``integration_method``, ``rtol``, ``atol``, ``strict_convergence`` and
``n_jobs``. They do not appear in this signature because they are not this
function's to declare, so ``help()`` on it will not list them: see
:func:`osc_prob`. The logging arguments are this function's own, and are
documented above.
angles : str, optional
How the mixing angles are stated: ``'sin'`` (default) their sines, ``'sin2'``
their sines *squared* -- which is what global fits report -- ``'rad'`` the angles
themselves in radians, or ``'deg'`` in degrees. Any other value raises. Under
``'deg'`` the CP phases are read as degrees too; under the other three
they stay in radians, a sine being no way to state a phase.
ratio_number_neutrons_to_protons : int, float, Callable, or None, optional
:math:`r = n_n/n_p` of the medium, a number or a function of position. Scales the
sterile states' entry in the matter term; see
:func:`magnus.matter.matter_potential_projector`. Default: None, which means 1.0
(isoscalar matter, i.e. :math:`Y_e = 0.5`) with the exponential profile, as before,
and the model's own composition with a standard solar model:
:math:`r = (1 - X)/(1 + X)` at every radius, from its hydrogen mass fraction :math:`X`
(see :func:`magnus.solarmodels.neutron_to_proton_ratio_profile`). A value passed here
is used as given, with either profile.
**The Sun is not isoscalar.** It is hydrogen-rich, so :math:`Y_e = (1 + X)/2` runs
from about 0.68 at the center to 0.88 near the surface, and :math:`r` from about 0.47
down to 0.14 -- nowhere near 1.0, unlike the Earth where the isoscalar value at least
sits among the layers. The exponential profile is a fit to the electron *number*
density and carries no composition to derive :math:`r` from, so 1.0 is kept there as
before; naming a standard solar model through ``density_profile`` supplies one. Left
at 1.0 the averaged survival probability moves by about 4e-03 at
:math:`\sin\theta_{14} = 0.4`, above the default tolerance. Three flavors are
unaffected -- the projector's sterile block is empty.
density_profile : str, optional
The Sun's electron density. ``'exp'``, the default, is the exponential fit described
above. The name of a standard solar model -- one of
:data:`magnus.solarmodels.SOLAR_MODELS`, such as ``'B16-GS98'``, in any case -- uses
that model's tabulated profile instead: interpolated in the logarithm of the density,
held flat below the first tabulated radius, and continued past the last along the slope
of the last interval (see :mod:`magnus.solarmodels`). With the sterile states it also supplies the composition; see
``ratio_number_neutrons_to_protons``. Default: 'exp'.
stop_at_table_edge : bool, optional
Only with a standard solar model. If True, a baseline reaching past the model's last
tabulated radius returns NaN instead of a probability, with a
:class:`SolarModelRangeWarning` naming the edge; if False, the profile is continued past
it. Default: False.
Returns
-------
float or np.ndarray
Oscillation probability matrix (or single channel, if ``nu_i``/``nu_f`` are given) for each (energy, L) point.
"""
_rho, ratio_number_neutrons_to_protons = _solar_profile(density_profile, ratio_number_neutrons_to_protons, 'osc_prob_5nu_sun_liv')
L, _beyond = _stop_at_table_edge(density_profile, stop_at_table_edge, L, L0, 'osc_prob_5nu_sun_liv')
P = osc_prob_5nu_matter_liv_exp_density(
energy=energy,
L=L,
L0=L0,
rho_central=gd.NUM_DENSITY_E_SUN_CENTRAL,
l_scale=gd.L_SCALE_SUN,
s12=s12,
s23=s23,
s13=s13,
dCP=dCP,
D21=D21,
D31=D31,
s14=s14,
s15=s15,
s24=s24,
s25=s25,
s34=s34,
s35=s35,
d14=d14,
d15=d15,
d24=d24,
d35=d35,
D41=D41,
D51=D51,
sxi12=sxi12,
sxi23=sxi23,
sxi13=sxi13,
dxi13=dxi13,
sxi14=sxi14,
dxi14=dxi14,
sxi15=sxi15,
dxi15=dxi15,
sxi24=sxi24,
dxi24=dxi24,
sxi25=sxi25,
sxi34=sxi34,
sxi35=sxi35,
dxi35=dxi35,
b1=b1,
b2=b2,
b3=b3,
b4=b4,
b5=b5,
Lambda=Lambda,
n_liv=n_liv,
# The solar profile is an ELECTRON NUMBER density -- the standard exponential
# fit, in which Y_e is already folded in -- so the mass-density conversion
# these four describe never runs. They are fixed here rather than exposed,
# matching osc_prob_Nnu_sun and osc_prob_Nnu_sun_nsi, which never took them:
# a parameter the caller can set and the calculation ignores is worse than
# no parameter, because nothing says it was ignored.
electron_fraction=0.5,
nubar=nubar,
nu_i=nu_i,
nu_f=nu_f,
density_matter_is_in_g_per_cm3=False,
density_is_of_number_of_electrons=True,
strategy=strategy,
validate_input=validate_input,
save_log=save_log,
filename_log=filename_log,
file_log=file_log,
close_file_log_upon_exit=close_file_log_upon_exit,
verbose=verbose,
angles=angles,
ratio_number_neutrons_to_protons=ratio_number_neutrons_to_protons,
_rho_func=_rho,
**kwargs
)
return _refuse_past_table_edge(P, _beyond)
from .oscprobstd import (
osc_prob_2nu_vacuum_std,
osc_prob_2nu_matter_std,
delta,
J,
osc_prob_3nu_vacuum_std,
)
__all__ = [
# re-exported from oscprobstd.py
'osc_prob_2nu_vacuum_std',
'osc_prob_2nu_matter_std',
'delta',
'J',
'osc_prob_3nu_vacuum_std',
'MAX_N_SLABS_DEFAULT',
'ToleranceNotAchievedWarning',
'HybridCertificationWarning',
'UnmarkedDiscontinuityWarning',
'SolarModelRangeWarning',
'HiddenFeatureWarning',
'ENGINE_FAMILIES',
'CrossCheckInconclusiveWarning',
'cross_check_strategies',
'print_banner',
'print_run_parameters',
'validate_input_battery',
'validate_input_osc_prob_earth',
'valid_flavor_indices_2nu',
'values_to_unspecified_osc_params',
'unpack_oscillation_params_from_dict',
'unpack_nsi_params_from_dict',
'unpack_liv_params_from_dict',
'compute_evolution_operator',
'compute_evolution_operator_multiple_slabs',
'osc_prob',
'osc_prob_energy_baseline',
'osc_prob_vacuum',
'osc_prob_matter_std_potential',
'osc_prob_matter_nsi',
'osc_prob_liv',
'osc_prob_2nu_vacuum',
'osc_prob_3nu_vacuum',
'osc_prob_4nu_vacuum',
'osc_prob_5nu_vacuum',
'osc_prob_2nu_matter_constant_density',
'osc_prob_3nu_matter_constant_density',
'osc_prob_4nu_matter_constant_density',
'osc_prob_5nu_matter_constant_density',
'osc_prob_2nu_matter_exp_density',
'osc_prob_3nu_matter_exp_density',
'osc_prob_4nu_matter_exp_density',
'osc_prob_5nu_matter_exp_density',
'osc_prob_2nu_earth',
'osc_prob_3nu_earth',
'osc_prob_4nu_earth',
'osc_prob_5nu_earth',
'osc_prob_earth',
'osc_prob_2nu_sun',
'osc_prob_3nu_sun',
'osc_prob_4nu_sun',
'osc_prob_5nu_sun',
'osc_prob_sun',
'osc_prob_2nu_matter_nsi_constant_density',
'osc_prob_3nu_matter_nsi_constant_density',
'osc_prob_4nu_matter_nsi_constant_density',
'osc_prob_5nu_matter_nsi_constant_density',
'osc_prob_2nu_matter_nsi_exp_density',
'osc_prob_3nu_matter_nsi_exp_density',
'osc_prob_4nu_matter_nsi_exp_density',
'osc_prob_5nu_matter_nsi_exp_density',
'osc_prob_2nu_earth_nsi',
'osc_prob_3nu_earth_nsi',
'osc_prob_4nu_earth_nsi',
'osc_prob_5nu_earth_nsi',
'osc_prob_2nu_sun_nsi',
'osc_prob_3nu_sun_nsi',
'osc_prob_4nu_sun_nsi',
'osc_prob_5nu_sun_nsi',
'osc_prob_2nu_vacuum_liv',
'osc_prob_3nu_vacuum_liv',
'osc_prob_4nu_vacuum_liv',
'osc_prob_5nu_vacuum_liv',
'osc_prob_2nu_matter_liv_constant_density',
'osc_prob_3nu_matter_liv_constant_density',
'osc_prob_4nu_matter_liv_constant_density',
'osc_prob_5nu_matter_liv_constant_density',
'osc_prob_2nu_matter_liv_exp_density',
'osc_prob_3nu_matter_liv_exp_density',
'osc_prob_4nu_matter_liv_exp_density',
'osc_prob_5nu_matter_liv_exp_density',
'osc_prob_2nu_earth_liv',
'osc_prob_3nu_earth_liv',
'osc_prob_4nu_earth_liv',
'osc_prob_5nu_earth_liv',
'osc_prob_2nu_sun_liv',
'osc_prob_3nu_sun_liv',
'osc_prob_4nu_sun_liv',
'osc_prob_5nu_sun_liv',
# Documented as knobs -- each docstring carries the population it was
# measured on -- and without this sphinx-autoapi does not document them,
# which left every cross-reference to them rendering as dead text.
'IP_EXP_N_SLABS_CAP',
'MIN_EFFECTIVE_REFINEMENT',
'BATCH_WORKING_ENTRIES',
'CUMULATIVE_AUTO_MIN_POINTS',
'HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS',
'CUMULATIVE_N_ACC_SAFETY',
'AUTO_LADDER_MAX_PHASE',
'AUTO_LADDER_MIN_TOLERANCE',
'AUTO_LADDER_TOLERANCE_MARGIN',
'AUTO_LADDER_MAX_FLOOR_FRACTION',
'OUTPUT_GUARD_MIN_BYTES',
'OUTPUT_GUARD_SAFETY',
'IP_EXP_LOOP_CAP',
'PARAMETER_SET_METADATA_KEYS',
'PASSTHROUGH_KWARGS_DOCUMENTED',
'PhaseAveragingWarning',
]