Source code for qtest.viz

"""Visualisation helpers for quantum measurement distributions.

These are convenience plotting functions for use in notebooks, debugging
sessions, and test-failure triage. They require matplotlib, installed via the
optional ``viz`` extra::

    pip install 'qtest[viz]'

matplotlib is imported lazily, so importing :mod:`qtest.viz` (or :mod:`qtest`)
does not require it — only calling a plotting function does. Each function
draws onto a provided ``Axes`` (or a freshly created one) and returns that
``Axes`` so callers can further customise or save the figure.
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

_MATPLOTLIB_MISSING_MSG = (
    "qtest.viz requires matplotlib. Install it with:\n  pip install 'qtest[viz]'"
)


def _require_pyplot() -> Any:
    try:
        import matplotlib.pyplot as plt
    except ImportError as exc:  # pragma: no cover - exercised only without matplotlib
        raise ImportError(_MATPLOTLIB_MISSING_MSG) from exc
    return plt


def _validate_distribution(name: str, dist: Mapping[str, float]) -> None:
    if not isinstance(dist, Mapping) or not dist:
        raise ValueError(f"{name} must be a non-empty mapping of bitstring -> value")


[docs] def plot_distribution( distribution: Mapping[str, float], ax: Any | None = None, *, title: str | None = None, color: str | None = None, sort_keys: bool = True, ) -> Any: """Bar-plot a single measurement distribution. Parameters ---------- distribution Mapping ``{bitstring: probability}`` (or counts). ax Existing matplotlib ``Axes`` to draw on. A new one is created if ``None``. title Optional axes title. color Optional bar colour. sort_keys Sort bitstrings lexicographically (default) for a stable x-axis. Returns ------- matplotlib.axes.Axes """ _validate_distribution("distribution", distribution) plt = _require_pyplot() if ax is None: _, ax = plt.subplots() keys = sorted(distribution) if sort_keys else list(distribution) values = [distribution[k] for k in keys] ax.bar(keys, values, color=color) ax.set_xlabel("outcome") ax.set_ylabel("probability") if title: ax.set_title(title) ax.tick_params(axis="x", rotation=90 if keys and len(keys[0]) > 4 else 0) return ax
[docs] def plot_distribution_comparison( measured: Mapping[str, float], expected: Mapping[str, float], ax: Any | None = None, *, title: str | None = None, ) -> Any: """Grouped bar-plot of *measured* vs *expected* distributions. Outcomes present in either distribution are shown side by side, making it easy to eyeball where a circuit's sampled output drifts from the target — handy when triaging an ``assert_distribution_close`` failure. Returns ------- matplotlib.axes.Axes """ _validate_distribution("measured", measured) _validate_distribution("expected", expected) plt = _require_pyplot() if ax is None: _, ax = plt.subplots() keys = sorted(set(measured) | set(expected)) x = range(len(keys)) width = 0.4 ax.bar( [i - width / 2 for i in x], [expected.get(k, 0.0) for k in keys], width, label="expected" ) ax.bar( [i + width / 2 for i in x], [measured.get(k, 0.0) for k in keys], width, label="measured" ) ax.set_xticks(list(x)) ax.set_xticklabels(keys, rotation=90 if keys and len(keys[0]) > 4 else 0) ax.set_xlabel("outcome") ax.set_ylabel("probability") ax.legend() if title: ax.set_title(title) return ax
__all__ = ["plot_distribution", "plot_distribution_comparison"]