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

plot_curves()

Curves against any swept variable, with an optional relative-error subpanel. Also serves the convergence and error studies.

plot_probability_vs_baseline()

The same, preset to a logarithmic baseline axis and a unit ordinate.

plot_probability_vs_energy()

The same, against neutrino energy.

plot_curves_stacked()

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.

plot_probability_with_profile()

Matter-density panel above one or more probability panels sharing an abscissa.

plot_probability_with_average()

An oscillating probability with its phase average overlaid; see Phase-Averaged Probabilities.

plot_biprobability()

Neutrino against antineutrino appearance probability, as \(\delta_{\rm CP}\) runs over its range.

plot_oscillogram()

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.