Source code for qarp.devices._noise_model

"""User-facing :class:`NoiseModel` wrapper around the C++ ``qx.NoiseModel``.

A thin Python class exposing ergonomic builders (depolarizing, pauli,
thermal_relaxation, ...) that delegates storage + sampling to the unified
C++ ``qx.NoiseModel`` consumed by ``QarpSimulator``.

Custom Python ``Command -> Channel`` closures are deliberately not
supported — Python callables in a per-shot-per-gate hot loop would erase
the unification's performance benefit.  Power users wanting parametric
channels should add them in C++ alongside the built-in channels.
"""

from typing import Iterable, List, Optional, Union

import qarpx as qx

GateSetLike = Union[str, "qx.GateSet", Iterable["qx.GateType"]]


def _resolve_gates(gate_set: Optional[GateSetLike], default_string: str) -> List["qx.GateType"]:
    """Normalise the various accepted forms into a flat list of GateType."""
    if gate_set is None:
        gate_set = default_string

    if isinstance(gate_set, str):
        s = gate_set.lower()
        if s in ("1q", "single", "single_qubit"):
            return _gateset_to_list(qx.full_gateset_1q())
        if s in ("2q", "two", "two_qubit"):
            return _gateset_to_list(qx.full_gateset_2q())
        if s in ("all", "1q+2q", "1q_2q"):
            return _gateset_to_list(qx.full_gateset_1q_2q())
        raise ValueError(f"NoiseModel: unknown gate_set shorthand '{gate_set}'")

    if isinstance(gate_set, qx.GateSet):
        return _gateset_to_list(gate_set)

    return [_coerce_gate(g) for g in gate_set]


def _coerce_gate(g):
    if isinstance(g, qx.GateType):
        return g
    if isinstance(g, str):
        try:
            return getattr(qx.GateType, g)
        except AttributeError as exc:
            raise ValueError(f"NoiseModel: unknown GateType '{g}'") from exc
    raise TypeError(f"NoiseModel: cannot coerce {g!r} to GateType")


# qx.GateSet exposes `contains(g)` but not an iterable view; brute-force by
# scanning the enum.  Cheap (~40 entries).
# A GateSet is a rebase target, so it admits the non-physical markers
# (Measure/Barrier/GPhase) that rebasing must not drop; they can carry no
# channel, so drop them here instead of failing the factories' arity check.
def _gateset_to_list(gs: "qx.GateSet") -> List["qx.GateType"]:
    out: List["qx.GateType"] = []
    for name in dir(qx.GateType):
        if name.startswith("_") or name == "name":
            continue
        try:
            g = getattr(qx.GateType, name)
        except AttributeError:
            continue
        if isinstance(g, qx.GateType) and gs.contains(g) and qx.gate_is_physical(g):
            out.append(g)
    return out


[docs] class NoiseModel: """Per-gate noise model attached to a :class:`qarp.devices.Device`. Ergonomic builders (:meth:`depolarizing`, :meth:`pauli`, :meth:`bit_flip`, :meth:`amplitude_damping`) cover the common cases. Compose with the ``+`` operator (right operand wins on collision). The underlying C++ object is reachable via :attr:`inner` for callers that need to pass it straight into ``qx.QarpSimulator``. Args: _inner: existing ``qx.NoiseModel`` to wrap (internal use). End users construct via the named class methods. """ def __init__(self, _inner: Optional["qx.NoiseModel"] = None): self._inner: "qx.NoiseModel" = _inner if _inner is not None else qx.NoiseModel() # ── Construction ────────────────────────────────────────────────────────
[docs] @classmethod def depolarizing(cls, p: float, gate_set: Optional[GateSetLike] = "2q") -> "NoiseModel": """Depolarizing channel of strength ``p``. 1q gates receive ``PauliChannel(p/3, p/3, p/3)``; 2q gates receive the uniform 2q depolarizing (each of the 15 non-identity Paulis with weight ``p/15``). """ gates = _resolve_gates(gate_set, "2q") return cls(qx.make_depolarizing(p, gates))
[docs] @classmethod def pauli( cls, p_x: float = 0.0, p_y: float = 0.0, p_z: float = 0.0, gate_set: Optional[GateSetLike] = "1q", ) -> "NoiseModel": """1q Pauli channel with explicit X/Y/Z weights.""" gates = _resolve_gates(gate_set, "1q") return cls(qx.make_pauli(p_x, p_y, p_z, gates))
[docs] @classmethod def bit_flip(cls, p: float, gate_set: Optional[GateSetLike] = "1q") -> "NoiseModel": """``PauliChannel(p, 0, 0)`` — flip the qubit with probability p.""" gates = _resolve_gates(gate_set, "1q") return cls(qx.make_bit_flip(p, gates))
[docs] @classmethod def amplitude_damping(cls, p: float, gate_set: Optional[GateSetLike] = "1q") -> "NoiseModel": """Amplitude damping (T1-like relaxation) of strength ``p``.""" gates = _resolve_gates(gate_set, "1q") return cls(qx.make_amplitude_damping(p, gates))
# ── Properties / composition ──────────────────────────────────────────── @property def inner(self) -> "qx.NoiseModel": """Underlying C++ ``qx.NoiseModel`` for direct simulator use.""" return self._inner @property def enabled(self) -> bool: return self._inner.enabled @enabled.setter def enabled(self, value: bool) -> None: self._inner.enabled = value
[docs] def has_channel(self, gate: "qx.GateType") -> bool: return self._inner.has_channel(gate)
[docs] def has_any_channel(self) -> bool: return self._inner.has_any_channel()
def __add__(self, other: "NoiseModel") -> "NoiseModel": return NoiseModel(self._inner + other._inner) def __iadd__(self, other: "NoiseModel") -> "NoiseModel": self._inner += other._inner return self