# -*- coding: utf-8 -*-
r"""cli.py
Command-line calculator for Mag$\nu$s: computes a single neutrino
oscillation probability (or probability matrix) from the command line,
without writing any Python. Wraps the same ``osc_prob_{2,3,4,5}nu_*``
functions used by the Python API (see :py:mod:`magnus.oscprob`
and :doc:`/cli`), dispatching to the right one based on ``--flavors``,
``--environment``, and ``--scenario``.
Installed as the ``magnus`` console script (see ``[project.scripts]``
in pyproject.toml) and also runnable as ``python -m magnus``.
Routine listings
----------------
* main - Entry point: parses argv, dispatches, prints the result
* build_parser - Builds the argparse.ArgumentParser
* FLAVOR_NAME_TO_INDEX - Maps flavor names (e, mu, tau, s, s1, s2)
to their globaldefs index
* ENERGY_UNITS, LENGTH_UNITS - Unit-name to :math:`\text{eV}` / :math:`\text{eV}^{-1}`
conversion factors
"""
__author__ = "Mauricio Bustamante"
__email__ = "mbustamante@gmail.com"
import argparse
import inspect
import json
import sys
from typing import Optional
import numpy as np
import magnus.oscprob as oscprob
import magnus.globaldefs as gd
from magnus.version import __version__
[docs]
ENERGY_UNITS = {
'eV': 1.0, 'keV': gd.UNIT_KEV, 'MeV': gd.UNIT_MEV,
'GeV': gd.UNIT_GEV, 'TeV': gd.UNIT_TEV, 'PeV': gd.UNIT_PEV,
}
[docs]
LENGTH_UNITS = {
'eV-1': 1.0, 'km': gd.UNIT_KM, 'cm': gd.UNIT_CM,
}
[docs]
FLAVOR_NAME_TO_INDEX = {
'e': gd.NUE, 'mu': gd.NUMU, 'tau': gd.NUTAU,
's': gd.NUS, 's1': gd.NUS1, 's2': gd.NUS2,
}
[docs]
FLAVOR_LABELS = {
2: ['0', '1'],
3: ['nu_e', 'nu_mu', 'nu_tau'],
4: ['nu_e', 'nu_mu', 'nu_tau', 'nu_s'],
5: ['nu_e', 'nu_mu', 'nu_tau', 'nu_s1', 'nu_s2'],
}
# Refinement/logging/numerics kwargs that every osc_prob_* wrapper accepts via
# **kwargs even where they are not explicit named parameters (see the "layer
# contract" in docs/source/architecture.rst) -- always safe to forward.
[docs]
ALWAYS_FORWARD = {'magnus_exp_order', 'n_jobs', 'integration_method', 'rtol', 'atol',
'strategy'}
def _flavor_index(value: str) -> int:
r"""argparse type= callback: accepts an int string or a flavor name."""
try:
return int(value)
except ValueError:
pass
key = value.strip().lower()
if key not in FLAVOR_NAME_TO_INDEX:
raise argparse.ArgumentTypeError(
f"invalid flavor {value!r}; expected an integer index or one of "
f"{sorted(FLAVOR_NAME_TO_INDEX)}")
return FLAVOR_NAME_TO_INDEX[key]
[docs]
def build_parser() -> argparse.ArgumentParser:
r"""Builds the ``magnus`` command-line argument parser.
.. versionadded:: 1.0.0
Returns
-------
argparse.ArgumentParser
The top-level parser, with the ``prob`` subcommand attached.
"""
parser = argparse.ArgumentParser(
prog='magnus',
description="Magνs: neutrino oscillation probabilities via the Magnus expansion.")
parser.add_argument('-V', '--version', action='version', version=f'magnus {__version__}')
sub = parser.add_subparsers(dest='command', required=True)
p = sub.add_parser('prob', help='Compute a single oscillation probability (matrix or channel).')
g_env = p.add_argument_group('Environment')
g_env.add_argument('--flavors', type=int, choices=[2, 3, 4, 5], default=3,
help='Number of neutrino flavors (default: 3).')
g_env.add_argument('--environment', choices=['vacuum', 'matter', 'earth', 'sun'], default='vacuum',
help='Propagation environment (default: vacuum).')
g_env.add_argument('--scenario', choices=['std', 'nsi', 'liv'], default='std',
help="Physics scenario on top of the environment: 'std' (Standard Model), "
"'nsi' (non-standard interactions), or 'liv' (Lorentz-invariance violation). "
"'nsi' is not available with --environment vacuum. Default: std.")
g_env.add_argument('--density-profile', choices=['constant', 'exp'], default='constant',
help="Matter density profile, only used with --environment matter: 'constant' "
"(requires --rho) or 'exp' (requires --rho-central and --l-scale). Default: constant.")
g_env.add_argument('--nubar', action='store_true',
help='Compute the probability for antineutrinos instead of neutrinos.')
g_kin = p.add_argument_group('Energy and baseline')
g_kin.add_argument('--energy', type=float, required=True, help='Neutrino energy.')
g_kin.add_argument('--energy-unit', choices=list(ENERGY_UNITS), default='GeV',
help='Unit of --energy (default: GeV).')
g_kin.add_argument('--baseline', type=float, default=None,
help='Baseline / final position. Required for vacuum, matter, and sun, and for earth '
'when using --costhz. Only computed automatically for earth when both --loc-ini '
'and --loc-fin are given instead.')
g_kin.add_argument('--l0', type=float, default=0.0,
help='Initial position (used by --environment sun and --density-profile exp). '
'Default: 0.0.')
g_kin.add_argument('--baseline-unit', choices=list(LENGTH_UNITS), default='km',
help='Unit of --baseline, --l0, and --l-scale (default: km).')
g_mat = p.add_argument_group('Matter (--environment matter)')
g_mat.add_argument('--rho', type=float, default=None,
help='Matter density (constant profile).')
g_mat.add_argument('--rho-central', type=float, default=None,
help='Matter density at the center of the profile, l=0 (exponential profile).')
g_mat.add_argument('--l-scale', type=float, default=None,
help='Length scale of the exponential density decrease (exponential profile).')
g_mat.add_argument('--density-unit', choices=['g/cm3', 'natural'], default='g/cm3',
help='Unit of --rho/--rho-central: g/cm3 (converted internally) or natural units '
'(eV^4). Default: g/cm3.')
g_mat.add_argument('--ratio-n-to-p', type=float, default=1.0,
help='Ratio of the number of neutrons to protons in matter. Default: 1.0.')
g_mat.add_argument('--electron-fraction', type=float, default=0.5,
help='Electron fraction of matter. Default: 0.5.')
g_earth = p.add_argument_group('Earth (--environment earth)')
g_earth.add_argument('--costhz', type=float, default=None,
help='Cosine of the neutrino zenith angle.')
g_earth.add_argument('--loc-ini', default=None,
help='Initial location name (e.g. fermilab); see magnus.earth.loc_coords_dms. '
'Must be given together with --loc-fin, as an alternative to --costhz.')
g_earth.add_argument('--loc-fin', default=None,
help='Final location name; see --loc-ini.')
g_osc = p.add_argument_group('Standard oscillation parameters (2-flavor)')
g_osc.add_argument('--sth', type=float, default=None, help='Sin(theta) (required for --flavors 2).')
g_osc.add_argument('--dm2', type=float, default=None, dest='Dm2',
help='Mass-squared difference Delta m^2 (required for --flavors 2).')
g_osc3 = p.add_argument_group('Standard oscillation parameters (3+ flavors)')
g_osc3.add_argument('--s12', type=float, default=None, help='Sin(theta_12). Default: NuFit 6.0.')
g_osc3.add_argument('--s23', type=float, default=None, help='Sin(theta_23). Default: NuFit 6.0.')
g_osc3.add_argument('--s13', type=float, default=None, help='Sin(theta_13). Default: NuFit 6.0.')
g_osc3.add_argument('--dcp', type=float, default=None, dest='dCP',
help='delta_CP [radian]. Default: NuFit 6.0.')
g_osc3.add_argument('--dm21', type=float, default=None, dest='D21',
help='Mass-squared difference Delta m^2_21. Default: NuFit 6.0.')
g_osc3.add_argument('--dm31', type=float, default=None, dest='D31',
help='Mass-squared difference Delta m^2_31. Default: NuFit 6.0.')
g_osc3.add_argument('--osc-params-set', default='OSC_PARAMS_DEFAULT',
dest='default_osc_params_set_name',
choices=['OSC_PARAMS_DEFAULT', 'OSC_PARAMS_NU_FIT_6_0_SK_NO', 'OSC_PARAMS_NU_FIT_6_0_SK_IO'],
help='Predefined set used to fill in any of s12/s23/s13/dCP/D21/D31 left unspecified: '
'normal ordering (..._NO, the default) or inverted ordering (..._IO).')
g_osc4 = p.add_argument_group('Additional sterile mixing (4+ flavors)')
g_osc4.add_argument('--s14', type=float, default=0.0, help='Sin(theta_14). Default: 0.0.')
g_osc4.add_argument('--d14', type=float, default=0.0, help='delta_14 [radian]. Default: 0.0.')
g_osc4.add_argument('--s24', type=float, default=0.0, help='Sin(theta_24). Default: 0.0.')
g_osc4.add_argument('--d24', type=float, default=0.0, help='delta_24 [radian]. Default: 0.0.')
g_osc4.add_argument('--s34', type=float, default=0.0, help='Sin(theta_34). Default: 0.0.')
g_osc4.add_argument('--dm41', type=float, default=0.0, dest='D41',
help='Mass-squared difference Delta m^2_41. Default: 0.0.')
g_osc5 = p.add_argument_group('Additional sterile mixing (5 flavors)')
g_osc5.add_argument('--s15', type=float, default=0.0, help='Sin(theta_15). Default: 0.0.')
g_osc5.add_argument('--d15', type=float, default=0.0, help='delta_15 [radian]. Default: 0.0.')
g_osc5.add_argument('--s25', type=float, default=0.0, help='Sin(theta_25). Default: 0.0.')
g_osc5.add_argument('--s35', type=float, default=0.0, help='Sin(theta_35). Default: 0.0.')
g_osc5.add_argument('--d35', type=float, default=0.0, help='delta_35 [radian]. Default: 0.0.')
g_osc5.add_argument('--dm51', type=float, default=0.0, dest='D51',
help='Mass-squared difference Delta m^2_51. Default: 0.0.')
g_nsi = p.add_argument_group('NSI parameters (--scenario nsi)')
g_nsi.add_argument('--eps-aa', type=float, default=0.0, help='2-flavor diagonal NSI coupling.')
g_nsi.add_argument('--eps-ab', type=float, default=0.0, help='2-flavor off-diagonal NSI coupling.')
g_nsi.add_argument('--eps-ee', type=float, default=0.0, help='Diagonal NSI coupling of nu_e.')
g_nsi.add_argument('--eps-em', type=float, default=0.0, help='Off-diagonal (e-mu) NSI coupling.')
g_nsi.add_argument('--eps-et', type=float, default=0.0, help='Off-diagonal (e-tau) NSI coupling.')
g_nsi.add_argument('--eps-mm', type=float, default=0.0, help='Diagonal NSI coupling of nu_mu.')
g_nsi.add_argument('--eps-mt', type=float, default=0.0, help='Off-diagonal (mu-tau) NSI coupling.')
g_nsi.add_argument('--eps-tt', type=float, default=0.0, help='Diagonal NSI coupling of nu_tau.')
g_nsi.add_argument('--eps-es', type=float, default=0.0, help='(4nu) Off-diagonal (e-s) NSI coupling.')
g_nsi.add_argument('--eps-ms', type=float, default=0.0, help='(4nu) Off-diagonal (mu-s) NSI coupling.')
g_nsi.add_argument('--eps-ts', type=float, default=0.0, help='(4nu) Off-diagonal (tau-s) NSI coupling.')
g_nsi.add_argument('--eps-ss', type=float, default=0.0, help='(4nu) Diagonal NSI coupling of nu_s.')
g_nsi.add_argument('--eps-es1', type=float, default=0.0, help='(5nu) Off-diagonal (e-s1) NSI coupling.')
g_nsi.add_argument('--eps-es2', type=float, default=0.0, help='(5nu) Off-diagonal (e-s2) NSI coupling.')
g_nsi.add_argument('--eps-ms1', type=float, default=0.0, help='(5nu) Off-diagonal (mu-s1) NSI coupling.')
g_nsi.add_argument('--eps-ms2', type=float, default=0.0, help='(5nu) Off-diagonal (mu-s2) NSI coupling.')
g_nsi.add_argument('--eps-ts1', type=float, default=0.0, help='(5nu) Off-diagonal (tau-s1) NSI coupling.')
g_nsi.add_argument('--eps-ts2', type=float, default=0.0, help='(5nu) Off-diagonal (tau-s2) NSI coupling.')
g_nsi.add_argument('--eps-s1s1', type=float, default=0.0, help='(5nu) Diagonal NSI coupling of nu_s1.')
g_nsi.add_argument('--eps-s1s2', type=float, default=0.0, help='(5nu) Off-diagonal (s1-s2) NSI coupling.')
g_nsi.add_argument('--eps-s2s2', type=float, default=0.0, help='(5nu) Diagonal NSI coupling of nu_s2.')
g_liv = p.add_argument_group('LIV parameters (--scenario liv)')
g_liv.add_argument('--sxi', type=float, default=0.0, help='2-flavor LIV mixing angle sine.')
g_liv.add_argument('--sxi12', type=float, default=0.0, help='LIV mixing angle sine xi_12.')
g_liv.add_argument('--sxi23', type=float, default=0.0, help='LIV mixing angle sine xi_23.')
g_liv.add_argument('--sxi13', type=float, default=0.0, help='LIV mixing angle sine xi_13.')
g_liv.add_argument('--dxicp', type=float, default=0.0, dest='dxiCP',
help='(3nu) LIV CP-violation phase [radian].')
g_liv.add_argument('--dxi13', type=float, default=0.0,
help='(4/5nu) LIV CP-violation phase [radian] (replaces --dxicp).')
g_liv.add_argument('--sxi14', type=float, default=0.0, help='(4/5nu) LIV mixing angle sine xi_14.')
g_liv.add_argument('--dxi14', type=float, default=0.0, help='(4/5nu) LIV CP-violation phase [radian].')
g_liv.add_argument('--sxi24', type=float, default=0.0, help='(4/5nu) LIV mixing angle sine xi_24.')
g_liv.add_argument('--dxi24', type=float, default=0.0, help='(4/5nu) LIV CP-violation phase [radian].')
g_liv.add_argument('--sxi34', type=float, default=0.0, help='(4/5nu) LIV mixing angle sine xi_34.')
g_liv.add_argument('--sxi15', type=float, default=0.0, help='(5nu) LIV mixing angle sine xi_15.')
g_liv.add_argument('--dxi15', type=float, default=0.0, help='(5nu) LIV CP-violation phase [radian].')
g_liv.add_argument('--sxi25', type=float, default=0.0, help='(5nu) LIV mixing angle sine xi_25.')
g_liv.add_argument('--sxi35', type=float, default=0.0, help='(5nu) LIV mixing angle sine xi_35.')
g_liv.add_argument('--dxi35', type=float, default=0.0, help='(5nu) LIV CP-violation phase [radian].')
g_liv.add_argument('--b1', type=float, default=0.0, help='LIV eigenvalue b1.')
g_liv.add_argument('--b2', type=float, default=0.0, help='LIV eigenvalue b2.')
g_liv.add_argument('--b3', type=float, default=0.0, help='LIV eigenvalue b3.')
g_liv.add_argument('--b4', type=float, default=0.0, help='LIV eigenvalue b4.')
g_liv.add_argument('--b5', type=float, default=0.0, help='LIV eigenvalue b5.')
g_liv.add_argument('--liv-lambda', type=float, default=1.0, dest='Lambda',
help='LIV energy scale Lambda. Default: 1.0.')
g_liv.add_argument('--n-liv', type=int, default=0,
help='Power of the energy dependence of the LIV operator. Default: 0.')
g_chan = p.add_argument_group('Channel selection')
g_chan.add_argument('--nu-i', type=_flavor_index, default=None,
help='Initial flavor (index or name: e, mu, tau, s, s1, s2). If given with --nu-f, '
'prints a single probability instead of the full matrix.')
g_chan.add_argument('--nu-f', type=_flavor_index, default=None,
help='Final flavor; see --nu-i.')
g_num = p.add_argument_group('Advanced numerics')
g_num.add_argument('--magnus-exp-order', type=int, default=4, dest='magnus_exp_order',
help='Highest order of the Magnus expansion (1-6). Default: 4.')
g_num.add_argument('--integration-method', choices=['gl', 'trapezoid', 'simpson'], default='gl',
help="Quadrature method. 'gl' (Gauss-Legendre collocation) needs only 1-3 Hamiltonian "
"evaluations per slab and matches its quadrature order to the expansion order, so "
"it is both the fastest and the most accurate for a smooth Hamiltonian. "
"'trapezoid'/'simpson' sample a uniform grid of --n-tpts-per-slab points instead, "
"and are the safer choice if the Hamiltonian is not smooth within a slab. "
"Default: gl.")
g_num.add_argument('--rtol', type=float, default=1.e-3,
help='Relative tolerance on the agreement between successive refinement levels -- a stopping rule, not a guaranteed accuracy. Default: 1e-3.')
g_num.add_argument('--atol', type=float, default=1.e-3,
help='Absolute tolerance on the same agreement; see --rtol. Default: 1e-3.')
g_num.add_argument('--n-jobs', type=int, default=1, dest='n_jobs',
help='Number of parallel joblib workers. Default: 1.')
g_num.add_argument('--strategy', choices=['auto', 'hybrid', 'magnus'], default='auto',
help="How to propagate a position-dependent Hamiltonian: 'magnus' uses only the "
"Magnus-expansion machinery; 'hybrid' also tries adiabatic transport with a "
"Magnus patch at each non-adiabatic window, warning if it cannot certify the "
"result; 'auto' tries hybrid and falls back to magnus silently. Ignored for "
"vacuum and constant-density environments. Default: auto.")
g_num.add_argument('--verbose', type=int, default=0, choices=[0, 1, 2],
help='Verbosity level. Default: 0.')
g_out = p.add_argument_group('Output')
g_out.add_argument('--json', action='store_true', help='Print the result as JSON instead of a table.')
g_out.add_argument('--precision', type=int, default=4, help='Decimal digits shown in table output. Default: 4.')
return parser
def _std_osc_kwargs(flavors: int, args: argparse.Namespace) -> dict:
if flavors == 2:
return {'sth': args.sth, 'Dm2': args.Dm2}
kw = {'s12': args.s12, 's23': args.s23, 's13': args.s13, 'dCP': args.dCP,
'D21': args.D21, 'D31': args.D31, 'default_osc_params_set_name': args.default_osc_params_set_name}
if flavors >= 4:
kw.update({'s14': args.s14, 'd14': args.d14, 's24': args.s24, 'd24': args.d24,
's34': args.s34, 'D41': args.D41})
if flavors == 5:
kw.update({'s15': args.s15, 'd15': args.d15, 's25': args.s25, 's35': args.s35,
'd35': args.d35, 'D51': args.D51})
return kw
def _nsi_kwargs(flavors: int, args: argparse.Namespace) -> dict:
if flavors == 2:
return {'eps_aa': args.eps_aa, 'eps_ab': args.eps_ab}
kw = {'eps_ee': args.eps_ee, 'eps_em': args.eps_em, 'eps_et': args.eps_et,
'eps_mm': args.eps_mm, 'eps_mt': args.eps_mt, 'eps_tt': args.eps_tt}
if flavors == 4:
kw.update({'eps_es': args.eps_es, 'eps_ms': args.eps_ms, 'eps_ts': args.eps_ts,
'eps_ss': args.eps_ss})
if flavors == 5:
kw.update({'eps_es1': args.eps_es1, 'eps_es2': args.eps_es2, 'eps_ms1': args.eps_ms1,
'eps_ms2': args.eps_ms2, 'eps_ts1': args.eps_ts1, 'eps_ts2': args.eps_ts2,
'eps_s1s1': args.eps_s1s1, 'eps_s1s2': args.eps_s1s2, 'eps_s2s2': args.eps_s2s2})
return kw
def _liv_kwargs(flavors: int, args: argparse.Namespace) -> dict:
if flavors == 2:
return {'sxi': args.sxi, 'b1': args.b1, 'b2': args.b2, 'Lambda': args.Lambda,
'n_liv': args.n_liv}
kw = {'sxi12': args.sxi12, 'sxi23': args.sxi23, 'sxi13': args.sxi13,
'b1': args.b1, 'b2': args.b2, 'b3': args.b3, 'Lambda': args.Lambda, 'n_liv': args.n_liv}
if flavors == 3:
kw['dxiCP'] = args.dxiCP
if flavors >= 4:
kw.update({'dxi13': args.dxi13, 'sxi14': args.sxi14, 'dxi14': args.dxi14,
'sxi24': args.sxi24, 'dxi24': args.dxi24, 'sxi34': args.sxi34, 'b4': args.b4})
if flavors == 5:
kw.update({'sxi15': args.sxi15, 'dxi15': args.dxi15, 'sxi25': args.sxi25,
'sxi35': args.sxi35, 'dxi35': args.dxi35, 'b5': args.b5})
return kw
def _env_kwargs(environment: str, density_profile: str, args: argparse.Namespace,
baseline_ev: Optional[float], l0_ev: float) -> dict:
if environment == 'vacuum':
return {'L': baseline_ev}
if environment == 'matter':
kw = {
'L': baseline_ev,
'ratio_number_neutrons_to_protons': args.ratio_n_to_p,
'electron_fraction': args.electron_fraction,
'density_matter_is_in_g_per_cm3': (args.density_unit == 'g/cm3'),
}
if density_profile == 'constant':
if args.rho is None:
raise SystemExit("magnus prob: --rho is required for --environment matter "
"--density-profile constant.")
kw['rho'] = args.rho
else:
if args.rho_central is None or args.l_scale is None:
raise SystemExit("magnus prob: --rho-central and --l-scale are required for "
"--environment matter --density-profile exp.")
kw['L0'] = l0_ev
kw['rho_central'] = args.rho_central
kw['l_scale'] = args.l_scale * LENGTH_UNITS[args.baseline_unit]
return kw
if environment == 'earth':
using_locations = bool(args.loc_ini and args.loc_fin)
if not using_locations:
if args.costhz is None:
raise SystemExit("magnus prob: --environment earth requires either --costhz "
"(together with --baseline) or both --loc-ini and --loc-fin.")
if baseline_ev is None:
raise SystemExit("magnus prob: --baseline is required together with --costhz "
"(only --loc-ini/--loc-fin compute the baseline automatically).")
return {'costhz': args.costhz, 'loc_ini': args.loc_ini, 'loc_fin': args.loc_fin,
'L': baseline_ev}
if environment == 'sun':
if baseline_ev is None: # pragma: no cover - pre-empted, see below
# Unreachable from the command line: main() rejects a missing --baseline for
# every environment except earth before it calls this function, so a solar run
# without one has already exited. Kept because this function is the one that
# knows what the sun branch needs, and a future caller reaching it by another
# route should still get a clear message rather than a KeyError downstream.
raise SystemExit("magnus prob: --baseline is required for --environment sun.")
return {'L': baseline_ev, 'L0': l0_ev}
raise AssertionError(environment) # pragma: no cover
def _wrapper_name(flavors: int, environment: str, scenario: str, density_profile: str) -> str:
if environment == 'vacuum':
if scenario == 'nsi':
raise SystemExit("magnus prob: --scenario nsi is not available with --environment "
"vacuum (NSI couplings scale the matter potential, which vacuum "
"has none of); use --environment matter/earth/sun instead.")
suffix = '_liv' if scenario == 'liv' else ''
return f'osc_prob_{flavors}nu_vacuum{suffix}'
if environment == 'matter':
density_suffix = 'constant_density' if density_profile == 'constant' else 'exp_density'
scenario_infix = {'std': '', 'nsi': '_nsi', 'liv': '_liv'}[scenario]
return f'osc_prob_{flavors}nu_matter{scenario_infix}_{density_suffix}'
if environment in ('earth', 'sun'):
scenario_suffix = {'std': '', 'nsi': '_nsi', 'liv': '_liv'}[scenario]
return f'osc_prob_{flavors}nu_{environment}{scenario_suffix}'
raise AssertionError(environment) # pragma: no cover
def _call(fn, candidate_kwargs: dict):
r"""Calls fn with only the keys it actually accepts explicitly (plus the
universally-forwarded refinement/logging/numerics kwargs)."""
sig = inspect.signature(fn)
explicit_names = {n for n, par in sig.parameters.items()
if par.kind != inspect.Parameter.VAR_KEYWORD}
kwargs = {k: v for k, v in candidate_kwargs.items()
if (k in ALWAYS_FORWARD) or (k in explicit_names)}
return fn(**kwargs)
def _format_table(P: np.ndarray, flavors: int, precision: int) -> str:
labels = FLAVOR_LABELS[flavors]
width = max(len(l) for l in labels) + 2
width = max(width, precision + 4)
header = ' ' * (max(len(l) for l in labels) + 2) + ''.join(
f'{l:>{width}}' for l in labels)
lines = [header]
for i, row_label in enumerate(labels):
row = ''.join(f'{P[i, j]:>{width}.{precision}f}' for j in range(len(labels)))
lines.append(f'{row_label:<{max(len(l) for l in labels) + 2}}{row}')
return '\n'.join(lines)
[docs]
def main(argv=None) -> int:
r"""Entry point for the ``magnus`` console script / ``python -m magnus``.
.. versionadded:: 1.0.0
Parameters
----------
argv : list of str, optional
Arguments to parse instead of ``sys.argv[1:]`` (mainly for testing).
Returns
-------
int
Process exit code (0 on success).
"""
parser = build_parser()
args = parser.parse_args(argv)
flavors = args.flavors
environment = args.environment
scenario = args.scenario
if flavors == 2 and (args.sth is None or args.Dm2 is None):
parser.error("--sth and --dm2 are both required for --flavors 2.")
energy_ev = args.energy * ENERGY_UNITS[args.energy_unit]
baseline_ev = None if args.baseline is None else args.baseline * LENGTH_UNITS[args.baseline_unit]
l0_ev = args.l0 * LENGTH_UNITS[args.baseline_unit]
if environment != 'earth' and baseline_ev is None:
parser.error(f"--baseline is required for --environment {environment}.")
fn_name = _wrapper_name(flavors, environment, scenario, args.density_profile)
fn = getattr(oscprob, fn_name)
candidate = {'energy': energy_ev}
candidate.update(_env_kwargs(environment, args.density_profile, args, baseline_ev, l0_ev))
candidate.update(_std_osc_kwargs(flavors, args))
if scenario == 'nsi':
candidate.update(_nsi_kwargs(flavors, args))
elif scenario == 'liv':
candidate.update(_liv_kwargs(flavors, args))
candidate.update({
'nubar': args.nubar, 'nu_i': args.nu_i, 'nu_f': args.nu_f,
'validate_input': True, 'verbose': args.verbose,
'magnus_exp_order': args.magnus_exp_order, 'n_jobs': args.n_jobs,
'integration_method': args.integration_method, 'rtol': args.rtol, 'atol': args.atol,
})
# `strategy` selects how a *position-dependent* Hamiltonian is propagated, so it is only
# forwarded where the Hamiltonian actually depends on position. Vacuum and constant-density
# environments have no such dependence and their wrappers forward unknown keywords all the
# way down to the Magnus core, which would reject it.
if environment in ('earth', 'sun') or (environment == 'matter'
and args.density_profile == 'exp'):
candidate['strategy'] = args.strategy
try:
P = _call(fn, candidate)
except ValueError as error:
# The library validates its own inputs and raises; surface that as a clean CLI error
# (exit code 2, like any other argument problem) rather than a raw traceback.
parser.error(str(error))
if args.json:
payload = {
'function': fn_name, 'flavors': flavors, 'environment': environment,
'scenario': scenario, 'nubar': args.nubar,
'energy_eV': energy_ev, 'baseline_eV-1': baseline_ev,
'probability': np.asarray(P).tolist(),
}
print(json.dumps(payload, indent=2))
return 0
print(f"Magνs {__version__} -- {fn_name}")
label = f"E = {args.energy:g} {args.energy_unit}"
if baseline_ev is not None:
label += f", L = {args.baseline:g} {args.baseline_unit}"
if args.nubar:
label += ", antineutrinos"
print(label)
print()
if args.nu_i is not None and args.nu_f is not None:
print(f"P = {float(P):.{args.precision}f}")
else:
print(_format_table(np.asarray(P), flavors, args.precision))
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())