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 twelve 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 neighbours.
Cataloguing the figures first turned out to matter more than writing the code. Most of them are the same figure with different data. Probability against baseline, probability against energy, probability against a sterile mixing angle, and the convergence studies of the matrix-exponential notebook all share one shape:
a set of curves against a swept variable, optionally over a short relative-error subpanel.
magnus.plotting.plot_curves() is that shape.
plot_probability_vs_baseline() and
plot_probability_vs_energy() are thin presets over it
that fix labels, scales and tick spacings.
One further shape earns its own function:
plot_curves_stacked(), the small-multiples form –
the same plot repeated once per configuration down a shared abscissa, as the
sterile-neutrino notebook does with one panel per detector. What makes it
worth packaging is that the reader’s comparison runs between panels, so
every panel must carry identical limits, scales and tick spacings, while the
parts that must not repeat – the abscissa labels, the title, the legend, the
ordinate label – belong to exactly one panel each. Hand-built, that is four
formatting loops and a frameless full-figure subplot added purely to hang a
shared label on; it is also where a stack quietly stops being readable when
one panel drifts.
Only three layouts beyond these are genuinely distinct: the density profile stacked over one or more probability panels, the bi-probability plane, and the oscillogram.
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-averaged limit overlaid; see Phase-Averaged (Decohered) 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.array([
oscprob.osc_prob_3nu_vacuum(energy, L * gd.CONV_KM_TO_INV_EV,
**osc)[gd.NUMU][gd.NUMU]
for L in distances])
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 = gd.S12_NO_BF_NUFIT_6_0, gd.D21_NO_BF_NUFIT_6_0
energy = 10.0 * gd.UNIT_MEV
L = np.logspace(1.0, 5.0, 400)
exact = np.array([
oscprobstd.osc_prob_2nu_vacuum_std(sth, Dm2, energy,
l * gd.CONV_KM_TO_INV_EV)[0][0]
for l in L])
approx = np.array([
oscprob.osc_prob_2nu_vacuum(energy, l * gd.CONV_KM_TO_INV_EV,
sth, Dm2)[0][0]
for l in L])
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 unrecognisable. 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 colour 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
flavour pair. A helper of this name was defined separately in several
notebooks, each covering only the three active flavours; 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.