Source code for magnus.oscprob

# -*- 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).

- :func:`osc_prob_pseudo_dirac_vacuum`: Three flavors with pseudo-Dirac
  partners on any of the mass states; its ``average=True`` is exact at any
  splitting.

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.

   A standard solar model can be used instead through the ``density_profile``
   argument of the Sun routines (see :mod:`magnus.solarmodels`); for any other
   profile, use the primordial function :func:`osc_prob`.

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 math
import os
import numbers
import numpy as np
import sys
import time
import warnings
import weakref
import contextvars
from contextlib import contextmanager, nullcontext
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
from magnus import _validate as _v


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). **The energy-batched engine** (``_osc_prob_scan_separable``) applies the same bound, on grids with breakpoints only, measuring refinement as grid points times points per slab so that the quadrature methods' growth in points per slab counts too. There it carries both fixes: the bound decides which agreements count, and the growth rule, rejected above as a *substitute* for the bound, keeps the ladder from wasting levels on steps the bound would refuse. Without it, on a PREM chord at two flavors and rtol = atol = 1e-3, two of twelve energies stopped on a 20 -> 22 point comparison and returned 2.5e-3 off, silently (issue #71); with both, the scan is within tolerance and faster (0.7 of the time at two flavors, 0.8 at three from 1e-4 on; 1.2 at three flavors and 1e-3, where the old ladder stopped on a false agreement). Without breakpoints the engine checks one step only: on ``'gl'``, the last step onto ``max_n_slabs``, which is clamped and so can refine the grid by a sliver. Its agreement is held to what such a step can vouch for rather than refused outright. At order ``p`` a real refinement ``r`` bounds the finer grid's error by ``|dP|/(r**p - 1)``, so for ``r**p - 1 < 1`` the difference must lie within that fraction of the tolerance: ========================== ================ =========== =========================== clamped step (slabs) ``r**p - 1`` agreement error of the answer ========================== ================ =========== =========================== 108 -> 115 (issue #94) 0.28 0.14 0.49: still certifies 18 346 -> 20 000 0.41 0.79 6.6: refused 19 926 -> 20 000 0.015 0.40 6.6: refused ========================== ================ =========== =========================== Agreement and error are in units of the tolerance, at order 4. The first row is a smooth three-flavor profile with the cap lowered to 115 slabs at ``rtol = atol = 1e-6``. The other two are the five-flavor scan of the paper's Listing 1 at ``rtol = 5e-13`` from floors of 19 and 13 slabs, scored against DOP853 at ``rtol = 1e-13``. Both returned their probabilities with no warning (issue #122). Refused, they end in the existing refinement-caps warning on the same probabilities: ``'gl'`` fixes the points per slab, so no further level is computed. .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 The energy-batched engine holds an agreement across the clamped last step onto the slab cap on ``'gl'`` to the fraction of the tolerance that step can vouch for. """
[docs] QUADRATURE_SEED_MIN_SLABS = 4
r"""int: Module-level constant Least phase-based starting slab count that the energy-batched engine applies to the quadrature rules (``'trapezoid'``, ``'simpson'``); below it they start from ``min_n_slabs``, as the per-point ladder always does for them. ``'gl'`` always takes the seed. Seeding the quadrature rules pays when the trajectory holds many oscillation lengths -- on a core-crossing chord at 50 energies, 1016 ms -> 40 ms -- and costs when it holds few: a seed of 2-3 slabs can send the ladder through one level more. Measured over 212 scans (smooth and breakpoint profiles, 2 to 40 energies, both rules, rtol = atol = 1e-3 and 1e-4), the work with the seed against without it, by seed: at 2, worst 2.14x (median 0.81x); at 3, worst 1.24x (0.67x); at 4, worst 0.87x (0.54x); at 5 and above, never more than 0.36x. So 4 is the smallest seed that never cost work. .. versionadded:: 1.2.0 """
[docs] BATCHED_GL_MAX_SLAB_NORM = 2.0*np.pi
r"""float: Module-level constant Largest slab norm :math:`\lVert\Omega\rVert_2` at which the energy-batched engine lets an energy's agreement between two levels count as convergence on ``'gl'``, while the ladder can still add slabs. Above it, that energy goes one level further; the others are accepted as before. **Why (issue #71).** On smooth profiles, two coarse ``'gl'`` levels can agree by chance: each slab spans several radians of phase, where the Magnus series has not yet reached the regime in which its error falls at its order, and the two answers happen to coincide. Over the 477 scans of the #71 measurement pool, all 27 energies that ``'gl'`` returned outside the tolerance without a warning (up to 19 times outside it) had been accepted at a level where their *own* slab norm was at least :math:`2\pi` (7.4 to 17.3); of the 1263 energies it returned within tolerance, 78 had. Refusing those acceptances took the 27 to zero, added none, and cost 0.9 % of the total work of the 159 ``'gl'`` scans. Twelve scans do more work; five of them had been within tolerance, and those take 1.03x to 1.23x the time (at most 0.7 ms more, on 2- and 4-energy scans of 2-3 ms). The worst case, a 40-energy scan that had been returning two energies outside the tolerance, takes 1.9x to 2.3x (two measurements). Adding antineutrino and ``magnus_exp_order`` 2 and 6 variants of the smooth scans (191 ``'gl'`` scans in all), the silent misses before the gate are 72, up to 102 times outside the tolerance; after it, 2, with none added, for 3.1 % more total work. **What it costs where the phase is concentrated.** The seed puts about :math:`2\pi` of phase in the *average* slab, so where the phase piles up in part of the path the widest slab stays above :math:`2\pi` for several levels, and until then every energy is refused, whether its agreement was real or not. On that set, 9 scans that had been within tolerance (at 0.10 to 0.99 of it) do more work; the worst is the 40-energy antineutrino scan on the 2500 km exponential profile at rtol = atol = 1e-3, within 0.54 of the tolerance before: 5.2x the work, 3.2x to 3.6x the time (6 ms to 20-23 ms). The alternatives measured to avoid it were all worse: other statistics of the slab norms (mean, median, 90th percentile, fractions above :math:`\pi` or :math:`2\pi`), a cap on the refusals, requiring one more agreement, sizing the next level from the norm, a grid that follows the phase, an error indicator from the samples' commutator terms, and splitting only the wide slabs each left more misses, created new ones, or cost more. **Why per energy.** A gate on the batch's largest norm was measured first and rejected: it refused every energy of a scan for the sake of one, and slowed smooth profiles by up to 15x. Here an energy is refused only for its own norm, which ``magnus._expm_stack`` already has (see ``magnus._row_slab_norms``): the per-energy maximum replaces the stack maximum it takes for :class:`magnus.magnus.MagnusConvergenceWarning`, which is unchanged. **Why only** ``'gl'``. On ``'trapezoid'`` and ``'simpson'`` the same test flags far more correct energies than wrong ones, so they are left as they were. **Why not at the slab cap.** There the ladder can no longer add slabs, so a refusal would only repeat the level; the engine keeps its previous behavior at the cap and on its last level. .. versionadded:: 1.2.0 """
[docs] BATCHED_PHASE_GROUPING = dict(slab_energy_us={2: 0.17, 3: 0.44, 4: 1.8, 5: 3.2}, sample_us=0.15, level_us=250.0, resolution=(22.0, 0.71, 0.18), margin=1.2, min_tol=1.0e-4, min_dim=4)
r"""dict: Module-level constant Cost model by which the energy-batched engine (``_osc_prob_scan_separable``) splits a scan into groups of energies of similar phase, each refined on its own slab grid, instead of refining every energy on one grid sized for the fastest (issue #111). **Why (issue #111).** The engine's shared grid starts from the phase of the fastest-oscillating energy, and the phase across an Earth chord varies as :math:`1/E`. With an eV-scale sterile splitting it is about :math:`10^4` rad, so on a 0.5-5 GeV scan the slowest energies were refined on ten times the slabs they needed: 3.8 times the slab-work of one ladder per energy. At four and five flavors each slab's exponential dominates, and batching lost to one call per energy -- on a chord at costhz = -0.5 with 40 energies, 4nu with :math:`\Delta m^2_{41}` = 1 eV², 640 ms against 370 ms; 5nu with :math:`\Delta m^2_{41}` = 1 and :math:`\Delta m^2_{51}` = 1.7 eV², 1720 against 745 ms -- while without steriles batching wins 20-80x. **What it does.** The seed's SVD already yields each energy's own phase (the engine used to take only the maximum), so each energy's own starting slab count is free. The energies are sorted by it, and a dynamic program over the distinct seed values chooses the split that minimizes the modeled cost of the ladders. An energy of seed ``S_e`` is expected to be accepted at R_e = max(g*S_e, B * S_e**p * (1e-8/tol)**q), tol = atol + 0.01*rtol, the ladder's second level at least (``g`` is ``growth_factor_n_slabs``), or the resolution the profile needs at this tolerance. On a grid seeded at ``S`` it then runs the levels ``S, g*S, ...`` until one reaches ``R_e``, and a group of energies on the grid of its largest seed costs, in slab-energies, sum over its energies of the slabs of their levels + k * (the slabs of the group's levels) + F * (their number), ``k`` being the sampling of the potential and projector shared by the group's energies and ``F`` the fixed cost of a level. The scan is split when the least such total is below the one-grid cost by at least the factor ``margin``; otherwise it is computed exactly as before, bit for bit. Each group then runs the unchanged ladder from its own largest seed, with the #71 slab-norm gate inside it and the warnings once per call. **Why the tolerance is in it.** At a loose tolerance every energy is accepted on its second level, so on the shared grid the slow ones pay ``S_max/S_e`` times their own first level for nothing, and the split wins by that ratio. At a tight one the slab count an energy needs is set by the profile rather than by its phase, and the shared grid's second level already meets it for the slow energies, which drop out there; their own ladders would climb from a lower seed through more levels to the same place. So the gain fades as the tolerance tightens, and turns into a loss: measured on 87 energies at 1-40 GeV, 4nu with :math:`\Delta m^2_{41}` = 1 eV², rtol = 1e-8 and atol = 1e-10, the split cost 1.37x (2.9 to 4.0 s; 5nu 1.32x), while at rtol = atol = 1e-6 on 0.5-5 GeV it still gained 3.2x. The resolution law is fitted to the levels the per-energy ladders actually ran: 183 energies of 4nu and 5nu Earth scans (:math:`\Delta m^2_{41}` = 0.1 and 1 eV², two chords, 0.5-40 GeV) at rtol = 1e-4, 1e-6 and 1e-8 with atol = 0.01*rtol give ``B, p, q`` = 22, 0.71, 0.18 with an rms residual factor of 1.32. ``atol`` leads the proxy because the ladder tests every channel: at rtol = 1e-8 the same scan takes two levels with atol = 1e-8 and four with atol = 1e-10. A 3nu chord needs about a fifth of the law's slabs, which only makes the model refuse splits it might have afforded. **The measured costs behind the keys** (one core, Earth chord with its 14 layer boundaries, ``'gl'`` at order 4): * ``slab_energy_us`` -- the cost of one slab of one energy (exponential, sample sum and slab product): 0.17, 0.44, 1.8 and 3.2 us at 2, 3, 4 and 5 flavors, from the slope of the level time in ``nE*n_slabs`` between 1 and 4 energies at 1 to 4096 slabs. It is flat in the number of energies from 1 to 40 at three flavors and above, which is what makes small groups affordable: the batched kernel gains nothing per slab from stacking energies there (at two flavors and 100 slabs it does, 0.66 to 0.09 us, which the larger ``F`` and ``k`` in those units keep the split away from). Dimensions above 5 take the 5-flavor value. * ``sample_us`` -- the per-slab cost of sampling the potential and projector on a level, shared by the group's energies: 0.1 to 0.3 us per slab (0.5 ms at 4000 slabs). * ``level_us`` -- the fixed cost of one ladder level (grid, samples, chunking, the convergence test): 100 to 240 us measured, the largest at five flavors and nearly all of it at one energy and one slab. Set at the upper end, so that at the margin it is the splits with the least to gain that are refused. * ``resolution`` -- ``(B, p, q)`` of the law above. * ``min_tol`` -- 1e-4: the split is applied only when ``atol + 0.01*rtol`` is at least this. A group's ladder starts at its own coarse seed, where two successive levels can agree by chance (issue #71), and the shared grid, ten to forty times finer than the slow energies' seeds, had been protecting them from that: scored against per-point references at rtol = atol = 1e-9 over 174 Earth scans (3nu to 5nu, sterile splittings 0.01 to 10 eV², three chords, 4 to 100 energies, ``'gl'`` and ``'simpson'``), the split at rtol = atol = 1e-6 returned one energy of each of five scans outside the tolerance without a warning (1.2 to 4.0 times outside) where one grid returned none, while at 1e-3 and 1e-4 (88 scans) neither route missed. Below the floor the scan is computed as before, and the gain given up is real (3.2x on the issue's 4nu scan at 1e-6) but smaller than at the default tolerance, where the ladders stop on their second level. * ``margin`` -- 1.2: the model's constants are good to some 30 %, and a split predicted to gain less than that is as likely to lose as to win. **Alternatives measured** (4nu, :math:`\Delta m^2_{41}` = 1 eV², 40 energies at 0.5-5 GeV, interleaved best of five; one grid takes 650 ms): * grouping by a fixed phase ratio ``b`` within a group, the first prototype: 204, 223, 247 and 295 ms at ``b`` = 1.25, 1.5, 2 and 3, against 214-218 ms for the dynamic program with three settings of its constants, which also holds at 200 energies (987, 1011, 1165, 1495 ms against 920-980) and at 10 (where ``b`` = 3 costs 1.25x); * one ladder per energy: 242 ms here, and 49 against 32 ms at :math:`\Delta m^2_{41}` = 0.1 eV², where the seeds are a few hundred slabs and ``F`` is what decides; * sending the whole scan to the per-point route: 370 ms. The per-point wrapper stack costs about 3 ms per energy (a 3nu scan takes 128 ms per point against 2.7 ms batched), so a group of one energy inside the engine is always cheaper than a per-point call, and that route is never the best one; * the per-point Earth path's palindrome halving: it halves the sampling of the profile, which this engine already shares across the energies of a group (``k``), so it could save at most a few percent here and was not added. **Where it is not applied.** Seeds are clipped to ``max_n_slabs`` before the model sees them, and a scan whose largest seed sits at the cap ends on its first level, warned, on any grid: groups below the cap would only add levels to a call that cannot converge (measured 1.1x to 1.4x slower), so such a scan is left as it was. Nor is it applied below ``min_dim`` = 4 flavors, where two- and three-flavor scans stay bit for bit as they were. There the shared grid is cheap, and the gain is modest: 17 to 30 % on 3nu Earth scans spanning two or three decades of energy, none on narrower ones. It would also cost accuracy that users of those scans have come to rely on. The shared grid resolves the low-phase energies far beyond the tolerance, and a group converges only to it. Against rtol = atol = 1e-9 references, the largest error of a 3nu scan at 0.1-100 GeV across the core rose from 5.5e-5 to 1.0e-3, inside the default tolerance at that point (1.4e-3), but some twenty times the error it used to have. * ``min_dim`` -- 4: the fewest flavors at which a scan is split. **Its own cost.** The dynamic program is quadratic in the number of distinct seeds: 0.05 ms at 4 energies, 0.23 ms at 40, 2.3 ms at 200 -- at most about 2 % of a scan it can fire on -- and is skipped by two lower bounds when even they would not reach the margin. The first is in scalars (one grid at the largest seed's levels against the fixed cost of the four levels a split runs at least), about 2 us; the second (every energy on its own seed, plus those four levels) takes some 25 us over the seeds. Below ``min_dim`` the function returns before either. Without the first bound and the flavor floor, a 100-energy two-flavor Earth scan paid 0.18 ms for the dynamic program, 10 % of its 1.9 ms. .. versionadded:: 1.2.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] N_JOBS_MIN_PARALLEL_WORK_S = 1.0
r"""float: Module-level constant Seconds of serial work below which ``n_jobs > 1`` finishes a per-point scan in the calling process instead of starting worker processes (issue #155 §3). :func:`osc_prob_energy_baseline` always computes the first point serially, to warm-start the rest. Its time, multiplied by the points left, estimates what the rest would cost serially. A process pool pays for starting its workers and importing Magnus in each before the first task runs: measured at 1.2 to 2.2 s for 2 to 4 workers on a 5-point scan whose serial cost was 0.07 s (1.4 to 3 s in the beta test that reported it). Below 1 s of serial work, then, the pool cannot win, and the scan runs serially: faster, never slower. Above it the pool runs as before. .. versionadded:: 1.2.0 """
[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] HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHT = 2
r"""int: Module-level constant What :data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS` becomes below :data:`AUTO_LADDER_MIN_TOLERANCE` (issue #125): under ``strategy='auto'``, a single-energy scan of two or more baselines at a tolerance tighter than 1e-6 goes to the cumulative scan, not to the hybrid strategy. ``tol = min(rtol, atol)``, over those of the two that are set, as for :data:`AUTO_LADDER_TIGHT_MAX_PHASE`. The threshold of 8 was set at 1e-3, where the hybrid strategy is cheap and the cumulative scan paid for its probe (up to 5.75x at N = 2 on a solar profile at three flavors). At tight tolerances that trade reverses. Measured on scans of 2, 4 and 7 baselines at ``rtol`` = 1e-7, 1e-9 and 1e-12 (``atol`` a hundredth of it), orders 4 and 8, scored against ``solve_ivp``/DOP853 at 1e-13 (``docs/dev/measurements/issue125_baseline_scans/``): * 96 scans on exponential profiles (25 to 25 000 km, two to five flavors, NSI, LIV), a multi-resonance profile and partial solar chords: the cumulative scan took 0.06 to 0.18 of the time at the median and at most 0.66, with no silent miss the hybrid strategy did not also make; * 14 scans on the physical population (tabulated profiles, the BS05 solar model, supernova shocks and turbulence, an Earth crust without declared breakpoints): 0.07 and 0.17 at the median, at most 1.08 where both sides already ran the cumulative scan, and no silent miss on either side. On these profiles the hybrid strategy declined in 24 of 28 cases and the request reached the cumulative scan anyway, after the wasted attempt. What it costs: on 3 to 18 of the 96 scans per setting, a :class:`ToleranceNotAchievedWarning` where the hybrid strategy was silent, every one on an answer inside the requested tolerance (the cumulative scan saying it could not verify convergence, as it does above the threshold). At 1e-6 and looser nothing changes: there the cumulative scan already answers such scans on smooth profiles, and the threshold of 8 still keeps coherent solar scans of fewer baselines on the hybrid strategy, as measured. Phase-averaged requests (``average=True``) never reach this choice. .. versionadded:: 1.2.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), at a tolerance of :data:`AUTO_LADDER_MIN_TOLERANCE` or looser, provided the ladder's starting slab count stays within :data:`AUTO_LADDER_MAX_FLOOR_FRACTION` of its cap. At a tighter tolerance the limit shrinks with it (see :data:`AUTO_LADDER_TIGHT_MAX_PHASE`). 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``). It does not apply to an energy scan at one baseline that the energy-batched scan will take (issue #84). That engine shares its slabs across the energies, while the hybrid pays its window search at every energy, so the limit, which prices the ladder one point at a time, would send such a scan to the slower route: 62 s against 0.4 s on 100 energies of a three-flavor, two-resonance profile whose estimate (1.05e4, at the lowest energy) sat just above it. The slab-count condition still applies, and keeps the full Sun on the hybrid. 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.2.0 """
[docs] AUTO_PROBE_POINTS = 200
r"""int: Module-level constant Points of the probe grid on which ``strategy='auto'`` tests the profile before handing a request to the ladder (``_auto_prefers_ladder``): the resolution test of ``magnus.adiabatic._profile_is_resolved``, the same grid the hybrid strategy starts from. .. versionadded:: 1.2.0 """
[docs] AUTO_SHARP_FEATURE_SLABS_PER_PROBE = 2
r"""int: Module-level constant Slabs per probe interval that the ladder starts from when ``strategy='auto'`` hands it a profile with a feature sharp at the probe scale but smooth below it (issue #161): a narrow density spike, which the ladder's seed, from the phase integrated along the path, cannot see. Every level from that seed can step over the spike and agree on the same wrong answer. On Gaussian spikes of widths 2, 5, 20 and 50 km at three positions along 4000 km, at 0.5, 1 and 3 GeV, 17 of the 36 were off by more than the requested 1e-3, up to 1.4e-2, with no warning; starting at two slabs per probe interval, 398 over the path, leaves 3, the 2 km spike that falls between two probes at every energy, which the probe grid cannot flag (see :class:`HiddenFeatureWarning`). The 5 km spike went from 4.0e-3 to 1.0e-8 at 3 GeV. A request the probe does not flag keeps its starting grid, and its answer, bit for bit. One it flags costs up to twice the time of a scan that was answered wrong, and up to 1.5 times that of one already answered right, as on the 50 km spike, where the error also falls, from 1e-6 to 1e-9. .. versionadded:: 1.2.0 """
[docs] UNDECLARED_JUMP_STEP_TOLERANCE = 0.1
r"""float: Module-level constant When :func:`osc_prob_energy_baseline` looks for jumps nobody declared (issue #161). After a point is computed, the largest change of the Hamiltonian's diagonal between consecutive quadrature nodes at the accepted level is compared with the level before it. Across a jump it stays at the size of the jump, a ratio of 1 within this tolerance. On a smooth profile it shrinks with the slab width, to :math:`1/1.5 = 0.67` of it at the default ``growth_factor_n_slabs``, or grows, several times over, where a finer level first lands a node on a feature the coarser one stepped over. Only a ratio within this tolerance of 1 sends the profile to the jump search, which costs about 400 evaluations of the Hamiltonian, so a smooth profile does not pay for it. .. versionadded:: 1.2.0 """
[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.2.0 """
[docs] AUTO_LADDER_MIN_TOLERANCE = 1.0e-6
r"""float: Module-level constant Tolerance, ``min(rtol, atol)``, below which ``strategy='auto'`` tightens its route to the Magnus ladder (issue #120). At it and looser, the route of :data:`AUTO_LADDER_MAX_PHASE` applies as measured for issue #70, with the ladder at a tenth of the tolerance (:data:`AUTO_LADDER_TOLERANCE_MARGIN`). Below it, and only with ``integration_method='gl'`` at a single baseline, the phase limit shrinks with the tolerance and the order, capped at :data:`AUTO_LADDER_TIGHT_MAX_PHASE`, and the ladder runs at the requested tolerance itself. A baseline scan goes to the cumulative scan there instead (:data:`HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHT`, issue #125). Until issue #120 this was a cut-off: no tighter request went to the ladder, because at 1e-9 the ladder, run at a tenth of the tolerance, cost 0.9 to 1.3 times the hybrid strategy at four and five flavors. Most of that was the margin. Run at the requested tolerance, the ladder answers the four scans of the paper's Listing 1 at ``rtol = 1e-12``, ``atol = 1e-14`` and order 8 in 0.015 to 0.13 of the hybrid strategy's time. .. versionadded:: 1.2.0 """
[docs] AUTO_LADDER_TIGHT_MAX_PHASE = 2000.0
r"""float: Module-level constant Largest estimated phase, in radians, at which ``strategy='auto'`` hands a request to the Magnus ladder when the tolerance is tighter than :data:`AUTO_LADDER_MIN_TOLERANCE` (issue #120). Below that tolerance the phase limit is ``AUTO_LADDER_MAX_PHASE*(tol/AUTO_LADDER_MIN_TOLERANCE)**(1/p)``, with ``tol = min(rtol, atol)`` and ``p`` the requested ``magnus_exp_order``: the ladder's slab count grows as ``tol**(-1/p)``, while the hybrid strategy's window search does not follow the tolerance. This constant caps it. For the paper's Listing 1 (``rtol = 1e-12``, ``atol = 1e-14``, order 8) the limit is 1 000 rad, and its four curves estimate 10 to 78. Measured on the 139 workloads the route takes at the default tolerance (Listing 1 at 26 energies per curve, the workloads behind :data:`AUTO_LADDER_MAX_PHASE`, a multi-resonance profile and partial solar chords of 0.1 to 0.3 R_sun), at ``rtol`` = 1e-7, 1e-9 and 1e-12 with ``atol`` a hundredth of it, and scored against ``solve_ivp``/DOP853 at 1e-13. At order 8 the ladder missed no tolerance without a warning, never warned where the hybrid strategy had certified, and took 0.12 to 0.19 of the hybrid strategy's time at the median (at most 0.82, warm, under four parallel workers). At order 4, four workloads at 1e-12 came back 1.01 to 1.15 times outside the tolerance without a warning; the hybrid strategy's own four misses there reach 1.3 times. Uncapped, the limit admits partial solar chords from 2 217 rad on at order 8, and on two of them the ladder warned :class:`ToleranceNotAchievedWarning` where the hybrid strategy certified. At 2 000 every such chord stays on the hybrid strategy. Unlike :data:`AUTO_LADDER_MAX_PHASE`, the tightened limit also applies to an energy scan that the energy-batched engine takes: the scans that exempted them were measured at loose tolerances (issue #84). .. versionadded:: 1.2.0 """
[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`. The margin applies at :data:`AUTO_LADDER_MIN_TOLERANCE` and looser. Below it the ladder runs at the requested tolerance (issue #120). There its rungs are deep in the asymptotic regime, where the difference between two of them overestimates the finer one's error by about ``1.5**p - 1`` (4 at order 4, 25 at order 8); and a tenth of 1e-12 cost more than 1.2 times the hybrid strategy on 18 of 139 workloads at order 4, over 8 times on the four-flavor scan of the paper's Listing 1. .. versionadded:: 1.2.0 """
[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 """
[docs] IP_EXP_LOOP_CAP = 30
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. Also raised when **neither engine of** ``strategy='auto'`` **reached the tolerance** at a point (issue #167): the hybrid declined it uncertified, and the ladder that answered instead ran out of room with no two levels to compare, or with two further apart than the hybrid's error estimate. The hybrid's answer is then returned, and ``strategy_info`` names the hybrid as the engine, with ``certified=False`` and its ``'error_estimate'`` in ``'trace'``: its :data:`magnus.adiabatic.GAMMA_TO_ERROR` times the largest non-adiabaticity along the path, a bound on the unpatched adiabatic answer's error. At 1 MeV across the Sun at ``rtol=atol=1e-8`` the ladder's single level of 20000 slabs was 4.0e-3 off; the hybrid's answer is 1.7e-6 off, against an estimate of 1.9e-5. Any slab-width or tolerance warning raised before it describes the ladder's answer, which was not returned. Three subclasses narrow the diagnosis: :class:`HybridCertificationWarning`, :class:`UnmarkedDiscontinuityWarning` and :class:`HiddenFeatureWarning`. Code filtering on this class catches all three. **Measured rates** (``docs/dev/adversarial_batteries/warn_fp.py``, the 160 of its 168 configurations that are valid input): fired 42 times, **29 true positives and 13 false positives -- a 31 % 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 .. versionchanged:: 1.2.0 Also raised when neither engine of ``strategy='auto'`` reached the tolerance and the hybrid's uncertified answer was returned (issue #167). The messages that say "Shown once per session", here and in the subclasses, are, even when the warning filters change between calls (issue #205). """
[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'``, an uncertified point instead goes to another engine (the general slab-refinement method raises :class:`ToleranceNotAchievedWarning` itself if *it* also fails to converge, and a point it answers one at a time then gets the hybrid's answer back when the ladder has less to show for its own; see that class), so for a probability this warning fires only when ``strategy='hybrid'`` was explicitly requested. With ``average=True`` it fires under any strategy, when the level-crossing probabilities of the averaged route could not be certified. 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.2.0 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: within each probe interval, the test asks what fraction of the change in ``H`` falls in one half (see ``magnus.adiabatic._profile_is_resolved`` and :data:`magnus.adiabatic.RESOLUTION_RATIO`). For a :math:`C^1` profile the halves carry about half each; a jump lands in one half. 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. The routes above do not raise it when ``t_breakpoints`` was supplied: the caller has then said where the edges are, and the grid honors them. It is raised by :func:`magnus.magnus.magnus_expansion_multislab`, whose slabs are given, when a declared breakpoint lies strictly inside one of them. **Measured rates** (``docs/dev/adversarial_batteries/warn_fp.py``, the 160 of its 168 configurations that are valid input, including random piecewise-constant profiles with the edges deliberately left undeclared): fired 56 times, **33 true positives and 23 false positives -- 41 %**. Read that number carefully: this reports a *condition about the input*, not a prediction about the error, and a false positive means the undeclared discontinuity was there but 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. **Also raised by** :func:`osc_prob_energy_baseline` **on its per-point path**, where the refinement ladder answers one point at a time (issue #161). There a slab straddling an undeclared jump converges as :math:`1/n` and the ladder can stop wherever two levels happen to agree: on a three-layer castle wall, 24 of 40 baselines were off by more than the requested 1e-3, up to 7.0e-2. When the ladder's own samples show a jump (see :data:`UNDECLARED_JUMP_STEP_TOLERANCE`), the jumps are located and declared, every point is computed with them, and this warning names them and the ``t_breakpoints`` to pass. The answer is then right: 4.5e-13 on that castle wall. .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 Raised by the per-point path of osc_prob_energy_baseline, which then declares the jumps it found (issue #161). """
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.2.0 ``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 every time it samples, naming the half-width of the window and the number of samples it used and the standard error of the mean; ``average_spread`` and ``average_n_samples`` set the first two (issue #134). .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 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.2.0 """
[docs] class IgnoredQuadratureSettingWarning(UserWarning): r"""Warns that a points-per-slab setting was given to the Gauss-Legendre method, which ignores it. ``n_tpts_per_slab``, ``min_n_tpts_per_slab``, ``max_n_tpts_per_slab`` and ``growth_factor_n_tpts_per_slab`` set how many points per slab the ``'trapezoid'`` and ``'simpson'`` quadratures sample the Hamiltonian at, and how that number is refined. The default ``integration_method='gl'`` evaluates the Hamiltonian at its own fixed nodes, one to four per slab by order, so it overrides all four: a value passed with it changed nothing, silently (issue #160 §5). The result is unaffected; the warning says the setting was not used. Only a value the caller passed is reported, never a default forwarded between layers. .. versionadded:: 1.2.0 """
#: The points-per-slab settings that only ``'trapezoid'`` and ``'simpson'`` use. _TPTS_SETTINGS = ('n_tpts_per_slab', 'min_n_tpts_per_slab', 'max_n_tpts_per_slab', 'growth_factor_n_tpts_per_slab') def _check_log_path(filename_log, where: str) -> None: r"""Refuses a ``filename_log`` that cannot be opened for writing, before any work is done. The log file used to be opened deep inside :func:`osc_prob`, so a missing directory or a path naming a directory surfaced as a raw ``FileNotFoundError`` or ``IsADirectoryError`` after the call had started (issue #160 §4). .. versionadded:: 1.2.0 """ import os path = os.fspath(filename_log) folder = os.path.dirname(os.path.abspath(path)) if os.path.isdir(path): why = "it is a directory" elif not os.path.isdir(folder): why = "its directory " + repr(folder) + " does not exist" elif not os.access(folder, os.W_OK): why = "its directory " + repr(folder) + " is not writable" else: return raise ValueError(_v._msg(where, "filename_log = " + repr(path) + " cannot be written: " + why + ". Give a file path in an existing, writable directory, " "or pass save_log=False.")) def _warn_ignored_quadrature_settings(where: str, given) -> None: r"""Raises :class:`IgnoredQuadratureSettingWarning` naming ``given``, at the caller's line. .. versionadded:: 1.2.0 """ frame, level = sys._getframe(0), 1 while frame is not None and frame.f_globals.get('__name__', '').startswith('magnus'): frame, level = frame.f_back, level + 1 warnings.warn( "oscprob." + where + ": " + ", ".join(given) + (" has" if len(given) == 1 else " have") + " no effect with integration_method='gl', which evaluates the Hamiltonian at its own " "Gauss-Legendre nodes; the points per slab apply to 'trapezoid' and 'simpson' only. " "The result is unaffected. Drop the setting, or pass " "integration_method='trapezoid' or 'simpson' to use it.", IgnoredQuadratureSettingWarning, stacklevel=level) #----------------------------------------------------------------------- # Helper functions #----------------------------------------------------------------------- def _not_a_single_number(source_func_name: str, names: list) -> ValueError: r"""The error for arguments given as arrays where only a single number is accepted (#116). Only ``energy`` and ``L`` take arrays. Without this, an array elsewhere failed deep in the call with a NumPy message ("The truth value of an array ... is ambiguous", "setting an array element with a sequence") that named neither the argument nor the rule. Built only on the failing path, so a valid call pays nothing for it. .. versionadded:: 1.2.0 """ return ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " + ", ".join(names) + (" is not a single number" if len(names) == 1 else " are not single numbers") + ". Only energy and L take arrays; to scan another " "parameter, make one call per value.") def _raise_if_array_params(source_func_name: str, params: dict) -> None: r"""Raise :func:`_not_a_single_number` for the entries of ``params`` that are not scalars. Called from the except clause of the parameter unpackers, so it runs only when building their array has already failed; returns quietly when every entry is a scalar, and the caller then re-raises the original error. .. versionadded:: 1.2.0 """ bad = [k for k, v in params.items() if (v is not None) and (np.ndim(v) != 0)] if bad: raise _not_a_single_number(source_func_name, bad) from None def _caller_name(default: str) -> str: r"""The public wrapper that called the scenario function running this, else ``default``. Error messages name the function the user called. The 56 ``osc_prob_{N}nu_*`` wrappers forward to four scenario functions, which used to sign every message with their own name, so a user of ``osc_prob_3nu_sun`` was told about ``osc_prob_matter_std_potential``. One frame lookup per call (issue #160). .. versionadded:: 1.2.0 """ # The outermost of a chain of wrappers: osc_prob_3nu_sun calls # osc_prob_3nu_matter_exp_density, and the user called the first. f, name = sys._getframe(2), default while f is not None and _WRAPPER_NAME_PREFIX(f.f_code.co_name): name, f = f.f_code.co_name, f.f_back return name def _WRAPPER_NAME_PREFIX(name: str) -> bool: return (name.startswith('osc_prob_') and len(name) > 12 and name[9] in '2345' and name[10:13] == 'nu_') def _flavor_index(name, x, where): return _v.check_int(name, x, where, lo=0, allow_none=True) def _fraction(name, x, where): return _v.check_real(name, x, where, lo=0.0, hi=1.0, allow_none=True) def _nonneg_or_none(name, x, where): return _v.check_real(name, x, where, nonnegative=True, allow_none=True) def _ratio(name, x, where): if callable(x): return x return _v.check_real(name, x, where, nonnegative=True, allow_none=True) def _str_or_none(name, x, where): if x is None or isinstance(x, str): return x raise _v.InputTypeError(_v._msg(where, name + " must be a string; got " + type(x).__name__ + ".")) def _str(name, x, where): if isinstance(x, str): return x raise _v.InputTypeError(_v._msg(where, name + " must be a string; got " + type(x).__name__ + ".")) def _flag(name, x, where): return _v.check_bool(name, x, where) # The rules for every argument the wrappers, the scenario functions and osc_prob_energy_baseline # take by name, beyond energy, L and the oscillation parameters (validated where they are # unpacked). Applied once per call by _validate_entry, before any engine is chosen, so that the # answer to "is this argument valid?" does not depend on which engine would have answered # (issue #160 §5: max_n_slabs=-1 used to be refused by the general ladder and answered by the # adiabatic engine). _ENTRY_RULES = dict(_v.REFINEMENT_RULES, **{ 'num_flavors': lambda name, x, where: _v.check_int(name, x, where, lo=2), 'L0': lambda name, x, where: _v.check_real(name, x, where), 'rho': _nonneg_or_none, 'rho_central': _nonneg_or_none, 'l_scale': lambda name, x, where: _v.check_real(name, x, where, positive=True), 'electron_fraction': _fraction, 'electron_fraction_core': _fraction, 'electron_fraction_mantle': _fraction, 'electron_fraction_crust': _fraction, 'electron_fraction_ocean': _fraction, 'ratio_number_neutrons_to_protons': _ratio, 'density_matter_ocean': _nonneg_or_none, 'source_depth': _nonneg_or_none, 'detector_depth': _nonneg_or_none, 'nu_i': _flavor_index, 'nu_f': _flavor_index, 'nubar': _flag, 'average': _flag, 'density_matter_is_in_g_per_cm3': _flag, 'density_is_of_number_of_electrons': _flag, 'return_evolution_operator': _flag, 'validate_input': _flag, 'save_log': _flag, 'close_file_log_upon_exit': _flag, 'H_func_is_function_only_of_energy': _flag, 'average_spread': lambda name, x, where: _v.check_real( name, x, where, nonnegative=True, allow_none=True), 'average_n_samples': lambda name, x, where: _v.check_int( name, x, where, lo=2, allow_none=True), 'average_initial_state': lambda name, x, where: _v.check_choice( name, x, where, ('flavor', 'decohered'), allow_none=True), 'strategy': lambda name, x, where: _v.check_choice( name, x, where, ('auto', 'hybrid', 'magnus')), 'filename_log': _str, 'default_osc_params_set_name': _str, 'angles': lambda name, x, where: _v.check_choice( name, x, where, ('sin', 'sin2', 'rad', 'deg')), }) _ENTRY_DEFAULTS = {} _NO_DEFAULT = object() # Arguments for which None is not a value: the flags, and the two settings whose None the # entry points used to pass on to code that could not take it (issue #160 §4). Every other # argument's None means "not given" and is skipped, as before. _NONE_REFUSED = frozenset(('nubar', 'average', 'density_matter_is_in_g_per_cm3', 'density_is_of_number_of_electrons', 'return_evolution_operator', 'strict_convergence', 'validate_input', 'save_log', 'close_file_log_upon_exit', 'H_func_is_function_only_of_energy', 'cumulative', 'default_osc_params_set_name', 'n_jobs')) def _validation_requested(validate_input, where: str) -> bool: r"""``validate_input``, checked before it decides whether anything else is (issue #160 §4). Read as a truth value before any check ran, so ``None`` turned validation off silently and an array raised NumPy's "truth value of an array is ambiguous". .. versionadded:: 1.2.0 """ if validate_input is True or validate_input is False: return validate_input return bool(_v.check_bool('validate_input', validate_input, "oscprob." + where)) _SCENARIO_FUNCTION_NAMES = frozenset(('osc_prob_vacuum', 'osc_prob_matter_std_potential', 'osc_prob_matter_nsi', 'osc_prob_liv')) def _entry_defaults(func) -> dict: d = _ENTRY_DEFAULTS.get(func) if d is None: d = {k: p.default for k, p in signature(func).parameters.items() if p.default is not p.empty} _ENTRY_DEFAULTS[func] = d return d def _exp_profile(rho_central, l_scale, source_func_name: str): r"""``matter.exp_density_profile``, with its two arguments checked in the wrapper's name. The factory checks its arguments only for callers outside the package, so a wrapper's ``l_scale=nan`` used to surface as "rho_func must be finite; it returned nan at L0" (issue #160 §1). .. versionadded:: 1.2.0 """ where = "oscprob." + source_func_name _ENTRY_RULES['rho_central']('rho_central', rho_central, where) _ENTRY_RULES['l_scale']('l_scale', l_scale, where) return matter.exp_density_profile(rho_central, l_scale) #: The arguments whose combinations _entry_combinations checks; only these are collected. _COMBINATION_KEYS = frozenset(('magnus_exp_order', 'integration_method', 'cumulative', 'save_log', 'n_tpts_per_slab', 'min_n_tpts_per_slab', 'max_n_tpts_per_slab', 'growth_factor_n_tpts_per_slab')) def _entry_combinations(source_func_name: str, where: str, merged: dict, given: list) -> None: r"""The checks of :func:`_validate_entry` that involve an argument the caller set. .. versionadded:: 1.2.0 """ if 'magnus_exp_order' in given or 'integration_method' in given: _v.check_gl_order(merged.get('magnus_exp_order', 4), merged.get('integration_method', 'gl'), where) # The phase average answers in closed form or by its own sampling, never by a baseline scan, # so a cumulative=True beside it was accepted and ignored (issue #160 §5). if 'cumulative' in given and merged.get('average') is True \ and merged.get('cumulative') is True: raise ValueError(_v._msg(where, "cumulative=True asks for a baseline scan by one " "traversal, and average=True returns the phase average, which " "takes no such scan: pass one or the other.")) if 'save_log' in given and merged.get('save_log') is True \ and merged.get('file_log') is None: _check_log_path(merged.get('filename_log', './out.log'), where) tpts = [k for k in _TPTS_SETTINGS if k in given] if tpts and merged.get('integration_method', 'gl') == 'gl': _warn_ignored_quadrature_settings(source_func_name, tpts) def _validate_entry(source_func_name: str, values: dict, func=None) -> None: r"""Apply :data:`_ENTRY_RULES` to the arguments in ``values`` (a scenario function's locals). ``values['kwargs']``, when present, is checked the same way, since the refinement keywords reach the scenario functions through it. Once per call; never inside an engine. .. versionadded:: 1.2.0 """ where = "oscprob." + source_func_name rules = _ENTRY_RULES # An argument still holding the signature's own default is valid by construction, and # most arguments of most calls are; skipping them by identity keeps this pass to about a # microsecond on a single-point call. defaults = _entry_defaults(func) if func is not None else {} # The arguments the caller actually set, collected on the way: the combination checks # below look only at these, so a call that sets none of them pays nothing for them. given = [] for key, x in values.items(): if x is defaults.get(key, _NO_DEFAULT) or (x is None and key not in _NONE_REFUSED): continue if key in _COMBINATION_KEYS: given.append(key) rule = rules.get(key) if rule is not None: rule(key, x, where) kw = values.get('kwargs') if kw: for key, x in kw.items(): if key in _COMBINATION_KEYS and (x is not None or key in _NONE_REFUSED): given.append(key) rule = rules.get(key) if rule is not None and (x is not None or key in _NONE_REFUSED): rule(key, x, where) merged = dict(values, **kw) else: merged = values if given: _entry_combinations(source_func_name, where, merged, given) # A floor above its ceiling is a contradiction. n_slabs above max_n_slabs is not: it is # clipped to the cap, with ToleranceNotAchievedWarning, by design. for lo_key, hi_key in (('min_n_slabs', 'max_n_slabs'), ('min_n_tpts_per_slab', 'max_n_tpts_per_slab')): lo, hi = merged.get(lo_key), merged.get(hi_key) if lo is not None and hi is not None and lo > hi: raise ValueError(_v._msg(where, lo_key + " (" + str(lo) + ") must be <= " + hi_key + " (" + str(hi) + ").")) _bp = merged.get('t_breakpoints') if _bp is not None: _bp = _v.check_real_array('t_breakpoints', _bp, where, allow_empty=True) # Breakpoints in raw kilometers lie far outside the path, so the jumps they mark go # undeclared and nothing warns (issue #141): the same test as for a baseline. _bpa = np.abs(np.asarray(_bp, dtype=float)) if _bpa.size and 0.0 < float(_bpa.max()) < gd.IMPLAUSIBLE_BASELINE_NATURAL_UNITS: warnings.warn(gd.WARNING_MSG_NO_COLOR + " " + where + ": t_breakpoints up to " + format(float(_bpa.max()), '.4g') + " were given. Every length crossing this " "API is in natural units, so these were most likely kilometers left " "unconverted, and they mark nothing on the path. Multiply by gd.UNIT_KM.", gd.BaselineUnitWarning, stacklevel=3) # Breakpoints of which none lies on the path mark nothing on it: a sign or a unit gone # wrong, accepted and ignored (issue #160 §6). Some off the path are fine -- the # breakpoints of a whole profile reused for a shorter path, as solar_models.rst does # with a table's rows -- since those on it still mark the jumps. The path runs from # L0 to the longest baseline. _Lr, _L0r = _minmax_1d(merged.get('L')), merged.get('L0', 0.0) _L0r = 0.0 if _L0r is None else _L0r if _bpa.size and _Lr is not None and _v.is_real_scalar(_L0r): _bpv = np.asarray(_bp, dtype=float) _lo, _hi = min(float(_L0r), _Lr[0]), max(float(_L0r), _Lr[1]) if not np.any((_bpv >= _lo) & (_bpv <= _hi)): raise ValueError(_v._msg(where, "none of the t_breakpoints lies on the path, " "between L0 = " + format(_lo, '.6g') + " and the longest baseline, " + format(_hi, '.6g') + " [eV^-1], so they mark nothing on it; the first is " + format(float(_bpv.ravel()[0]), '.6g') + ". Check their sign and unit.")) # Slab edges given for a single baseline are checked here, where the message can name the # function the caller called; osc_prob checks them again for its own direct callers. _te = merged.get('t_slab_edges') if _te is not None: _Lv, _L0v = merged.get('L'), merged.get('L0', 0.0) _L0v = 0.0 if _L0v is None else _L0v if _v.is_real_scalar(_Lv) and _v.is_real_scalar(_L0v): _v.check_slab_edges(_te, float(_L0v), float(_Lv), where) def _as_float(x): r"""A real scalar of any NumPy type as ``float``; anything else unchanged (issue #158).""" return float(x) if (type(x) is not float) and _v.is_real_scalar(x) else x def _minmax_1d(x): r"""``(min, max)`` of a plain float or a non-empty 1-D real array (or list); else None. None sends the caller to the detailed checks, which name what is wrong. """ if type(x) is float: return x, x if isinstance(x, (list, np.ndarray)) and not isinstance(x, np.ma.MaskedArray): a = np.asarray(x) if a.ndim == 1 and a.size and a.dtype.kind in 'iuf': return float(a.min()), float(a.max()) return None
[docs] def validate_input_battery( source_func_name: str, energy: Optional[Union[int, float, list, np.ndarray]]=None, L: Optional[Union[int, float, list, np.ndarray]]=None, L0: Optional[Union[int, float]]=None, num_flavors: Optional[int]=None, nu_i: Optional[int]=None, nu_f: Optional[int]=None, osc_params: Optional[Union[list, np.ndarray]]=None, rho_func: Optional[Union[Callable, int, float]]=None, ratio_number_neutrons_to_protons: Optional[Union[int, float, Callable]]=1.0, electron_fraction: Optional[Union[int, float]]=0.5, validate_energy_and_L: Optional[bool]=True, validate_flavor_indices: Optional[bool]=True, validate_osc_params: Optional[bool]=True, validate_initial_position: Optional[bool]= False, validate_density: Optional[bool]=False ) -> None: r"""Validates the inputs common to the ``osc_prob_*`` family of functions. Runs a battery of type/shape/value checks (selected by the ``validate_*`` flags below) and raises :class:`ValueError` with a descriptive message identifying the offending argument and the calling function (via ``source_func_name``) if any check fails, rather than letting an invalid input propagate into a cryptic NumPy/linear-algebra error deep inside the Magnus core. .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 A single-entry energy or L is accepted against a longer one (issue #160 §1). Parameters ---------- source_func_name : str Name of the calling function, used to build more informative error messages. energy : int, float, list, or np.ndarray, optional Neutrino energy/energies to validate (checked if ``validate_energy_and_L`` is True). L : int, float, list, or np.ndarray, optional Baseline(s) to validate (checked if ``validate_energy_and_L`` is True). L0 : int or float, optional Initial position to validate (checked if ``validate_initial_position`` is True). num_flavors : int, optional Number of neutrino flavors, used to validate ``nu_i``/``nu_f``/``osc_params``. nu_i : int, optional Initial flavor index to validate (checked if ``validate_flavor_indices`` is True). nu_f : int, optional Final flavor index to validate (checked if ``validate_flavor_indices`` is True). osc_params : list or np.ndarray, optional Unpacked oscillation parameters to validate (checked if ``validate_osc_params`` is True). rho_func : Callable, int, or float, optional Matter density (function or constant) to validate (checked if ``validate_density`` is True). ratio_number_neutrons_to_protons : int, float, or Callable, optional Ratio of the number of neutrons to protons in matter, validated alongside the density. A callable (a position-resolved ratio, :math:`r(l)`) passes through unvalidated: one probe evaluation could not establish nonnegativity along the whole trajectory anyway. Default: 1.0. electron_fraction : int or float, optional Electron fraction, validated alongside the density. Default: 0.5. validate_energy_and_L : bool, optional If True, validate ``energy`` and ``L``. Default: True. validate_flavor_indices : bool, optional If True, validate ``nu_i`` and ``nu_f`` against ``num_flavors``. Default: True. validate_osc_params : bool, optional If True, validate ``osc_params``. Default: True. validate_initial_position : bool, optional If True, validate ``L0``. Default: False. validate_density : bool, optional If True, validate ``rho_func``, ``ratio_number_neutrons_to_protons``, and ``electron_fraction``. Default: False. Returns ------- None Raises ------ ValueError If any requested check fails. The message names the offending argument and the calling function. Warns ----- magnus.globaldefs.BaselineUnitWarning When a baseline looks like a number of kilometers rather than a length in eV^-1. """ if validate_energy_and_L: _where = "oscprob." + source_func_name # The common cases -- plain floats, or clean 1-D real arrays -- are settled by one # min/max pair per argument: a NaN makes the minimum NaN and fails every comparison, # so the pair establishes finiteness, sign and L >= L0 at once. Anything else, or a # failure, takes the general checks below, which name the entry at fault. _E_mm, _L_mm = _minmax_1d(energy), _minmax_1d(L) _L0v = 0.0 if L0 is None else L0 _fast = (_E_mm is not None and _L_mm is not None and type(_L0v) in (float, int) and 0.0 < _E_mm[0] and _E_mm[1] < math.inf and _L0v <= _L_mm[0] and _L_mm[1] < math.inf) if validate_energy_and_L and not _fast: # Every entry is checked: finite, and of a real type (not bool, not complex). A NaN # used to return a NaN row, an inf energy a probability of 0 (or 1 on the averaged # route), and np.int64/np.float32 scalars were refused outright (issue #160 §1). _v.check_real_array('energy', energy, _where) _v.check_real_array('L', L, _where) # A non-positive energy is rejected rather than propagated, because it does not # fail: E < 0 flips the sign of the whole Hamiltonian, which is CP conjugation, so # the call returns the ANTINEUTRINO probability -- exactly unitary, entirely # plausible, and an answer to a question the caller did not ask. Measured on a # 1 GeV, 1300 km baseline, P(-E) matches P(nubar=True) to 1e-15 and differs from # P(+E) by 2.3e-02 in vacuum and 4.4e-02 in matter. A caller who wants # antineutrinos has `nubar=True`; a caller who arrives here has a sign error. # E = 0 is rejected with it: the vacuum phase goes as L/E and returns NaN. if not np.all(np.asarray(energy, dtype=float) > 0.0): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": energy must be positive. A negative energy is not an error the" + \ " calculation reports: it returns the antineutrino probability, which is" + \ " unitary and looks correct. Use nubar=True for antineutrinos.") # The path runs from L0 to L, so every L must be at least L0. Refused here, by name, # rather than several layers down as "t_fin must be >= t_ini" -- or not at all, on the # engines that answer before that check (issue #160 §1). _L0 = 0.0 if L0 is None else L0 if _v.is_real_scalar(_L0): _La = np.asarray(L, dtype=float) if not np.all(_La >= _L0): _i = int(np.argmin(_La >= _L0)) if _La.ndim else 0 raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": L must be >= L0, the path running from L0 to L; got L = " + repr(float(_La.ravel()[_i])) + (" (entry " + str(_i) + ")" if _La.ndim else "") + " and L0 = " + repr(float(_L0)) + ".") if validate_energy_and_L: # A single-entry array against a longer one is broadcast, as osc_prob_energy_baseline # always did (issue #160 §1); the wrappers used to refuse it. if ( (isinstance(energy, list) or isinstance(energy, np.ndarray)) and \ (isinstance(L, list) or isinstance(L, np.ndarray)) and \ (len(energy) != len(L)) and len(energy) != 1 and len(L) != 1 ): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": since the input energy and L are both lists or NumPy arrays, they must have " + \ "the same length, or one of them a single entry.") # An energy that looks like MeV or GeV left unconverted (issue #160 §1); see # globaldefs.EnergyUnitWarning. _smallest_E = _E_mm[0] if _fast else float(np.min(np.asarray(energy, dtype=float))) if _smallest_E < gd.IMPLAUSIBLE_ENERGY_NATURAL_UNITS: warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": an energy of " + format(_smallest_E, '.4g') + " eV was given. Energies " "crossing this API are in eV, so this was most likely MeV or GeV left " "unconverted; multiply by gd.UNIT_MEV or gd.UNIT_GEV. Silence with " "warnings.filterwarnings('ignore', category=gd.EnergyUnitWarning).", gd.EnergyUnitWarning, stacklevel=3) # A baseline that looks like kilometers. This does not fail on its own: the call # returns a converged, unitary probability for a baseline a few meters long, which # is why it is worth a warning. See globaldefs.BaselineUnitWarning. if L is not None: _largest_L = (max(abs(_L_mm[0]), abs(_L_mm[1])) if _fast else float(np.max(np.abs(np.asarray(L, dtype=float))))) if 0.0 < _largest_L < gd.IMPLAUSIBLE_BASELINE_NATURAL_UNITS: warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + source_func_name + ": a baseline of " + format(_largest_L, '.4g') + " was given. Every length " "crossing this API is in natural units, and " + format(_largest_L, '.4g') + " eV^-1 is about " + format(_largest_L/gd.CONV_KM_TO_INV_EV*1.0e3, '.2g') + " m, so this was most likely read in kilometers and left unconverted. The " "call will not fail: it will return a converged, unitary probability for a " "baseline a few meters long, which looks like an ordinary answer rather " "than a wrong one. Multiply by gd.UNIT_KM (or gd.CONV_KM_TO_INV_EV) to " "convert. Silence with warnings.filterwarnings('ignore', " "category=gd.BaselineUnitWarning).", gd.BaselineUnitWarning, stacklevel=3) if (((nu_i is not None) and (nu_f is None)) or ((nu_i is None) and (nu_f is not None))): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": if either nu_i or nu_f is not None, the other flavor must also be not None.") if validate_flavor_indices: # Integers only: 1.0 passed the membership test below (1.0 == 1) and then failed as a # raw IndexError, and True was read as 1 (issue #158 §2). if nu_i is not None and np.ndim(nu_i) == 0: nu_i = _v.check_int('nu_i', nu_i, "oscprob." + source_func_name, lo=0) if nu_f is not None and np.ndim(nu_f) == 0: nu_f = _v.check_int('nu_f', nu_f, "oscprob." + source_func_name, lo=0) if ((nu_i is not None) and (nu_f is not None)): if (num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS): if ((num_flavors == 2) or (num_flavors == 3)): flavors = set([gd.NUE, gd.NUMU, gd.NUTAU]) elif (num_flavors == 4): flavors = set([gd.NUE, gd.NUMU, gd.NUTAU, gd.NUS]) elif (num_flavors == 5): flavors = set([gd.NUE, gd.NUMU, gd.NUTAU, gd.NUS1, gd.NUS2]) try: unknown = (nu_i not in flavors) or (nu_f not in flavors) except TypeError: raise _not_a_single_number(source_func_name, ['nu_i or nu_f']) from None if unknown: if ((num_flavors == 2) or (num_flavors == 3)): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": if nu_i and nu_f are not None, they must be either gd.NUE (" + \ str(gd.NUE) + "), gd.NUMU (" + str(gd.NUMU) + "), or gd.NUTAU (" + \ str(gd.NUTAU) + ") only.") elif (num_flavors == 4): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": if nu_i and nu_f are not None, they must be either gd.NUE (" + \ str(gd.NUE) + "), gd.NUMU (" + str(gd.NUMU) + "), gd.NUTAU (" + \ str(gd.NUTAU) + "), or gd.NUS (" + str(gd.NUS) + ") only.") elif (num_flavors == 5): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": if nu_i and nu_f are not None, they must be either gd.NUE (" + \ str(gd.NUE) + "), gd.NUMU (" + str(gd.NUMU) + "), gd.NUTAU (" + \ str(gd.NUTAU) + "), gd.NUS1 (" + str(gd.NUS1) + "), or gd.NUS2 (" + \ str(gd.NUS2) + ") only.") else: print(gd.WARNING_MSG_IN_COLOR + " " + source_func_name + \ ": nu_i and nu_f are not None, but, since num_flavors = " + str(num_flavors) + \ " > globaldefs.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS = " + \ str(gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS) + ", input validation cannot " + \ "check if nu_e and nu_f are valid indices.") if validate_osc_params: # Named one by one where the keys are known (unpack_oscillation_params_from_dict); # this is the backstop for callers that pass the list directly. for _k, _x in enumerate(osc_params): if _x is not None: _v.check_real('oscillation parameter #' + str(_k), _x, "oscprob." + source_func_name) if validate_initial_position: _v.check_real('L0', L0, "oscprob." + source_func_name) if validate_density: _where = "oscprob." + source_func_name # The constant-density wrappers take the density as `rho`; the message names what the # caller passed (issue #160 §1). _rho_name = 'rho' if source_func_name.endswith('constant_density') else 'rho_func' # A callable ratio (r resolved per position, matter_potential_projector's widened # form) is not range-checked: a single probe evaluation would prove nothing about # the rest of the trajectory, and rho_func already sets the precedent that functions # are trusted on their values. if not callable(ratio_number_neutrons_to_protons): _v.check_real('ratio_number_neutrons_to_protons', ratio_number_neutrons_to_protons, _where, nonnegative=True) _v.check_real('electron_fraction', electron_fraction, _where, lo=0.0, hi=1.0, what="between 0 and 1 (the number of electrons per atomic mass unit)") if ((callable(rho_func)) and (_n_required_params(rho_func) > 1)): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ":"+\ " the provided rho_func is a function of more than one parameter.") rho_test = rho_func(L0) if callable(rho_func) else rho_func _rho_what = 'rho_func(L0)' if callable(rho_func) else _rho_name if np.ndim(rho_test) != 0: raise _not_a_single_number(source_func_name, [_rho_what]) # Any real scalar: a vectorized profile written with np.where returns a 0-d array for a # scalar position, and np.float32 is a float (issues #144 §5, #158). Checked before # the density reaches the unit guards, because a NaN reaches them as a comparison that # is False either way and used to be reported as a *units* mistake. if not _v.is_real_scalar(rho_test): raise _v.InputTypeError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " + (("rho_func must return a real number; it returned " + repr(rho_test) + ".") if callable(rho_func) else (_rho_name + " must be a real number; got " + repr(rho_test) + "."))) if not np.isfinite(float(rho_test)): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " + (("rho_func must be finite; it returned " + str(rho_test) + " at L0.") if callable(rho_func) else (_rho_name + " must be finite; got " + str(rho_test) + "."))) if (rho_test < 0.0): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " + _rho_what + " must be non-negative; got " + str(rho_test) + ".") # Two more samples of a callable profile, at the middle and the far end of the path: a # profile that turns NaN partway used to surface as a raw LinAlgError from the # eigensolver (issue #160 §7). Two evaluations per call, never per node. # A wrapper builds its profile itself (PREM, exponential, solar table), so only a # rho_func the user wrote is probed. if callable(rho_func) and L is not None and _v.is_real_scalar(L0) and \ not _WRAPPER_NAME_PREFIX(source_func_name): _Lend = float(np.max(np.asarray(L, dtype=float))) for _pos in (0.5*(float(L0) + _Lend), _Lend): _r = rho_func(_pos) if np.ndim(_r) != 0 or not _v.is_real_scalar(_r) or not np.isfinite(float(_r)) \ or _r < 0.0: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": rho_func must return a finite, non-negative real number along the " "whole path; at position " + format(_pos, '.6g') + " it returned " + repr(_r) + ".")
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". .. versionchanged:: 1.2.0 A ratio is refused at two and three flavors, where it has no effect, and a callable one is checked at its first value (issue #160 §1). """ # 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 # A real number, not a bool (True was read as 1) and not complex (issue #160 §1). _v.check_real(name, value, "oscprob." + source_func_name, lo=0.0, hi=1.0, lo_open=True, what="in (0, 1], being an electron fraction, " "Y_e = <Z/A>") _ratio('ratio_number_neutrons_to_protons', ratio_number_neutrons_to_protons, "oscprob." + source_func_name) # The caller's ratio enters only the sterile projector: the density derives its own from # Y_e. At two and three flavors there is no sterile block, so a ratio, scalar or callable, # was accepted and ignored; above that a callable's first sample is checked, as rho_func's # is, since a NaN one came back as NaN probabilities (issue #160 §1). if ratio_number_neutrons_to_protons is not None: if num_flavors in (2, 3): raise ValueError(_v._msg("oscprob." + source_func_name, "ratio_number_neutrons_to_protons has no effect here: the Earth wrappers " "derive the density's ratio from Y_e, and at %d flavors the caller's ratio " "enters nothing." % num_flavors)) if callable(ratio_number_neutrons_to_protons): _first = ratio_number_neutrons_to_protons(0.0) _v.check_real_array('ratio_number_neutrons_to_protons(0)', np.ravel(_first) if np.ndim(_first) else _first, "oscprob." + source_func_name, nonnegative=True) if density_matter_ocean is not None: density_matter_ocean = _v.check_real('density_matter_ocean', density_matter_ocean, "oscprob." + source_func_name, nonnegative=True) 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) # rho N_A Y_e, the layer's own Y_e (issue #168: the neutron-to-proton ratio no longer # enters the conversion). The ratio's one 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, electron_fraction=ye_of_r(r), 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
[docs] def validate_input_osc_prob_earth( source_func_name: str, loc_ini: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None, loc_fin: Optional[Union[Tuple[float, float], list, np.ndarray, str]]=None, costhz: Optional[Union[int, float]]=None, L: Optional[Union[float, list, np.ndarray]]=None, verbose: Optional[int]=0, source_depth: Optional[float]=0.0, detector_depth: Optional[float]=0.0, ) -> Tuple[float, np.ndarray]: r"""Resolves (costhz, L) for :func:`osc_prob_earth`, from either two locations or costhz+L. Implements the two mutually exclusive ways of specifying an Earth-crossing trajectory: either give both ``loc_ini`` and ``loc_fin`` (the chord's zenith angle and length are computed from their coordinates), or give ``costhz`` and ``L`` directly. Aborts with a descriptive error if exactly one location is given, or if neither locations nor (costhz, L) are given. Burying an endpoint changes which of those the caller has to supply. ``detector_depth`` says where the trajectory ends, and so does ``L``; giving both is an error rather than a silent choice between them. With either depth set and ``L`` left as None, the baseline is computed from the geometry. At the default depths of zero nothing changes: ``L`` stays required alongside ``costhz``, as in every earlier version. .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 Added ``source_depth`` and ``detector_depth``. Parameters ---------- source_func_name : str Name of the calling function, used to build more informative error messages. loc_ini : tuple, list, np.ndarray, or str, optional Initial location, as (latitude, longitude) coordinates or a predefined location name (see :data:`magnus.earth.loc_coords_dms`). Must be given together with ``loc_fin``. loc_fin : tuple, list, np.ndarray, or str, optional Final location, same format as ``loc_ini``. Must be given together with ``loc_ini``. costhz : int or float, optional Cosine of the zenith angle of the neutrino. Used only if ``loc_ini``/``loc_fin`` are not given. L : float, list, or np.ndarray, optional Baseline(s). Used only if ``loc_ini``/``loc_fin`` are not given. verbose : int, optional Verbosity level: if > 0, print a note when the chord between the two given locations is used as the baseline. Default: 0. source_depth : float, optional Depth of the entry point below the surface [:math:`\text{eV}^{-1}`]. Default: 0.0. detector_depth : float, optional Depth of the detector below the surface [:math:`\text{eV}^{-1}`]. Default: 0.0. Returns ------- (float, np.ndarray) The resolved ``(costhz, L)`` pair. """ # Both depths are declared Optional, so None has to mean "no depth"; see # earth._depths_or_zero for why normalizing beats letting float(None) surface. source_depth, detector_depth = earth._depths_or_zero(source_depth, detector_depth) _where = "oscprob." + source_func_name if type(source_depth) is not float: source_depth = _v.check_real('source_depth', source_depth, _where, nonnegative=True) if type(detector_depth) is not float: detector_depth = _v.check_real('detector_depth', detector_depth, _where, nonnegative=True) buried = (source_depth != 0.0) or (detector_depth != 0.0) if buried and (loc_ini is not None or loc_fin is not None): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": the two " "locations on the surface of the Earth (loc_ini, loc_fin) fix a surface-to-surface " "chord, so neither source_depth nor detector_depth applies to them. Give costhz " "with the depths instead, or drop the depths.") if (detector_depth != 0.0) and (L is not None): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "detector_depth says where the trajectory ends and so does L, and the two need " "not agree. Give one or the other: omit L to have the baseline computed from the " "geometry, or omit detector_depth to stop the trajectory at the L you name.") # 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. # 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). if ( ((loc_ini is None) and (loc_fin is not None)) or \ ((loc_ini is not None) and (loc_fin is None)) ): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": only one of the two " + \ "locations on Earth (loc_ini or loc_fin) has been given. If one location is " + \ "given (i.e., is not None), the other one must also be given. Alternatively, " + \ "both locations can be set to None, and the given value of costhz will be used " +\ "(if it is not None).") elif ((loc_ini is not None) and (loc_fin is not None)): # Check that the location is a two-entry tuple, list, or array # Unpacking a sequence of the wrong length raises ValueError, and unpacking # something that is not iterable at all raises TypeError; neither is a KeyError, # which is what this used to catch, so the message below could never be reached. # A three-entry tuple reported "too many values to unpack (expected 2)", and an # int escaped as a TypeError -- breaking the convention that bad input to this # package raises ValueError with a message naming the parameter at fault. try: lat_ini, lon_ini = loc_ini except (TypeError, ValueError): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": if the initial " + \ "location (loc_ini) is given as coordinates, it must be a two-entry tuple," + \ " list, or NumPy array.") try: lat_fin, lon_fin = loc_fin except (TypeError, ValueError): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": if the final " + \ "location (loc_fin) is given as coordinates, it must be a two-entry tuple," + \ " list, or NumPy array.") # Each coordinate a (degrees, minutes, seconds) triple in range (issue #160 §3): a # latitude of 146 degrees shifted P by 0.56, and decimal pairs failed as a raw TypeError. for _name, _dms, _lo, _hi in (('loc_ini latitude', lat_ini, -90.0, 90.0), ('loc_ini longitude', lon_ini, -180.0, 360.0), ('loc_fin latitude', lat_fin, -90.0, 90.0), ('loc_fin longitude', lon_fin, -180.0, 360.0)): earth._check_dms(_name, _dms, _where, _lo, _hi) # We use the function earth.costhz_between_points_on_surface to compute the cosine of the # zenith angle of the chord that joins two locations on the surface of the Earth, measured # at one position (any of the two locations will give the same result). costhz = earth.costhz_between_points_on_surface(lat_ini, lon_ini, lat_fin, lon_fin) # Length of the chord is the baseline L = earth.distance_traveled_inside_earth(costhz)*gd.UNIT_KM # [eV^{-1}] if verbose > 0: print(gd.WARNING_MSG_IN_COLOR + " oscprob." + source_func_name + ": using as " + \ "baseline the chord that joins the given initial and final locations on the " + \ "surface of the Earth.") return costhz, L else: # (loc_ini is None) and (loc_fin is None) if (not isinstance(costhz, (int, float))) and (costhz is not None) and \ (np.ndim(costhz) != 0): raise _not_a_single_number(source_func_name, ['costhz']) if costhz is not None: # Checked by name before anything samples the profile: a NaN or out-of-range # costhz used to reach the user as "rho_func must be finite" or "value of r cannot # exceed EARTH_RADIUS" (issue #160 §3). costhz = _v.check_real('costhz', costhz, _where, lo=-1.0, hi=1.0, what="in [-1, 1] (the cosine of the zenith angle)") if costhz is None: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": no" + \ " initial and final locations on the surface of the Earth given, and no " + \ "value of costhz given. This function requires either the two locations " + \ "or, alternatively, the value of costhz.") if L is None: # With an endpoint underground there is a baseline to compute, so a missing L is # an omission the geometry can fill rather than an error. At the default depths # this branch is unreachable and the error below is the one raised, unchanged. if buried: return costhz, earth.distance_traveled_inside_earth( costhz, source_depth/gd.UNIT_KM, detector_depth/gd.UNIT_KM)*gd.UNIT_KM raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + \ ": since two locations on the surface of the Earth have not been given, " + \ "the value of costhz will be used to define the chord length, but the" + \ " baseline, L, cannot be None.") # A baseline longer than the chord leaves the Earth; named here rather than several # layers down as "value of l cannot be larger ..." (issue #160 §3). A shorter one is # a partial path, which is allowed. if _v.is_real_scalar(L) or isinstance(L, (list, tuple, np.ndarray)): _v.check_real_array('L', L, _where) _chord = earth.distance_traveled_inside_earth( costhz, source_depth/gd.UNIT_KM, detector_depth/gd.UNIT_KM) _Lmax = float(np.max(np.asarray(L, dtype=float)))/gd.UNIT_KM if _Lmax > _chord*(1.0 + 1e-9) + 1e-9: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": L = " + format(_Lmax, '.6g') + " km is longer than the chord through " "the Earth at costhz = " + format(costhz, '.6g') + ", which is " + format(_chord, '.6g') + " km.") return costhz, L
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.2.0 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
def _reject_parameter_set_name_without_set(num_flavors: int, default_osc_params_set_name: str, source_func_name: str) -> None: r"""Refuse ``default_osc_params_set_name`` where no parameter set applies (issue #110). The shipped sets fill in the three-flavor parameters left unset, at three to five flavors. At two flavors ``sth`` and ``Dm2`` are always required, and above five the vacuum Hamiltonian is supplied whole, so a set name there changed nothing -- silently, while a misspelled keyword on the same call raised. One string comparison on a valid call. .. versionadded:: 1.2.0 """ if (default_osc_params_set_name != 'OSC_PARAMS_DEFAULT') and \ not (2 < num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "default_osc_params_set_name=" + repr(default_osc_params_set_name) + " was given, " "but no parameter set applies at " + str(num_flavors) + " flavors: " + ("sth and Dm2 are always required at two flavors." if num_flavors == 2 else "above " + str(gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS) + " flavors the vacuum " "Hamiltonian is given whole, through h_vac_energy_indep.") + " Remove the keyword.")
[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
# The keys each parameter dict takes, per flavor count (issue #160 §2). A key outside the set # for the flavor count in use was ignored in silence: a typo'd 'dcp' left dCP at its previous # value, and 's14' at three flavors changed nothing. _OSC_KEYS = { 2: ('sth', 'Dm2'), 3: ('s12', 's23', 's13', 'dCP', 'D21', 'D31'), 4: ('s12', 's23', 's13', 'dCP', 's14', 'd14', 's24', 'd24', 's34', 'D21', 'D31', 'D41'), 5: ('s12', 's23', 's13', 'dCP', 's14', 'd14', 's15', 'd15', 's24', 'd24', 's25', 's34', 's35', 'd35', 'D21', 'D31', 'D41', 'D51'), } _NSI_KEYS = { 2: ('eps_aa', 'eps_ab'), 3: ('eps_ee', 'eps_em', 'eps_et', 'eps_mm', 'eps_mt', 'eps_tt'), 4: ('eps_ee', 'eps_em', 'eps_et', 'eps_es', 'eps_mm', 'eps_mt', 'eps_ms', 'eps_tt', 'eps_ts', 'eps_ss'), 5: ('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'), } # Diagonal couplings are real by hermiticity; the off-diagonal ones may be complex. _NSI_DIAGONAL = frozenset(('eps_aa', 'eps_ee', 'eps_mm', 'eps_tt', 'eps_ss', 'eps_s1s1', 'eps_s2s2')) _LIV_KEYS = { 2: ('sxi', 'b1', 'b2', 'Lambda', 'n_liv'), 3: ('sxi12', 'sxi23', 'sxi13', 'dxiCP', 'b1', 'b2', 'b3', 'Lambda', 'n_liv'), 4: ('sxi12', 'sxi23', 'sxi13', 'dxiCP', 'sxi14', 'dxi14', 'sxi24', 'dxi24', 'sxi34', 'b1', 'b2', 'b3', 'b4', 'Lambda', 'n_liv', 'dxi13'), 5: ('sxi12', 'sxi23', 'sxi13', 'dxiCP', 'sxi14', 'dxi14', 'sxi15', 'dxi15', 'sxi24', 'dxi24', 'sxi25', 'sxi34', 'sxi35', 'dxi35', 'b1', 'b2', 'b3', 'b4', 'b5', 'Lambda', 'n_liv', 'dxi13'), } _DICT_METADATA_KEYS = frozenset(('name', 'description')) def _check_param_dict(source_func_name: str, params, dict_name: str, num_flavors: int, table: dict) -> dict: r"""Check a parameter dict's keys and values once; return it with the values normalized. Unknown keys are refused, with the nearest valid name, or with the flavor count a key belongs to. Every value is ``None`` or a finite number: real, except the off-diagonal NSI couplings; ``Lambda`` above zero; ``n_liv`` an integer >= 0. A complex value with a zero imaginary part is accepted where a real one is required, and converted (issue #160 §2). .. versionadded:: 1.2.0 """ import collections.abc import difflib where = "oscprob." + source_func_name if not isinstance(params, collections.abc.Mapping): raise _v.InputTypeError(_v._msg(where, dict_name + " must be a dict (a mapping from " "parameter names to values); got " + type(params).__name__ + ".")) allowed = table.get(num_flavors) if allowed is None: return params allowed_set = frozenset(allowed) unknown = [k for k in params if k not in allowed_set and k not in _DICT_METADATA_KEYS] if unknown: notes = [] for k in unknown: other = [n for n, keys in sorted(table.items()) if k in keys and n != num_flavors] if other: notes.append(repr(k) + " is a " + str(other[0]) + "-flavor parameter, and with " "num_flavors=" + str(num_flavors) + " it would be ignored") else: near = ([a for a in allowed if a.lower() == str(k).lower()] or difflib.get_close_matches(str(k), allowed, n=1)) notes.append(repr(k) + " is not one" + (" (did you mean " + repr(near[0]) + "?)" if near else "")) raise ValueError(_v._msg(where, dict_name + " has unknown key" + ("s" if len(unknown) > 1 else "") + ": " + "; ".join(notes) + ". The keys at " + str(num_flavors) + " flavors are " + ", ".join(allowed) + ".")) # Every array at once, as before: a caller scanning two parameters hears about both. _arrays = [k for k, x in params.items() if k not in _DICT_METADATA_KEYS and x is not None and type(x) is not float and not isinstance(x, (str, bytes)) and np.ndim(x) != 0] if _arrays: raise _not_a_single_number(source_func_name, _arrays) out = None for k, x in params.items(): if x is None or k in _DICT_METADATA_KEYS: continue # The common case, a finite plain float, is valid for every key but these two. if (type(x) is float and k != 'n_liv' and math.isfinite(x) and (k != 'Lambda' or x > 0.0)): continue # A wrapper takes these as keywords, so its caller knows them by the bare name. name = (k if (dict_name == 'osc_params' or _WRAPPER_NAME_PREFIX(source_func_name)) else dict_name + "['" + k + "']") if table is _NSI_KEYS and k not in _NSI_DIAGONAL: if isinstance(x, (complex, np.complexfloating)) and not isinstance(x, bool): if not (math.isfinite(x.real) and math.isfinite(x.imag)): raise ValueError(_v._msg(where, name + " must be finite; got " + repr(x) + ".")) continue y = _v.check_real(name, x, where) else: if isinstance(x, (complex, np.complexfloating)) and x.imag == 0: x = x.real if k == 'n_liv': y = _v.check_int(name, x, where, lo=0, what="an integer >= 0 (the operator " "dimension minus 3)") elif k == 'Lambda': y = _v.check_real(name, x, where, positive=True) else: if isinstance(x, (complex, np.complexfloating)): raise _v.InputTypeError(_v._msg(where, name + " must be real" + (" (a diagonal NSI coupling is real by hermiticity)" if k in _NSI_DIAGONAL else "") + "; got " + repr(x) + ".")) y = _v.check_real(name, x, where) if y is not x: if out is None: out = dict(params) out[k] = y return params if out is None else out
[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 .. versionchanged:: 1.2.0 h_vac_energy_indep is refused up to MAGNUS_MAX_PREDEFINED_NUM_FLAVORS flavors, where it was ignored (issue #160 §1). 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. """ osc_params = _check_param_dict(source_func_name, osc_params, 'osc_params', num_flavors, _OSC_KEYS) # Up to MAGNUS_MAX_PREDEFINED_NUM_FLAVORS the Hamiltonian is built from osc_params, so a # matrix given here was accepted and ignored (issue #160 §1). if (h_vac_energy_indep is not None) and \ isinstance(num_flavors, (int, np.integer)) and \ (2 <= num_flavors <= gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "h_vac_energy_indep has no effect at num_flavors = " + str(num_flavors) + ": up to " + str(gd.MAGNUS_MAX_PREDEFINED_NUM_FLAVORS) + " flavors the vacuum Hamiltonian is " "built from osc_params. To propagate a Hamiltonian of your own, pass it to " "osc_prob_energy_baseline as H_func.") if (num_flavors == 2): try: sth = osc_params['sth'] Dm2 = osc_params['Dm2'] return np.array([sth, Dm2]) except ValueError: _raise_if_array_params(source_func_name, osc_params) raise 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 ValueError: _raise_if_array_params(source_func_name, osc_params) raise 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 ValueError: _raise_if_array_params(source_func_name, osc_params) raise 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 ValueError: _raise_if_array_params(source_func_name, osc_params) raise 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). """ nsi_params = _check_param_dict(source_func_name, nsi_params, 'nsi_params', num_flavors, _NSI_KEYS) 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 ValueError: _raise_if_array_params(source_func_name, nsi_params) raise 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 ValueError: _raise_if_array_params(source_func_name, nsi_params) raise 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 ValueError: _raise_if_array_params(source_func_name, nsi_params) raise 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 'sxi14', 'dxi14', 'sxi24', 'dxi24', 'sxi34', 'b4'. For 5, additionally 'sxi15', 'dxi15', 'sxi25', 'sxi35', 'dxi35', 'b5'. At four and five flavors, 'dxi13', the former name of 'dxiCP', is still accepted, with a :class:`FutureWarning`. 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). """ liv_params = _check_param_dict(source_func_name, liv_params, 'liv_params', num_flavors, _LIV_KEYS) 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 ValueError: _raise_if_array_params(source_func_name, liv_params) raise 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): liv_params = _angles.take_renamed_key('oscprob.' + source_func_name, liv_params, 'dxi13', 'dxiCP') 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'] 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, dxiCP, sxi14, dxi14, sxi24, dxi24, sxi34, b1, b2, b3, b4, Lambda, n_liv]) except ValueError: _raise_if_array_params(source_func_name, liv_params) raise 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', 'dxiCP'," + \ " 'sxi14', 'dxi14', 'sxi24', 'dxi24', 'sxi34', 'b1', 'b2', 'b3', 'b4'," + \ " 'Lambda', and 'n_liv'.") elif (num_flavors == 5): liv_params = _angles.take_renamed_key('oscprob.' + source_func_name, liv_params, 'dxi13', 'dxiCP') 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'] 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, dxiCP, sxi14, dxi14, sxi15, dxi15, sxi24, dxi24, sxi25, sxi34, sxi35, dxi35, b1, b2, b3, b4, b5, Lambda, n_liv]) except ValueError: _raise_if_array_params(source_func_name, liv_params) raise 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', 'dxiCP'," + \ " '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.2.0 Added ``return_evolution_operator``. .. versionchanged:: 1.2.0 An odd magnus_exp_order is refused with 'gl'; a points-per-slab setting given with 'gl' warns that it does nothing; new_recursion_limit is checked; an unwritable filename_log is refused before any work; the rule for counts above their caps is documented (issue #160 §4, §5). 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: ``'gl'`` starts from the phase-based estimate (:func:`magnus.magnus.suggest_n_slabs`), never below ``min_n_slabs``, and ``'trapezoid'``/``'simpson'`` start at ``min_n_slabs``. **The caps and the requested counts.** ``max_n_slabs`` and ``max_n_tpts_per_slab`` are ceilings on the *refinement*, so they apply only when a tolerance is requested. There, an ``n_slabs`` floor above ``max_n_slabs`` is clipped to ``max_n_slabs``, and the points per slab climb from ``min_n_tpts_per_slab`` and are clipped at ``max_n_tpts_per_slab``; stopping at either cap without two levels agreeing raises :class:`ToleranceNotAchievedWarning`. On a fixed grid (``rtol`` and ``atol`` both ``None``) nothing is refined, so ``n_slabs`` and ``n_tpts_per_slab`` are used as given, above their caps or not. 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. Used on a fixed grid; with a tolerance the points per slab climb from ``min_n_tpts_per_slab`` instead (see ``n_slabs`` for the caps). Used by ``'trapezoid'`` and ``'simpson'`` only: ``'gl'`` evaluates the Hamiltonian at its own nodes and ignores it, and a value passed with it raises :class:`IgnoredQuadratureSettingWarning`. 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``). With ``integration_method='gl'``, an even order from 2 to 8: each Gauss-Legendre scheme has an even order, so an odd one is refused rather than run as the next even scheme under another name. .. versionchanged:: 1.2.0 An odd order with ``'gl'`` is refused; it ran the next even order, bit for bit. 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). Used by ``'trapezoid'`` and ``'simpson'`` only: ``'gl'`` evaluates the Hamiltonian at its own nodes and ignores it, and a value passed with it raises :class:`IgnoredQuadratureSettingWarning`. max_num_loops : int, optional Maximum number of refinement loops. min_n_slabs : int, optional Least number of slabs of the first refinement loop: the starting count for ``'trapezoid'``/``'simpson'``, and a floor under the phase-based estimate ``'gl'`` starts from. 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. Used by ``'trapezoid'`` and ``'simpson'`` only: ``'gl'`` evaluates the Hamiltonian at its own nodes and ignores it, and a value passed with it raises :class:`IgnoredQuadratureSettingWarning`. max_n_tpts_per_slab : int, optional Maximum allowed number of time points per slab. Used by ``'trapezoid'`` and ``'simpson'`` only: ``'gl'`` evaluates the Hamiltonian at its own nodes and ignores it, and a value passed with it raises :class:`IgnoredQuadratureSettingWarning`. 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: t_ini = _v.check_real('t_ini', t_ini, "oscprob.osc_prob") t_fin = _v.check_real('t_fin', t_fin, "oscprob.osc_prob") if (t_fin < t_ini): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob: t_fin must be >=" + \ " t_ini.") # Types and ranges, by the rules the scenario functions apply (issue #160 §5): the # comparisons below accepted n_slabs=2.5, rtol=nan and magnus_exp_order=True. _v.check_refinement("oscprob.osc_prob", dict( n_slabs=n_slabs, n_tpts_per_slab=n_tpts_per_slab, 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, verbose=verbose)) # A caller's points-per-slab setting under 'gl' is overridden below; said once, at # the entry point the caller used, so an internal call does not repeat it. if integration_method == 'gl': _tp = (n_tpts_per_slab, min_n_tpts_per_slab, max_n_tpts_per_slab, growth_factor_n_tpts_per_slab) _d = _OSC_PROB_TPTS_DEFAULTS if (_tp[0] is not _d[0] or _tp[1] is not _d[1] or _tp[2] is not _d[2] or _tp[3] is not _d[3]) and not _v._called_from_inside(2): _given = [k for k, x, d in zip(_TPTS_SETTINGS, _tp, _d) if x is not None and x is not d] if _given: _warn_ignored_quadrature_settings('osc_prob', _given) # Checked here rather than by the rule table, which costs a call on every point. if new_recursion_limit is not None and not (type(new_recursion_limit) is int and new_recursion_limit >= 1): _v.check_int('new_recursion_limit', new_recursion_limit, "oscprob.osc_prob", lo=1) # A gap, an overlap, a zero-width slab or a grid stopping short of t_fin used to be # integrated as given, off by up to 9.4e-2; NaN edges returned NaN (issue #160 §6). if t_slab_edges is not None: t_slab_edges = _v.check_slab_edges(t_slab_edges, t_ini, t_fin, "oscprob.osc_prob") 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: if save_log: _check_log_path(filename_log, "oscprob.osc_prob") 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 # 2, not 1: the Gauss-Legendre schemes have even orders only (issue #160 §5), and on a # constant Hamiltonian every order is exact on one slab anyway. magnus_exp_order = 2 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 = 2, 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)) # The breakpoints the loop below inserts into every grid, handed on to the slab kernel for # 'trapezoid' and 'simpson', which sample slab endpoints: at a declared jump each slab then # samples its own side (see magnus.magnus_expansion_multislab). 'gl' samples no endpoints, # so it gets None and pays nothing. bp_one_sided = None if ((t_slab_edges_original is None) and (integration_method != 'gl') and (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0)): bp_one_sided = np.atleast_1d(np.asarray(t_breakpoints, dtype=float)) bp_one_sided = bp_one_sided[(bp_one_sided > t_ini) & (bp_one_sided < t_fin)] if bp_one_sided.size == 0: bp_one_sided = None 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") magnus._warn_once("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, t_breakpoints=bp_one_sided, **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.2.0 """ _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. The test is relative as well as absolute (issue #163): a point is recomputed when some probability moves by the gate, or by the gate times its own size. Absolute alone kept the zero-phase limit, about 1e-32, for a probability of 4e-7 at small phase. Entries below :data:`_PHASE_AVERAGE_RELATIVE_FLOOR` on both sides are round-off, and have no relative change. .. versionadded:: 1.2.0 """ _PHASE_AVERAGE_RELATIVE_FLOOR = 1.0e-14 r"""float: Module-level constant Size below which a probability takes no part in the relative test of :data:`_PHASE_AVERAGE_GATE`: double precision leaves such an entry no reliable relative digits. .. versionadded:: 1.2.0 """ def _phase_average_moved(P_new, P_old, gate): r"""Whether the phase average moves a point by the gate, absolutely or relatively. ``P_new`` and ``P_old`` are ``(..., d, d)``; returns a boolean of shape ``...``. True when some entry changes by at least ``gate``, or by at least ``gate`` times the larger of its two values where that exceeds :data:`_PHASE_AVERAGE_RELATIVE_FLOOR` (issue #163). .. versionadded:: 1.2.0 """ diff = np.abs(np.asarray(P_new) - np.asarray(P_old)) size = np.maximum(np.abs(P_new), np.abs(P_old)) relative = (size > _PHASE_AVERAGE_RELATIVE_FLOOR) & (diff >= gate*size) return np.any((diff >= gate) | relative, axis=(-2, -1)) # osc_prob's own defaults for the points-per-slab settings, compared by identity on every call. _OSC_PROB_TPTS_DEFAULTS = tuple(signature(osc_prob).parameters[_k].default for _k in _TPTS_SETTINGS) def _refuse_average_keywords(source_func_name: str, average_spread, average_n_samples, average_initial_state) -> None: r"""Refuses the keywords that shape ``average=True`` on a call without it. A call without ``average`` takes no averaging route at all, so these were accepted and ignored: the sample count (issue #160 §1), and the spread and the initial state (issue #160, by the author's decision). None is the default of all three, and passes. .. versionadded:: 1.2.0 """ for name, value in (('average_n_samples', average_n_samples), ('average_spread', average_spread), ('average_initial_state', average_initial_state)): if value is not None: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " + name + " sets how average=True averages, and has no effect without it; " "pass average=True or remove " + name + ".") def _decoupled_degenerate_flavors(htot: Callable, energy: float, l0: float, l1: float, n_positions: int = 5) -> Optional[Tuple[list, list]]: r"""The flavors nothing couples to, when one of them is degenerate with another level. A flavor whose row and column of the Hamiltonian are zero off the diagonal at every one of ``n_positions`` positions from ``l0`` to ``l1`` is decoupled: an eigenstate at every position, which never oscillates. Returned as ``(coupled, decoupled)`` index lists only when some decoupled flavor's eigenvalue also coincides, within ``adiabatic.DEGENERACY_ULPS``, with another eigenvalue at every one of those positions -- a degeneracy along the whole path, which the adiabatic averaging route treats as a crossing spanning the path and does not finish (issue #148; the 4nu and 5nu Sun at their default sterile parameters). None otherwise, and the call is averaged as before. .. versionadded:: 1.2.0 """ def uncoupled(Hs): d = Hs.shape[-1] off = np.abs(Hs.reshape((-1, d, d))).max(axis=0) np.fill_diagonal(off, 0.0) return np.flatnonzero(~(off.any(axis=0) | off.any(axis=1))).tolist() # One sample settles the common case, a Hamiltonian with no zero entry, in a few # microseconds; the other positions are sampled only when it has one. H0 = np.asarray(htot(energy, float(l0))) if np.count_nonzero(H0) == H0.size or not uncoupled(H0): return None ls = np.linspace(l0, l1, n_positions) Hs = np.array([H0] + [np.asarray(htot(energy, float(l))) for l in ls[1:]], dtype=complex) d = Hs.shape[-1] decoupled = uncoupled(Hs) if not decoupled or len(decoupled) == d: return None lam = np.linalg.eigvalsh(Hs) tol = adiabatic.DEGENERACY_ULPS*np.finfo(float).eps*np.max(np.abs(lam), axis=-1) for f in decoupled: near = np.abs(lam - Hs[:, f, f].real[:, None]) <= tol[:, None] if np.all(np.count_nonzero(near, axis=-1) >= 2): return [i for i in range(d) if i not in decoupled], decoupled return None 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, average_n_samples: Optional[int] = None, average_initial_state: Optional[str] = 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 within the tighter of ``rtol`` and ``atol`` in ``engine_kwargs`` when that is tighter still, absolutely and relative to each probability (issue #163). ``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.2.0 Returns the phase average; takes ``average_spread`` and ``energy_dependent``. .. versionchanged:: 1.2.0 Refuses average_n_samples, average_spread and average_initial_state without average (issue #160 §1); the energy-window route raises one warning per class (issue #144 §2); the warning for an energy-independent Hamiltonian says no spread can decohere it (issue #144 §3). On a smooth profile, flavors nothing couples to and degenerate with another level along the whole path are averaged out (issue #148): see _decoupled_degenerate_flavors. 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`. On the energy-window route, the half-width of the window as a fraction of the energy, strictly between 0 and 1; None there means :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`. average_n_samples : int or None Number of energies sampled on the energy-window route, at least 2. None means :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`. Refused on the other routes, which do not sample (issue #134). 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: _refuse_average_keywords(source_func_name, average_spread, average_n_samples, average_initial_state) 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) initial = 'flavor' if average_initial_state is None else average_initial_state if not isinstance(initial, str) or initial not in ('flavor', 'decohered'): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "average_initial_state is the state the neutrino starts in and must be 'flavor' or " "'decohered', not " + repr(average_initial_state) + ".") decohered_start = (initial == 'decohered') sample_numerically = (not htot_is_function_only_of_energy) and (not smooth_profile) if sample_numerically and decohered_start: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "average_initial_state='decohered' is not available on a profile with declared " "discontinuities: there is no instantaneous eigenbasis to decohere in at a jump, and " "this route averages the probability over an energy window starting from the flavor " "state. Leave average_initial_state at 'flavor' there.") if average_n_samples is not None: if not sample_numerically: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "average_n_samples sets how many energies the energy-window average samples, " "and that average is taken only on a profile with declared discontinuities. " "This call takes the closed-form or adiabatic route, which samples nothing; " "remove average_n_samples.") if (isinstance(average_n_samples, bool) or not isinstance(average_n_samples, (int, np.integer)) or average_n_samples < 2): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "average_n_samples is the number of energies sampled across the window and " "must be an integer of at least 2, not " + repr(average_n_samples) + ".") if sample_numerically: window = (avgprob.AVG_DEFAULT_ENERGY_SPREAD if average_spread is None else spread) if not (0.0 < window < 1.0): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": " "average_spread is, on a profile with declared discontinuities, the half-width " "of the energy window as a fraction of the energy, and must lie strictly " "between 0 and 1, not " + repr(average_spread) + ".") n_samples = (avgprob.AVG_DEFAULT_N_SAMPLES if average_n_samples is None else int(average_n_samples)) 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)) if (not htot_is_function_only_of_energy) and smooth_profile: # A flavor nothing couples to, degenerate with another level along the whole path: the # adiabatic route would window the whole path and never return (issue #148). It is an # eigenstate at every position and never oscillates, so the rest is averaged without it. split = _decoupled_degenerate_flavors(htot, float(np.min(energy_arr)), float(L0), float(np.max(L_arr))) if split is not None: coupled, decoupled = split def htot_coupled(enu, l): return np.asarray(htot(enu, l))[..., coupled, :][..., :, coupled] P_c = _avg_prob_dispatch(htot_coupled, False, energy_arr, L_arr, L0, None, None, True, source_func_name, smooth_profile=True, engine_kwargs=engine_kwargs, average_spread=average_spread, average_initial_state=average_initial_state, energy_dependent=energy_dependent) d = len(coupled) + len(decoupled) P_out = np.zeros((len(energy_arr), d, d)) P_out[:, np.ix_(coupled, coupled)[0], np.ix_(coupled, coupled)[1]] = P_c P_out[:, decoupled, decoupled] = 1.0 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)) 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: 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; on the closed-form route as well since issue #163). 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 _phase_average_moved(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 decohered_start: # An incoherent mixture of the eigenstates never interferes: the limit, exactly, # whatever the phases (paper Sec. 4.10.4), with no phase average to compute. P_out[:] = avgprob.averaged_probabilities_from_eigenbasis(eigenvectors) decohered[:] = True elif 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 and not decohered_start: # 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 = _phase_average_moved(P_new, P_out, 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 # Every sample is a full propagation, and each warned on its own: 41 samples gave 41 # MagnusConvergenceWarning and 41 ToleranceNotAchievedWarning to anything recording # warnings (issue #144 §2). Collected here and raised once per class below, with how # many samples raised it; the caller's filters apply to that one, as they would have. # One propagation can raise the same class more than once, so the count is of the # propagations that raised it, not of the warnings. _n_raising = {} with warnings.catch_warnings(record=True) as _caught: warnings.simplefilter('always') for i in range(n_pts): L_i = float(L_arr[i]) def prob_of_energy(enu, L_i=L_i): _start = len(_caught) P = osc_prob_energy_baseline(htot, enu, L_i, L0, None, None, htot_is_function_only_of_energy, **eng, **extra) for _category in {_w.category for _w in _caught[_start:]}: _n_raising[_category] = _n_raising.get(_category, 0) + 1 return P P_out[i], sem = avgprob.averaged_probabilities_numerically(prob_of_energy, float(energy_arr[i]), relative_spread=window, n_samples=n_samples) worst_sem = max(worst_sem, sem) _by_class = {} for _w in _caught: _by_class.setdefault(_w.category, []).append(_w) for _category, _ws in _by_class.items(): warnings.warn(str(_ws[0].message) + " (Raised by " + str(_n_raising.get(_category, len(_ws))) + " of the " + str(n_pts*n_samples) + " propagations across the energy window of the average; " "shown once here for all of them.)", _category, stacklevel=3) 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*window) + "% and averaged over " + str(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 " "average_spread to set the half-width of the window to the resolution of the " "measurement, and average_n_samples to reduce the error, which falls as the " "inverse square root of the number of samples.", 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) # With the phase average, the coherence report of the limit is never read: skip its # phase integrals, about 0.2 ms an energy, which is what the guard below costs. P_out[i], report = avgprob.averaged_probabilities_adiabatic(H_of_l, float(L0), float(L_arr[i]), _coherence=not phase_average) 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 not phase_average: continue 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) # Without a window, a decohered start carried adiabatically has no interference to # keep: the value above is already the phase average. A start in flavor has, unless # every term is damped from production on, as for a neutrino produced in the solar # core; that is checked on a few points before paying for the transport (issue #73). if not report['windows'] and (decohered_start or avgprob._interference_damped( H_of_l, D_of_l, float(L0), float(L_arr[i]), spread, dH_dlnE_step=h)): continue rho0 = None if decohered_start: _, V0 = np.linalg.eigh(np.asarray(H_of_l(float(L0)), dtype=complex)) W0 = V0.real**2 + V0.imag**2 rho0 = np.zeros((d, d, d), dtype=complex) rho0[:, np.arange(d), np.arange(d)] = W0 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, rho0=rho0) 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.2.0 -- 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.", 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.", 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.", 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.", 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. The Hamiltonian does not depend on " "energy -- a matrix, or a function of position alone -- so no energy spread can " "decohere that pair, and no averaged expression describes it: the oscillation " "probability itself (average=False) is the meaningful quantity there.", PhaseAveragingWarning, stacklevel=3) window_detail = (dict(window_half_width=window, n_samples=n_samples, largest_sem=worst_sem) if sample_numerically else {}) _note_engine('average', average_spread=spread, recomputed=recomputed_points, sigma_sensitivity=largest_sensitivity, **window_detail) 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))
[docs] PARAMETER_SET_METADATA_KEYS = ('name', 'description')
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 import inspect # The misspelled keyword is as often a physics one (`eps_mue` for `eps_em`, `theta12` # for `s12`) as an engine one, and the physics keywords belong to the public wrapper # the caller invoked, not to this entry point. Find that wrapper -- the outermost # public function of this module on the stack -- and offer its own keywords too. This # runs only on the error path, so the stack walk costs nothing on a valid call. own_name, own_params = None, [] frame = inspect.currentframe().f_back while frame is not None: name = frame.f_code.co_name if (frame.f_globals.get('__name__') == __name__ and not name.startswith('_') and callable(globals().get(name))): own_name = name frame = frame.f_back if own_name is not None: # Listed: the wrapper's physics and scenario keywords. Left out: the engine # keywords, listed separately below, and the logging and validation switches. bookkeeping = {'kwargs', 'energy', 'L', 'validate_input', 'save_log', 'filename_log', 'file_log', 'close_file_log_upon_exit', 'verbose', 'default_osc_params_set_name', 'new_recursion_limit'} own_params = [p for p in inspect.signature(globals()[own_name]).parameters if p not in bookkeeping and p not in valid] # 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. candidates = sorted(valid | set(own_params)) lines = [] for key in sorted(unknown): close = difflib.get_close_matches(key, candidates, n=1, cutoff=0.6) lines.append("'" + key + "'" + (", did you mean '" + close[0] + "'?" if close else '')) own = (" The keywords of " + own_name + " itself are: " + ', '.join(own_params) + "." if own_params else '') listed = '; '.join(lines) raise ValueError( gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": unrecognized keyword " "argument(s): " + listed + ('' if listed.endswith('?') else '.') + own + " 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 use the same Magnus slab kernel (:func:`magnus.magnus.evolution_operators_from_samples`; the first two through :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`` (label ``'expm'``) and the constant-Hamiltonian engine (``'constant'``, ``magnus.magnus._expm_stack``), used only where a single exponential 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 = contextvars.ContextVar('_ENGINE_TRACE', default=None) r"""ContextVar holding a list or None: set to a list by ``_engine_probe`` while a diagnostic is watching; ``None`` (and therefore free) on every ordinary call. A context variable, so that a call in one thread neither sees nor adds to another's trace (issue #153).""" _ENGINES_DISABLED = contextvars.ContextVar('_ENGINES_DISABLED', default=frozenset()) r"""ContextVar holding a 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. Per thread: ``strategy='magnus'`` in one thread disabled the other engines for every thread while it ran (issue #153).""" 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 """ trace = _ENGINE_TRACE.get() if trace is not None: trace.append(dict(engine=label, answered=answered, **detail)) def _hybrid_fallback(energy: float, L: float, L0: float) -> Optional[Dict]: r"""The uncertified hybrid answer at this point, if the hybrid declined it in this call. Read from the decline note ``_hybrid_propagator_scan`` leaves under ``strategy='auto'``, and only if no engine has answered since: nested probes share one trace, and a note left by an earlier call is behind that call's answer. None otherwise. .. versionadded:: 1.2.0 """ trace = _ENGINE_TRACE.get() for entry in reversed(trace or ()): if entry['answered']: return None fallback = entry.get('_fallback') if ((fallback is not None) and (fallback['energy'] == float(energy)) and (fallback['L'] == float(L)) and (fallback['L0'] == float(L0))): return fallback return None @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 """ prev_trace, prev_disabled = _ENGINE_TRACE.get(), _ENGINES_DISABLED.get() # 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) trace_token = _ENGINE_TRACE.set(trace) disabled_token = _ENGINES_DISABLED.set(prev_disabled | frozenset(disabled)) try: yield trace finally: _ENGINES_DISABLED.reset(disabled_token) _ENGINE_TRACE.reset(trace_token) 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. Two differences: the phase-based starting slab count is applied to ``'trapezoid'``/``'simpson'`` too, when it is at least :data:`QUADRATURE_SEED_MIN_SLABS` (:func:`osc_prob` seeds ``'gl'`` only); and, as there, a quadrature rule samples each side of a declared breakpoint with its own values (see :func:`magnus.magnus.magnus_expansion_multislab`). With ``t_breakpoints``, which are inserted into every level's grid, the ladder grows the real grid rather than the nominal slab count, and an agreement counts only between levels whose real grids differ by at least :data:`MIN_EFFECTIVE_REFINEMENT` (grid points times points per slab); a level cut short by ``max_n_slabs`` below that bound ends in a :class:`ToleranceNotAchievedWarning` instead. Without breakpoints the nominal and real steps coincide and neither rule applies. An energy accepted on a level that refined only the points per slab, because the slab count was already pinned at ``max_n_slabs``, raises a :class:`ToleranceNotAchievedWarning`: that agreement verifies the quadrature inside each slab, not the slab count. The warning changes no result and no work. .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 Warns on an acceptance at the slab cap that refined only the points per slab (#71). 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 or None Stacked probability matrices, shape (nE, d, d); None when ``VCC_func`` cannot be evaluated on an array of positions, for the caller to decline to the per-point path. """ # 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 not tol_requested: # The seed's samples below check VCC_func under a tolerance; without one, the ladder # samples it directly, so it is checked here on two positions (issue #113). try: v = np.asarray(VCC_func(np.array([L0, L_val]))) except Exception: return None if v.ndim == 0: VCC_func = _vectorized_potential(VCC_func) s_nodes = 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. The quadrature rules take it only when it is at least # QUADRATURE_SEED_MIN_SLABS (see there); n_tpts_per_slab still starts at its minimum. ts = np.linspace(L0, L_val, 17) # These samples double as the check that VCC_func takes an array of positions # (issue #113); see _vectorized_potential. try: V17 = np.asarray(VCC_func(ts)) except Exception: return None if V17.ndim == 0: VCC_func = _vectorized_potential(VCC_func) V17 = np.broadcast_to(V17, ts.shape) 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) # The seed of each energy on its own, kept for the grouping below; None when the SVD # was skipped or failed. seeds = None if ((integration_method != 'gl') and not (np.ceil(np.max(np.sqrt(np.sum( np.abs(M)**2, axis=(-2, -1))))/(2.0*np.pi)) >= QUADRATURE_SEED_MIN_SLABS)): # The Frobenius norm bounds the spectral one from above, so the seed is already # below the threshold: skip the SVD. phase = 0.0 else: try: # Singular values come sorted, largest first: column 0 is each energy's own # phase, and its maximum is the batch's, the number this always took. phases = np.linalg.svd(M, compute_uv=False)[:, 0] phase = np.max(phases) seeds = np.ceil(phases/(2.0*np.pi)) except np.linalg.LinAlgError: phase = 0.0 seed = np.ceil(phase/(2.0*np.pi)) if (integration_method != 'gl') and not (seed >= QUADRATURE_SEED_MIN_SLABS): seed = 0.0 n_slabs = int(np.clip(max(min_n_slabs, seed), 1, max_n_slabs)) if seeds is not None: # Each energy's own starting slab count, exactly as the line above would set it # for a scan holding that energy alone (the quadrature rules' minimum seed # included). if integration_method != 'gl': seeds[~(seeds >= QUADRATURE_SEED_MIN_SLABS)] = 0.0 seeds = np.clip(np.maximum(min_n_slabs, seeds), 1, max_n_slabs) # A scan whose largest seed sits at the slab cap ends on its first level # (nothing to refine to), warned; groups below the cap would only add levels # to a call that cannot converge, so it is left as it was. groups = (_phase_groups(seeds, dim, growth_factor_n_slabs, atol + 0.01*rtol) if n_slabs < max_n_slabs else None) if groups is not None: # Several ladders, one per group of energies of similar phase (issue #111). # Each is the ladder above run on that group alone, starting from the # group's own largest seed; the warnings stay once per call. P_out = np.empty((nE, dim, dim)) warned = {} for g in groups: P_out[g] = _osc_prob_scan_separable_ladder( H_E[g], VCC_func, h_matt, L0, L_val, t_breakpoints, magnus_exp_order, integration_method, rtol, atol, growth_factor_n_slabs, growth_factor_n_tpts_per_slab, max_num_loops, max_n_slabs, max_n_tpts_per_slab, int(seeds[g].max()), n_tpts_per_slab, tol_requested, s_nodes, warned) return P_out return _osc_prob_scan_separable_ladder( H_E, VCC_func, h_matt, L0, L_val, t_breakpoints, magnus_exp_order, integration_method, rtol, atol, growth_factor_n_slabs, growth_factor_n_tpts_per_slab, max_num_loops, max_n_slabs, max_n_tpts_per_slab, n_slabs, n_tpts_per_slab, tol_requested, s_nodes, {}) def _osc_prob_scan_separable_ladder( 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, max_n_slabs: int, max_n_tpts_per_slab: int, n_slabs: int, n_tpts_per_slab: int, tol_requested: bool, s_nodes: Optional[np.ndarray], warned: Dict ) -> np.ndarray: r"""The refinement ladder of :func:`_osc_prob_scan_separable`, on one shared slab grid. Runs from the starting ``n_slabs``/``n_tpts_per_slab`` the caller has already resolved (seed, floor and cap applied; ``max_n_slabs`` already the method's cap when None was given). ``warned`` is the caller's record of the warnings this call has emitted, so that a scan split into several groups of energies (see :func:`_phase_groups`) still warns once per call. .. versionadded:: 1.2.0 """ nE, dim = H_E.shape[0], H_E.shape[-1] # Breakpoints strictly inside the path, sorted and without duplicates: the same set every # level's grid gets, and what _n_grid_points counts. bp_in = (np.unique(np.atleast_1d(np.asarray(t_breakpoints, dtype=float))) if t_breakpoints is not None else np.zeros(0)) bp_in = bp_in[(bp_in > L0) & (bp_in < L_val)] # Refinement of the previous level's real grid (grid points times points per slab); see # MIN_EFFECTIVE_REFINEMENT. Checked at every level with breakpoints; without them, only # for the clamped last step onto the slab cap on 'gl' (issue #122). r_prev = None # The previous level's slab count, to tell a level that refined only the points per slab. n_slabs_prev_level = None # 'gl' refuses an energy's agreement while its own slab norm is large, as long as the # ladder can still add slabs (BATCHED_GL_MAX_SLAB_NORM); the norms come from the # exponentials this level computes anyway (magnus._row_slab_norms). gl_gate = tol_requested and (integration_method == 'gl') and (growth_factor_n_slabs > 1.0) 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): grid = np.unique(np.concatenate([grid, bp_in])) 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) if bp_in.size and (integration_method != 'gl'): # At a breakpoint, each slab samples its own side of the jump (see # magnus._one_sided_samples). bp_in values are the grid's edges exactly. f = np.zeros(len(grid), bool) f[np.searchsorted(grid, bp_in)] = True # bp_in values are grid points exactly magnus._one_sided_samples(tgrid, edges, widths, f[:-1], f[1:], 8.0*np.spacing(max(abs(L0), abs(L_val)))) 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)) with (magnus._row_slab_norms() if gl_gate else nullcontext()) as row_norms: 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)) r_level = len(grid)*n_tpts_per_slab if (r_prev is not None) and (r_level < MIN_EFFECTIVE_REFINEMENT*r_prev): if bp_in.size: # As in osc_prob: two grids that differ by a few breakpoint-dominated percent # agree without having converged, so their agreement does not count. conv[:] = False elif (integration_method == 'gl') and (n_slabs >= max_n_slabs): # The last step onto the slab cap is clamped, so it can refine the grid by a # sliver: 19 926 -> 20 000 slabs agreed to 0.4 of the tolerance on # probabilities 6.6x outside it, with no warning (issue #122). A real # refinement r bounds the finer grid's error at order p by |dP|/(r**p - 1), so # the agreement counts only within that fraction of the tolerance; the capped # 108 -> 115 step of issue #94 (fraction 0.28, agreement 0.14) still certifies. # An energy refused ends in the caps warning below on the same probabilities: # 'gl' fixes the points per slab, so no further level runs. shrink = min(1.0, (r_level/r_prev)**magnus_exp_order - 1.0) conv &= np.all(np.abs(P_new - prev) <= shrink*(atol + rtol*np.abs(prev)), axis=(-1, -2)) r_prev = r_level if gl_gate and conv.any() and (n_slabs < max_n_slabs) and (loop_count < max_num_loops): # A chance agreement between two coarse grids (issue #71): refused for the energies # whose own slabs are still wide, which go one level further. norms = row_norms[0] if len(row_norms) == 1 else np.concatenate(row_norms) if norms.shape == conv.shape: conv &= norms < BATCHED_GL_MAX_SLAB_NORM if (conv.any() and (growth_factor_n_slabs > 1.0) and (n_slabs >= max_n_slabs) and (n_slabs_prev_level == n_slabs) and not warned.get('slab_cap')): # Pinned at the slab cap, this level refined only the points per slab. Agreeing with # the last one verifies the quadrature inside each slab, not the slab count, so an # energy accepted here can sit well outside the tolerance (issue #71). warnings.warn("osc_prob (energy-batched scan): some energies were accepted at the " "slab cap (max_n_slabs = " + str(max_n_slabs) + "), where successive levels " "refined only the points per slab; that verifies the quadrature inside each " "slab but not the number of slabs, so the returned probabilities may be " "inaccurate. Raise max_n_slabs, or use integration_method='gl' (default cap " + str(MAX_N_SLABS_DEFAULT['gl']) + ").", ToleranceNotAchievedWarning, stacklevel=3) warned['slab_cap'] = True n_slabs_prev_level = n_slabs 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: if (not warned.get('caps')) and at_caps and (loop_count == 1): # The seed already sat at the caps, so no second level was ever computed and # nothing was checked (issue #184). Across the Sun this is every energy below # about 20 MeV once rtol/atol are 1e-4 or tighter: the returned level was # measured 2.0e-3 off over 0.5-20 MeV, where strategy='hybrid' was within # 6.3e-5 (and the cumulative engine within 1.4e-4). warnings.warn("osc_prob (energy-batched scan): requested tolerance not " "achieved for some energies: their slab count starts at max_n_slabs, so " "one level was computed with nothing to compare it with, and those " "probabilities are unverified. Across the Sun this happens at every " "energy once rtol/atol are about 1e-4 or tighter; there, compare with " "strategy='hybrid' (on the default solar profile it was 30 times closer), " "or loosen rtol/atol for a verified answer. Otherwise, raise max_n_slabs.", ToleranceNotAchievedWarning, stacklevel=3) warned['caps'] = True if not warned.get('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=3) warned['caps'] = True 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 if bp_in.size and (growth_factor_n_slabs > 1.0): # The breakpoints are in every level's grid, so at small counts the nominal step # barely refines it (on a PREM chord, 4 -> 6 slabs is 21 -> 23 points), and the # agreement test above would refuse it. Grow the real grid instead; the point # count is solved for in one step, since it is n_slabs + 1 + the breakpoints not # on a node. r_old = len(grid)*n_tpts_old if (_n_grid_points(L0, L_val, n_slabs, bp_in)*n_tpts_per_slab < MIN_EFFECTIVE_REFINEMENT*r_old): target = max(growth_factor_n_slabs, MIN_EFFECTIVE_REFINEMENT)*r_old/n_tpts_per_slab n_slabs = int(min(max(n_slabs, np.ceil(target) - 1 - bp_in.size), max_n_slabs)) while ((n_slabs < max_n_slabs) and (_n_grid_points(L0, L_val, n_slabs, bp_in) < target)): n_slabs += 1 loop_count += 1 def _vectorized_potential(VCC_func: Callable) -> Callable: r"""``VCC_func`` broadcast to the shape of the positions it is given. A potential written as ``lambda l: 3.0*UNIT_G_PER_CM3`` returns one number for an array of positions. The per-point path accepts it, but the energy-batched engine indexes its samples, and crashed on it (issue #113). Wrapped only when the engine's own samples come back 0-dimensional, so a vectorized potential pays nothing. .. versionadded:: 1.2.0 """ def vcc(l): return np.broadcast_to(np.asarray(VCC_func(l)), np.shape(l)) return vcc def _phase_groups(seeds: np.ndarray, dim: int, growth_factor_n_slabs: float, tol: float): r"""Split the energies of a batched scan into groups of similar phase, or not. ``seeds`` holds each energy's own starting slab count (floor and cap applied) and ``tol`` the tolerance that binds the ladder's convergence test (``atol + 0.01*rtol``, see :data:`BATCHED_PHASE_GROUPING`). Returns a list of index arrays, one per group in order of increasing seed, or None when one shared grid is best. The choice minimizes the cost model of :data:`BATCHED_PHASE_GROUPING`, found by dynamic programming over the distinct seed values in increasing order (energies with the same seed never gain from being split). .. versionadded:: 1.2.0 """ cost = BATCHED_PHASE_GROUPING if (dim < cost['min_dim']) or not (tol >= cost['min_tol']): return None c = cost['slab_energy_us'].get(dim, cost['slab_energy_us'][max(cost['slab_energy_us'])]) k = cost['sample_us']/c F = cost['level_us']/c margin = cost['margin'] g = growth_factor_n_slabs B, p, q = cost['resolution'] if not (g > 1.0): # Without growth the ladder never refines the slab count, so no grid is cheaper. return None log_g = math.log(g) # A bound in scalars first: every energy is accepted by the level the largest seed needs, # so one grid costs at most this, and a split at least the fixed cost of four levels. # Scans the model leaves alone, most of all at two and three flavors, where the fixed # cost dominates, stop here without touching the seeds. smax = float(seeds.max()) lev_top = max(1.0, math.ceil(math.log(max(g*smax, B*smax**p*(1.0e-8/tol)**q)/smax)/log_g - 1.0e-9)) if (smax*(seeds.shape[0] + k)*(g**(lev_top + 1.0) - 1.0)/(g - 1.0) + F*(lev_top + 1.0)) < 4.0*F*margin: return None # The slab count each energy is expected to be accepted at: the ladder's second level # at least, and what the profile needs at this tolerance. R = np.maximum(g*seeds, B*seeds**p*(1.0e-8/tol)**q) def levels(S, R_): # Levels beyond the first that a grid seeded at S runs until it reaches R_ (>= 1). return np.maximum(1.0, np.ceil(np.log(R_/S)/log_g - 1.0e-9)) def geom(lev): # Slabs of all levels up to lev, in units of the first level's. return (g**(lev + 1.0) - 1.0)/(g - 1.0) lev = levels(smax, R) one = smax*(geom(lev).sum() + k*geom(lev.max())) + F*(lev.max() + 1.0) # No split can beat every energy on its own seed plus the fixed cost of the fewest levels # a split can run -- two groups of at least two levels each, since a split returned below # always has two groups or more -- so when even that is not worth the margin, nothing # below is run: this is all a scan the model leaves alone pays. if ((seeds*geom(levels(seeds, R))).sum() + 4.0*F)*margin > one: return None S, counts = np.unique(seeds, return_counts=True) # ascending M = S.shape[0] if M < 2: return None R_S = np.maximum(g*S, B*S**p*(1.0e-8/tol)**q) cum = np.concatenate([[0], np.cumsum(counts)]) best = np.empty(M + 1) best[0] = 0.0 cut = np.empty(M, dtype=int) for j in range(1, M + 1): # best[j]: least cost of the j smallest seed values; the last group holds the values # i..j-1 on a grid seeded at S[j-1], for the i that minimizes this. Its levels grow # with the seed, so the group's are its largest member's. lev_j = levels(S[j - 1], R_S[:j]) per = np.cumsum(counts[:j]*geom(lev_j))*S[j - 1] shared = S[j - 1]*k*geom(lev_j[-1]) + F*(lev_j[-1] + 1.0) c_ = best[:j] + (per[-1] - np.concatenate([[0.0], per[:-1]])) + shared m = int(np.argmin(c_)) best[j] = c_[m] cut[j - 1] = m if best[M]*margin > one: return None order = np.argsort(seeds, kind='stable') groups = [] j = M while j > 0: i = cut[j - 1] groups.append(order[cum[i]:cum[j]]) j = i return groups[::-1] def _n_grid_points(L0: float, L_val: float, n_slabs: int, bp_in: np.ndarray) -> int: r"""Number of points of the grid ``np.unique(np.concatenate([np.linspace(L0, L_val, n_slabs + 1), bp_in]))``, without building it. ``bp_in`` must be sorted, without duplicates, and strictly inside (``L0``, ``L_val``), as :func:`_osc_prob_scan_separable` prepares it. A breakpoint that falls exactly on a node of the uniform grid adds no point. """ nodes = np.linspace(L0, L_val, n_slabs + 1) i = np.minimum(np.searchsorted(nodes, bp_in), n_slabs) return n_slabs + 1 + int(np.count_nonzero(nodes[i] != bp_in)) 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, in which case the caller falls back to the generic per-point path. It declines ``cumulative=True`` on a position-dependent potential, or across several energies; unknown extra arguments; user-provided slab edges; parallel or logged runs; ``verbose >= 1``; a baseline below ``L0`` (or NaN); an order or quadrature, or refinement bounds, the ladder would reject; a callable ``h_matt`` over a constant potential; and, for a position-dependent potential, fewer than two energies or unequal baselines. A constant potential is served by its own exact engine (:func:`_osc_prob_scan_constant_h`), single points and per-point baselines included. .. versionadded:: 1.0.0 .. versionchanged:: 1.2.0 ``cumulative=True`` over a constant potential, at one energy, is served by the constant engine: the same answer, 2-3x faster (issue #160). 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 (served by the energy-batched engine), or a constant (served by the exact constant-Hamiltonian engine). 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. """ # cumulative=True asks for one pass along the baselines, reusing each prefix. A constant # potential needs no pass at all: the constant engine's one exact exponential per baseline # is the same answer, 2-3x faster (issue #160, by the author's decision), so the request # goes there, and strategy_info names 'constant'. A position-dependent potential keeps # the cumulative engine it asked for. cumulative_asked = scan_kwargs.get('cumulative') is True if cumulative_asked and callable(VCC_func): 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.get(): 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 # A cumulative scan is one energy along many baselines; several energies are refused, by # name, on the cumulative path, and are sent there to be. if cumulative_asked and not np.all(energy_arr == energy_arr[0]): 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) if P is None: # The potential takes one position at a time; the per-point path evaluates it # that way (issue #113). return NotImplemented _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 Accepted for a uniform signature and ignored: the slab count doubles on each loop. max_num_loops : int Accepted and ignored: loops are capped at :data:`IP_EXP_LOOP_CAP`. min_n_slabs, max_n_slabs : int ``min_n_slabs`` is a floor on the starting count. ``max_n_slabs`` is accepted and ignored: the slab budget is :data:`IP_EXP_N_SLABS_CAP`, decoupled from the caps that suit the quadrature-based ladders (see the note in the body). 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 (a Hamiltonian that is not two-level, ``VCC_func`` not a genuine exponential profile built via :func:`magnus.matter.exp_density_profile`, user-provided slab edges or breakpoints, unequal baselines, ``cumulative=True``, unknown extra arguments, 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 two-flavor :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.get()) 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 _earth_breakpoints(prem_breakpoints, user_breakpoints, L, source_func_name: str): r"""The breakpoints an Earth entry point passes on: the PREM crossings on the path, and the caller's own. The crossings are computed along the whole chord, so a path that stops short of it (a partial ``L``) used to carry crossings past its end. Those mark nothing on the path, and since breakpoints off the path are refused (issue #160 §6), the ones past the longest baseline are dropped here; the caller's are kept as given, to be checked like any others. .. versionadded:: 1.2.0 """ bp = np.atleast_1d(np.asarray(prem_breakpoints, dtype=float)) L_max = _minmax_1d(_as_float(L) if not isinstance(L, (list, np.ndarray)) else L) if L_max is not None: bp = bp[bp <= L_max[1]] if user_breakpoints is None: return bp user = np.atleast_1d(_v.check_real_array('t_breakpoints', user_breakpoints, "oscprob." + source_func_name, allow_empty=True)) user = np.asarray(user, dtype=float) # Checked on their own: merged with the PREM crossings, which lie on the path, a set of # which none does would pass the check the scenario function makes on the merged set. if user.size and L_max is not None and not np.any((user >= 0.0) & (user <= L_max[1])): raise ValueError(_v._msg("oscprob." + source_func_name, "none of the t_breakpoints " "lies on the path, between 0 and the longest baseline, " + format(L_max[1], '.6g') + " [eV^-1], so they mark nothing on it; the first is " + format(float(user[0]), '.6g') + ". Check their sign and unit.")) return np.unique(np.concatenate([bp, user])) def _array_capable_rho(rho_func, L0, L, where: str): r"""``rho_func`` as a callable that takes an array of positions, warning if it could not. A scalar-only ``rho_func`` -- ``lambda l: 3.0 if l < mid else 8.0`` -- makes the whole Hamiltonian scalar-only, and the engine's :class:`magnus.magnus.ScalarHamiltonianWarning` then told the caller to rewrite ``H_func``, a function they never wrote (issue #144 §4, issue #160 §7). Probed once here, at three points of the path: if the array call raises, returns the wrong shape, or disagrees with the calls one position at a time, the warning names ``rho_func`` and the fix, and ``rho_func`` is evaluated position by position here, so the Hamiltonian built on it stays array-capable and the engine does not warn a second time. The values are the ones the engine would have computed point by point. .. versionadded:: 1.2.0 """ # Only a caller's own rho_func needs this: the wrappers pass profiles the package built, # which take arrays, and probing them cost ~10% of a single Earth point. A density reaches # a scenario function from the caller only when the caller called it directly. if not callable(rho_func) or _v._called_from_inside(3): return rho_func Lr = _minmax_1d(L) if Lr is None or not _v.is_real_scalar(L0): return rho_func pts = np.array([float(L0), 0.5*(float(L0) + Lr[1]), Lr[1]]) try: with warnings.catch_warnings(): warnings.simplefilter('ignore') vec = rho_func(pts) one = [rho_func(float(x)) for x in pts] ok = (np.ndim(vec) == 0 or np.shape(vec) == pts.shape) and np.allclose( np.broadcast_to(np.asarray(vec, dtype=float), pts.shape), np.asarray(one, dtype=float), rtol=1e-12, atol=0.0, equal_nan=True) except Exception: ok = False if ok: return rho_func frame, level = sys._getframe(0), 1 while frame is not None and frame.f_globals.get('__name__', '').startswith('magnus'): frame, level = frame.f_back, level + 1 warnings.warn( "oscprob." + where + ": rho_func could not be evaluated for several positions at once, " "so it is being called one position at a time. This is correct but slower, since the " "engine samples the density at every quadrature node of every slab. To take the fast " "path, write rho_func with NumPy operations so that it accepts an array of positions: " "np.where(l < mid, 3.0, 8.0) rather than '3.0 if l < mid else 8.0'.", magnus.ScalarHamiltonianWarning, stacklevel=level) def rho_one_at_a_time(l): a = np.asarray(l, dtype=float) if a.ndim == 0: return rho_func(float(a)) return np.array([rho_func(float(x)) for x in a.ravel()], dtype=float).reshape(a.shape) return functools.update_wrapper(rho_one_at_a_time, rho_func) def _refuse_start_keyword(where: str, kwargs: dict, environment: str) -> None: r"""Refuses ``L0`` where the entry point fixes the start of the path itself (issue #160 §1). The vacuum and Earth entry points declare no ``L0``: vacuum paths start at 0, and an Earth path starts where ``costhz`` and the depths put it. One passed anyway reached the engine through ``**kwargs`` and collided with the start set there, as a ``TypeError`` naming a function the caller never called -- or, on the vacuum LIV wrappers, was used as the start. .. versionadded:: 1.2.0 """ if 'L0' in kwargs: why = ("a vacuum path starts at 0: give its length as L" if environment == 'vacuum' else "the start of an Earth path is fixed by costhz and source_depth, or by loc_ini") raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + where + ": L0 is not an argument " "here; " + why + ".") def _refuse_breakpoints_in_vacuum(where: str, kwargs: dict) -> None: r"""Refuses ``t_breakpoints`` on a vacuum call (issue #160 §1). The vacuum Hamiltonian does not vary along the path, so there is no jump to mark: the breakpoints were accepted and ignored. .. versionadded:: 1.2.0 """ if kwargs.get('t_breakpoints') is not None: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + where + ": t_breakpoints marks " "discontinuities of a profile, and has no effect in vacuum, where there is none.") 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.2.0 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`. tolerance_margin : float or None What :meth:`request` divides the tolerances by: 1 below :data:`AUTO_LADDER_MIN_TOLERANCE` (issue #120), or None for :data:`AUTO_LADDER_TOLERANCE_MARGIN`, read when the request is made. """ __slots__ = ('min_n_slabs', 'tolerance_margin') def __init__(self, min_n_slabs: int, tolerance_margin: Optional[float] = None): self.min_n_slabs = int(min_n_slabs) self.tolerance_margin = tolerance_margin def __repr__(self): if self.tolerance_margin is None: return '_PreferLadder(min_n_slabs=%d)' % self.min_n_slabs return '_PreferLadder(min_n_slabs=%d, tolerance_margin=%g)' % (self.min_n_slabs, self.tolerance_margin) 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 :attr:`tolerance_margin`, or by :data:`AUTO_LADDER_TOLERANCE_MARGIN` when that is None (a ``None`` tolerance 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.2.0 """ m = AUTO_LADDER_TOLERANCE_MARGIN if self.tolerance_margin is None else self.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.2.0 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_ladder_max_phase(tol: float, magnus_exp_order: int, batched_scan: bool) -> float: r"""The largest estimated phase at which ``strategy='auto'`` hands a request to the ladder. At :data:`AUTO_LADDER_MIN_TOLERANCE` and looser, :data:`AUTO_LADDER_MAX_PHASE`, or no limit for an energy scan that the energy-batched engine takes (issue #84). Tighter (issue #120), ``AUTO_LADDER_MAX_PHASE*(tol/AUTO_LADDER_MIN_TOLERANCE)**(1/magnus_exp_order)`` capped at :data:`AUTO_LADDER_TIGHT_MAX_PHASE`, for a scan as well. .. versionadded:: 1.2.0 """ if tol >= AUTO_LADDER_MIN_TOLERANCE: return np.inf if batched_scan else AUTO_LADDER_MAX_PHASE return min(AUTO_LADDER_TIGHT_MAX_PHASE, AUTO_LADDER_MAX_PHASE*(tol/AUTO_LADDER_MIN_TOLERANCE)**(1.0/magnus_exp_order)) 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, batched_scan: bool = False, magnus_exp_order: int = 4, integration_method: str = 'gl') -> Optional[_PreferLadder]: r"""Whether ``strategy='auto'`` should hand a smooth-profile request to the ladder (issue #70). Yes when the estimated accumulated phase is at most the limit of :func:`_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. Below :data:`AUTO_LADDER_MIN_TOLERANCE` (issue #120) only with ``integration_method='gl'``, an order it supports (an even integer from 2 to 8; any other order keeps the hybrid strategy's path and its errors) and a single baseline, and the ladder then runs at the requested tolerance rather than a tenth of it. A baseline scan gets here below it only with ``cumulative=False``: otherwise the hybrid dispatcher stands aside for the cumulative scan first (issue #125). Records the decision in ``strategy_info`` when it is taken. With ``batched_scan`` -- several energies at one shared baseline, which the energy-batched engine answers in one pass -- the phase condition is dropped at :data:`AUTO_LADDER_MIN_TOLERANCE` and looser, leaving only the slab-count condition. The phase limit prices the ladder one point at a time; the batched engine shares its slabs across the energies, so the hybrid strategy, which pays its window search at every energy, is the slower route there by far: 62 s against 0.4 s on 100 energies of a three-flavor, two-resonance profile whose phase estimate (1.05e4, at the lowest energy) sat just above the limit. The slab-count condition still keeps the full Sun on the hybrid strategy. 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.2.0 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: return None tol = min(tols) tight = tol < AUTO_LADDER_MIN_TOLERANCE order_ok = (isinstance(magnus_exp_order, numbers.Integral) and not isinstance(magnus_exp_order, bool) and (1 <= magnus_exp_order <= 8)) one_baseline = np.unique(np.asarray(L_arr, dtype=float)).size == 1 if tight and not ((integration_method == 'gl') and order_ok and one_baseline): return None phase, n_floor = _estimated_phase(H_at_energy, energy_arr, L_arr, L0) phase_limit = _auto_ladder_max_phase(tol, magnus_exp_order, batched_scan) if (phase > phase_limit) 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, sharp = adiabatic._profile_resolution(H_lo, l0, l1, AUTO_PROBE_POINTS) resolved = resolved or adiabatic._profile_is_resolved(H_lo, l0, l1, 6400) if resolved and sharp: # A feature sharp at the probe scale and smooth below it: the ladder's seed, from the # phase integrated along the path, cannot see it, and every level can step over it and # agree on the same wrong answer (issue #161). Start at the probe spacing instead. n_floor = max(n_floor, AUTO_SHARP_FEATURE_SLABS_PER_PROBE*(AUTO_PROBE_POINTS - 1)) margin = 1.0 if tight else AUTO_LADDER_TOLERANCE_MARGIN detail = dict(estimated_phase=phase, phase_limit=phase_limit, min_n_slabs=n_floor, magnus_exp_order=magnus_exp_order, tolerance_margin=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, 1.0 if tight else None) 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, NotImplemented or _PreferLadder 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; or, only with ``strategy == 'auto'``, a :class:`_PreferLadder` asking the caller to answer on the slab ladder instead. """ if (strategy == 'magnus') or ('hybrid' in _ENGINES_DISABLED.get()): 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 HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS (8), or # HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHT (2) below AUTO_LADDER_MIN_TOLERANCE. The # cumulative scan is cheaper on median at every size measured; 8 is where its worst case # (solar, d = 3) stopped costing more than the hybrid. See those constants 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. # # Below AUTO_LADDER_MIN_TOLERANCE the threshold drops to # HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHT (issue #125): there the hybrid's per-point cost # is the larger, and on realistic profiles it mostly declines and reaches the cumulative # scan anyway. _set_tols = [t for t in (rtol, atol) if t > 0.0] _min_points = (HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHT if _set_tols and min(_set_tols) < AUTO_LADDER_MIN_TOLERANCE else HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS) if (strategy == 'auto') and (scan_kwargs.get('cumulative') is not False) \ and _cumulative_scan_would_serve(energy_arr, L_arr, L0, _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). An energy scan the # energy-batched engine will take is handed over whatever its phase (see # _auto_prefers_ladder); the conditions below mirror that engine's own declines. batched_scan = ((np.unique(energy_arr).size > 1) and (np.unique(L_arr).size == 1) and (scan_kwargs.get('n_jobs', 1) == 1) and (scan_kwargs.get('verbose', 0) < 1) and ('separable' not in _ENGINES_DISABLED.get())) 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), batched_scan=batched_scan, magnus_exp_order=magnus_exp_order, integration_method=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 """ magnus._warn_once( "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': # The uncertified answer is kept on the decline note: if the ladder that answers # instead runs out of slabs, osc_prob_energy_baseline returns this one when it is # the better supported of the two (issue #167). Not on an unresolved profile, # where the adiabatic answer means nothing. fallback = None if unresolved else dict( energy=float(energy_arr[i]), L=float(L_arr[i]), L0=float(L0), P=np.swapaxes(U.real**2 + U.imag**2, -1, -2), error_estimate=adiabatic.GAMMA_TO_ERROR*float(info['gamma_max'])) _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'), _fallback=fallback) 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: magnus._warn_once("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, NotImplemented or _PreferLadder 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; or, only with ``strategy == 'auto'``, a :class:`_PreferLadder` asking the caller to answer on the slab ladder instead. """ if (strategy == 'magnus') or ('hybrid' in _ENGINES_DISABLED.get()): 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), magnus_exp_order=magnus_exp_order, integration_method=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) # The breakpoints the grid contains, for the endpoint-sampling rules only (see osc_prob); # each block ignores those outside its own span. bp_one_sided = None if ((integration_method != 'gl') and (t_breakpoints is not None) and (len(np.atleast_1d(t_breakpoints)) > 0)): bp_one_sided = np.atleast_1d(np.asarray(t_breakpoints, dtype=float)) bp_one_sided = bp_one_sided[(bp_one_sided > edges[0]) & (bp_one_sided < edges[-1])] if bp_one_sided.size == 0: bp_one_sided = None _, 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, t_breakpoints=bp_one_sided, **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 class _JumpsDeclared(Exception): r"""Raised inside :func:`osc_prob_energy_baseline` when undeclared jumps were found and declared, so that the points computed without them are computed again (issue #161). .. versionadded:: 1.2.0 """
[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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=None, strategy_info: Optional[Dict]=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.2.0 Added ``return_evolution_operator`` and ``average``. .. versionchanged:: 1.2.0 A scan with n_jobs > 1 that would finish within N_JOBS_MIN_PARALLEL_WORK_S runs in the calling process (issue #155 §3); an H_func taking no argument or returning an object array is refused by name (issue #160 §7). average_spread and average_initial_state are refused without average=True (issue #160). Finds jumps nobody declared and declares them, with UnmarkedDiscontinuityWarning (issue #161): see UNDECLARED_JUMP_STEP_TOLERANCE. Where strategy='auto' declined a point on the hybrid and the ladder then ran out of room, returns the hybrid's answer when the ladder has less to show for its own, with ToleranceNotAchievedWarning (issue #167). 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 Number of worker processes over the (energy, L) points of a per-point scan: 1 (the default) runs in the calling process, -1 uses every core, and any other positive value is capped at the points left and the cores. The first point is always computed here, to warm-start the rest; if the rest would take under :data:`N_JOBS_MIN_PARALLEL_WORK_S` (1 s) at that point's pace, they are computed here too, since starting the workers costs more (issue #155 §3). A scan that a batched engine would answer takes the per-point path instead when ``n_jobs != 1``, which runs one refinement ladder per point where the batched engine runs one for all of them: the two agree to within the tolerance, not bit for bit (up to 3.7e-5 on a 40-energy Earth chord at the default 1e-3; issue #166 §1). The workers are joblib's reusable loky pool: they stay alive after the call, so the next parallel call starts at once, until they have been idle for joblib's timeout (300 s by default), and then exit. A process list shows them in that window; they hold no state from the call. 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. The one exception is ``return_evolution_operator=True``, which the cumulative scan cannot serve: every point then takes the per-point path. * ``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. To reproduce pre-1.0.0 numbers exactly, pass ``cumulative=False`` here, or ``strategy='magnus'`` to the wrappers (which opts out of the adiabatic strategy and already implies ``cumulative=False``); this function has no ``strategy`` parameter. Both hold for ``'gl'``; ``'trapezoid'`` and ``'simpson'`` results at declared breakpoints moved in 1.2.0 (see :func:`magnus.magnus.magnus_expansion_multislab`). ``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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.0 strategy_info : dict, optional If given, filled on return with the engine that answered and the route the request took, as on the wrappers: ``strategy_info['engine']`` is ``'average'``, ``'constant'``, ``'magnus'``, ... (see :func:`osc_prob_matter_std_potential`). Default: None. .. versionadded:: 1.2.0 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. """ # strategy_info (issue #114): until 1.2.0 this entry point had no such keyword, so it fell # into **kwargs and was rejected by osc_prob as unknown on every route but average=True, # which returned before any keyword check and left the caller's dict empty. When given, # the call re-enters itself under the same probe the wrappers open; when not, this test is # its whole cost. if strategy_info is not None: _args = {k: v for k, v in locals().items() if k not in ('kwargs', 'strategy_info')} with _engine_probe(info=strategy_info): return osc_prob_energy_baseline(**_args, **kwargs) # The same entry checks as the scenario functions (issue #160): every keyword by its rule, # energy and L entry by entry, and the Hamiltonian itself -- callable, or a square matrix. # A string or None used to fail as a raw IndexError, a masked L had its mask ignored, and # L=True was a baseline of 1 eV^-1. # Skipped when a scenario function is the caller: it has run the same checks already. _direct = sys._getframe(1).f_code.co_name not in _SCENARIO_FUNCTION_NAMES if _direct and (validate_input is True or _validation_requested(validate_input, 'osc_prob_energy_baseline')): _where = "oscprob.osc_prob_energy_baseline" _validate_entry('osc_prob_energy_baseline', locals(), osc_prob_energy_baseline) _v.check_real_array('energy', energy, _where, positive=True) _v.check_real_array('L', L, _where) L0 = _v.check_real('L0', L0, _where) if not np.all(np.asarray(L, dtype=float) >= L0): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline: L must" " be >= L0, the path running from L0 to L.") if not callable(H_func): H_func = _v.check_hamiltonian_sample('H_func', H_func, _where) energy = _as_float(energy) L = _as_float(L) 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. # A callable taking no argument, or more than two, fits none of the forms above and used # to fail on its first call, as a TypeError naming a lambda (issue #160 §7). if validate_input and _direct and callable(H_func): _n_args = _n_required_params(H_func) if isinstance(_n_args, int) and _n_args not in (1, 2): raise _v.InputTypeError(gd.ERROR_MSG_NO_COLOR + " oscprob.osc_prob_energy_baseline: " "H_func must take (energy, l), (l), or (energy) with " "H_func_is_function_only_of_energy=True; it requires " + str(_n_args) + " argument" + ("" if _n_args == 1 else "s") + ". A constant Hamiltonian can be " "passed as the matrix itself.") H_first = H_at_energy(energy[0]) # The first sample of a user Hamiltonian, checked once (issue #160 §7): an array (a list # failed later as a raw TypeError from the probe), square, finite (a NaN ran the whole # refinement ladder before returning NaN), and Hermitian (a non-Hermitian H returned # probabilities above 1 without a word). One evaluation, at L0. if validate_input and _direct and callable(H_func): _where = "oscprob.osc_prob_energy_baseline" _H0 = H_first(L0) if callable(H_first) else H_first if not isinstance(_H0, np.ndarray): raise _v.InputTypeError(gd.ERROR_MSG_NO_COLOR + " " + _where + ": H_func must " "return a NumPy array; it returned " + type(_H0).__name__ + ". Wrap its " "return value in np.array(..., dtype=complex).") try: _v.check_hamiltonian_sample('H_func', _H0, _where, at=('energy ' + format(float(energy[0]), '.4g') + (', position L0' if callable(H_first) else ''))) except ValueError as _e: # An object array cannot be tested for finiteness, and is refused as it stands. if (_H0.dtype.kind in 'iufc' and not np.all(np.isfinite(_H0)) and _n_required_params(H_func) == 1 and not H_func_is_function_only_of_energy): raise ValueError(str(_e) + " H_func takes one argument, which is read as the " "position; if it is a function of energy, pass " "H_func_is_function_only_of_energy=True.") from None raise # 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 not average: # The wrappers refuse these inside _avg_prob_dispatch, which this route reaches only # with average=True. _refuse_average_keywords('osc_prob_energy_baseline', average_spread, average_n_samples, average_initial_state) if average: # This route answers without reaching osc_prob, whose keyword check the per-point # route relies on, so a typo here (rtoll=...) used to be ignored in silence (#114). _check_passthrough_kwargs(kwargs, 'osc_prob_energy_baseline') 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, average_n_samples=average_n_samples, average_initial_state=average_initial_state, 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.", 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)): magnus._warn_once( "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))) # Undeclared jumps (issue #161). A slab straddling a jump converges as 1/n, not at the # order of the method, so the ladder thrashes and stops wherever two levels happen to agree: # measured on a three-layer castle wall at 40 baselines, 24 were off by more than the # requested 1e-3, up to 7.0e-2, with only the generic MagnusConvergenceWarning. The ladder's # own samples show it: see UNDECLARED_JUMP_STEP_TOLERANCE. Then, once per call, the profile is # checked on the probe grid of the hybrid strategy's resolution test, and any jump found is # declared for every point and the points already computed are recomputed with it. Calls # that declare their own t_breakpoints or t_slab_edges, or ask for no tolerance, never # reach the check. jump_check = bool(callable(H_first) and (t_slab_edges is None) and ((rtol is not None) or (atol is not None)) and ((kwargs.get('t_breakpoints') is None) or (len(np.atleast_1d(kwargs.get('t_breakpoints'))) == 0))) jumps_checked = [False] def check_for_jumps(enu: float) -> bool: # True when jumps were found and declared: the caller recomputes what it has. jumps_checked[0] = True jumps = adiabatic._located_jumps(H_at_energy(enu), float(L0), float(np.max(np.asarray(L, dtype=float))), AUTO_PROBE_POINTS) if not jumps: return False osc_prob_kwargs['t_breakpoints'] = np.asarray(jumps, dtype=float) conv_info.clear() osc_prob_kwargs['min_n_slabs'] = min_n_slabs osc_prob_kwargs['min_n_tpts_per_slab'] = min_n_tpts_per_slab warnings.warn( "osc_prob_energy_baseline: the Hamiltonian jumps at l = " + ", ".join(format(j, '.6e') for j in jumps) + " [eV^-1], and no t_breakpoints " "were given. A slab straddling a jump converges slowly and erratically, so the " "refinement can stop on an answer outside the tolerance; the jumps were located and " "every point was computed with them declared. Pass t_breakpoints=[" + ", ".join(format(j, '.6e') for j in jumps) + "] to skip this search.", UnmarkedDiscontinuityWarning, stacklevel=5) return True from_hybrid = [] # points answered by the hybrid's uncertified answer (issue #167) def compute_single_point(enu: float, baseline: float): if jump_check and not jumps_checked[0]: steps = [] token = magnus._NODE_STEP_OBSERVER.set(steps) try: out = osc_prob(H_at_energy(enu), L0, baseline, **osc_prob_kwargs) finally: magnus._NODE_STEP_OBSERVER.reset(token) # Across a jump the largest change between consecutive nodes stays at the size of # the jump from one level to the next; on a smooth profile it shrinks with the slab # width, by 1/growth_factor_n_slabs a level. Only the first kind is checked. if len(steps) >= 2: previous, accepted = magnus._node_step(steps[0]), magnus._node_step(steps[1]) steps.clear() if ((previous > 0.0) and (abs(accepted/previous - 1.0) <= UNDECLARED_JUMP_STEP_TOLERANCE) and check_for_jumps(enu)): raise _JumpsDeclared() steps.clear() else: out = osc_prob(H_at_energy(enu), L0, baseline, **osc_prob_kwargs) P, U = out if return_evolution_operator else (out, None) # Neither engine reached the tolerance (issue #167). Under strategy='auto' the hybrid # declined this point uncertified and the ladder then ran out of room: at 1 MeV across # the Sun at 1e-8, its one level of 20000 slabs was 4.0e-3 off, where the hybrid's # answer was 1.7e-6 off. The hybrid's answer is returned when the ladder has less to # show for its own: no two levels to compare, or two further apart than the hybrid's # error estimate. Read only after the ladder failed, so a call that converges pays nothing. # conv_info describes this point only while it is passed to osc_prob; the parallel # workers run without it. if ((conv_info.get('tolerance_achieved') is False) and not return_evolution_operator and ('convergence_info' in osc_prob_kwargs)): fallback = _hybrid_fallback(enu, baseline, L0) gap = conv_info.get('last_gap') if (fallback is not None) and ((gap is None) or (gap > fallback['error_estimate'])): P = fallback['P'] from_hybrid.append(dict(error_estimate=fallback['error_estimate'], ladder_last_gap=gap)) magnus._warn_once( "osc_prob_energy_baseline (strategy='auto'): neither engine reached the " "requested tolerance at this point. The adiabatic hybrid did not certify it, " "and the Magnus ladder then reached max_n_slabs without converging. The " "hybrid's answer was returned, since the ladder had less to show for its own: " "its estimated error is in strategy_info['trace'] ('error_estimate'), and any " "slab-width or tolerance warning above describes the ladder's answer, which " "was not returned. For an answer certified at this tolerance, raise " "max_n_slabs; or request rtol/atol no tighter than the accuracy you need. " "Shown once per session.", ToleranceNotAchievedWarning, stacklevel=4) # 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 def run_points(parallelize_over_points: bool) -> list: probs = None 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.) _t_first = time.perf_counter() probs = [compute_single_point(energy[0], L[0])] _t_first = time.perf_counter() - _t_first apply_warm_start() # Too little work left to pay for starting the workers (issue #155 §3): finish here. if _t_first*(len(energy) - 1) < N_JOBS_MIN_PARALLEL_WORK_S: parallelize_over_points = False for enu, baseline in zip(energy[1:], L[1:]): apply_warm_start() probs.append(compute_single_point(enu, baseline)) if parallelize_over_points: osc_prob_kwargs.pop('convergence_info', None) # The first point, computed above, decided whether to look for jumps; a worker # cannot declare them for the others. jumps_checked[0] = True # 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) # No more workers than there are points left, or cores (issue #160 §5): n_jobs=1000 on # a 20-energy scan used to start about 626 worker processes and stall the machine. _workers = len(energy) - 1 _cores = os.cpu_count() or 1 _n_workers = min(_workers, _cores) if n_jobs == -1 else min(n_jobs, _workers, _cores) probs += Parallel(n_jobs=_n_workers)(delayed(compute_single_point_in_worker)( enu, baseline) for enu, baseline in zip(energy[1:], L[1:])) elif probs is None: probs = [] for enu, baseline in zip(energy, L): apply_warm_start() probs.append(compute_single_point(enu, baseline)) return probs try: probs = run_points(parallelize_over_points) except _JumpsDeclared: # Every point, the ones already computed included, again with the jumps declared. from_hybrid.clear() probs = run_points(parallelize_over_points) # Which engine answered: the hybrid, when every point is its answer; otherwise the ladder, # with the hybrid's points noted before it. hybrid_note = (dict(certified=False, n_points=len(from_hybrid), error_estimate=max(p['error_estimate'] for p in from_hybrid), reason='the ladder did not converge either') if from_hybrid else None) if hybrid_note and len(from_hybrid) < n_points: _note_engine('hybrid', **hybrid_note) # 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, # The caller's own breakpoints: jumps found by the search below are this call's business, # and the 'expm' reference of cross_check_strategies must see what the caller declared. t_breakpoints=kwargs.get('t_breakpoints'))) if hybrid_note and len(from_hybrid) == n_points: _note_engine('hybrid', **hybrid_note) # 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.2.0 """ 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 .. versionchanged:: 1.2.0 Reports ``'unverified'`` and ``'max_spread_verified'`` (issue #166). 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: any of ``'hybrid'``, ``'ip_exp'``, ``'separable'``, ``'constant'``, ``'cumulative'``, ``'magnus'`` and ``'expm'`` (the labels of :data:`ENGINE_FAMILIES` except ``'average'``, which answers a different question). 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'``. ``'unverified'`` The labels that said they did not reach the requested tolerance: they raised :class:`ToleranceNotAchievedWarning` itself (a refinement ladder that ran out of room) or :class:`HybridCertificationWarning`. **A spread that involves one of them points at that engine first.** Measured on the Sun at 1 MeV (issue #166): the certified hybrid was 3.8e-5 from a 2e6-slab reference and the ladder, capped at 20000 slabs, 1.9e-3 -- and ``max_spread_independent`` named the pair without saying which one had already warned. ``'max_spread_verified'``, ``'max_spread_verified_pair'`` ``'max_spread_independent'`` and its pair over the engines not in ``'unverified'``: 0.0 and None when fewer than two of them, from different families, ran. 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. """ # engines: None, or a non-empty list of labels. A bare string was iterated letter by # letter, an int raised a raw TypeError, and [] returned {} as though every check had # passed (issue #160 §14). _where = "oscprob.cross_check_strategies" if not callable(entry_point): raise _v.InputTypeError(_v._msg(_where, "entry_point must be one of the osc_prob " "functions; got " + type(entry_point).__name__ + ".")) if engines is not None: if isinstance(engines, str) or not isinstance(engines, (list, tuple, set, frozenset)): raise _v.InputTypeError(_v._msg(_where, "engines must be None or a list of engine " "labels, such as ['hybrid', 'magnus']; got " + type(engines).__name__ + " " + repr(engines) + ".")) if len(engines) == 0: raise ValueError(_v._msg(_where, "engines is empty, so there is nothing to " "compare; pass None for every engine.")) 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, unverified = {}, {}, {}, {}, [] 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 any(w.category in (ToleranceNotAchievedWarning, HybridCertificationWarning) for w in caught): unverified.append(label) 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 best_ver, best_ver_pair = 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) if (ENGINE_FAMILIES[a] != ENGINE_FAMILIES[b] and s > best_ver and a not in unverified and b not in unverified): best_ver, best_ver_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, 'unverified': tuple(lab for lab in ran if lab in unverified), 'max_spread_verified': best_ver, 'max_spread_verified_pair': best_ver_pair, }
#----------------------------------------------------------------------- # 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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=None, strategy_info: Optional[Dict]=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.2.0 Added ``return_evolution_operator`` and ``strategy_info``. .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused, and flags given as None (issue #160 §1, §4). average_spread and average_initial_state are refused without average=True (issue #160). 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``, and refused at or below it, where it would be ignored. 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.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. 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 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. \**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. strategy_info : dict, optional If given, filled in place with which engine answered, as for :func:`osc_prob_matter_std_potential`: ``'engine'`` is ``'average'`` for ``average=True``, ``'constant'`` for the one stacked exponential that answers every other request, and ``'magnus'`` when the request goes to the ladder (with ``return_evolution_operator=True``, or a keyword the constant engine does not take). The other keys (``'family'``, ``'certified'``, ``'declined'``, ``'trace'``) have the meaning given there. Costs nothing when omitted. Default: None. .. versionadded:: 1.2.0 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)``. """ # The public name to sign errors with, and the one validation pass that every engine # below sits behind, so that whether an argument is refused does not depend on which # engine would have answered (issue #160). _where = _caller_name('osc_prob_vacuum') if validate_input is True or _validation_requested(validate_input, _where): _validate_entry(_where, locals(), osc_prob_vacuum) _refuse_start_keyword(_where, kwargs, 'vacuum') _refuse_breakpoints_in_vacuum(_where, kwargs) energy, L = _as_float(energy), _as_float(L) # Unpack oscillation parameters from the osc_params dict, check if all values are available osc_params_list = unpack_oscillation_params_from_dict(_where, 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(_where, 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. _reject_parameter_set_name_without_set(num_flavors, default_osc_params_set_name, _where) 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, _where) _check_passthrough_kwargs(kwargs, _where) # 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`). # Everything below is dispatch: which engine gets the request. Watched as a unit so that # strategy_info can report which one answered, as in osc_prob_matter_std_potential; see # _engine_probe. Costs one list allocation per call when nobody is watching. with _engine_probe(info=strategy_info): 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, _where) 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, _where, average_spread=average_spread, average_n_samples=average_n_samples, average_initial_state=average_initial_state) 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_pseudo_dirac_vacuum( energy: Union[int, float, list, np.ndarray], L: Union[int, float, list, np.ndarray], pairs: Optional[Dict[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, mixing_matrix: Optional[Union[list, np.ndarray]]=None, mass_squared: Optional[Union[list, np.ndarray]]=None, average: Optional[bool]=False, average_spread: Optional[float]=None, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=None, strategy_info: Optional[Dict]=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, 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 oscillation probabilities in vacuum for a pseudo-Dirac spectrum. Each mass state named in ``pairs`` is two states split by a tiny :math:`\delta m^2_j`, as in :mod:`magnus.hamiltonians.hamiltonians_pseudodirac`; the others stay single. The flavor space has ``n = 3 + len(pairs)`` rows: the three active flavors, then one sterile partner per paired state, in ascending order of that state's index. Without ``average`` this is the ordinary oscillation probability, from the same Hamiltonian and the same engine as ``osc_prob_energy_baseline`` given :func:`magnus.hamiltonians.hamiltonians_pseudodirac.hamiltonian_pseudo_dirac_vacuum`, bit for bit. With ``average=True`` it is the phase average of :func:`osc_prob_energy_baseline`, but computed from the known eigensystem instead of an eigendecomposition (:func:`magnus.avgprob.phase_averaged_probabilities_pseudo_dirac`). The generic route finds each pair's phase as the difference of two eigenvalues of the Hamiltonian, which a diagonalization resolves only to about :math:`\epsilon\,\max m^2`, so its error grows as :math:`\delta m^2_j/\max m^2` falls: against a 50-digit reference, over pair phases of 0.5 to 30 rad, it is :math:`2\times10^{-7}` at a ratio of :math:`10^{-10}`, :math:`7\times10^{-5}` at :math:`10^{-12}`, :math:`6\times10^{-3}` at :math:`10^{-14}` and 0.25 at :math:`4\times10^{-16}` (issue #165). Here each pair phase is formed from :math:`\delta m^2_j` directly, and its slope in :math:`\ln E` is exact, so the result is within :math:`1.1\times10^{-16}` of the reference at every one of those ratios. .. versionadded:: 1.2.0 Parameters ---------- energy : int, float, list, or np.ndarray Neutrino energy/energies [eV]. L : int, float, list, or np.ndarray Baseline(s) [:math:`\text{eV}^{-1}`]. Paired with ``energy`` point by point, or broadcast when one of the two is a single value. pairs : dict Mapping from mass-state index (0, 1 or 2) to its splitting :math:`\delta m^2_j` [eV^2]; see :func:`magnus.hamiltonians.hamiltonians_pseudodirac.pseudo_dirac_mixing_matrix`. An empty mapping is the ordinary three-flavor spectrum. s12, s23, s13, dCP, D21, D31 : int or float, optional The standard three-flavor parameters, read as ``angles`` says; each one left at None takes its value from ``default_osc_params_set_name``. mixing_matrix : list or np.ndarray, optional The active-sector mixing matrix, in place of the angles; given together with ``mass_squared``, and never with any of ``s12`` to ``D31``. Its size sets the number of active flavors. mass_squared : list or np.ndarray, optional The active-sector mass-squared values [eV^2], with ``mixing_matrix``. average : bool, optional If True, return the phase-averaged probability. Default: False. average_spread : float, optional Relative energy spread of the phase average; None means :data:`magnus.avgprob.AVG_PHASE_SPREAD`. Only with ``average=True``. average_n_samples : int, optional Refused: this route samples nothing. Accepted as None for a signature shared with the other wrappers. average_initial_state : str, optional ``'flavor'`` (the default) or ``'decohered'``. Only with ``average=True``. strategy_info : dict, optional Filled with the engine that answered, as in :func:`osc_prob_vacuum`. nubar : bool, optional If True, antineutrinos. Default: False. nu_i, nu_f : int, optional Initial and final flavor, in ``[0, n)``; if both are given, the single probability is returned. default_osc_params_set_name : str, optional Parameter set that fills the standard parameters left at None. validate_input : bool, optional Whether to validate the input. Default: True. verbose : int, optional Verbosity, as in :func:`osc_prob_vacuum`. angles : str, optional How ``s12``, ``s23`` and ``s13`` are read: ``'sin'``, ``'sin2'``, ``'rad'`` or ``'deg'``. Default: ``'sin'``. return_evolution_operator : bool, optional If True, also return the evolution operator. Not with ``average=True``. **kwargs Engine settings forwarded to :func:`osc_prob_energy_baseline` without ``average``. Returns ------- float or np.ndarray The probability, or the ``n`` x ``n`` probabilities, per (energy, L) point, shaped as :func:`osc_prob_vacuum` returns them. Raises ------ ValueError If the angles are given together with ``mixing_matrix``, only one of ``mixing_matrix`` and ``mass_squared`` is given, a flavor index is outside ``[0, n)``, or an averaging keyword is given without ``average=True``. Warns ----- PhaseAveragingWarning With ``average=True``, where the average depends on ``average_spread``, as in :func:`osc_prob_vacuum`. Examples -------- A splitting of :math:`10^{-17}` eV^2 at 100 TeV over 100 Mpc, where the pair phase is a few radians. .. jupyter-execute:: import magnus.globaldefs as gd import magnus.oscprob as oscprob L = 100.0*3.0856775814913673e19*gd.UNIT_KM # 100 Mpc P = oscprob.osc_prob_pseudo_dirac_vacuum( 100.0*gd.UNIT_TEV, L, {0: 1.0e-17, 1: 1.0e-17, 2: 1.0e-17}, nu_i=gd.NUE, nu_f=gd.NUE, average=True, average_spread=0.01) print(round(float(P), 6)) """ _where = 'osc_prob_pseudo_dirac_vacuum' if validate_input is True or _validation_requested(validate_input, _where): _validate_entry(_where, locals(), osc_prob_pseudo_dirac_vacuum) _refuse_start_keyword(_where, kwargs, 'vacuum') _refuse_breakpoints_in_vacuum(_where, kwargs) energy, L = _as_float(energy), _as_float(L) angle_values = dict(s12=s12, s23=s23, s13=s13, dCP=dCP, D21=D21, D31=D31) if (mixing_matrix is None) != (mass_squared is None): raise ValueError(_v._msg("oscprob." + _where, "mixing_matrix and mass_squared go " "together: give both, or neither and the standard parameters s12 to D31.")) if mixing_matrix is not None: given = [name for name, value in angle_values.items() if value is not None] if given: raise ValueError(_v._msg("oscprob." + _where, "the mixing is given twice: by " "mixing_matrix and mass_squared, and by " + ", ".join(given) + ". Give one " "or the other.")) U = np.asarray(mixing_matrix, dtype=complex) m2 = np.asarray(mass_squared, dtype=float).ravel() if U.ndim != 2 or U.shape[0] != U.shape[1] or m2.size != U.shape[0]: raise ValueError(_v._msg("oscprob." + _where, "mixing_matrix must be square and " "mass_squared hold one value per row of it; got shapes " + str(U.shape) + " and " + str(m2.shape) + ".")) else: _reject_parameter_set_name_without_set(3, default_osc_params_set_name, _where) 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) U = hamiltonians.pmns_mixing_matrix(s12, s23, s13, dCP, angles=angles) m2 = np.array([0.0, float(D21), float(D31)]) # Validates pairs, and warns on a splitting that is not small (PseudoDiracSplittingWarning). H_energy_indep = hamiltonians.hamiltonian_pseudo_dirac_vacuum_energy_independent( U, m2, pairs, nubar=nubar) d = H_energy_indep.shape[0] if validate_input: validate_input_battery(_where, energy=energy, L=L, L0=0.0, nu_i=nu_i, nu_f=nu_f, validate_energy_and_L=True, validate_flavor_indices=False, validate_osc_params=False, validate_initial_position=False, validate_density=False) for name, index in (('nu_i', nu_i), ('nu_f', nu_f)): if index is not None and not 0 <= int(index) < d: raise ValueError(_v._msg("oscprob." + _where, name + " = " + str(index) + " is " "outside the " + str(d) + " flavors of this spectrum, [0, " + str(d) + "): " "the active flavors, then one sterile partner per paired mass state.")) _reject_parameter_set_metadata(kwargs, _where) _check_passthrough_kwargs(kwargs, _where) with _engine_probe(info=strategy_info): if not average: _refuse_average_keywords(_where, average_spread, average_n_samples, average_initial_state) def htot(enu: Union[int, float]) -> np.ndarray: # What hamiltonian_pseudo_dirac_vacuum returns, formed once. return H_energy_indep/enu return osc_prob_energy_baseline(htot, energy, L, 0.0, nu_i, nu_f, True, validate_input=validate_input, verbose=verbose, return_evolution_operator=return_evolution_operator, **kwargs) if return_evolution_operator: _check_operator_request(average, None, _where) if kwargs: raise ValueError(_v._msg("oscprob." + _where, "average=True is computed in closed " "form, and takes no engine settings; remove " + ", ".join(sorted(kwargs)) + ".")) if average_n_samples is not None: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + _where + ": " "average_n_samples sets how many energies the energy-window average samples, " "and that average is taken only on a profile with declared discontinuities. " "This call takes the closed-form route, which samples nothing; remove " "average_n_samples.") 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." + _where + ": 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) initial = 'flavor' if average_initial_state is None else average_initial_state if not isinstance(initial, str) or initial not in ('flavor', 'decohered'): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + _where + ": " "average_initial_state is the state the neutrino starts in and must be " "'flavor' or 'decohered', not " + repr(average_initial_state) + ".") energy_arr, L_arr, return_float, ok = _normalize_energy_L(energy, L) if not ok: raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + _where + ": the energy " "and L arrays must have the same length, or one of them must be a single value.") energy_arr, L_arr = np.broadcast_arrays(np.asarray(energy_arr, dtype=float), np.asarray(L_arr, dtype=float)) n_pts = energy_arr.size if initial == 'decohered': # An incoherent mixture of the eigenstates never interferes, whatever the phases. W = hamiltonians.pseudo_dirac_mixing_matrix(U, pairs) if nubar: W = np.conj(W) P_out = np.broadcast_to(avgprob.averaged_probabilities_from_eigenbasis(W), (n_pts, d, d)).copy() sens = np.zeros(n_pts) else: P_out, sens = avgprob.phase_averaged_probabilities_pseudo_dirac(U, m2, pairs, energy_arr, L_arr, spread=spread, nubar=nubar) spread_sensitive_points = int(np.count_nonzero( sens > avgprob.PHASE_SPREAD_SENSITIVITY_THRESHOLD)) largest_sensitivity = float(np.max(sens, initial=0.0)) if spread_sensitive_points > 0: warnings.warn(gd.WARNING_MSG_NO_COLOR + " oscprob." + _where + ": 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.", PhaseAveragingWarning, stacklevel=2) _note_engine('average', average_spread=spread, recomputed=0, 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))
[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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=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.2.0 Added ``return_evolution_operator``. .. versionchanged:: 1.2.0 A scalar-only rho_func is evaluated position by position and warned about by name (issue #144 §4); flags given as None are refused (issue #160 §4). average_spread and average_initial_state are refused without average=True (issue #160). 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. A function may take one position or an array of them; the two give the same numbers, and the array form is faster. One written for a single position raises :class:`magnus.magnus.ScalarHamiltonianWarning` naming ``rho_func``. 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``, and refused at or below it, where it would be ignored. 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 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.0 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 -- with ``'gl'``, the default. With ``'trapezoid'``/``'simpson'`` the numbers moved in 1.2.0: at declared breakpoints they now sample each side of a jump with its own values, and the energy-batched engine seeds their starting slab count when it is at least :data:`QUADRATURE_SEED_MIN_SLABS`. * ``'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 to the ``'magnus'`` strategies above for any point where it does not apply or fails to self-certify -- without a warning, except when the hybrid declines because the profile is not resolved at its probe scale (an undeclared jump), which raises :class:`UnmarkedDiscontinuityWarning`. 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 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. An energy scan at one baseline that the energy-batched scan will take is handed over whatever its phase (issue #84): that engine shares its slabs across the energies, so the phase limit, which prices the ladder point by point, does not apply to it. Below :data:`AUTO_LADDER_MIN_TOLERANCE`, as ``min(rtol, atol)``, the hand-over needs ``integration_method='gl'``, a single baseline and a phase within a limit that shrinks with the tolerance and the order, capped at :data:`AUTO_LADDER_TIGHT_MAX_PHASE`, for an energy scan as well; the ladder then runs at the requested tolerance itself (issue #120). The paper's Listing 1, at ``rtol = 1e-12``, ``atol = 1e-14`` and ``magnus_exp_order = 8``, is handed over this way. ``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.2.0 ``'auto'`` hands a moderate phase at a loose tolerance to the ladder (issue #70), an energy scan the energy-batched scan will take at any phase (issue #84), and a small phase at a tight tolerance (issue #120). 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 (except the hybrid's decline of an unresolved profile, which raises :class:`UnmarkedDiscontinuityWarning`) -- 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 attempted the request and gave up: the adiabatic engine (not certified, the profile not resolved at the probe scale, or, under ``strategy='auto'``, handed to the ladder) and the interaction picture (did not converge). An engine whose conditions the request does not meet is not listed, so the list is often empty. * ``'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, 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)``. """ # The public name to sign errors with, and the one validation pass that every engine # below sits behind, so that whether an argument is refused does not depend on which # engine would have answered (issue #160). _where = _caller_name('osc_prob_matter_std_potential') if validate_input is True or _validation_requested(validate_input, _where): _validate_entry(_where, locals(), osc_prob_matter_std_potential) energy, L = _as_float(energy), _as_float(L) L0 = _as_float(L0) if not callable(rho_func): rho_func = _as_float(rho_func) if return_evolution_operator: _check_operator_request(average, strategy, _where) if validate_input and (strategy not in ('auto', 'hybrid', 'magnus')): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + _where + ":" + \ " strategy must be 'auto', 'hybrid', or 'magnus'.") # Unpack oscillation parameters from the osc_params dict, check if all values are available osc_params_list = unpack_oscillation_params_from_dict(_where, 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(_where, 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. _reject_parameter_set_name_without_set(num_flavors, default_osc_params_set_name, _where) 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]. if validate_input and callable(rho_func): rho_func = _array_capable_rho(rho_func, L0, L, _where) 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, _where) _check_passthrough_kwargs(kwargs, _where) # 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, _where, smooth_profile=_profile_is_smooth, engine_kwargs=scan_kwargs, average_spread=average_spread, average_n_samples=average_n_samples, average_initial_state=average_initial_state) 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 (at the tolerance itself below AUTO_LADDER_MIN_TOLERANCE, issue # #120), 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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=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.2.0 Added ``return_evolution_operator``. .. versionchanged:: 1.2.0 A scalar-only rho_func is evaluated position by position and warned about by name (issue #144 §4); flags given as None are refused (issue #160 §4). average_spread and average_initial_state are refused without average=True (issue #160). 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``, and refused at or below it, where it would be ignored. 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 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.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 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 (except the hybrid's decline of an unresolved profile, which raises :class:`UnmarkedDiscontinuityWarning`) -- 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 attempted the request and gave up: the adiabatic engine (not certified, the profile not resolved at the probe scale, or, under ``strategy='auto'``, handed to the ladder) and the interaction picture (did not converge). An engine whose conditions the request does not meet is not listed, so the list is often empty. * ``'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, 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)``. """ # The public name to sign errors with, and the one validation pass that every engine # below sits behind, so that whether an argument is refused does not depend on which # engine would have answered (issue #160). _where = _caller_name('osc_prob_matter_nsi') if validate_input is True or _validation_requested(validate_input, _where): _validate_entry(_where, locals(), osc_prob_matter_nsi) energy, L = _as_float(energy), _as_float(L) L0 = _as_float(L0) if not callable(rho_func): rho_func = _as_float(rho_func) if return_evolution_operator: _check_operator_request(average, strategy, _where) if validate_input and (strategy not in ('auto', 'hybrid', 'magnus')): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + _where + ": strategy" + \ " must be 'auto', 'hybrid', or 'magnus'.") # Unpack oscillation parameters from the osc_params dict, check if all values are available osc_params_list = unpack_oscillation_params_from_dict(_where, num_flavors, osc_params, h_vac_energy_indep) nsi_params_list = unpack_nsi_params_from_dict(_where, 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(_where, 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. _reject_parameter_set_name_without_set(num_flavors, default_osc_params_set_name, _where) 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]. if validate_input and callable(rho_func): rho_func = _array_capable_rho(rho_func, L0, L, _where) 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, _where) _check_passthrough_kwargs(kwargs, _where) # 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, _where, smooth_profile=_profile_is_smooth, engine_kwargs=scan_kwargs, average_spread=average_spread, average_n_samples=average_n_samples, average_initial_state=average_initial_state) 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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=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.2.0 Added ``return_evolution_operator``. .. versionchanged:: 1.2.0 A scalar-only rho_func is evaluated position by position and warned about by name (issue #144 §4); flags given as None are refused (issue #160 §4). average_spread and average_initial_state are refused without average=True (issue #160). For antineutrinos, the LIV term changes sign when ``n_liv`` is even, at every flavor count (issue #162). 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``, and refused at or below it, where it would be ignored. 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``. It is used as given, ``nubar`` or not: for antineutrinos, pass the antineutrino operator. 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 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: the mixing matrices are conjugated, the matter potential changes sign, and so does the LIV term when ``n_liv`` is even, an operator of dimension ``n_liv + 3`` being CPT-odd when that is odd (see :func:`magnus.hamiltonians.hamiltonians3nu.hamiltonian_3nu_liv`). 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.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 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 (except the hybrid's decline of an unresolved profile, which raises :class:`UnmarkedDiscontinuityWarning`) -- 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 attempted the request and gave up: the adiabatic engine (not certified, the profile not resolved at the probe scale, or, under ``strategy='auto'``, handed to the ladder) and the interaction picture (did not converge). An engine whose conditions the request does not meet is not listed, so the list is often empty. * ``'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, 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)``. """ # The public name to sign errors with, and the one validation pass that every engine # below sits behind, so that whether an argument is refused does not depend on which # engine would have answered (issue #160). _where = _caller_name('osc_prob_liv') if validate_input is True or _validation_requested(validate_input, _where): _validate_entry(_where, locals(), osc_prob_liv) energy, L = _as_float(energy), _as_float(L) L0 = _as_float(L0) if not callable(rho_func): rho_func = _as_float(rho_func) if return_evolution_operator: _check_operator_request(average, strategy, _where) if validate_input and (strategy not in ('auto', 'hybrid', 'magnus')): raise ValueError(gd.ERROR_MSG_NO_COLOR + " oscprob." + _where + ": strategy must be" + \ " 'auto', 'hybrid', or 'magnus'.") # Unpack oscillation parameters from the osc_params dict, check if all values are available osc_params_list = unpack_oscillation_params_from_dict(_where, num_flavors, osc_params, h_vac_energy_indep) liv_params_list = unpack_liv_params_from_dict(_where, 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, dxiCP, 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, dxiCP, 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(_where, 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. _reject_parameter_set_name_without_set(num_flavors, default_osc_params_set_name, _where) 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, nubar=nubar, 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, dxiCP, 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, dxiCP, 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]. if validate_input and callable(rho_func): rho_func = _array_capable_rho(rho_func, L0, L, _where) 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, _where) _check_passthrough_kwargs(kwargs, _where) # 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, _where, smooth_profile=_profile_is_smooth, engine_kwargs=scan_kwargs, average_spread=average_spread, average_n_samples=average_n_samples, average_initial_state=average_initial_state) 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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name, and the refinement keywords are documented as having no effect in vacuum (issue #160 §1). 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. \**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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name, and the refinement keywords are documented as having no effect in vacuum (issue #160 §1). 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. \**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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name, and the refinement keywords are documented as having no effect in vacuum (issue #160 §1). 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. \**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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name, and the refinement keywords are documented as having no effect in vacuum (issue #160 §1). 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. \**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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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:: Can use a fast, closed-form interaction-picture Magnus integrator (two flavors, see ``_osc_prob_ip_exp_dispatch``) when the accumulated matter phase stays small enough to certify. Under the default ``strategy='auto'`` the adiabatic hybrid is tried first, and the interaction-picture integrator only where the hybrid declines without preferring the slab ladder; with ``strategy='magnus'`` it is tried directly. Requests it cannot certify go to the general slab-refinement method. .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_2nu_matter_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_3nu_matter_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_4nu_matter_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_5nu_matter_exp_density') 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_2nu_earth', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_2nu_earth') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_3nu_earth', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_3nu_earth') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_4nu_earth', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_4nu_earth') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_5nu_earth', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_5nu_earth') # 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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=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.2.0 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``. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path (issue #160 §1, §6). average_spread and average_initial_state are refused without average=True (issue #160). 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``. The path length from the source along the chord: a value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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 does not use it (:math:`n_e = \rho N_A Y_e` since 1.2.0, issue #168), 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.0 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. _refuse_start_keyword('osc_prob_earth', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_earth') # 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, average_n_samples=average_n_samples, average_initial_state=average_initial_state, 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, average_n_samples: Optional[int] = None, average_initial_state: Optional[str] = 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.2.0 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. .. versionchanged:: 1.2.0 H_func's first sample is checked: an array, square, finite and Hermitian (issue #160 §7). average_spread and average_initial_state are refused without average=True (issue #160). 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.0 \**kwargs Additional arguments forwarded to :func:`osc_prob_energy_baseline`. The engine keywords travel this way (``t_breakpoints`` excepted, which this function takes explicitly). The ones worth knowing are listed in :data:`magnus.oscprob.PASSTHROUGH_KWARGS_DOCUMENTED`; two of them decide whether an answer on a hard profile is right at all: ``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) # The first sample, checked as osc_prob_energy_baseline checks it for its direct callers # (issue #160 §7): a list or an object array used to run here, and fail or not depending # on which engine answered. One evaluation, at L0 and the first energy. if validate_input: _e0 = float(np.atleast_1d(np.asarray(energy, dtype=float))[0]) _H0 = htot(_e0, L0) if not isinstance(_H0, np.ndarray): raise _v.InputTypeError(gd.ERROR_MSG_NO_COLOR + " oscprob." + source_func_name + ": H_func must return a NumPy array; it returned " + type(_H0).__name__ + ". Wrap its return value in np.array(..., dtype=complex).") _v.check_hamiltonian_sample('H_func', _H0, "oscprob." + source_func_name, at='energy ' + format(_e0, '.4g') + ', position L0') # 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, average_n_samples=average_n_samples, average_initial_state=average_initial_state) 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 (at the tolerance itself below AUTO_LADDER_MIN_TOLERANCE, issue #120) 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.2.0 """ 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 _sun_entry(source_func_name: str, L0, kwargs: dict, num_flavors) -> None: r"""Checks shared by the Sun wrappers, before anything is built (issue #160 §1). * ``L0`` is a radius, so it is 0 or more: a negative one was accepted. * ``electron_fraction`` has no effect: every Sun wrapper hands the engine an electron number density, so it was accepted and ignored. * ``ratio_number_neutrons_to_protons`` acts only through the sterile neutral-current term, so at two and three flavors it was accepted and ignored. .. versionadded:: 1.2.0 """ where = "oscprob." + source_func_name if not (type(L0) is float and L0 >= 0.0): _v.check_real('L0', L0, where, nonnegative=True, what="0 or more: it is the radius where the path starts") unused = [k for k in ('electron_fraction',) if kwargs.get(k) is not None] if num_flavors in (2, 3) and kwargs.get('ratio_number_neutrons_to_protons') is not None: unused.append('ratio_number_neutrons_to_protons') if unused: raise ValueError(_v._msg(where, ', '.join(unused) + " has no effect here: the Sun " "wrappers use the solar model's electron density directly" + ("" if unused == ['electron_fraction'] else ", and at %d flavors the neutron-to-proton ratio enters " "nothing" % num_flavors) + ".")) 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.2.0 """ 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.2.0 """ 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.2.0 Takes ``density_profile``, to use a tabulated standard solar model in place of the exponential fit, and ``stop_at_table_edge``. .. note:: Can use a fast, closed-form interaction-picture Magnus integrator (two flavors, see ``_osc_prob_ip_exp_dispatch``) when the accumulated matter phase stays small enough to certify. Under the default ``strategy='auto'`` the adiabatic hybrid is tried first, and the interaction-picture integrator only where the hybrid declines without preferring the slab ladder; with ``strategy='magnus'`` it is tried directly. Requests it cannot certify go 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`) first, before the interaction-picture integrator and the general slab-refinement method. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect, and ratio_number_neutrons_to_protons, which has none at 2 flavors (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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) _sun_entry('osc_prob_2nu_sun', L0, kwargs, 2) _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.2.0 Takes ``density_profile``, to use a tabulated standard solar model in place of the exponential fit, and ``stop_at_table_edge``. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect, and ratio_number_neutrons_to_protons, which has none at 3 flavors (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. """ _sun_entry('osc_prob_3nu_sun', L0, kwargs, 3) _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.2.0 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. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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` at every radius from its hydrogen mass fraction :math:`X`, the rest counted as helium-4 (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 its electrons per atomic mass unit run from about 0.67 at the center to 0.86 near the surface, and :math:`r` from about 0.49 down to 0.15 -- 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. """ _sun_entry('osc_prob_4nu_sun', L0, kwargs, 4) _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.2.0 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. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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` at every radius from its hydrogen mass fraction :math:`X`, the rest counted as helium-4 (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 its electrons per atomic mass unit run from about 0.67 at the center to 0.86 near the surface, and :math:`r` from about 0.49 down to 0.15 -- 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. """ _sun_entry('osc_prob_5nu_sun', L0, kwargs, 5) _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, average_n_samples: Optional[int]=None, average_initial_state: Optional[str]=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.2.0 Added ``average``, ``density_profile`` and ``stop_at_table_edge``. .. versionchanged:: 1.2.0 A negative L0 and electron_fraction, which has no effect, are refused; H_func's first sample is checked (issue #160 §1, §7). average_spread and average_initial_state are refused without average=True (issue #160). 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}`], 0 or more. A single number, not an array: for several production points, loop over them. 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`). On a profile with declared discontinuities, where ``average=True`` averages over an energy window instead, it is the half-width of that window as a fraction of the energy, and must lie between 0 and 1 (see :data:`magnus.avgprob.AVG_DEFAULT_ENERGY_SPREAD`). Refused without ``average=True``. Default: None, meaning 0.1. average_n_samples : int, optional Number of energies sampled across the window by ``average=True`` on a profile with declared discontinuities, at least 2. The standard error of the window average falls as the inverse square root of this number, and each sample costs a full propagation. No other route samples, nor does a call without ``average``, so it is refused there. Default: None, meaning :data:`magnus.avgprob.AVG_DEFAULT_N_SAMPLES`, 41. .. versionadded:: 1.2.0 average_initial_state : str, optional The state the neutrino starts in, for ``average=True``: ``'flavor'`` or ``'decohered'``. ``'flavor'``: the flavor state :math:`\nu_\alpha`, as for a neutrino produced in the medium. ``'decohered'``: an incoherent mixture of the eigenstates at the start of the path, with weights :math:`|V_{\alpha i}|^2`, as for a neutrino that lost its coherence before reaching it, e.g. one arriving at the Sun from a distant source. The two differ only where the phases from production onward have not averaged away (paper Eq. (flavor_vs_decohered)). Not available on a profile with declared discontinuities, whose average starts in flavor. Refused without ``average=True``. Default: None, meaning ``'flavor'``. .. versionadded:: 1.2.0 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 _sun_entry(source_func_name, L0, kwargs, None) _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, average_n_samples=average_n_samples, average_initial_state=average_initial_state, **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:: Can use a fast, closed-form interaction-picture Magnus integrator (two flavors, see ``_osc_prob_ip_exp_dispatch``; the NSI couplings are folded into the same fast path) when the accumulated matter phase stays small enough to certify. Under the default ``strategy='auto'`` the adiabatic hybrid is tried first, and the interaction-picture integrator only where the hybrid declines without preferring the slab ladder; with ``strategy='magnus'`` it is tried directly. Requests it cannot certify go to the general slab-refinement method. .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_2nu_matter_nsi_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_3nu_matter_nsi_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_4nu_matter_nsi_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_5nu_matter_nsi_exp_density') 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_2nu_earth_nsi', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_2nu_earth_nsi') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_3nu_earth_nsi', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_3nu_earth_nsi') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_4nu_earth_nsi', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_4nu_earth_nsi') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_5nu_earth_nsi', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_5nu_earth_nsi') # 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.2.0 Takes ``density_profile``, to use a tabulated standard solar model in place of the exponential fit, and ``stop_at_table_edge``. .. note:: Can use a fast, closed-form interaction-picture Magnus integrator (two flavors, see ``_osc_prob_ip_exp_dispatch``; the NSI couplings are folded into the same fast path) when the accumulated matter phase stays small enough to certify. Under the default ``strategy='auto'`` the adiabatic hybrid is tried first, and the interaction-picture integrator only where the hybrid declines without preferring the slab ladder; with ``strategy='magnus'`` it is tried directly. Requests it cannot certify go to the general slab-refinement method. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect, and ratio_number_neutrons_to_protons, which has none at 2 flavors (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. """ _sun_entry('osc_prob_2nu_sun_nsi', L0, kwargs, 2) _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.2.0 Takes ``density_profile``, to use a tabulated standard solar model in place of the exponential fit, and ``stop_at_table_edge``. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect, and ratio_number_neutrons_to_protons, which has none at 3 flavors (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. """ _sun_entry('osc_prob_3nu_sun_nsi', L0, kwargs, 3) _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.2.0 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. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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` at every radius from its hydrogen mass fraction :math:`X`, the rest counted as helium-4 (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 its electrons per atomic mass unit run from about 0.67 at the center to 0.86 near the surface, and :math:`r` from about 0.49 down to 0.15 -- 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. """ _sun_entry('osc_prob_4nu_sun_nsi', L0, kwargs, 4) _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.2.0 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. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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` at every radius from its hydrogen mass fraction :math:`X`, the rest counted as helium-4 (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 its electrons per atomic mass unit run from about 0.67 at the center to 0.86 near the surface, and :math:`r` from about 0.49 down to 0.15 -- 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. """ _sun_entry('osc_prob_5nu_sun_nsi', L0, kwargs, 5) _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', nubar: Optional[bool]=False, **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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name (issue #160 §1). Declares ``nubar``, which now has an effect: the LIV term changes sign for antineutrinos when ``n_liv`` is even (issue #162). 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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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. nubar : bool, optional If True, compute the probability for antineutrinos. In vacuum at two flavors this matters only through the LIV term, which changes sign for antineutrinos when ``n_liv`` is even (see :func:`magnus.hamiltonians.hamiltonians2nu.hamiltonian_2nu_liv`). 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) _refuse_start_keyword('osc_prob_2nu_vacuum_liv', kwargs, 'vacuum') _refuse_breakpoints_in_vacuum('osc_prob_2nu_vacuum_liv', kwargs) 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}, 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_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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name (issue #160 §1). 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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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. """ _refuse_start_keyword('osc_prob_3nu_vacuum_liv', kwargs, 'vacuum') _refuse_breakpoints_in_vacuum('osc_prob_3nu_vacuum_liv', kwargs) 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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name (issue #160 §1). 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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. """ _refuse_start_keyword('osc_prob_4nu_vacuum_liv', kwargs, 'vacuum') _refuse_breakpoints_in_vacuum('osc_prob_4nu_vacuum_liv', kwargs) 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, 'dxiCP': dxiCP, '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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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 .. versionchanged:: 1.2.0 L0 and t_breakpoints are refused by name (issue #160 §1). 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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. In vacuum the Hamiltonian does not vary along the path and each point is computed exactly, so the refinement keywords (``rtol``, ``atol``, the slab and order controls) are accepted, for a call shared with the matter wrappers, and have no effect; ``t_breakpoints`` is refused. 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. """ _refuse_start_keyword('osc_prob_5nu_vacuum_liv', kwargs, 'vacuum') _refuse_breakpoints_in_vacuum('osc_prob_5nu_vacuum_liv', kwargs) 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, 'dxiCP': dxiCP, '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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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, 'dxiCP': dxiCP, '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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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, 'dxiCP': dxiCP, '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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_2nu_matter_liv_exp_density') 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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=(_exp_profile(rho_central, l_scale, 'osc_prob_3nu_matter_liv_exp_density') 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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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, 'dxiCP': dxiCP, '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=(_exp_profile(rho_central, l_scale, 'osc_prob_4nu_matter_liv_exp_density') 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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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 .. versionchanged:: 1.2.0 A NaN or otherwise invalid l_scale or rho_central is refused naming it, rather than as a non-finite rho_func (issue #160 §1). 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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 not (isinstance(rho_central, (int, float)) and isinstance(l_scale, (int, float))) and \ ((np.ndim(rho_central) != 0) or (np.ndim(l_scale) != 0)): raise _not_a_single_number(sys._getframe().f_code.co_name, [n for n, v in (('rho_central', rho_central), ('l_scale', l_scale)) if np.ndim(v)]) 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, 'dxiCP': dxiCP, '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=(_exp_profile(rho_central, l_scale, 'osc_prob_5nu_matter_liv_exp_density') 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_2nu_earth_liv', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_2nu_earth_liv') # 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_3nu_earth_liv', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_3nu_earth_liv') # 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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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, dxiCP=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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_4nu_earth_liv', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_4nu_earth_liv') # 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, 'dxiCP': dxiCP, '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 )
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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 each be given as a (latitude, longitude) pair, each coordinate a (degree, minute, second) triple -- e.g. ``loc_ini=((41, 49, 55), (-88, 15, 26))``. 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']) A named location is passed by name, e.g. ``loc_ini='fermilab'``. [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, dxiCP=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.2.0 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. .. versionchanged:: 1.2.0 L0 is refused by name, and so are t_breakpoints of which none lies on the path; the PREM crossings past the end of a partial path are no longer passed on (issue #160 §1, §6). 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): the path length from the source along the chord. A value shorter than the chord is a partial path, which stops inside the Earth; one longer than the chord is refused. 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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, and its value at the start of the path is checked, the way ``rho_func``'s is. ``electron_fraction=0.5`` describes genuinely uniform isoscalar matter, and with it these wrappers reproduce the numbers from before composition was layered. At two and three flavors the projector's sterile block is empty, so a ratio has nothing to act on and is refused. .. 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. .. versionchanged:: 1.2.0 Refused at two and three flavors, where it was accepted and ignored; a callable's first value is checked. 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. _refuse_start_keyword('osc_prob_5nu_earth_liv', kwargs, 'earth') _user_breakpoints = kwargs.pop('t_breakpoints', None) t_breakpoints = _earth_breakpoints(t_breakpoints, _user_breakpoints, L, 'osc_prob_5nu_earth_liv') # 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, 'dxiCP': dxiCP, '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.2.0 Takes ``density_profile``, to use a tabulated standard solar model in place of the exponential fit, and ``stop_at_table_edge``. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect, and ratio_number_neutrons_to_protons, which has none at 2 flavors (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. """ _sun_entry('osc_prob_2nu_sun_liv', L0, kwargs, 2) _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.2.0 Takes ``density_profile``, to use a tabulated standard solar model in place of the exponential fit, and ``stop_at_table_edge``. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect, and ratio_number_neutrons_to_protons, which has none at 3 flavors (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. """ _sun_entry('osc_prob_3nu_sun_liv', L0, kwargs, 3) _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)
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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.2.0 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. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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` at every radius from its hydrogen mass fraction :math:`X`, the rest counted as helium-4 (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 its electrons per atomic mass unit run from about 0.67 at the center to 0.86 near the surface, and :math:`r` from about 0.49 down to 0.15 -- 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. """ _sun_entry('osc_prob_4nu_sun_liv', L0, kwargs, 4) _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, dxiCP=dxiCP, 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)
@_angles.renamed_keyword('dxi13', 'dxiCP')
[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, dxiCP: 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.2.0 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. .. versionchanged:: 1.2.0 A negative L0 is refused, and so is electron_fraction, which has no effect (issue #160 §1). Parameters ---------- energy : float, list, or np.ndarray Neutrino energy/energies. L : float, list, or np.ndarray Baseline(s). L0 : int or float Initial position: the radius where the path starts, 0 or more [eV^-1]. A single number, not an array: for several production points, loop over them. 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. dxiCP : int or float, optional CP-violation phase of the 1-3 rotation of the LIV operator [radian], named as at three flavors. Its former name, ``dxi13``, is still accepted, with a :class:`FutureWarning`. 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` at every radius from its hydrogen mass fraction :math:`X`, the rest counted as helium-4 (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 its electrons per atomic mass unit run from about 0.67 at the center to 0.86 near the surface, and :math:`r` from about 0.49 down to 0.15 -- 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. """ _sun_entry('osc_prob_5nu_sun_liv', L0, kwargs, 5) _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, dxiCP=dxiCP, 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_pseudo_dirac_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', 'QUADRATURE_SEED_MIN_SLABS', 'BATCHED_GL_MAX_SLAB_NORM', 'BATCHED_PHASE_GROUPING', 'BATCH_WORKING_ENTRIES', 'CUMULATIVE_AUTO_MIN_POINTS', 'N_JOBS_MIN_PARALLEL_WORK_S', 'HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS', 'HYBRID_YIELDS_TO_CUMULATIVE_MIN_POINTS_TIGHT', 'CUMULATIVE_N_ACC_SAFETY', 'AUTO_LADDER_MAX_PHASE', 'AUTO_LADDER_MIN_TOLERANCE', 'AUTO_LADDER_TIGHT_MAX_PHASE', 'AUTO_LADDER_TOLERANCE_MARGIN', 'AUTO_LADDER_MAX_FLOOR_FRACTION', 'AUTO_PROBE_POINTS', 'AUTO_SHARP_FEATURE_SLABS_PER_PROBE', 'UNDECLARED_JUMP_STEP_TOLERANCE', 'OUTPUT_GUARD_MIN_BYTES', 'OUTPUT_GUARD_SAFETY', 'IP_EXP_LOOP_CAP', 'PARAMETER_SET_METADATA_KEYS', 'PASSTHROUGH_KWARGS_DOCUMENTED', 'PhaseAveragingWarning', 'IgnoredQuadratureSettingWarning', ]