"""Quantum error models for noisy simulation.
This module defines various quantum noise models that can be applied to
circuit simulations, including bit-flip, phase-flip, depolarizing, amplitude
damping, and Kraus operator errors.
Key exports:
- ErrorModel: Base class for all error models.
- BitFlip: Bit-flip (X) error model.
- PhaseFlip: Phase-flip (Z) error model.
- Depolarizing: Single-qubit depolarizing channel.
- TwoQubitDepolarizing: Two-qubit depolarizing channel.
- AmplitudeDamping: Amplitude damping (T1) error model.
- PauliError1Q: Single-qubit Pauli error model.
- PauliError2Q: Two-qubit Pauli error model.
- Kraus1Q: Single-qubit Kraus operator error model.
- ThermalRelaxation: T1/T2 thermal relaxation error model.
- ErrorLoader: Base error loader interface.
- ErrorLoader_GenericError: Generic error loader.
- ErrorLoader_GateTypeError: Gate-type specific error loader.
- ErrorLoader_GateSpecificError: Gate-specific error loader.
"""
from __future__ import annotations
__all__ = [
"ErrorModel",
"BitFlip",
"PhaseFlip",
"Depolarizing",
"TwoQubitDepolarizing",
"AmplitudeDamping",
"PauliError1Q",
"PauliError2Q",
"Kraus1Q",
"ThermalRelaxation",
"ErrorLoader",
"ErrorLoader_GenericError",
"ErrorLoader_GateTypeError",
"ErrorLoader_GateSpecificError",
]
import math
from typing import Any
# Opcode type: (gate_name, qubits, params, prob, None, None)
OpCode = tuple[str, int | list[int], Any, float | tuple[float, float, float] | list[complex] | None, None, None]
[docs]
class ErrorModel:
"""Base class for quantum error models."""
def __init__(self) -> None: ...
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
...
[docs]
class BitFlip(ErrorModel):
"""Bit-flip error model.
Args:
p: Bit-flip probability.
"""
p: float
def __init__(self, p: float) -> None:
self.p = p
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate BitFlip error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
if isinstance(qubits, int):
qubits = [qubits]
return [("BitFlip", q, None, self.p, None, None) for q in qubits]
[docs]
class PhaseFlip(ErrorModel):
"""Phase-flip (Z) error model.
Args:
p: Phase-flip probability.
"""
p: float
def __init__(self, p: float) -> None:
self.p = p
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate PhaseFlip error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
if isinstance(qubits, int):
qubits = [qubits]
return [("PhaseFlip", q, None, self.p, None, None) for q in qubits]
[docs]
class Depolarizing(ErrorModel):
"""Depolarizing error model for single qubits.
Args:
p: Depolarizing probability.
"""
p: float
def __init__(self, p: float) -> None:
self.p = p
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate Depolarizing error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
if isinstance(qubits, int):
qubits = [qubits]
return [("Depolarizing", q, None, self.p, None, None) for q in qubits]
[docs]
class TwoQubitDepolarizing(ErrorModel):
"""Two-qubit depolarizing error model.
Args:
p: Depolarizing probability.
"""
p: float
def __init__(self, p: float) -> None:
self.p = p
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate TwoQubitDepolarizing error opcodes for the given qubit pair.
Args:
qubits: List of exactly two qubits.
Returns:
List of error opcodes.
Raises:
ValueError: If not exactly two qubits are provided.
"""
if not isinstance(qubits, list) or len(qubits) != 2:
raise ValueError("TwoQubitDepolarizing error model requires two qubits")
return [("TwoQubitDepolarizing", qubits, None, self.p, None, None)]
[docs]
class AmplitudeDamping(ErrorModel):
"""Amplitude damping error model.
Args:
gamma: Damping rate (0 to 1).
"""
gamma: float
def __init__(self, gamma: float) -> None:
self.gamma = gamma
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate AmplitudeDamping error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
if isinstance(qubits, int):
qubits = [qubits]
return [("AmplitudeDamping", q, None, self.gamma, None, None) for q in qubits]
[docs]
class PauliError1Q(ErrorModel):
"""Single-qubit Pauli error model with independent X, Y, Z probabilities.
Args:
p_x: Probability of X error.
p_y: Probability of Y error.
p_z: Probability of Z error.
"""
p_x: float
p_y: float
p_z: float
def __init__(self, p_x: float, p_y: float, p_z: float) -> None:
self.p_x = p_x
self.p_y = p_y
self.p_z = p_z
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate PauliError1Q opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
if isinstance(qubits, int):
qubits = [qubits]
return [("PauliError1Q", q, None, (self.p_x, self.p_y, self.p_z), None, None) for q in qubits]
[docs]
class PauliError2Q(ErrorModel):
"""Two-qubit Pauli error model with 15 independent probabilities.
Args:
ps: List of 15 Pauli error probabilities.
"""
ps: list[float]
def __init__(self, ps: list[float]) -> None:
self.ps = ps
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate PauliError2Q opcodes for the given qubit pair.
Args:
qubits: List of exactly two qubits.
Returns:
List of error opcodes.
Raises:
ValueError: If not exactly two qubits are provided.
"""
if not isinstance(qubits, list) or len(qubits) != 2:
raise ValueError("PauliError2Q error model requires two qubits")
return [("PauliError2Q", qubits, None, self.ps, None, None)]
[docs]
class Kraus1Q(ErrorModel):
"""Single-qubit Kraus operator error model.
Args:
kraus_ops: List of Kraus operators (2x2 matrices).
"""
kraus_ops: list[list[complex]]
def __init__(self, kraus_ops: list[list[complex]]) -> None:
self.kraus_ops = kraus_ops
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate Kraus1Q error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes.
"""
if isinstance(qubits, int):
qubits = [qubits]
return [("Kraus1Q", q, None, self.kraus_ops, None, None) for q in qubits]
[docs]
class ThermalRelaxation(ErrorModel):
"""Thermal relaxation (T1/T2) error model for a gate of fixed duration.
Emits per-qubit amplitude-damping (T1) and phase-flip (pure dephasing,
T2) opcodes whose rates are derived from the gate duration ``t``:
- ``gamma = 1 - exp(-t / T1)`` (amplitude damping, only when T1 is set)
- ``p_phi = 0.5 * (1 - exp(-t * (1/T2 - 1/(2*T1))))`` when both T1 and T2
are set, or ``p_phi = 0.5 * (1 - exp(-t / T2))`` (pure dephasing) when
only T2 is set.
Args:
t1_ns: T1 time in nanoseconds. Either a single value applied to every
qubit, or a per-qubit mapping. ``None`` disables amplitude damping.
t2_ns: T2 time in nanoseconds, same shape as ``t1_ns``. ``None``
disables pure dephasing.
gate_time_ns: Duration of the gate in nanoseconds. Must be positive.
Raises:
ValueError: If both ``t1_ns`` and ``t2_ns`` are None, if any time is
non-positive, or if ``T2 > 2 * T1`` for a qubit (which would make
the dephasing probability negative).
"""
def __init__(
self,
t1_ns: float | dict[int, float] | None,
t2_ns: float | dict[int, float] | None = None,
gate_time_ns: float = 0.0,
) -> None:
if t1_ns is None and t2_ns is None:
raise ValueError("ThermalRelaxation requires at least one of t1_ns / t2_ns")
if gate_time_ns <= 0:
raise ValueError(f"ThermalRelaxation requires a positive gate_time_ns, got {gate_time_ns}")
for label, value in (("t1_ns", t1_ns), ("t2_ns", t2_ns)):
values = value.values() if isinstance(value, dict) else ([value] if value is not None else [])
for v in values:
if v <= 0:
raise ValueError(f"ThermalRelaxation {label} must be positive, got {v}")
self.t1_ns = t1_ns
self.t2_ns = t2_ns
self.gate_time_ns = float(gate_time_ns)
@staticmethod
def _value_for(param: float | dict[int, float] | None, qubit: int) -> float | None:
if isinstance(param, dict):
return param.get(qubit)
return param
[docs]
@staticmethod
def relaxation_rates(t1: float | None, t2: float | None, t: float) -> tuple[float | None, float | None]:
"""Return ``(gamma, p_phi)`` for a qubit with the given T1/T2 and gate time ``t``."""
gamma: float | None = None
p_phi: float | None = None
if t1 is not None:
gamma = 1.0 - math.exp(-t / t1)
if t2 is not None:
if t1 is not None:
if t2 > 2.0 * t1:
raise ValueError(f"ThermalRelaxation requires T2 <= 2*T1 (got T1={t1}, T2={t2})")
p_phi = 0.5 * (1.0 - math.exp(-t * (1.0 / t2 - 1.0 / (2.0 * t1))))
else:
p_phi = 0.5 * (1.0 - math.exp(-t / t2))
if gamma is not None:
gamma = min(1.0, max(0.0, gamma))
if p_phi is not None:
p_phi = min(1.0, max(0.0, p_phi))
return gamma, p_phi
[docs]
def generate_error_opcode(self, qubits: int | list[int]) -> list[OpCode]:
"""Generate thermal relaxation error opcodes for the given qubits.
Args:
qubits: Qubit or list of qubits to apply error to.
Returns:
List of error opcodes (AmplitudeDamping and/or PhaseFlip).
"""
if isinstance(qubits, int):
qubits = [qubits]
opcodes: list[OpCode] = []
for q in qubits:
gamma, p_phi = self.relaxation_rates(
self._value_for(self.t1_ns, q),
self._value_for(self.t2_ns, q),
self.gate_time_ns,
)
if gamma is not None:
opcodes.append(("AmplitudeDamping", q, None, gamma, None, None))
if p_phi is not None and p_phi > 0.0:
opcodes.append(("PhaseFlip", q, None, p_phi, None, None))
return opcodes
[docs]
class ErrorLoader:
"""Load opcodes into the simulator with noise models.
Base class that inserts error opcodes into the program.
"""
opcodes: list[OpCode]
def __init__(self) -> None:
self.opcodes = []
[docs]
def insert_error(self, opcode: OpCode) -> None:
"""Insert error opcodes for the given opcode. Override in subclasses."""
...
[docs]
def insert_opcode(self, opcode: OpCode) -> None:
"""Append the original opcode and insert associated errors."""
self.opcodes.append(opcode)
self.insert_error(opcode)
[docs]
def process_opcodes(self, opcodes: list[OpCode]) -> None:
"""Process a list of opcodes, inserting errors for each."""
self.opcodes = []
for opcode in opcodes:
self.insert_opcode(opcode)
[docs]
class ErrorLoader_GenericError(ErrorLoader):
"""Load opcodes with generic (gate-independent) noise.
Applies the same set of error models to every gate.
"""
generic_error: list[ErrorModel]
def __init__(self, generic_error: list[ErrorModel]) -> None:
super().__init__()
self.generic_error = generic_error if generic_error else []
[docs]
def insert_error(self, opcode: OpCode) -> None:
"""Insert generic errors for the given opcode."""
_, qubits, _, _, _, _ = opcode
for noise_model in self.generic_error:
noise_opcodes = noise_model.generate_error_opcode(qubits)
self.opcodes.extend(noise_opcodes)
[docs]
class ErrorLoader_GateTypeError(ErrorLoader_GenericError):
"""Load opcodes with gate-dependent noise.
Supports both generic errors (applied to all gates) and per-gate-type
errors (applied only to specific gate types).
"""
gatetype_error: dict[str, list[ErrorModel]]
def __init__(
self,
generic_error: list[ErrorModel],
gatetype_error: dict[str, list[ErrorModel]],
) -> None:
super().__init__(generic_error)
self.gatetype_error = gatetype_error if gatetype_error else {}
[docs]
def insert_error(self, opcode: OpCode) -> None:
"""Insert generic and gate-type-specific errors for the given opcode."""
gate, qubits, _, _, _, _ = opcode
for noise_model in self.generic_error:
noise_opcodes = noise_model.generate_error_opcode(qubits)
self.opcodes.extend(noise_opcodes)
gate_error = self.gatetype_error.get(gate, [])
for noise_model in gate_error:
noise_opcodes = noise_model.generate_error_opcode(qubits)
self.opcodes.extend(noise_opcodes)
[docs]
class ErrorLoader_GateSpecificError(ErrorLoader_GateTypeError):
"""Load opcodes with gate-specific noise (including per qubit-pair).
Supports generic errors, per-gate-type errors, and per-gate-instance
errors (keyed by gate name and specific qubit(s)).
"""
gate_specific_error: dict[tuple[str, tuple[int, int]], list[ErrorModel]]
def __init__(
self,
generic_error: list[ErrorModel],
gatetype_error: dict[str, list[ErrorModel]],
gate_specific_error: dict[tuple[str, tuple[int, int]], list[ErrorModel]],
) -> None:
super().__init__(generic_error, gatetype_error)
self.gate_specific_error = gate_specific_error if gate_specific_error else {}
[docs]
def insert_error(self, opcode: OpCode) -> None:
"""Insert generic, gate-type, and gate-specific errors for the given opcode."""
gate, qubits, _, _, _, _ = opcode
super().insert_error(opcode)
if gate == "CZ":
qubits = [min(qubits[0], qubits[1]), max(qubits[0], qubits[1])]
qubits_list = qubits if isinstance(qubits, list) else [qubits]
key = (gate, tuple(qubits_list))
gate_specific_error = self.gate_specific_error.get(key, [])
for noise_model in gate_specific_error:
noise_opcodes = noise_model.generate_error_opcode(qubits_list)
self.opcodes.extend(noise_opcodes)