Source code for qtest.assertions.robustness

"""``assert_robust_to_noise`` — noise-degradation regression assertion.

Where :func:`qtest.assert_distribution_close` checks a circuit at a single
(possibly noisy) operating point, :func:`assert_robust_to_noise` sweeps a
*ladder* of increasing noise strengths and asserts that the circuit's output
distribution never drifts further than ``max_distance`` from *expected*. This
is the natural shape of an error-mitigation / error-correction regression
test: "as noise rises to level X, my mitigated circuit must still track the
ideal result to within Y".
"""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from typing import Any, Callable

from qtest.assertions.distribution import (
    _has_measurements,
    _normalize_counts,
    _validate_bitstring_lengths,
    _validate_expected,
)
from qtest.backends import Backend
from qtest.backends.registry import get_backend
from qtest.config import _resolve_value, get_config
from qtest.metrics import hellinger_distance, total_variation_distance
from qtest.noise import NoiseModel, bit_flip, depolarizing, phase_flip, readout_error

# Single-parameter noise constructors selectable via ``noise_type``.
_NOISE_FACTORIES: dict[str, Callable[[float], NoiseModel]] = {
    "depolarizing": depolarizing,
    "bit_flip": bit_flip,
    "phase_flip": phase_flip,
    "readout": readout_error,
}

_DistanceFn = Callable[[Mapping[str, float], Mapping[str, float]], float]
_DISTANCE_METRICS: dict[str, _DistanceFn] = {
    "tv": total_variation_distance,
    "hellinger": hellinger_distance,
}


[docs] def assert_robust_to_noise( circuit: Any, expected: dict[str, float], noise_levels: Sequence[float] = (0.001, 0.01, 0.05), max_distance: float = 0.15, noise_type: str = "depolarizing", metric: str = "tv", shots: int | None = None, backend: Backend | None = None, seed: int | None = None, msg: str | None = None, ) -> None: """Assert *circuit*'s output stays within *max_distance* of *expected* under noise. For each value ``p`` in *noise_levels* a noise model of type *noise_type* and strength ``p`` is applied, the circuit is sampled, and the distance between the measured and *expected* distributions is computed. The assertion fails if **any** level exceeds *max_distance*. Parameters ---------- circuit A backend-native circuit with at least one measurement instruction. expected Reference distribution as ``{bitstring: probability}`` (summing to 1). noise_levels Increasing noise strengths to sweep. Each is passed as the single probability argument of the chosen *noise_type* constructor. max_distance Maximum permitted distance at every noise level, in ``[0, 1]``. noise_type One of ``"depolarizing"``, ``"bit_flip"``, ``"phase_flip"``, ``"readout"``. metric Distance metric: ``"tv"`` (default) or ``"hellinger"``. shots, backend, seed Per-call overrides for the corresponding global config defaults. msg Optional prefix prepended to the assertion failure message. Raises ------ ValueError For malformed input (bad noise_type, metric, empty noise_levels, out-of-range max_distance, circuit without measurements, ...). AssertionError When the measured distribution exceeds *max_distance* at any level. """ if noise_type not in _NOISE_FACTORIES: raise ValueError( f"Unknown noise_type {noise_type!r}. Must be one of {sorted(_NOISE_FACTORIES)}." ) if metric not in _DISTANCE_METRICS: raise ValueError(f"metric must be one of {sorted(_DISTANCE_METRICS)}, got {metric!r}.") if not noise_levels: raise ValueError("noise_levels must be a non-empty sequence of probabilities.") if not isinstance(max_distance, (int, float)) or not (0.0 <= float(max_distance) <= 1.0): raise ValueError(f"max_distance must be in [0, 1], got {max_distance!r}") shots = _resolve_value("default_shots", shots) seed = _resolve_value("default_seed", seed) expected_norm = _validate_expected(expected) if not _has_measurements(circuit): raise ValueError( "circuit has no measurement instructions; assert_robust_to_noise " "requires a circuit that produces classical bitstrings." ) if backend is None: backend = get_backend(get_config().default_backend) noise_factory = _NOISE_FACTORIES[noise_type] distance_fn = _DISTANCE_METRICS[metric] results: list[tuple[float, float, bool]] = [] for level in noise_levels: model = noise_factory(level) raw_counts = backend.run_circuit(circuit, shots=shots, seed=seed, noise_model=model) measured = _normalize_counts({k.replace(" ", ""): v for k, v in raw_counts.items()}) _validate_bitstring_lengths(expected_norm, measured) all_keys = set(expected_norm) | set(measured) p_padded = {k: expected_norm.get(k, 0.0) for k in all_keys} q_padded = {k: measured.get(k, 0.0) for k in all_keys} distance = distance_fn(p_padded, q_padded) results.append((float(level), distance, distance <= max_distance)) if all(passed for _, _, passed in results): return raise AssertionError( _format_failure_message( circuit=circuit, backend=backend, noise_type=noise_type, metric=metric, max_distance=max_distance, shots=shots, results=results, user_msg=msg, ) )
def _format_failure_message( *, circuit: Any, backend: Backend, noise_type: str, metric: str, max_distance: float, shots: int, results: list[tuple[float, float, bool]], user_msg: str | None, ) -> str: """Build the multi-line ``AssertionError`` payload for a robustness sweep.""" name = getattr(circuit, "name", None) or type(circuit).__name__ lines: list[str] = [] if user_msg: lines.extend([user_msg, ""]) lines.append("Circuit is not robust to noise") lines.append("") lines.append(f" Circuit: {name}") lines.append(f" Backend: {backend.name}") lines.append(f" Noise type: {noise_type}") lines.append(f" Metric: {metric} distance") lines.append(f" Shots: {shots}") lines.append(f" Max allowed distance: {max_distance}") lines.append("") lines.append(" Noise level distance status") for level, distance, passed in results: status = "ok" if passed else "FAIL" lines.append(f" {level:<12.5g} {distance:<12.6f} {status}") return "\n".join(lines)