Pre-Packaged Plotting Tools
This page documents magnus.plotting, a small set of functions that
produce the figures the example notebooks use, so that a plot costs one call
rather than thirty lines of Matplotlib. See Tutorial notebooks for the
notebooks it was extracted from.
Why it exists
Every figure across the notebooks used to be built by hand. A typical
one ran to twenty-five or forty lines: a gridspec_kw dictionary, a
subplots_adjust call, the plotting itself, a nine-keyword legend
invocation, four MultipleLocator assignments, axis limits and scales, and
a savefig. That block was copied from figure to figure and varied
slightly each time, which is exactly the arrangement in which a figure
quietly stops matching its neighbors.
Cataloguing them first mattered more than writing the code, because most are
the same figure with different data: a set of curves against a swept
variable, optionally over a short relative-error subpanel. That shape is
magnus.plotting.plot_curves(), and the probability-against-baseline and
probability-against-energy functions are thin presets over it that fix labels,
scales and tick spacings.
Only five layouts beyond it are genuinely distinct: small multiples (where the comparison runs between panels, so every panel needs identical limits while the labels, title and legend must each appear exactly once), a density profile stacked over probability panels, an oscillating probability under its phase average, the bi-probability plane, and the oscillogram.
What it draws
Function |
Layout |
|---|---|
Curves against any swept variable, with an optional relative-error subpanel. Also serves the convergence and error studies. |
|
The same, preset to a logarithmic baseline axis and a unit ordinate. |
|
The same, against neutrino energy. |
|
Small multiples: one panel per case, stacked over a shared abscissa, with the axes forced to match so the panels can be read against each other. |
|
Matter-density panel above one or more probability panels sharing an abscissa. |
|
An oscillating probability with its phase average overlaid; see Phase-Averaged Probabilities. |
|
Neutrino against antineutrino appearance probability, as \(\delta_{\rm CP}\) runs over its range. |
|
Probability over zenith angle and energy, as a filled contour map. |
Installation
Nothing to do: Matplotlib is a dependency of Magνs, so pip install magnuspy
brings it and magnus.plotting is available straight away.
from magnus import plotting
A first figure
import matplotlib
matplotlib.use('Agg')
import numpy as np
import magnus.globaldefs as gd
import magnus.oscprob as oscprob
from magnus.plotting import plot_probability_vs_baseline
osc = gd.load_nufit_params('NuFIT 6.1')
energy = 1.0 * gd.UNIT_GEV
distances = np.logspace(1.0, 4.0, 300) # [km]
prob = np.asarray(oscprob.osc_prob_3nu_vacuum(
np.full(distances.size, energy), distances*gd.CONV_KM_TO_INV_EV,
**osc))[:, gd.NUMU, gd.NUMU]
fig, ax = plot_probability_vs_baseline(
distances,
[dict(y=prob, label='Magnus expansion', color='C1')],
nu_i=gd.NUMU, nu_f=gd.NUMU, num_flavors=3,
xlim=(distances[0], distances[-1]),
title=r'$3\nu$~vacuum, $E_\nu = 1$~GeV',
legend_title='Calculation method',
)
print(ax.get_ylabel())
Three-neutrino probability,~$P_{\nu_\mu \to \nu_\mu}$
Note that osc comes from magnus.globaldefs.load_nufit_params(),
which returns exactly the six mixing parameters. Splatting
gd.OSC_PARAMS_PREDEFINED[...] instead would also forward its name and
description strings, which the probability functions reject.
Adding the error subpanel
Passing residual adds the short lower panel the notebooks use to compare a
Magnus result against a closed-form one. The two panels then share their
abscissa limits and scale, and the upper panel’s tick labels are suppressed,
so they read as a single figure:
import magnus.oscprobstd as oscprobstd
from magnus.plotting import plot_curves
sth, Dm2 = osc['s12'], osc['D21']
energy = 10.0 * gd.UNIT_MEV
L = np.logspace(1.0, 5.0, 400)
L_nat = L * gd.CONV_KM_TO_INV_EV
# One call each. The point axis lands last in the closed form and first in
# osc_prob, which is the only difference between the two lines.
exact = np.asarray(
oscprobstd.osc_prob_2nu_vacuum_std(sth, Dm2, energy, L_nat))[0, 0]
approx = np.asarray(oscprob.osc_prob_2nu_vacuum(
np.full(L.size, energy), L_nat, sth, Dm2))[:, 0, 0]
fig, ax = plot_curves(
L,
[dict(y=approx, label='Magnus expansion', color='C1'),
dict(y=exact, label='Standard formula', color='k', ls='--')],
xlabel=r'Baseline, $L$ [km]',
ylabel=r'Two-neutrino probability,~$P_{\nu_e \to \nu_e}$',
xlim=(L[0], L[-1]), ylim=(0.0, 1.0), xscale='log',
ymajor=0.10, yminor=0.02,
residual=(approx - exact) / np.maximum(exact, 1.0e-300) / 1.0e-12,
residual_label=r'$\epsilon_{\rm rel}~[\times 10^{-12}]$',
legend_title='Calculation method',
)
print(f'panels: {len(ax)}')
panels: 2
API conventions
Named arguments, and no catch-all
Each function takes named arguments for the things every figure has –
data, labels, limits, scales, tick spacings, title, legend placement, output
path – and explicit pass-through dictionaries for the long tail of
Matplotlib settings: legend_kw, grid_kw, savefig_kw,
subplots_kw, and, per curve, any
Line2D keyword.
There is deliberately no bare **kwargs on any of them. A catch-all
signature accepts a misspelled keyword in silence, and this project has
already paid for that: oscprob’s keyword chain used to forward unknown
names down several layers before failing somewhere unrecognizable. Here
every keyword either appears in the signature, so a typo is a
TypeError at the call site, or lands in a dictionary destined for one
specific Matplotlib call, so a typo is an error from that call naming the
offending key. Nothing is swallowed:
try:
plot_curves(L, [dict(y=approx)], ylabl='typo')
except TypeError as error:
print(type(error).__name__, '->', error)
TypeError -> plot_curves() got an unexpected keyword argument 'ylabl'
Curves
curves is a sequence, one entry per line. An entry is either a bare
ordinate array or a dictionary carrying the ordinate under 'y' plus any
Line2D keyword. Entries without an explicit color take the 'C0',
'C1', … cycle in order, matching the notebooks; reference curves are
conventionally given color='k', ls='--'.
Returning (fig, ax)
Every function returns both, so that a pre-packaged figure is a starting
point rather than a dead end. ax is a single
Axes for the single-panel layouts and an array for
the multi-panel ones – with a residual subpanel, ax[0] is the main panel
and ax[1] the residual:
fig, ax = plot_curves(L, [dict(y=approx, label='m')], xscale='log')
ax.axvline(1.0e3, color='0.6', ls=':', lw=1)
ax.set_title('annotated after the fact', fontsize=20)
print(ax.get_title())
annotated after the fact
What is not set here
Global styling – fonts, tick sizes and directions, LaTeX rendering – lives
in notebooks/matplotlibrc and is picked up because the notebooks run from
that directory. This module sets only what the notebooks were overriding per
figure: figure size, the legend keyword block, gridspec ratios, tick
spacings. The house values are exposed as
HOUSE_FIGSIZE,
HOUSE_LEGEND_KW,
HOUSE_GRID_KW and
HOUSE_SAVEFIG_KW, so a caller can build on them
rather than restate them.
Labels
prob_label() builds the LaTeX for a probability from a
flavor pair. A helper of this name was defined separately in several
notebooks, each covering only the three active flavors; this one also covers
the sterile states, so the sterile-neutrino notebook can use it:
from magnus.plotting import prob_label
print(prob_label(gd.NUMU, gd.NUE))
print(prob_label(gd.NUMU, gd.NUE, nubar=True))
print(prob_label(gd.NUE, gd.NUS1))
$P_{\nu_\mu \to \nu_e}$
$P_{\bar{\nu}_\mu \to \bar{\nu}_e}$
$P_{\nu_e \to \nu_{s_1}}$
Saving
Pass savefig to write the figure; savefig_kw is merged over
HOUSE_SAVEFIG_KW, which is dpi=200. The
notebooks write PDFs into ../fig/, whose contents are ignored by git.