Source code for specmod.plotting.pair

"""One signal-and-noise pair, and the band it selected."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import matplotlib.pyplot as plt

from .style import tidy_frequency_axis

if TYPE_CHECKING:  # pragma: no cover
    from matplotlib.axes import Axes

    from ..core import SpectrumPair

__all__ = ["plot_pair"]


def _fits(fit: Any) -> list[tuple[str, Any]]:
    """Normalise ``fit`` to ``[(label, fit), ...]``, dropping anything unfitted.

    Accepts one fit or a mapping of label to fit, so a caller comparing two
    minimisers does not have to reach into the axes afterwards.
    """
    if fit is None:
        return []
    if hasattr(fit, "items"):
        return [
            (str(k), v)
            for k, v in fit.items()
            if getattr(v, "result", None) is not None
        ]
    return [("best fit", fit)] if getattr(fit, "result", None) is not None else []


[docs] def plot_pair( pair: SpectrumPair, ax: Axes | None = None, *, id: str = "", fit: Any = None, show_binned: bool = False, ) -> Axes: """Draw one signal-and-noise pair, with the band it selected. Parameters ---------- pair What to draw. ax Draw here; a new figure is made if omitted. id Station label. A frozen pair does not carry one, so the caller that knows the key supplies it — ``pair.signal.meta["id"]`` is used when it is there and this is not. fit A :class:`~specmod.fitting.FitSpectrum` whose model to overlay, if it has been fitted — or a **mapping of label to fit**, to draw several at once. Passed in rather than read off the spectrum: the containers are frozen precisely so a result cannot write itself back into its own input. Several matters more than it sounds. Fitting a source model is not a unique inversion, and two minimisers can reach the same goodness of fit at corner frequencies differing by tens of percent — which is a factor of several in stress drop, since it scales as ``fc**3``. Drawing them together is how that stops being invisible. show_binned Also draw the log-binned spectra the signal-to-noise ratio is actually computed on. Off by default because it doubles the lines, on when the question is why a band came out where it did. """ if ax is None: _, ax = plt.subplots(1, 1) label = id or pair.signal.meta.get("id", "signal") ax.loglog(pair.noise.freq, pair.noise.amp, "b--", lw=1, label="noise") ax.loglog(pair.signal.freq, pair.signal.amp, "k", lw=1, label=label) if show_binned: ax.loglog( pair.binned_signal.freq, pair.binned_signal.amp, "o-", color="0.4", ms=3, lw=1, label="binned signal", ) ax.loglog( pair.binned_noise.freq, pair.binned_noise.amp, "s--", color="steelblue", ms=3, lw=1, label="binned noise", ) # Not `label` — that name holds the station id the title is built from. for name, one in _fits(fit): ax.loglog(one.mod_freq, 10**one.result.best_fit, lw=2, label=name) if pair.band is None: ax.set_title(f"{label} — no usable band") else: low = min(float(pair.noise.amp.min()), float(pair.signal.amp.min())) high = max(float(pair.noise.amp.max()), float(pair.signal.amp.max())) for edge in pair.band: ax.vlines(edge, low, high, color="r", linestyles="dashed") ax.set_title(f"{label} — {pair.band[0]:.2f} to {pair.band[1]:.2f} Hz") # The floor is drawn because a band clamped to it looks arbitrary # otherwise: it is the lowest frequency the *shorter* of the two windows # can resolve, and it is why the band often does not open where the signal # first rises above the noise. if pair.resolution_floor > 0: ax.axvline( pair.resolution_floor, color="0.7", lw=1, zorder=0, label="resolution floor", ) tidy_frequency_axis(ax) ax.legend(fontsize="small") ax.set_xlabel("frequency [Hz]") ax.set_ylabel(f"amplitude [{pair.signal.unit}]") return ax