Source code for surface_sim.setups.setup

from __future__ import annotations

from collections.abc import Callable, Collection
from copy import deepcopy
from pathlib import Path
from typing import TypedDict

import yaml

from .random import RandomSetupDict

Param = float | int | bool | str | None


class SetupDict(TypedDict):
    setup: Collection[dict[str, Param]]


ANNOTATIONS = {"tick": "TICK", "qubit_coords": "QUBIT_COORDS"}
SQ_GATES = {
    "idle": "I",
    "x_gate": "X",
    "z_gate": "Z",
    "hadamard": "H",
    "h_gate": "H",
    "s_gate": "S",
    "s_dag_gate": "S_DAG",
    "c_nxyz_gate": "C_NXYZ",
    "c_nzyx_gate": "C_NZYX",
    "c_xnyz_gate": "C_XNYZ",
    "c_xynz_gate": "C_XYNZ",
    "c_xyz_gate": "C_XYZ",
    "c_znyx_gate": "C_ZNYX",
    "c_zynx_gate": "C_ZYNX",
    "c_zyx_gate": "C_ZYX",
    "h_nxy_gate": "H_NXY",
    "h_nxz_gate": "H_NXZ",
    "h_nyz_gate": "H_NYZ",
    "h_xy_gate": "H_XY",
    "h_xz_gate": "H",  # stim changes the name
    "h_yz_gate": "H_YZ",
    "sqrt_x_gate": "SQRT_X",
    "sqrt_x_dag_gate": "SQRT_X_DAG",
    "sqrt_y_gate": "SQRT_Y",
    "sqrt_y_dag_gate": "SQRT_Y_DAG",
    "sqrt_z_gate": "S",  # stim changes the name
    "sqrt_z_dag_gate": "S_DAG",  # stim changes the name
}
TQ_GATES = {
    "cnot": "CX",  # stim changes the name
    "cx": "CX",
    "cxswap": "CXSWAP",
    "cy": "CY",
    "cphase": "CZ",
    "cz": "CZ",
    "czswap": "CZSWAP",
    "idleidle": "II",
    "iswap": "ISWAP",
    "iswap_dag": "ISWAP_DAG",
    "sqrt_xx": "SQRT_XX",
    "sqrt_xx_dag": "SQRT_XX_DAG",
    "sqrt_yy": "SQRT_YY",
    "sqrt_yy_dag": "SQRT_YY_DAG",
    "sqrt_zz": "SQRT_ZZ",
    "sqrt_zz_dag": "SQRT_ZZ_DAG",
    "swap": "SWAP",
    "swapcx": "SWAPCX",
    "swapcz": "CZSWAP",  # stim changes the name
    "xcx": "XCX",
    "xcy": "XCY",
    "xcz": "XCZ",
    "ycx": "YCX",
    "ycy": "YCY",
    "ycz": "YCZ",
    "zcx": "CX",  # stim changes the name
    "zcy": "CY",  # stim changes the name
    "zcz": "CZ",  # stim changes the name
}
LONG_RANGE_TQ_GATES = {f"long_range_{k}": v for k, v in TQ_GATES.items()}
SQ_MEASUREMENTS = {
    "measure": "M",
    "measure_x": "MX",
    "measure_y": "MY",
    "measure_z": "M",  # stim changes the name
}
SQ_RESETS = {
    "reset": "R",
    "reset_x": "RX",
    "reset_y": "RY",
    "reset_z": "R",  # stim changes the name
}

SQ_PARENTS = (
    {f"{n}_error_prob": "sq_error_prob" for n in SQ_GATES}
    | {f"{n}_error_prob": "reset_error_prob" for n in SQ_RESETS}
    | {f"{n}_error_prob": "meas_error_prob" for n in SQ_MEASUREMENTS}
)
TQ_PARENTS = {f"{n}_error_prob": "tq_error_prob" for n in TQ_GATES} | {
    f"{n}_error_prob": "long_range_tq_error_prob" for n in LONG_RANGE_TQ_GATES
}
PARENTS = SQ_PARENTS | TQ_PARENTS

SQ_PARAMS = (
    set(SQ_PARENTS)
    | set(SQ_PARENTS.values())
    | {
        "assign_error_flag",
        "assign_error_prob",
        "extra_idle_meas_or_reset_error_prob",
        "biased_pauli",
        "biased_factor",
    }
)
TQ_PARAMS = (
    set(TQ_PARENTS)
    | set(TQ_PARENTS.values())
    | {"long_coupler_distance", "long_range_tq_error_prob"}
)


[docs] class Setup: PARENTS: dict[str, str] = PARENTS.copy()
[docs] def __init__(self, setup: SetupDict = dict(setup=[{}])) -> None: """Initialises the ``Setup`` class. Parameters ---------- setup Dictionary with the configuration. Must have the key ``"setup"`` containing the information. The information must not have ``None`` as value. It can also include ``"name"``, ``"description"`` and ``"gate_durations"`` keys with the corresponding information. """ self._mode: str = "standard" self._qubit_params: dict[str | tuple[str, ...], dict[str, Param]] = dict() self._global_params: dict[str, Param] = dict() self.uniform: bool = False _setup: SetupDict = deepcopy(setup) self.name: str | None = _setup.pop("name", None) self.description: str | None = _setup.pop("description", None) self._gate_durations: dict[str, float | int] = _setup.pop("gate_durations", {}) # self._load_setup requires self._var_params to be initialized. # load var params after self._load_setup because it sets all of them to 'None'. self._var_params: dict[str, Param] = {} self._load_setup(_setup) self._var_params |= _setup.pop("var_params", {}) if self._qubit_params == {}: self.uniform = True # random samplers self._free_param_samplers: dict[str, Callable[[], Param]] = {} self._standard_qubit_params: dict[str | tuple[str, ...], dict[str, Param]] = ( dict() ) return
def _load_setup(self, setup: SetupDict) -> None: params = setup.get("setup") if not params: raise ValueError("'setup['setup']' not found or contains no information.") for params_dict in params: if "qubit" in params_dict: qubit = str(params_dict.pop("qubit")) qubits = (qubit,) elif "qubits" in params_dict: qubits = tuple(params_dict.pop("qubits")) else: qubits = None if any(not isinstance(v, Param) for v in params_dict.values()): raise TypeError(f"Params must be {Param}, but {params_dict} was given.") if qubits: if qubits in self._qubit_params.keys(): raise ValueError("Parameters defined repeatedly in the setup.") self._qubit_params[qubits] = params_dict else: self._global_params.update(params_dict) for val in params_dict.values(): if isinstance(val, str): for p in _get_var_params(val): self._var_params[p] = None return @property def free_params(self) -> list[str]: """Returns the names of the unset variable parameters.""" return [param for param, val in self._var_params.items() if val is None] @property def var_params(self) -> list[str]: """Returns the names of all variable parameters.""" return list(self._var_params) @property def global_params(self) -> list[str]: """Returns the names of the global parameters.""" return list(self._global_params) @classmethod def from_yaml(cls: type[Setup], filename: str | Path) -> Setup: """Create new ``surface_sim.setup.Setup`` instance from YAML configuarion file. Parameters ---------- filename The YAML file name. Returns ------- T The initialised ``surface_sim.setup.Setup`` object based on the yaml. """ with open(filename, "r") as file: setup: SetupDict = yaml.safe_load(file) return cls(setup) def to_dict(self) -> SetupDict: """Returns a dictionary that can be used to initialize ``Setup``.""" setup = dict() setup["name"] = self.name setup["description"] = self.description setup["gate_durations"] = self._gate_durations setup["var_params"] = { k: v for k, v in self._var_params.items() if v is not None } qubit_params: list[dict[str, Param]] = [] if self._global_params: qubit_params.append(self._global_params) for qubits, params in self._qubit_params.items(): params_copy = deepcopy(params) num_qubits = len(qubits) if num_qubits == 1: params_copy["qubit"] = qubits[0] elif num_qubits == 2: params_copy["qubits"] = tuple(qubits) qubit_params.append(params_copy) setup["setup"] = qubit_params return setup def to_yaml(self, filename: str | Path) -> None: """Stores the current ``Setup`` configuration in the given file in YAML format. Parameters ---------- filename Name of the file in which to store the configuration. """ setup = self.to_dict() with open(filename, "w") as file: yaml.dump(setup, file, default_flow_style=False) return def convert_to_random(self, **free_param_samplers: Callable[[], Param]) -> None: """Converts this setup into a random setup, in which the free parameters are randomly sampled for each qubit and qubit pair. The free parameters (``Setup.free_params``) are sampled on the fly the first time they are requested and then stored. If a qubit or qubit pair has some parameters already set, their value is not modified, that is: no free parameter is sampled for this qubit or qubit pair. Because the free parameters are sampled on the fly, the setup must be stored **after** generating the wanted circuit. Otherwise, the parameters are not yet sampled nor stored in this setup. The qubit-specific parameters that are generated from the randomly sampled values of the free parameter correspond to the global parameters of this setup (``Setup.global_params``). Parameters ---------- **free_param_samplers Samplers for the free parameters. """ if self._mode != "standard": raise ValueError( f"Setup must be in 'standard' mode, but it is in '{self._mode}'." ) if set(free_param_samplers) < set(self.free_params): raise ValueError( f"All free parameters ({', '.join(self.free_params)}) must be specified." ) self._mode = "random" self._free_param_samplers = dict(free_param_samplers) self._standard_qubit_params = dict(self._qubit_params) self._qubit_params = RandomSetupDict(self._standard_qubit_params) self.uniform = False global_params_copy = dict(self._global_params) var_params_copy = dict(self._var_params) free_param_samplers_copy = dict(free_param_samplers) def sq_noise_sampler() -> dict[str, Param]: free_params = {p: s() for p, s in free_param_samplers_copy.items()} var_params = dict(var_params_copy) var_params.update(free_params) qubit_params: dict[str, Param] = {} for name, value in global_params_copy.items(): if name not in SQ_PARAMS: continue qubit_params[name] = _eval_param_val(value, var_params) return qubit_params def tq_noise_sampler() -> dict[str, Param]: free_params = {p: s() for p, s in free_param_samplers_copy.items()} var_params = dict(var_params_copy) var_params.update(free_params) qubit_params: dict[str, Param] = {} for name, value in global_params_copy.items(): if name not in TQ_PARAMS: continue qubit_params[name] = _eval_param_val(value, var_params) return qubit_params self._qubit_params.sq_noise_sampler = sq_noise_sampler self._qubit_params.tq_noise_sampler = tq_noise_sampler return def new_randomization(self) -> None: """Restores the original setup without the sampled parameters.""" if self._mode != "random": raise ValueError( f"Setup must be in 'random' mode, but it is in '{self._mode}'." ) self._qubit_params = RandomSetupDict(self._standard_qubit_params) return def var_param(self, var_param: str) -> Param: """Returns the value of the given variable parameter name. Parameters ---------- var_param Name of the variable parameter. Returns ------- Value of the specified ``var_param``. """ val = self._var_params.get(var_param) if (val is None) and (var_param in self.PARENTS): return self.var_param(self.PARENTS[var_param]) if val is None: raise ValueError( f"Variable param {var_param} not specified or does not exist." ) return val def set_var_param(self, var_param: str, val: Param) -> None: """Sets the given value to the given variable parameter. Parameters ---------- var_param Name of the variable parameter. val Value to set to ``var_param``. """ if self._mode == "random": raise ValueError("Parameters cannot be changed in 'random' mode.") if not isinstance(var_param, str): raise TypeError( f"'var_param' must be a str, but {type(var_param)} was given." ) if not isinstance(val, Param): raise TypeError(f"'val' must be {Param}, but {type(val)} was given.") self._var_params[var_param] = val return def set_param( self, param: str, param_val: Param, qubits: str | tuple[str, ...] = tuple() ) -> None: """Sets the given value to the given parameter of the given qubit(s). For example, setting the CZ error probability requires ``qubits = tuple[str, str]``. Parameters ---------- param Name of the parameter. param_val Value to set to ``param``. qubits Qubit(s) of which to set the parameter. """ if self._mode == "random": raise ValueError("Parameters cannot be changed in 'random' mode.") if not isinstance(param, str): raise TypeError(f"'param' must be a str, but {type(param)} was given.") if not isinstance(param_val, Param): raise TypeError( f"'param_val' must be {Param} but {type(param_val)} was given." ) if isinstance(qubits, str): qubits = (qubits,) if (not isinstance(qubits, Collection)) or ( any(not isinstance(q, str) for q in qubits) ): raise TypeError( f"'qubits' must be a tuple[str], but {type(qubits)} was given." ) qubits = tuple(qubits) if not qubits: self._global_params[param] = param_val else: if qubits not in self._qubit_params: raise ValueError( f"'{param}' for '{'-'.join(qubits)}' is not a param of this setup." ) self._qubit_params[qubits][param] = param_val return def param(self, param: str, qubits: str | Collection[str] = tuple()) -> Param: """Returns the value of the given parameter for the specified qubit(s). For example, getting the CZ error probability requires ``qubits = tuple[str, str]``. Parameters ---------- param Name of the parameter. qubits Qubit(s) of which to get the parameter. Returns ------- val Value of the parameter. """ if not isinstance(param, str): raise TypeError(f"'param' must be a str, but {type(param)} was given.") if isinstance(qubits, str): qubits = (qubits,) if (not isinstance(qubits, Collection)) or ( any(not isinstance(q, str) for q in qubits) ): raise TypeError( f"'qubits' must be a tuple[str], but {type(qubits)} was given." ) qubits = tuple(qubits) if self._mode == "standard": if qubits in self._qubit_params and param in self._qubit_params[qubits]: val = self._qubit_params[qubits][param] return _eval_param_val(val, self._var_params) if param in self._global_params: val = self._global_params[param] return _eval_param_val(val, self._var_params) elif self._mode == "random": if len(qubits) == 0: raise ValueError("In 'random' mode, 'qubits' must be specified.") params = self._qubit_params[qubits] if param in params: # parameter may need to be still evaluated if it has been # fixed by the user. return _eval_param_val(params[param], self._var_params) # if none of the previous works, try loading from 'parent' parameter if param in self.PARENTS: return self.param(self.PARENTS[param], qubits=qubits) if qubits: raise KeyError( f"'{param}' for '{'-'.join(qubits)}' is not a param of this setup." ) raise KeyError(f"Global parameter {param} not defined") def gate_duration(self, name: str) -> float: """Returns the duration of the specified gate. Parameters ---------- name Name of the gate. Returns ------- Duration of the gate. """ try: return self._gate_durations[name] except KeyError: raise ValueError(f"No gate duration specified for '{name}'")
def _get_var_params(string: str) -> list[str]: params: list[str] = [] for s in string.split("{")[1:]: if "}" not in s: raise ValueError( "Only one level of brakets is allowed. Ensure that brakets are matched." ) param = s.split("}")[0] if param == "": raise ValueError("Params must be non-empty strings.") params.append(param) return params def _eval_param_val(val: Param, var_params: dict[str, Param]) -> Param: # Parameter values can refer to another parameter (i.e. a variable parameter) if not isinstance(val, str): return val if params := _get_var_params(val): for p in params: if var_params[p] is None: raise ValueError(f"The free param '{p}' has not been specified.") # if val = "{parameter}", then no evaluation is needed # this is important if the value of 'parameter' is a string because # we don't want to do 'eval("value_of_parameter") in this case if val == f"{{{params[0]}}}" and isinstance(var_params[params[0]], str): return val.format(**var_params) val = val.format(**var_params) # ensure that eval only performs mathematical operations val_check = val.replace("True", "").replace("False", "").replace(" ", "") if set(val_check) > set("0123456789.*/+-^%~|()=<>?"): raise ValueError( "The strings with variable parameters can only be mathematical expressions." ) val = eval(val) return val