"""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.
- 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",
"ErrorLoader",
"ErrorLoader_GenericError",
"ErrorLoader_GateTypeError",
"ErrorLoader_GateSpecificError",
]
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", q, None, self.p, None, None) for q in qubits]
[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", q, None, self.ps, None, None) for q in qubits]
[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 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."""
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])]
if isinstance(qubits, list):
qubits = tuple(qubits)
key = (gate, qubits)
gate_specific_error = self.gate_specific_error.get(key, [])
for noise_model in gate_specific_error:
noise_opcodes = noise_model.generate_error_opcode(qubits)
self.opcodes.extend(noise_opcodes)