Source code for croak.plotting.uncertainty

"""Uncertainty figures: FWHM distributions, intensity bands, thickness systematic."""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from matplotlib.figure import Figure

from .figure import _prepare_fig

if TYPE_CHECKING:
    from ..uncertainty import UncertaintyResult


# ---------------------------------------------------------------------------
# Uncertainty
# ---------------------------------------------------------------------------
[docs] def plot_fwhm_distribution( ax, uresult: UncertaintyResult, *, truth: float | None = None ) -> None: """Histogram of bootstrap FWHM samples with the point estimate and intervals. Parameters ---------- ax : matplotlib.axes.Axes Axis to draw into. uresult : UncertaintyResult The bootstrap result (its ``samples`` are histogrammed). truth : float, optional A known true FWHM (s) to mark with a dashed line (synthetic tests). """ samples = np.asarray(uresult.samples, dtype=float) / 1e-15 nbins = int(np.clip(samples.size // 5, 12, 40)) ax.hist(samples, bins=nbins, color="C0", alpha=0.7, edgecolor="white", lw=0.4) lo95, hi95 = uresult.interval_95 lo68, hi68 = uresult.interval_68 ax.axvspan(lo95 / 1e-15, hi95 / 1e-15, color="C0", alpha=0.10, label="95 %") ax.axvspan(lo68 / 1e-15, hi68 / 1e-15, color="C0", alpha=0.20, label="68 %") ax.axvline(uresult.point_estimate / 1e-15, c="C3", lw=1.5, label="estimate") if truth is not None: ax.axvline(truth / 1e-15, c="0.35", ls="--", lw=1.2, label="truth") ax.set_xlabel("FWHM (fs)") ax.set_ylabel("Replicates") ax.set_title( f"{uresult.point_estimate / 1e-15:.2f} ± {uresult.plus_minus / 1e-15:.2f} fs" f" (B={uresult.n_converged})" ) ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small")
[docs] def plot_intensity_band(ax, uresult: UncertaintyResult) -> None: """Plot the median temporal intensity and pointwise 68 %/95 % resampling bands. Requires the bootstrap to have been run with ``collect_profiles=True``. The profiles are peak-aligned (translation gauge removed by ``shiftnorm``), so the band is the spread of the aligned intensity envelope across replicates. The bands are pointwise, not simultaneous bands for the whole curve. """ if uresult.profiles is None or uresult.t_profile is None: raise ValueError( "uresult has no profiles; run the bootstrap with collect_profiles=True" ) t = np.asarray(uresult.t_profile, dtype=float) / 1e-15 profiles = np.asarray(uresult.profiles, dtype=float) med = np.median(profiles, axis=0) lo68, hi68 = np.percentile(profiles, [16.0, 84.0], axis=0) lo95, hi95 = np.percentile(profiles, [2.5, 97.5], axis=0) ax.fill_between(t, lo95, hi95, color="C0", alpha=0.15, label="95 %") ax.fill_between(t, lo68, hi68, color="C0", alpha=0.30, label="68 %") ax.plot(t, med, c="C0", lw=1.5, label="median") ax.set_xlabel("Time (fs)") ax.set_ylabel("Intensity (a.u.)") ax.set_ylim(bottom=0.0) ax.set_title("Temporal intensity") # zoom to where the envelope carries signal support = t[med > 0.01 * med.max()] if support.size: pad = 0.2 * (support.max() - support.min() + 1.0) ax.set_xlim(support.min() - pad, support.max() + pad) ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small")
[docs] def plot_uncertainty( uresult: UncertaintyResult, *, truth: float | None = None, figsize=(11.0, 4.2), fig: Figure | None = None, ) -> Figure: """Composite uncertainty figure: FWHM histogram + a confidence band if present. Parameters ---------- uresult : UncertaintyResult The bootstrap result. truth : float, optional Known true FWHM (s) for the histogram (synthetic tests). figsize : tuple, optional Figure size. fig : matplotlib.figure.Figure, optional Existing figure to draw into (cleared first), e.g. a Qt canvas figure. Returns ------- matplotlib.figure.Figure """ # A thickness-systematic result carries per-draw thicknesses; show the dedicated # sensitivity figure so the GUI canvas path needs no special-casing. if getattr(uresult, "thicknesses", None) is not None: return plot_thickness_sensitivity(uresult, truth=truth, fig=fig) fig = _prepare_fig(fig, figsize) if uresult.profiles is not None and uresult.t_profile is not None: ax_h, ax_b = fig.subplots(1, 2) plot_fwhm_distribution(ax_h, uresult, truth=truth) plot_intensity_band(ax_b, uresult) else: ax_h = fig.subplots(1, 1) plot_fwhm_distribution(ax_h, uresult, truth=truth) fig.suptitle(uresult.summary(), fontsize="small") return fig
# --------------------------------------------------------------------------- # Substrate-thickness systematic # --------------------------------------------------------------------------- def _mark_thickness_prior(ax, uresult: UncertaintyResult) -> None: """Shade the measured thickness prior ``L₀ ± σ_L`` (µm) on a thickness axis.""" if uresult.thickness_central is None or uresult.thickness_sigma is None: return central_um = uresult.thickness_central / 1e-6 sigma_um = uresult.thickness_sigma / 1e-6 ax.axvspan( central_um - sigma_um, central_um + sigma_um, color="0.8", alpha=0.5, label="L₀ ± σ_L", ) ax.axvline(central_um, color="0.4", ls=":", lw=1.0) def plot_fwhm_vs_thickness( ax, uresult: UncertaintyResult, *, truth: float | None = None ) -> None: """Scatter the retrieved FWHM against the drawn substrate thickness. The horizontal bands give the point estimate and its 68 % interval; the vertical band is the measured thickness prior. The slope of the cloud is the sensitivity ``dFWHM/dL`` the analysis propagates; solver variation adds to the spread. Parameters ---------- ax : matplotlib.axes.Axes Axis to draw into. uresult : UncertaintyResult A ``method="thickness"`` result (its ``thicknesses`` are the x-values). truth : float, optional A known true FWHM (s) to mark with a dashed line (synthetic tests). """ if uresult.thicknesses is None: raise ValueError("uresult has no thicknesses; run method='thickness'") thick_um = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 fwhm_fs = np.asarray(uresult.samples, dtype=float) / 1e-15 _mark_thickness_prior(ax, uresult) ax.scatter(thick_um, fwhm_fs, s=14, color="C0", alpha=0.6, edgecolors="none") ax.axhline(uresult.point_estimate / 1e-15, color="C3", lw=1.5, label="estimate") lo68, hi68 = uresult.interval_68 ax.axhspan(lo68 / 1e-15, hi68 / 1e-15, color="C3", alpha=0.15, label="68 %") if truth is not None: ax.axhline(truth / 1e-15, color="0.35", ls="--", lw=1.2, label="truth") ax.set_xlabel("Substrate thickness (µm)") ax.set_ylabel("FWHM (fs)") ax.set_title("FWHM vs thickness") ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") def plot_error_vs_thickness(ax, uresult: UncertaintyResult) -> None: """Scatter the FROG error against the drawn thickness (a consistency check). A flat cloud means the data do not constrain the thickness (the full prior propagates); a clear minimum near ``L₀`` means the trace itself prefers a thickness under the chosen model. The plot does not reweight the prior. """ if uresult.thicknesses is None: raise ValueError("uresult has no thicknesses; run method='thickness'") thick_um = np.asarray(uresult.thicknesses, dtype=float) / 1e-6 errors = np.asarray(uresult.frog_errors, dtype=float) _mark_thickness_prior(ax, uresult) ax.scatter(thick_um, errors, s=14, color="C1", alpha=0.6, edgecolors="none") ax.set_xlabel("Substrate thickness (µm)") ax.set_ylabel("FROG error R") ax.set_title("Trace misfit vs thickness") ax.legend(loc="best", handlelength=1.0, frameon=False, fontsize="small") def plot_thickness_sensitivity( uresult: UncertaintyResult, *, truth: float | None = None, figsize=(13.0, 4.2), fig: Figure | None = None, ) -> Figure: """Composite figure for the substrate-thickness systematic. Panels: FWHM vs thickness, FROG error vs thickness, and — when profiles were collected — the temporal intensity confidence band. Parameters ---------- uresult : UncertaintyResult A ``method="thickness"`` result. truth : float, optional Known true FWHM (s) for the FWHM panel (synthetic tests). figsize : tuple, optional Figure size. fig : matplotlib.figure.Figure, optional Existing figure to draw into (cleared first), e.g. a Qt canvas figure. Returns ------- matplotlib.figure.Figure """ fig = _prepare_fig(fig, figsize) has_band = uresult.profiles is not None and uresult.t_profile is not None axes = fig.subplots(1, 3 if has_band else 2) plot_fwhm_vs_thickness(axes[0], uresult, truth=truth) plot_error_vs_thickness(axes[1], uresult) if has_band: plot_intensity_band(axes[2], uresult) fig.suptitle(uresult.summary(), fontsize="small") return fig