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