Source code for tqec.noise_models.noise_model

"""Implementation of different noise models for Stim.

# Important note:

This file has been recovered from https://zenodo.org/records/7487893
and is under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/legalcode)
It is part of the code from the paper

    Gidney, C. (2022). Data for "Inplace Access to the Surface Code Y Basis".
    https://doi.org/10.5281/zenodo.7487893


Modifications to the original code:
1. Formatting with ruff.
2. Fixing typing issues and adapting a few imports to personal taste.
3. A minor adjustment to the measurement noise rules of `uniform_depolarizing`
and `si1000` noise models: the depolarizing error on the measured qubits after
measurement is removed because, in our library, all measurements are immediately
followed by resets. As a result, the depolarizing error has no effect.
4. Remove the `depolarizing_two_body_measurement_noise` noise model.
"""

from collections import Counter, defaultdict
from typing import AbstractSet, Iterator

import stim

CLIFFORD_1Q = "C1"
CLIFFORD_2Q = "C2"
ANNOTATION = "info"
MPP = "MPP"
MEASURE_RESET_1Q = "MR1"
JUST_MEASURE_1Q = "M1"
JUST_RESET_1Q = "R1"
NOISE = "!?"

OP_TYPES = {
    "I": CLIFFORD_1Q,
    "X": CLIFFORD_1Q,
    "Y": CLIFFORD_1Q,
    "Z": CLIFFORD_1Q,
    "C_XYZ": CLIFFORD_1Q,
    "C_ZYX": CLIFFORD_1Q,
    "H": CLIFFORD_1Q,
    "H_XY": CLIFFORD_1Q,
    "H_XZ": CLIFFORD_1Q,
    "H_YZ": CLIFFORD_1Q,
    "S": CLIFFORD_1Q,
    "SQRT_X": CLIFFORD_1Q,
    "SQRT_X_DAG": CLIFFORD_1Q,
    "SQRT_Y": CLIFFORD_1Q,
    "SQRT_Y_DAG": CLIFFORD_1Q,
    "SQRT_Z": CLIFFORD_1Q,
    "SQRT_Z_DAG": CLIFFORD_1Q,
    "S_DAG": CLIFFORD_1Q,
    "CNOT": CLIFFORD_2Q,
    "CX": CLIFFORD_2Q,
    "CY": CLIFFORD_2Q,
    "CZ": CLIFFORD_2Q,
    "ISWAP": CLIFFORD_2Q,
    "ISWAP_DAG": CLIFFORD_2Q,
    "SQRT_XX": CLIFFORD_2Q,
    "SQRT_XX_DAG": CLIFFORD_2Q,
    "SQRT_YY": CLIFFORD_2Q,
    "SQRT_YY_DAG": CLIFFORD_2Q,
    "SQRT_ZZ": CLIFFORD_2Q,
    "SQRT_ZZ_DAG": CLIFFORD_2Q,
    "SWAP": CLIFFORD_2Q,
    "XCX": CLIFFORD_2Q,
    "XCY": CLIFFORD_2Q,
    "XCZ": CLIFFORD_2Q,
    "YCX": CLIFFORD_2Q,
    "YCY": CLIFFORD_2Q,
    "YCZ": CLIFFORD_2Q,
    "ZCX": CLIFFORD_2Q,
    "ZCY": CLIFFORD_2Q,
    "ZCZ": CLIFFORD_2Q,
    "MPP": MPP,
    "MR": MEASURE_RESET_1Q,
    "MRX": MEASURE_RESET_1Q,
    "MRY": MEASURE_RESET_1Q,
    "MRZ": MEASURE_RESET_1Q,
    "M": JUST_MEASURE_1Q,
    "MX": JUST_MEASURE_1Q,
    "MY": JUST_MEASURE_1Q,
    "MZ": JUST_MEASURE_1Q,
    "R": JUST_RESET_1Q,
    "RX": JUST_RESET_1Q,
    "RY": JUST_RESET_1Q,
    "RZ": JUST_RESET_1Q,
    "DETECTOR": ANNOTATION,
    "OBSERVABLE_INCLUDE": ANNOTATION,
    "QUBIT_COORDS": ANNOTATION,
    "SHIFT_COORDS": ANNOTATION,
    "TICK": ANNOTATION,
    "E": ANNOTATION,
    "DEPOLARIZE1": NOISE,
    "DEPOLARIZE2": NOISE,
    "PAULI_CHANNEL_1": NOISE,
    "PAULI_CHANNEL_2": NOISE,
    "X_ERROR": NOISE,
    "Y_ERROR": NOISE,
    "Z_ERROR": NOISE,
    # Not supported.
    # 'CORRELATED_ERROR': NOISE,
    # 'E': NOISE,
    # 'ELSE_CORRELATED_ERROR',
}
OP_MEASURE_BASES = {
    "M": "Z",
    "MX": "X",
    "MY": "Y",
    "MZ": "Z",
    "MPP": "",
}
COLLAPSING_OPS = {
    op
    for op, t in OP_TYPES.items()
    if t == JUST_RESET_1Q or t == JUST_MEASURE_1Q or t == MPP or t == MEASURE_RESET_1Q
}


[docs] class NoiseRule: """Describes how to add noise to an operation."""
[docs] def __init__(self, *, after: dict[str, float], flip_result: float = 0): """ Args: after: A dictionary mapping noise rule names to their probability argument. For example, {"DEPOLARIZE2": 0.01, "X_ERROR": 0.02} will add two qubit depolarization with parameter 0.01 and also add 2% bit flip noise. These noise channels occur after all other operations in the moment and are applied to the same targets as the relevant operation. flip_result: The probability that a measurement result should be reported incorrectly. Only valid when applied to operations that produce measurement results. """ if not (0 <= flip_result <= 1): raise ValueError(f"not (0 <= {flip_result=} <= 1)") for k, p in after.items(): if OP_TYPES[k] != NOISE: raise ValueError(f"not a noise channel: {k} from {after=}") if not (0 <= p <= 1): raise ValueError(f"not (0 <= {p} <= 1) from {after=}") self.after = after self.flip_result = flip_result
def append_noisy_version_of( self, *, split_op: stim.CircuitInstruction, out_during_moment: stim.Circuit, after_moments: defaultdict[tuple[str, float], stim.Circuit], immune_qubits: AbstractSet[int], ) -> None: targets = split_op.targets_copy() if immune_qubits and any( (t.is_qubit_target or t.is_x_target or t.is_y_target or t.is_z_target) and t.value in immune_qubits for t in targets ): out_during_moment.append(split_op) return args = split_op.gate_args_copy() if self.flip_result: t = OP_TYPES[split_op.name] assert t == MPP or t == JUST_MEASURE_1Q or t == MEASURE_RESET_1Q assert len(args) == 0 args = [self.flip_result] out_during_moment.append(split_op.name, targets, args) raw_targets = [t.value for t in targets if not t.is_combiner] for op_name, arg in self.after.items(): after_moments[(op_name, arg)].append(op_name, raw_targets, arg)
[docs] class NoiseModel:
[docs] def __init__( self, idle_depolarization: float, additional_depolarization_waiting_for_m_or_r: float = 0, gate_rules: dict[str, NoiseRule] | None = None, measure_rules: dict[str, NoiseRule] | None = None, any_clifford_1q_rule: NoiseRule | None = None, any_clifford_2q_rule: NoiseRule | None = None, ): self.idle_depolarization = idle_depolarization self.additional_depolarization_waiting_for_m_or_r = ( additional_depolarization_waiting_for_m_or_r ) self.gate_rules = gate_rules self.measure_rules = measure_rules self.any_clifford_1q_rule = any_clifford_1q_rule self.any_clifford_2q_rule = any_clifford_2q_rule
@staticmethod def si1000(p: float) -> "NoiseModel": """Superconducting inspired noise. As defined in "A Fault-Tolerant Honeycomb Memory": https://arxiv.org/abs/2108.10457 Small tweak from the paper: The measurement result is probabilistically flipped instead of the input qubit. """ return NoiseModel( idle_depolarization=p / 10, additional_depolarization_waiting_for_m_or_r=2 * p, any_clifford_1q_rule=NoiseRule(after={"DEPOLARIZE1": p / 10}), any_clifford_2q_rule=NoiseRule(after={"DEPOLARIZE2": p}), measure_rules={ "Z": NoiseRule(after={}, flip_result=p * 5), }, gate_rules={ "R": NoiseRule(after={"X_ERROR": p * 2}), }, ) @staticmethod def uniform_depolarizing(p: float) -> "NoiseModel": """Near-standard circuit depolarizing noise. Everything has the same parameter p. Single qubit clifford gates get single qubit depolarization. Two qubit clifford gates get single qubit depolarization. Dissipative gates have their result probabilistically bit flipped (or phase flipped if appropriate). Non-demolition measurement is treated a bit unusually in that it is the result that is flipped instead of the input qubit. """ return NoiseModel( idle_depolarization=p, any_clifford_1q_rule=NoiseRule(after={"DEPOLARIZE1": p}), any_clifford_2q_rule=NoiseRule(after={"DEPOLARIZE2": p}), measure_rules={ "X": NoiseRule(after={}, flip_result=p), "Y": NoiseRule(after={}, flip_result=p), "Z": NoiseRule(after={}, flip_result=p), "XX": NoiseRule(after={}, flip_result=p), "YY": NoiseRule(after={}, flip_result=p), "ZZ": NoiseRule(after={}, flip_result=p), }, gate_rules={ "RX": NoiseRule(after={"Z_ERROR": p}), "RY": NoiseRule(after={"X_ERROR": p}), "R": NoiseRule(after={"X_ERROR": p}), }, ) def _noise_rule_for_split_operation( self, *, split_op: stim.CircuitInstruction ) -> NoiseRule | None: if occurs_in_classical_control_system(split_op): return None if self.gate_rules is not None: rule = self.gate_rules.get(split_op.name) if rule is not None: return rule t = OP_TYPES[split_op.name] if self.any_clifford_1q_rule is not None and t == CLIFFORD_1Q: return self.any_clifford_1q_rule if self.any_clifford_2q_rule is not None and t == CLIFFORD_2Q: return self.any_clifford_2q_rule if self.measure_rules is not None: measure_basis = _measure_basis(split_op=split_op) assert measure_basis is not None rule = self.measure_rules.get(measure_basis) if rule is not None: return rule raise ValueError(f"No noise (or lack of noise) specified for {split_op=}.") def _append_idle_error( self, *, moment_split_ops: list[stim.CircuitInstruction], out: stim.Circuit, system_qubits: AbstractSet[int], immune_qubits: AbstractSet[int], ) -> None: collapse_qubits: list[int] = [] clifford_qubits: list[int] = [] for split_op in moment_split_ops: if occurs_in_classical_control_system(split_op): continue if split_op.name in COLLAPSING_OPS: qubits_out = collapse_qubits else: qubits_out = clifford_qubits for target in split_op.targets_copy(): if not target.is_combiner: qubits_out.append(target.value) # Safety check for operation collisions. usage_counts = Counter(collapse_qubits + clifford_qubits) qubits_used_multiple_times = {q for q, c in usage_counts.items() if c != 1} if qubits_used_multiple_times: moment = stim.Circuit() for op in moment_split_ops: moment.append(op) raise ValueError( f"Qubits were operated on multiple times without a TICK in between:\n" f"multiple uses: {sorted(qubits_used_multiple_times)}\n" f"moment:\n" f"{moment}" ) collapse_qubits_set = set(collapse_qubits) clifford_qubits_set = set(clifford_qubits) idle = sorted( system_qubits - collapse_qubits_set - clifford_qubits_set - immune_qubits ) if idle and self.idle_depolarization: out.append("DEPOLARIZE1", idle, self.idle_depolarization) waiting_for_mr = sorted(system_qubits - collapse_qubits_set - immune_qubits) if ( collapse_qubits_set and waiting_for_mr and self.additional_depolarization_waiting_for_m_or_r ): out.append( "DEPOLARIZE1", idle, self.additional_depolarization_waiting_for_m_or_r ) def _append_noisy_moment( self, *, moment_split_ops: list[stim.CircuitInstruction], out: stim.Circuit, system_qubits: AbstractSet[int], immune_qubits: AbstractSet[int], ) -> None: after: defaultdict[tuple[str, float], stim.Circuit] = defaultdict(stim.Circuit) for split_op in moment_split_ops: rule = self._noise_rule_for_split_operation(split_op=split_op) if rule is None: out.append(split_op) else: rule.append_noisy_version_of( split_op=split_op, out_during_moment=out, after_moments=after, immune_qubits=immune_qubits, ) for k in sorted(after.keys()): out += after[k] self._append_idle_error( moment_split_ops=moment_split_ops, out=out, system_qubits=system_qubits, immune_qubits=immune_qubits, ) def noisy_circuit( self, circuit: stim.Circuit, *, system_qubits: set[int] | None = None, immune_qubits: set[int] | None = None, ) -> stim.Circuit: """Returns a noisy version of the given circuit, by applying the receiving noise model. Args: circuit: The circuit to layer noise over. system_qubits: All qubits used by the circuit. These are the qubits eligible for idling noise. immune_qubits: Qubits to not apply noise to, even if they are operated on. Returns: The noisy version of the circuit. """ if system_qubits is None: system_qubits = set(range(circuit.num_qubits)) if immune_qubits is None: immune_qubits = set() result = stim.Circuit() first = True for moment_split_ops in _iter_split_op_moments( circuit, immune_qubits=immune_qubits ): if first: first = False elif result and isinstance(result[-1], stim.CircuitRepeatBlock): pass else: result.append("TICK") if isinstance(moment_split_ops, stim.CircuitRepeatBlock): noisy_body = self.noisy_circuit( moment_split_ops.body_copy(), system_qubits=system_qubits, immune_qubits=immune_qubits, ) noisy_body.append("TICK") result.append( stim.CircuitRepeatBlock( repeat_count=moment_split_ops.repeat_count, body=noisy_body ) ) else: self._append_noisy_moment( moment_split_ops=moment_split_ops, out=result, system_qubits=system_qubits, immune_qubits=immune_qubits, ) return result
[docs] def occurs_in_classical_control_system(op: stim.CircuitInstruction) -> bool: """Determines if an operation is an annotation or a classical control system update.""" t = OP_TYPES[op.name] if t == ANNOTATION: return True if t == CLIFFORD_2Q: targets = op.targets_copy() for k in range(0, len(targets), 2): a = targets[k] b = targets[k + 1] classical_0 = a.is_measurement_record_target or a.is_sweep_bit_target classical_1 = b.is_measurement_record_target or b.is_sweep_bit_target if not (classical_0 or classical_1): return False return True return False
def _split_targets_if_needed( op: stim.CircuitInstruction, immune_qubits: AbstractSet[int] ) -> Iterator[stim.CircuitInstruction]: """Splits operations into pieces as needed (e.g. MPP into each product, classical control away from quantum ops).""" t = OP_TYPES[op.name] if t == CLIFFORD_2Q: yield from _split_targets_if_needed_clifford_2q(op, immune_qubits) elif t == MPP: yield from _split_targets_if_needed_m_basis(op, immune_qubits) elif t in [NOISE, ANNOTATION]: yield op else: yield from _split_targets_if_needed_clifford_1q(op, immune_qubits) def _split_targets_if_needed_clifford_1q( op: stim.CircuitInstruction, immune_qubits: AbstractSet[int] ) -> Iterator[stim.CircuitInstruction]: if immune_qubits: args = op.gate_args_copy() for t in op.targets_copy(): yield stim.CircuitInstruction(op.name, [t], args) else: yield op def _split_targets_if_needed_clifford_2q( op: stim.CircuitInstruction, immune_qubits: AbstractSet[int] ) -> Iterator[stim.CircuitInstruction]: """Splits classical control system operations away from things actually happening on the quantum computer.""" assert OP_TYPES[op.name] == CLIFFORD_2Q targets = op.targets_copy() if immune_qubits or any(t.is_measurement_record_target for t in targets): args = op.gate_args_copy() for k in range(0, len(targets), 2): yield stim.CircuitInstruction(op.name, targets[k : k + 2], args) else: yield op def _split_targets_if_needed_m_basis( op: stim.CircuitInstruction, immune_qubits: AbstractSet[int] ) -> Iterator[stim.CircuitInstruction]: """Splits an MPP operation into one operation for each Pauli product it measures.""" targets = op.targets_copy() args = op.gate_args_copy() k = 0 start = k while k < len(targets): if k + 1 == len(targets) or not targets[k + 1].is_combiner: yield stim.CircuitInstruction(op.name, targets[start : k + 1], args) k += 1 start = k else: k += 2 assert k == len(targets) def _iter_split_op_moments( circuit: stim.Circuit, *, immune_qubits: AbstractSet[int] ) -> Iterator[stim.CircuitRepeatBlock | list[stim.CircuitInstruction]]: """Splits a circuit into moments and some operations into pieces. Classical control system operations like CX rec[-1] 0 are split from quantum operations like CX 1 0. MPP operations are split into one operation per Pauli product. Yields: Lists of operations corresponding to one moment in the circuit, with any problematic operations like MPPs split into pieces. (A moment is the time between two TICKs.) """ cur_moment: list[stim.CircuitInstruction] = [] for op in circuit: if isinstance(op, stim.CircuitRepeatBlock): if cur_moment: yield cur_moment cur_moment = [] yield op else: # if isinstance(op, stim.CircuitInstruction): if op.name == "TICK": yield cur_moment cur_moment = [] else: cur_moment.extend( _split_targets_if_needed(op, immune_qubits=immune_qubits) ) if cur_moment: yield cur_moment def _measure_basis(*, split_op: stim.CircuitInstruction) -> str | None: """Converts an operation into a string describing the Pauli product basis it measures. Returns: None: This is not a measurement (or not *just* a measurement). str: Pauli product string that the operation measures (e.g. "XX" or "Y"). """ result = OP_MEASURE_BASES.get(split_op.name) targets = split_op.targets_copy() if result == "": for k in range(0, len(targets), 2): t = targets[k] if t.is_x_target: result += "X" elif t.is_y_target: result += "Y" elif t.is_z_target: result += "Z" else: raise NotImplementedError(f"{targets=}") return result