"""ASR diagnostic plots."""
from __future__ import annotations
import numpy as np
from .theme import (
COLORS,
get_series_color,
style_axes,
themed_figure,
)
try:
import matplotlib.pyplot as plt
except ImportError:
plt = None
__all__ = [
"plot_asr_repair_timeline",
"plot_asr_calibration_fraction",
"plot_asr_component_reconstruction",
"plot_guided_asr_weights",
]
# ---------------------------------------------------------------------------
# Shared output helper
# ---------------------------------------------------------------------------
def _finish(fig, ax, *, show: bool, fname: str | None):
"""Apply the save-and-show convention shared by every plot in this module."""
if fname is not None:
fig.savefig(fname, dpi=fig.dpi, bbox_inches="tight")
if show and plt is not None:
plt.show()
return fig, ax
# ---------------------------------------------------------------------------
# Repair timeline
# ---------------------------------------------------------------------------
[docs]
def plot_asr_repair_timeline(
estimator,
*,
title: str | None = None,
ax=None,
show: bool = True,
fname: str | None = None,
):
"""Plot the per-window count of reconstructed components over time.
Parameters
----------
estimator : ASR | AdaptiveASR | JugglerASR
A fitted estimator that has been used to ``transform`` data, so that
``diagnostics_`` is populated.
title : str | None, default=None
Custom title for the plot. If None, an auto-generated title showing
the percentage of modified windows is used.
ax : matplotlib.axes.Axes | None, default=None
Axes to plot into. If None, a new themed figure is created.
show : bool, default=True
Whether to call ``plt.show()`` after plotting.
fname : str | None, default=None
If provided, the figure is saved to this file path.
Returns
-------
fig : matplotlib.figure.Figure
The figure containing the plot.
ax : matplotlib.axes.Axes
The axes with the repair timeline.
Raises
------
ValueError
If the estimator has no transform diagnostics or if diagnostics
contain no processing windows.
"""
diag = getattr(estimator, "diagnostics_", None)
sfreq = float(getattr(estimator, "sfreq_", 0.0) or 0.0)
if not diag or sfreq <= 0:
raise ValueError("estimator has no transform diagnostics; run transform first.")
starts = np.asarray(diag.get("window_starts", []), dtype=float)
stops = np.asarray(diag.get("window_stops", []), dtype=float)
counts = np.asarray(diag.get("n_components_reconstructed", []), dtype=float)
if starts.size == 0:
raise ValueError("diagnostics contain no processing windows.")
centers = (starts + stops) / 2.0 / sfreq
if ax is None:
fig, ax = themed_figure(figsize=(11, 3.2))
else:
fig = ax.figure
ax.fill_between(centers, counts, step="mid", color=COLORS["primary"], alpha=0.5)
ax.plot(centers, counts, color=COLORS["primary"], lw=0.8, drawstyle="steps-mid")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Components reconstructed")
frac = float(np.mean(counts > 0)) * 100 if counts.size else 0.0
ax.set_title(title or f"ASR repair timeline ({frac:.0f}% of windows modified)")
style_axes(ax, grid=True)
fig.tight_layout()
return _finish(fig, ax, show=show, fname=fname)
# ---------------------------------------------------------------------------
# Calibration / reference fraction
# ---------------------------------------------------------------------------
[docs]
def plot_asr_calibration_fraction(
estimators,
*,
labels=None,
title: str | None = None,
ax=None,
show: bool = True,
fname: str | None = None,
):
"""Bar chart of the clean calibration fraction for one or more estimators.
Parameters
----------
estimators : fitted estimator | list of fitted estimators
One or more fitted ASR-family estimators. Each contributes one bar
extracted from its ``calibration_info_`` attribute.
labels : list of str | None, default=None
Bar labels. If None, defaults to the class name of each estimator.
title : str | None, default=None
Custom title for the plot. If None, uses a sensible default.
ax : matplotlib.axes.Axes | None, default=None
Axes to plot into. If None, a new themed figure is created.
show : bool, default=True
Whether to call ``plt.show()`` after plotting.
fname : str | None, default=None
If provided, the figure is saved to this file path.
Returns
-------
fig : matplotlib.figure.Figure
The figure containing the plot.
ax : matplotlib.axes.Axes
The axes with the bar chart.
"""
if not isinstance(estimators, list | tuple):
estimators = [estimators]
if labels is None:
labels = [type(e).__name__ for e in estimators]
fracs = []
for e in estimators:
info = getattr(e, "calibration_info_", {}) or {}
n_sel = info.get("reference_selected_samples")
n_cand = info.get("reference_candidate_samples")
if n_sel is not None and n_cand:
fracs.append(100.0 * n_sel / n_cand)
continue
n_clean = info.get("n_clean_windows")
n_tot = info.get("n_calibration_windows")
if n_clean is not None and n_tot:
fracs.append(100.0 * n_clean / n_tot)
else:
fracs.append(np.nan)
if ax is None:
fig, ax = themed_figure(figsize=(1.6 * len(labels) + 2, 4.2))
else:
fig = ax.figure
x = np.arange(len(labels))
ax.bar(x, fracs, color=[get_series_color(i) for i in range(len(labels))])
for xi, f in zip(x, fracs):
if np.isfinite(f):
ax.text(xi, f, f"{f:.0f}%", ha="center", va="bottom", fontsize=9)
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=15, ha="right")
ax.set_ylabel("Calibration fraction (%)")
ax.set_title(title or "ASR calibration / reference fraction")
style_axes(ax, grid=True)
fig.tight_layout()
return _finish(fig, ax, show=show, fname=fname)
# ---------------------------------------------------------------------------
# Component-reconstruction map
# ---------------------------------------------------------------------------
[docs]
def plot_asr_component_reconstruction(
estimator,
*,
title: str | None = None,
ax=None,
show: bool = True,
fname: str | None = None,
):
"""Heatmap of per-window component variance relative to rejection thresholds.
Parameters
----------
estimator : ASR | AdaptiveASR | JugglerASR
A fitted estimator that has been used to ``transform`` data, so that
``diagnostics_`` is populated.
title : str | None, default=None
Custom title for the plot. If None, uses a sensible default.
ax : matplotlib.axes.Axes | None, default=None
Axes to plot into. If None, a new themed figure is created.
show : bool, default=True
Whether to call ``plt.show()`` after plotting.
fname : str | None, default=None
If provided, the figure is saved to this file path.
Returns
-------
fig : matplotlib.figure.Figure
The figure containing the plot.
ax : matplotlib.axes.Axes
The axes with the heatmap.
Raises
------
ValueError
If the estimator has no transform diagnostics or if diagnostics
lack per-window ``component_variances``.
"""
diag = getattr(estimator, "diagnostics_", None)
sfreq = float(getattr(estimator, "sfreq_", 0.0) or 0.0)
if not diag:
raise ValueError("estimator has no transform diagnostics; run transform first.")
cv = np.asarray(diag.get("component_variances", []), dtype=float)
ct = np.asarray(diag.get("component_thresholds", []), dtype=float)
if cv.ndim != 2 or cv.size == 0:
raise ValueError("diagnostics lack per-window component_variances.")
# ratio > 1 → component exceeded threshold → reconstructed
ratio = cv / np.maximum(ct, np.finfo(float).eps)
starts = np.asarray(diag.get("window_starts", []), dtype=float)
extent = None
if starts.size == cv.shape[0] and sfreq > 0:
extent = [starts[0] / sfreq, starts[-1] / sfreq, 0, cv.shape[1]]
if ax is None:
fig, ax = themed_figure(figsize=(11, 3.6))
else:
fig = ax.figure
im = ax.imshow(
ratio.T,
aspect="auto",
origin="lower",
cmap="magma",
vmin=0.0,
vmax=2.0,
extent=extent,
)
fig.colorbar(im, ax=ax, label="variance / threshold")
ax.set_xlabel("Time (s)" if extent else "Window index")
ax.set_ylabel("Component")
ax.set_title(title or "ASR component reconstruction map")
fig.tight_layout()
return _finish(fig, ax, show=show, fname=fname)
[docs]
def plot_guided_asr_weights(
estimator,
*,
title: str | None = None,
ax=None,
show: bool = True,
fname: str | None = None,
):
"""Plot experimental GuidedASR soft weights by window and component.
Parameters
----------
estimator : GuidedASR
A fitted estimator that has run ``transform`` and whose
``diagnostics_`` contains ``soft_weights``.
title : str | None, default=None
Custom figure title.
ax : matplotlib.axes.Axes | None, default=None
Axes to plot into. If None, create a themed figure.
show : bool, default=True
Whether to call ``plt.show()``.
fname : str | None, default=None
Optional output path.
Returns
-------
fig : matplotlib.figure.Figure
Figure containing the heatmap.
ax : matplotlib.axes.Axes
Axes containing the heatmap.
"""
diag = getattr(estimator, "diagnostics_", None)
if not diag or "soft_weights" not in diag:
raise ValueError(
"estimator has no soft_weights; fit + transform a GuidedASR first."
)
weights = np.asarray(diag["soft_weights"], dtype=float)
if weights.ndim != 2 or weights.size == 0:
raise ValueError(
"soft_weights must be a non-empty (n_windows, n_components) array."
)
sfreq = float(getattr(estimator, "sfreq_", 0.0) or 0.0)
starts = np.asarray(diag.get("window_starts", []), dtype=float)
stops = np.asarray(diag.get("window_stops", []), dtype=float)
extent = None
if starts.size == weights.shape[0] and sfreq > 0:
stop = stops[-1] if stops.size == starts.size else starts[-1] + 1.0
extent = [starts[0] / sfreq, stop / sfreq, 0, weights.shape[1]]
if ax is None:
fig, ax = themed_figure(figsize=(11, 3.6))
else:
fig = ax.figure
image = ax.imshow(
weights.T,
aspect="auto",
origin="lower",
cmap="RdYlGn",
vmin=0.0,
vmax=1.0,
extent=extent,
)
fig.colorbar(image, ax=ax, label="soft weight (1 = keep, 0 = suppress)")
ax.set_xlabel("Time (s)" if extent else "Window index")
ax.set_ylabel("Component")
ax.set_title(title or f"GuidedASR soft weights (mean = {weights.mean():.3f})")
fig.tight_layout()
return _finish(fig, ax, show=show, fname=fname)