Source code for surface_sim.models.model

from __future__ import annotations

import pathlib
from collections.abc import Collection, Sequence
from copy import deepcopy

import yaml
from stim import Circuit, CircuitInstruction, GateTarget, target_rec
from typing_extensions import override

from ..layouts import Layout
from ..setups import Setup
from ..setups.setup import (
    ANNOTATIONS,
    LONG_RANGE_TQ_GATES,
    SQ_GATES,
    SQ_MEASUREMENTS,
    SQ_RESETS,
    TQ_GATES,
)

ALL_TQ_GATES = TQ_GATES | LONG_RANGE_TQ_GATES
ALL_OPS = ANNOTATIONS | SQ_GATES | ALL_TQ_GATES | SQ_MEASUREMENTS | SQ_RESETS


[docs] class Model: """Noise model class for generating the ``stim.Circuit`` for each of the physical operations including noise channels. IMPORTANT The noise models assume that operation layers are separated by ``Model.tick()``, and that all qubits participiate in an operation in the opertion layers. Note that ``Model.idle`` and ``Model.idleidle`` considered an operation, i.e. ``"I"`` and ``"II"`` respectively, while ``Model.idle_noise`` is not. When designing new noise model classes, 1. the method output should be a ``stim.Circuit`` that must include the operation of the corresponding method (e.g. ``"X"`` for ``Model.x_gate``) and (optionally) noise channels. It should not include anything else. 2. ``Model.tick``s do not contain any noise except from the one called by ``Model.flush_noise``. ``Model.flush_noise`` adds all the "still-not-added" noise from the previous operation layer (this is useful in e.g. ``T1T2NoiseModel``). Note that if ``Model.tick`` are followed one after the other, ``Model.flush_noise`` is only called for the first one. This is done so that there are no issues when merging operation layers and because TICKs are just annotations, not noise. If noise wants to be present between TICKs, then idling gates must be added. For more information, read the comments in issue #232. """ DEFAULT_SETUP: type[Setup] | None = None
[docs] def __init__(self, qubit_inds: dict[str, int], setup: Setup | None = None) -> None: self._setup: Setup = setup if setup is not None else self._get_default_setup() self._qubit_inds: dict[str, int] = qubit_inds self.new_circuit() return
@classmethod def from_layouts( cls: type[Model], *layouts: Layout, setup: Setup | None = None ) -> "Model": """Creates a ``Model`` object using the information from the layouts.""" qubit_inds: dict[str, int] = {} for layout in layouts: qubit_inds |= layout.qubit_inds # updates dict return cls(qubit_inds=qubit_inds, setup=setup) def _get_default_setup(self) -> Setup: if self.DEFAULT_SETUP is None: raise ValueError( "This model does not have a default setup, so it must be specified." ) return self.DEFAULT_SETUP() @override def __getattribute__(self, name: str) -> object: """ Stores the name of the last operation called in this class. The operations include: annotations, gates, measurements and resets. """ attr: object = object.__getattribute__(self, name) if callable(attr) and (name in ALL_OPS): # this function is before running the called method. # if I only store the last operation it will be overwritten # by the new called method, thus I need to store the last and # new operations. self._last_op = deepcopy(self._new_op) self._new_op = name return attr @property def setup(self) -> Setup: return self._setup @property def qubits(self) -> list[str]: return list(self._qubit_inds.keys()) @property def uniform(self) -> bool: return self._setup.uniform def gate_duration(self, name: str) -> float: return self._setup.gate_duration(name) def get_inds(self, qubits: Collection[str]) -> list[int]: return [self._qubit_inds[q] for q in qubits] def param(self, *args, **kargs) -> object: return self._setup.param(*args, **kargs) # easier detector definition def add_meas(self, qubit: str) -> None: """Adds a measurement record for the specified qubit. This information is used in the ``meas_target`` function. """ if qubit not in self._qubit_inds: raise ValueError(f"{qubit} is not in the specified qubit_inds.") self._meas_order[qubit].append(self._num_meas) self._num_meas += 1 return def meas_target(self, qubit: str, rel_meas_ind: int) -> GateTarget: """Returns the global measurement index for ``stim.target_rec`` for the specified qubit and its relative measurement index (for the given qubit). Instead of working with global measurement indexing (as ``stim`` does), this function allows to work with local measurement indexing for each qubit (see ``Notes`` for an example). Parameters ---------- qubit Label of the qubit. rel_meas_ind Relative measurement index for the given qubit. Returns ------- GateTarget Target measurement index (``stim.target_rec``) for building the detectors and observables. Notes ----- To access the first measurement in the following example .. code-block:: M 0 M 1 M 0 one would use ``-3`` in ``stim``'s indexing. However, in the "local measurement indexing for each qubit", it would correspond to (q0, -2). This makes it easier for building the detectors as they correspond to the XOR of syndrome outcomes (ancilla outcomes) between different QEC rounds (or some linear combination of syndrome outcomes). """ num_meas_qubit = len(self._meas_order[qubit]) if (rel_meas_ind > num_meas_qubit) or (rel_meas_ind < -num_meas_qubit): raise ValueError( f"{qubit} has only {num_meas_qubit} measurements, but {rel_meas_ind} was accessed." ) abs_meas_ind = self._meas_order[qubit][rel_meas_ind] return target_rec(abs_meas_ind - self._num_meas) def new_circuit(self) -> None: """Empties the variables used for ``meas_target``. This must be called when creating a new circuit.""" self._meas_order: dict[str, list[int]] = {q: [] for q in self._qubit_inds} self._num_meas: int = 0 self._last_op: str = "" self._new_op: str = "" return def store_state(self, filename: str | pathlib.Path) -> None: """Stores the current state to the given YAML file. This is useful in a conditioned circuit to not encode the first part of the circuit for every realization.""" state = { "meas_order": deepcopy(self._meas_order), "num_meas": deepcopy(self._num_meas), "last_op": deepcopy(self._last_op), "new_op": deepcopy(self._new_op), "setup": deepcopy(self._setup.to_dict()), "qubit_inds": deepcopy(self._qubit_inds), } with open(filename, "w") as file: yaml.dump(state, file) return @classmethod def load_state(cls, filename: str | pathlib.Path) -> "Model": """Loads the state inside the given YAML file. See ``Model.store_state`` for more information.""" with open(filename, "r") as file: state = yaml.safe_load(file) model = cls(setup=Setup(state["setup"]), qubit_inds=state["qubit_inds"]) model._meas_order = state["meas_order"] model._num_meas = state["num_meas"] model._last_op = state["last_op"] model._new_op = state["new_op"] return model # annotation operations def tick(self) -> Circuit: if self._last_op != "tick": missing_noise = self.flush_noise() return missing_noise + Circuit("TICK") return Circuit("TICK") def qubit_coords(self, coords: dict[str, Sequence[float | int]]) -> Circuit: if set(coords) > set(self._qubit_inds): raise ValueError( "'coords' have qubits not defined in the model:\n" f"coords={list(coords.keys())}\nmodel={list(self._qubit_inds.keys())}." ) circ = Circuit() # sort the qubit coordinate definitions by index so that it is reproducible ind_coord_pairs = [(self._qubit_inds[label], c) for label, c in coords.items()] ind_coord_pairs.sort(key=lambda x: x[0]) for q_ind, q_coords in ind_coord_pairs: circ.append(CircuitInstruction("QUBIT_COORDS", [q_ind], q_coords)) return circ # gate/measurement/reset operations def x_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def z_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def hadamard(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def s_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def s_dag_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_nxyz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_nzyx_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_xnyz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_xynz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_xyz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_znyx_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_zynx_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def c_zyx_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_nxy_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_nxz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_nyz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_xy_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_xz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def h_yz_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def sqrt_x_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def sqrt_x_dag_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def sqrt_y_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def sqrt_y_dag_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def sqrt_z_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def sqrt_z_dag_gate(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def cnot(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def cx(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def cxswap(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def cy(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def cphase(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def cz(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def czswap(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def idleidle(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def iswap(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def iswap_dag(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def sqrt_xx(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def sqrt_xx_dag(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def sqrt_yy(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def sqrt_yy_dag(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def sqrt_zz(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def sqrt_zz_dag(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def swap(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def swapcx(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def swapcz(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def xcx(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def xcy(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def xcz(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def ycx(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def ycy(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def ycz(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def zcx(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def zcy(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def zcz(self, qubits: Sequence[str]) -> Circuit: raise NotImplementedError def measure(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def measure_x(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def measure_y(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def measure_z(self, qubits: Collection[str]) -> Circuit: return self.measure(qubits) def reset(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def reset_x(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def reset_y(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def reset_z(self, qubits: Collection[str]) -> Circuit: return self.reset(qubits) def idle(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError # noise methods def flush_noise(self) -> Circuit: return Circuit() def idle_noise(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError def incoming_noise(self, qubits: Collection[str]) -> Circuit: raise NotImplementedError