From 26a2dd74ccaaf26103ab7634d06e2cb38a92f25b Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Tue, 16 Jul 2024 11:39:57 +0200 Subject: [PATCH 01/13] First rework of the NoiseModel interface --- pulser-core/pulser/noise_model.py | 355 ++++++++++++------ .../pulser_simulation/simconfig.py | 133 ++++--- tests/test_noise_model.py | 18 +- tests/test_simconfig.py | 2 +- tests/test_simulation.py | 6 +- 5 files changed, 331 insertions(+), 183 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 674518132..c26e52192 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -15,7 +15,7 @@ from __future__ import annotations import json -from dataclasses import asdict, dataclass, field, fields +from dataclasses import asdict, dataclass from typing import Any, Literal, get_args import numpy as np @@ -27,7 +27,7 @@ __all__ = ["NoiseModel"] -NOISE_TYPES = Literal[ +NoiseTypes = Literal[ "doppler", "amplitude", "SPAM", @@ -37,51 +37,102 @@ "eff_noise", ] - -@dataclass(frozen=True) +NOISE_TYPE_PARAMS = { + "doppler": ("temperature",), + "amplitude": ("laser_waist", "amp_sigma"), + "SPAM": ("p_false_pos", "p_false_neg", "state_prep_error"), + "dephasing": ("dephasing_rate", "hyperfine_dephasing_rate"), + "relaxation": ("relaxation_rate",), + "depolarizing": ("depolarizing_rate",), + "eff_noise": ("eff_noise_rates", "eff_noise_opers"), +} + +_PARAM_TO_NOISE_TYPE = { + param: noise_type + for noise_type, params in NOISE_TYPE_PARAMS.items() + for param in params +} + +# Parameter characterization + +_POSITIVE = { + "dephasing_rate", + "hyperfine_dephasing_rate", + "relaxation_rate", + "depolarizing_rate", +} +_STRICT_POSITIVE = { + "runs", + "samples_per_run", + "temperature", + "laser_waist", +} +_PROBABILITY_LIKE = { + "state_prep_error", + "p_false_pos", + "p_false_neg", + "amp_sigma", +} + +_LEGACY_DEFAULTS = { + "runs": 15, + "samples_per_run": 5, + "state_prep_error": 0.005, + "p_false_pos": 0.01, + "p_false_neg": 0.05, + "temperature": 50.0, + "laser_waist": 175.0, + "amp_sigma": 5e-2, + "relaxation_rate": 0.01, + "dephasing_rate": 0.05, + "hyperfine_dephasing_rate": 1e-3, + "depolarizing_rate": 0.05, + "eff_noise_rates": (), + "eff_noise_opers": (), +} + + +@dataclass(init=False, frozen=True) class NoiseModel: """Specifies the noise model parameters for emulation. - Select the desired noise types in `noise_types` and, if necessary, - modifiy the default values of related parameters. - Non-specified parameters will have reasonable default values which - are only taken into account when the related noise type is selected. - - Args: - noise_types: Noise types to include in the emulation. - Available options: + Supported noise types: + - "relaxation": Noise due to a decay from the Rydberg to + the ground state (parametrized by `relaxation_rate`), commonly + characterized experimentally by the T1 time. - - "relaxation": Noise due to a decay from the Rydberg to - the ground state (parametrized by `relaxation_rate`), commonly - characterized experimentally by the T1 time. + - "dephasing": Random phase (Z) flip (parametrized + by `dephasing_rate`), commonly characterized experimentally + by the T2* time. - - "dephasing": Random phase (Z) flip (parametrized - by `dephasing_rate`), commonly characterized experimentally - by the T2* time. + - "depolarizing": Quantum noise where the state is + turned into the maximally mixed state with rate + `depolarizing_rate`. While it does not describe a physical + phenomenon, it is a commonly used tool to test the system + under a uniform combination of phase flip (Z) and + bit flip (X) errors. - - "depolarizing": Quantum noise where the state is - turned into the maximally mixed state with rate - `depolarizing_rate`. While it does not describe a physical - phenomenon, it is a commonly used tool to test the system - under a uniform combination of phase flip (Z) and - bit flip (X) errors. + - "eff_noise": General effective noise channel defined by + the set of collapse operators `eff_noise_opers` + and the corresponding rates distribution + `eff_noise_rates`. - - "eff_noise": General effective noise channel defined by - the set of collapse operators `eff_noise_opers` - and the corresponding rates distribution - `eff_noise_rates`. + - "doppler": Local atom detuning due to termal motion of the + atoms and Doppler effect with respect to laser frequency. + Parametrized by the `temperature` field. - - "doppler": Local atom detuning due to termal motion of the - atoms and Doppler effect with respect to laser frequency. - Parametrized by the `temperature` field. + - "amplitude": Gaussian damping due to finite laser waist and + laser amplitude fluctuations. Parametrized by `laser_waist` + and `amp_sigma`. - - "amplitude": Gaussian damping due to finite laser waist and - laser amplitude fluctuations. Parametrized by `laser_waist` - and `amp_sigma`. - - - "SPAM": SPAM errors. Parametrized by - `state_prep_error`, `p_false_pos` and `p_false_neg`. + - "SPAM": SPAM errors. Parametrized by + `state_prep_error`, `p_false_pos` and `p_false_neg`. + Args: + noise_types: *Deprecated, simply define the approriate parameters + instead*. Noise types to include in the emulation. Defining + noise in this way will rely on legacy defaults for the relevant + parameters whenever a custom value is not provided. runs: When reconstructing the Hamiltonian from random noise is necessary, this determines how many times that happens. Not to be confused with the number of times the resulting @@ -113,115 +164,169 @@ class NoiseModel: eff_noise_opers: The operators for the effective noise model. """ - noise_types: tuple[NOISE_TYPES, ...] = () - runs: int = 15 - samples_per_run: int = 5 - state_prep_error: float = 0.005 - p_false_pos: float = 0.01 - p_false_neg: float = 0.05 - temperature: float = 50.0 - laser_waist: float = 175.0 - amp_sigma: float = 5e-2 - relaxation_rate: float = 0.01 - dephasing_rate: float = 0.05 - hyperfine_dephasing_rate: float = 1e-3 - depolarizing_rate: float = 0.05 - eff_noise_rates: tuple[float, ...] = field(default_factory=tuple) - eff_noise_opers: tuple[ArrayLike, ...] = field(default_factory=tuple) - - def __post_init__(self) -> None: - positive = { - "dephasing_rate", - "hyperfine_dephasing_rate", - "relaxation_rate", - "depolarizing_rate", - } - strict_positive = { - "runs", - "samples_per_run", - "temperature", - "laser_waist", - } - probability_like = { - "state_prep_error", - "p_false_pos", - "p_false_neg", - "amp_sigma", - } - # The two share no common terms - assert not strict_positive.intersection(probability_like) - - for f in fields(self): - is_valid = True - param = f.name - value = getattr(self, param) - if param in positive: - is_valid = value is None or value >= 0 - comp = "None or greater than or equal to zero" - if param in strict_positive: - is_valid = value > 0 - comp = "greater than zero" - elif param in probability_like: - is_valid = 0 <= value <= 1 - comp = ( - "greater than or equal to zero and smaller than " - "or equal to one" - ) - if not is_valid: - raise ValueError(f"'{param}' must be {comp}, not {value}.") + noise_types: tuple[NoiseTypes, ...] + runs: int | None + samples_per_run: int | None + state_prep_error: float + p_false_pos: float + p_false_neg: float + temperature: float | None + laser_waist: float | None + amp_sigma: float + relaxation_rate: float + dephasing_rate: float + hyperfine_dephasing_rate: float + depolarizing_rate: float + eff_noise_rates: tuple[float, ...] + eff_noise_opers: tuple[ArrayLike, ...] + + def __init__( + self, + noise_types: tuple[NoiseTypes, ...] | None = None, + runs: int | None = None, + samples_per_run: int | None = None, + state_prep_error: float | None = None, + p_false_pos: float | None = None, + p_false_neg: float | None = None, + temperature: float | None = None, + laser_waist: float | None = None, + amp_sigma: float | None = None, + relaxation_rate: float | None = None, + dephasing_rate: float | None = None, + hyperfine_dephasing_rate: float | None = None, + depolarizing_rate: float | None = None, + eff_noise_rates: tuple[float, ...] = (), + eff_noise_opers: tuple[ArrayLike, ...] = (), + ) -> None: + """Initializes a noise model.""" def to_tuple(obj: tuple) -> tuple: if isinstance(obj, (tuple, list, np.ndarray)): obj = tuple(to_tuple(el) for el in obj) return obj - # Turn lists and arrays into tuples - for f in fields(self): - if f.name == "noise_types" or "eff_noise" in f.name: - object.__setattr__( - self, f.name, to_tuple(getattr(self, f.name)) - ) + param_vals = dict( + runs=runs, + samples_per_run=samples_per_run, + state_prep_error=state_prep_error, + p_false_neg=p_false_neg, + p_false_pos=p_false_pos, + temperature=temperature, + laser_waist=laser_waist, + amp_sigma=amp_sigma, + relaxation_rate=relaxation_rate, + dephasing_rate=dephasing_rate, + hyperfine_dephasing_rate=hyperfine_dephasing_rate, + depolarizing_rate=depolarizing_rate, + eff_noise_rates=to_tuple(eff_noise_rates), + eff_noise_opers=to_tuple(eff_noise_opers), + ) + relevant_params = set() + if noise_types is not None: + # TODO: Deprecate + self._check_noise_types(noise_types) + for nt in noise_types: + relevant_params.update(NOISE_TYPE_PARAMS[nt]) + for p_ in relevant_params: + # Replace undefined relevant params by the legacy default + if param_vals[p_] is None: + param_vals[p_] = _LEGACY_DEFAULTS[p_] + if any( + n_ + in ( + "doppler", + "amplitude", + ) # TODO: Consider case when amp_sigma == 0. + or (n_ == "SPAM" and param_vals["state_prep_error"] > 0.0) + for n_ in noise_types + ): + # Define runs and samples per run from the legacy defaults + # when randomization is required + run_params = ("runs", "samples_per_run") + relevant_params.update(run_params) + for p_ in run_params: + param_vals[p_] = _LEGACY_DEFAULTS[p_] + + # Get rid of unnecessary None's + for p_ in _POSITIVE | _PROBABILITY_LIKE: + param_vals[p_] = param_vals[p_] or 0.0 + + true_noise_types = set() + for param_ in param_vals: + if param_vals[param_] and param_ in _PARAM_TO_NOISE_TYPE: + noise_type_ = _PARAM_TO_NOISE_TYPE[param_] + true_noise_types.add(noise_type_) + relevant_params.update(NOISE_TYPE_PARAMS[noise_type_]) + if noise_type_ in ("doppler", "amplitude") or ( + noise_type_ == "SPAM" + and param_vals["state_prep_error"] > 0.0 + ): + relevant_params.update(("runs", "samples_per_run")) + + self._check_eff_noise( + param_vals["eff_noise_rates"], + param_vals["eff_noise_opers"], + "eff_noise" in (noise_types or true_noise_types), + ) + + if noise_types is not None and true_noise_types != set(noise_types): + raise ValueError( # TODO: Write better + "Explicitly defining noise parameters without using the noise" + ) + + relevant_param_vals = { + p: param_vals[p] + for p in param_vals + if param_vals[p] is not None or (p in relevant_params) + } + self._validate_parameters(relevant_param_vals) - self._check_noise_types() - self._check_eff_noise() + object.__setattr__(self, "noise_types", tuple(true_noise_types)) + for param_ in param_vals: + object.__setattr__(self, param_, param_vals[param_]) - def _check_noise_types(self) -> None: - for noise_type in self.noise_types: - if noise_type not in get_args(NOISE_TYPES): + @staticmethod + def _check_noise_types(noise_types: tuple[NoiseTypes, ...]) -> None: + for noise_type in noise_types: + if noise_type not in get_args(NoiseTypes): raise ValueError( f"'{noise_type}' is not a valid noise type. " + "Valid noise types: " - + ", ".join(get_args(NOISE_TYPES)) + + ", ".join(get_args(NoiseTypes)) ) - def _check_eff_noise(self) -> None: - if len(self.eff_noise_opers) != len(self.eff_noise_rates): + @staticmethod + def _check_eff_noise( + eff_noise_rates: tuple[float, ...], + eff_noise_opers: tuple[tuple, ...], + check_contents: bool, + ) -> None: + if len(eff_noise_opers) != len(eff_noise_rates): raise ValueError( - f"The operators list length({len(self.eff_noise_opers)}) " + f"The operators list length({len(eff_noise_opers)}) " "and rates list length" - f"({len(self.eff_noise_rates)}) must be equal." + f"({len(eff_noise_rates)}) must be equal." ) - for rate in self.eff_noise_rates: + for rate in eff_noise_rates: if not isinstance(rate, float): raise TypeError( "eff_noise_rates is a list of floats," f" it must not contain a {type(rate)}." ) - if "eff_noise" not in self.noise_types: - # Stop here if effective noise is not selected + if not check_contents: return - if not self.eff_noise_opers or not self.eff_noise_rates: + if not eff_noise_opers or not eff_noise_rates: raise ValueError( "The effective noise parameters have not been filled." ) - if np.any(np.array(self.eff_noise_rates) < 0): + if np.any(np.array(eff_noise_rates) < 0): raise ValueError("The provided rates must be greater than 0.") # Check the validity of operators - for op in self.eff_noise_opers: + for op in eff_noise_opers: # type checking try: operator = np.array(op, dtype=complex) @@ -237,6 +342,26 @@ def _check_eff_noise(self) -> None: f"Operator's shape must be (2,2) not {operator.shape}." ) + @staticmethod + def _validate_parameters(param_vals: dict[str, Any]) -> None: + for param in param_vals: + is_valid = True + value = param_vals[param] + if param in _POSITIVE: + is_valid = value >= 0 + comp = "greater than or equal to zero" + elif param in _STRICT_POSITIVE: + is_valid = value is not None and value > 0 + comp = "greater than zero" + elif param in _PROBABILITY_LIKE: + is_valid = 0 <= value <= 1 + comp = ( + "greater than or equal to zero and smaller than " + "or equal to one" + ) + if not is_valid: + raise ValueError(f"'{param}' must be {comp}, not {value}.") + def _to_abstract_repr(self) -> dict[str, Any]: all_fields = asdict(self) eff_noise_rates = all_fields.pop("eff_noise_rates") diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index d05767663..67df82944 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -15,13 +15,18 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from math import sqrt -from typing import Any, Optional, Tuple, Type, TypeVar, Union, cast +from typing import Any, Optional, Type, TypeVar, Union import qutip -from pulser.noise_model import NOISE_TYPES, NoiseModel +from pulser.noise_model import ( + _LEGACY_DEFAULTS, + NOISE_TYPE_PARAMS, + NoiseModel, + NoiseTypes, +) MASS = 1.45e-25 # kg KB = 1.38e-23 # J/K @@ -99,19 +104,21 @@ class SimConfig: solver_options: Options for the qutip solver. """ - noise: Union[NOISE_TYPES, tuple[NOISE_TYPES, ...]] = () - runs: int = 15 - samples_per_run: int = 5 - temperature: float = 50.0 - laser_waist: float = 175.0 - amp_sigma: float = 5e-2 - eta: float = 0.005 - epsilon: float = 0.01 - epsilon_prime: float = 0.05 - relaxation_rate: float = 0.01 - dephasing_rate: float = 0.05 - hyperfine_dephasing_rate: float = 1e-3 - depolarizing_rate: float = 0.05 + noise: Union[NoiseTypes, tuple[NoiseTypes, ...]] = () + runs: int = _LEGACY_DEFAULTS["runs"] + samples_per_run: int = _LEGACY_DEFAULTS["samples_per_run"] + temperature: float = _LEGACY_DEFAULTS["temperature"] + laser_waist: float = _LEGACY_DEFAULTS["laser_waist"] + amp_sigma: float = _LEGACY_DEFAULTS["amp_sigma"] + eta: float = _LEGACY_DEFAULTS["state_prep_error"] + epsilon: float = _LEGACY_DEFAULTS["p_false_pos"] + epsilon_prime: float = _LEGACY_DEFAULTS["p_false_neg"] + relaxation_rate: float = _LEGACY_DEFAULTS["relaxation_rate"] + dephasing_rate: float = _LEGACY_DEFAULTS["dephasing_rate"] + hyperfine_dephasing_rate: float = _LEGACY_DEFAULTS[ + "hyperfine_dephasing_rate" + ] + depolarizing_rate: float = _LEGACY_DEFAULTS["depolarizing_rate"] eff_noise_rates: list[float] = field(default_factory=list, repr=False) eff_noise_opers: list[qutip.Qobj] = field(default_factory=list, repr=False) solver_options: Optional[qutip.Options] = None @@ -119,43 +126,51 @@ class SimConfig: @classmethod def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: """Creates a SimConfig from a NoiseModel.""" - return cls( - noise=noise_model.noise_types, - runs=noise_model.runs, - samples_per_run=noise_model.samples_per_run, - temperature=noise_model.temperature, - laser_waist=noise_model.laser_waist, - amp_sigma=noise_model.amp_sigma, - eta=noise_model.state_prep_error, - epsilon=noise_model.p_false_pos, - epsilon_prime=noise_model.p_false_neg, - dephasing_rate=noise_model.dephasing_rate, - hyperfine_dephasing_rate=noise_model.hyperfine_dephasing_rate, - relaxation_rate=noise_model.relaxation_rate, - depolarizing_rate=noise_model.depolarizing_rate, - eff_noise_rates=list(noise_model.eff_noise_rates), - eff_noise_opers=list(map(qutip.Qobj, noise_model.eff_noise_opers)), - ) + custom_param_map = { + "noise_types": "noise", + "state_prep_error": "eta", + "p_false_pos": "epsilon", + "p_false_neg": "epsilon_prime", + } + kwargs = {} + relevant_params = {"noise_types"} + for nt in noise_model.noise_types: + relevant_params.update(NOISE_TYPE_PARAMS[nt]) + if nt in ("doppler", "amplitude") or ( + nt == "SPAM" and noise_model.state_prep_error != 0.0 + ): + relevant_params.update(("runs", "samples_per_run")) + for param in relevant_params: + kwargs[custom_param_map.get(param, param)] = getattr( + noise_model, param + ) + return cls(**kwargs) def to_noise_model(self) -> NoiseModel: """Creates a NoiseModel from the SimConfig.""" - return NoiseModel( - noise_types=cast(Tuple[NOISE_TYPES, ...], self.noise), - runs=self.runs, - samples_per_run=self.samples_per_run, - state_prep_error=self.eta, - p_false_pos=self.epsilon, - p_false_neg=self.epsilon_prime, - temperature=self.temperature * 1e6, # Converts back to µK - laser_waist=self.laser_waist, - amp_sigma=self.amp_sigma, - dephasing_rate=self.dephasing_rate, - hyperfine_dephasing_rate=self.hyperfine_dephasing_rate, - relaxation_rate=self.relaxation_rate, - depolarizing_rate=self.depolarizing_rate, - eff_noise_rates=tuple(self.eff_noise_rates), - eff_noise_opers=tuple(op.full() for op in self.eff_noise_opers), - ) + custom_param_map = { + "noise_types": "noise", + "state_prep_error": "eta", + "p_false_pos": "epsilon", + "p_false_neg": "epsilon_prime", + } + kwargs = {} + for noise_type in self.noise: + for param in NOISE_TYPE_PARAMS[noise_type]: + kwargs[param] = getattr( + self, custom_param_map.get(param, param) + ) + if any( + noise_type in ("doppler", "amplitude") + or (noise_type == "SPAM" and self.eta != 0.0) + for noise_type in self.noise + ): + kwargs["runs"] = self.runs + kwargs["samples_per_run"] = self.samples_per_run + + if "temperature" in kwargs: + kwargs["temperature"] *= 1e6 # Converts back to µK + return NoiseModel(**kwargs) def __post_init__(self) -> None: # only one noise was given as argument : convert it to a tuple @@ -169,13 +184,12 @@ def __post_init__(self) -> None: ) self._change_attribute("temperature", self.temperature / 1e6) - # Kept to show error messages with the right parameter names + NoiseModel._check_noise_types(self.noise) self._check_spam_dict() - - self._check_eff_noise_opers_type() - - # Runs the noise model checks - self.to_noise_model() + self._check_eff_noise() + NoiseModel._validate_parameters( + {f.name: getattr(self, f.name) for f in fields(self)} + ) @property def spam_dict(self) -> dict[str, float]: @@ -240,7 +254,7 @@ def _check_spam_dict(self) -> None: def _change_attribute(self, attr_name: str, new_value: Any) -> None: object.__setattr__(self, attr_name, new_value) - def _check_eff_noise_opers_type(self) -> None: + def _check_eff_noise(self) -> None: # Check the validity of operators for operator in self.eff_noise_opers: # type checking @@ -250,6 +264,11 @@ def _check_eff_noise_opers_type(self) -> None: raise TypeError( "Operators are supposed to be of Qutip type 'oper'." ) + NoiseModel._check_eff_noise( + self.eff_noise_rates, + self.eff_noise_opers, + "eff_noise" in self.noise, + ) @property def supported_noises(self) -> dict: diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index c466caef5..93b346235 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -48,8 +48,8 @@ def test_init_rate_like(self, param, value): if value < 0: with pytest.raises( ValueError, - match=f"'{param}' must be None or greater " - f"than or equal to zero, not {value}.", + match=f"'{param}' must be greater than " + f"or equal to zero, not {value}.", ): NoiseModel(**{param: value}) else: @@ -72,7 +72,14 @@ def test_init_prob_like(self, param, value): match=f"'{param}' must be greater than or equal to zero and " f"smaller than or equal to one, not {value}", ): - NoiseModel(**{param: value}) + NoiseModel( + # Define the strict positive quantities first so that their + # absence doesn't trigger their own errors + runs=1, + samples_per_run=1, + laser_waist=1.0, + **{param: value}, + ) @pytest.fixture def matrices(self): @@ -132,15 +139,16 @@ def test_eff_noise_opers(self, matrices): def test_eq(self, matrices): final_fields = dict( - noise_types=("SPAM", "eff_noise"), + p_false_pos=0.1, eff_noise_rates=(0.1, 0.4), eff_noise_opers=(((0, 1), (1, 0)), ((0, -1j), (1j, 0))), ) noise_model = NoiseModel( - noise_types=["SPAM", "eff_noise"], + p_false_pos=0.1, eff_noise_rates=[0.1, 0.4], eff_noise_opers=[matrices["X"], matrices["Y"]], ) assert noise_model == NoiseModel(**final_fields) + assert set(noise_model.noise_types) == {"SPAM", "eff_noise"} for param in final_fields: assert final_fields[param] == getattr(noise_model, param) diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index 5a48ccfb7..cd218795e 100644 --- a/tests/test_simconfig.py +++ b/tests/test_simconfig.py @@ -49,7 +49,7 @@ def test_init(): and "100" in str_config and "Solver Options" in str_config ) - config = SimConfig(noise=("depolarizing", "relaxation")) + config = SimConfig(noise=("depolarizing", "relaxation", "doppler")) assert config.temperature == 5e-5 assert config.to_noise_model().temperature == 50 str_config = config.__str__(True) diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 29bc0a62a..5418a9818 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -733,11 +733,7 @@ def test_noise_with_zero_epsilons(seq, matrices): noise=("SPAM"), eta=0.0, epsilon=0.0, epsilon_prime=0.0 ), ) - assert sim2.config.spam_dict == { - "eta": 0, - "epsilon": 0.0, - "epsilon_prime": 0.0, - } + assert sim2.config.noise == () assert sim.run().sample_final_state() == sim2.run().sample_final_state() From 46d45a483bebc376596bbfa4f53e7fd1559ab063 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Tue, 16 Jul 2024 12:21:56 +0200 Subject: [PATCH 02/13] Fix typing --- pulser-core/pulser/noise_model.py | 26 +++++++++++-------- .../pulser_simulation/hamiltonian.py | 16 +++++++----- .../pulser_simulation/simconfig.py | 8 +++--- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index c26e52192..48d74a8d8 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -15,8 +15,9 @@ from __future__ import annotations import json +from collections.abc import Sequence from dataclasses import asdict, dataclass -from typing import Any, Literal, get_args +from typing import Any, Literal, cast, get_args import numpy as np from numpy.typing import ArrayLike @@ -87,8 +88,8 @@ "dephasing_rate": 0.05, "hyperfine_dephasing_rate": 1e-3, "depolarizing_rate": 0.05, - "eff_noise_rates": (), - "eff_noise_opers": (), + # "eff_noise_rates": (), + # "eff_noise_opers": (), } @@ -221,7 +222,7 @@ def to_tuple(obj: tuple) -> tuple: eff_noise_rates=to_tuple(eff_noise_rates), eff_noise_opers=to_tuple(eff_noise_opers), ) - relevant_params = set() + relevant_params: set[str] = set() if noise_types is not None: # TODO: Deprecate self._check_noise_types(noise_types) @@ -237,7 +238,10 @@ def to_tuple(obj: tuple) -> tuple: "doppler", "amplitude", ) # TODO: Consider case when amp_sigma == 0. - or (n_ == "SPAM" and param_vals["state_prep_error"] > 0.0) + or ( + n_ == "SPAM" + and cast(float, param_vals["state_prep_error"]) > 0.0 + ) for n_ in noise_types ): # Define runs and samples per run from the legacy defaults @@ -259,13 +263,13 @@ def to_tuple(obj: tuple) -> tuple: relevant_params.update(NOISE_TYPE_PARAMS[noise_type_]) if noise_type_ in ("doppler", "amplitude") or ( noise_type_ == "SPAM" - and param_vals["state_prep_error"] > 0.0 + and cast(float, param_vals["state_prep_error"]) > 0.0 ): relevant_params.update(("runs", "samples_per_run")) self._check_eff_noise( - param_vals["eff_noise_rates"], - param_vals["eff_noise_opers"], + cast(tuple, param_vals["eff_noise_rates"]), + cast(tuple, param_vals["eff_noise_opers"]), "eff_noise" in (noise_types or true_noise_types), ) @@ -286,7 +290,7 @@ def to_tuple(obj: tuple) -> tuple: object.__setattr__(self, param_, param_vals[param_]) @staticmethod - def _check_noise_types(noise_types: tuple[NoiseTypes, ...]) -> None: + def _check_noise_types(noise_types: Sequence[NoiseTypes]) -> None: for noise_type in noise_types: if noise_type not in get_args(NoiseTypes): raise ValueError( @@ -297,8 +301,8 @@ def _check_noise_types(noise_types: tuple[NoiseTypes, ...]) -> None: @staticmethod def _check_eff_noise( - eff_noise_rates: tuple[float, ...], - eff_noise_opers: tuple[tuple, ...], + eff_noise_rates: Sequence[float], + eff_noise_opers: Sequence[ArrayLike], check_contents: bool, ) -> None: if len(eff_noise_opers) != len(eff_noise_rates): diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index 1dd17b7ef..7a39d4c4d 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -218,10 +218,13 @@ def add_noise( # Gaussian beam loss in amplitude for global pulses only # Noise is drawn at random for each pulse if "amplitude" in self.config.noise_types and is_global_pulse: - position = self._qdict[qid] - r = np.linalg.norm(position) - w0 = self.config.laser_waist - noise_amp = noise_amp_base * np.exp(-((r / w0) ** 2)) + amp_fraction = 1.0 + if self.config.laser_waist is not None: + position = self._qdict[qid] + r = np.linalg.norm(position) + w0 = self.config.laser_waist + amp_fraction = np.exp(-((r / w0) ** 2)) + noise_amp = noise_amp_base * amp_fraction samples_dict[qid]["amp"][slot.ti : slot.tf] *= noise_amp if local_noises: @@ -307,10 +310,9 @@ def _update_noise(self) -> None: ) self._bad_atoms = dict(zip(self._qid_index, dist)) if "doppler" in self.config.noise_types: + temp = cast(float, self.config.temperature) * 1e-6 detune = np.random.normal( - 0, - doppler_sigma(self.config.temperature / 1e6), - size=len(self._qid_index), + 0, doppler_sigma(temp), size=len(self._qid_index) ) self._doppler_detune = dict(zip(self._qid_index, detune)) diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 67df82944..889f1e469 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -17,7 +17,7 @@ from dataclasses import dataclass, field, fields from math import sqrt -from typing import Any, Optional, Type, TypeVar, Union +from typing import Any, Optional, Tuple, Type, TypeVar, Union, cast import qutip @@ -105,8 +105,8 @@ class SimConfig: """ noise: Union[NoiseTypes, tuple[NoiseTypes, ...]] = () - runs: int = _LEGACY_DEFAULTS["runs"] - samples_per_run: int = _LEGACY_DEFAULTS["samples_per_run"] + runs: int = cast(int, _LEGACY_DEFAULTS["runs"]) + samples_per_run: int = cast(int, _LEGACY_DEFAULTS["samples_per_run"]) temperature: float = _LEGACY_DEFAULTS["temperature"] laser_waist: float = _LEGACY_DEFAULTS["laser_waist"] amp_sigma: float = _LEGACY_DEFAULTS["amp_sigma"] @@ -184,7 +184,7 @@ def __post_init__(self) -> None: ) self._change_attribute("temperature", self.temperature / 1e6) - NoiseModel._check_noise_types(self.noise) + NoiseModel._check_noise_types(cast(Tuple[NoiseTypes], self.noise)) self._check_spam_dict() self._check_eff_noise() NoiseModel._validate_parameters( From e7d050284902f34e5fd3e297c78ad6e0258ff424 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Tue, 16 Jul 2024 15:09:20 +0200 Subject: [PATCH 03/13] Avoid resampling when amp_sigma=0 --- pulser-core/pulser/noise_model.py | 44 +++++++++---------- .../pulser_simulation/hamiltonian.py | 6 +++ .../pulser_simulation/simconfig.py | 15 ++++--- .../pulser_simulation/simulation.py | 13 +++++- 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 48d74a8d8..3338ff0d5 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -232,24 +232,6 @@ def to_tuple(obj: tuple) -> tuple: # Replace undefined relevant params by the legacy default if param_vals[p_] is None: param_vals[p_] = _LEGACY_DEFAULTS[p_] - if any( - n_ - in ( - "doppler", - "amplitude", - ) # TODO: Consider case when amp_sigma == 0. - or ( - n_ == "SPAM" - and cast(float, param_vals["state_prep_error"]) > 0.0 - ) - for n_ in noise_types - ): - # Define runs and samples per run from the legacy defaults - # when randomization is required - run_params = ("runs", "samples_per_run") - relevant_params.update(run_params) - for p_ in run_params: - param_vals[p_] = _LEGACY_DEFAULTS[p_] # Get rid of unnecessary None's for p_ in _POSITIVE | _PROBABILITY_LIKE: @@ -261,11 +243,6 @@ def to_tuple(obj: tuple) -> tuple: noise_type_ = _PARAM_TO_NOISE_TYPE[param_] true_noise_types.add(noise_type_) relevant_params.update(NOISE_TYPE_PARAMS[noise_type_]) - if noise_type_ in ("doppler", "amplitude") or ( - noise_type_ == "SPAM" - and cast(float, param_vals["state_prep_error"]) > 0.0 - ): - relevant_params.update(("runs", "samples_per_run")) self._check_eff_noise( cast(tuple, param_vals["eff_noise_rates"]), @@ -278,6 +255,27 @@ def to_tuple(obj: tuple) -> tuple: "Explicitly defining noise parameters without using the noise" ) + if any( + n_ == "doppler" + or ( + n_ == "amplitude" + and cast(float, param_vals["amp_sigma"]) > 0.0 + ) + or ( + n_ == "SPAM" + and cast(float, param_vals["state_prep_error"]) > 0.0 + ) + for n_ in true_noise_types + ): + relevant_params.update(run_params := ("runs", "samples_per_run")) + if noise_types is not None: + for p_ in run_params: + param_vals[p_] = param_vals[p_] or _LEGACY_DEFAULTS[p_] + + # Disregard laser_waist when not defined + if param_vals["laser_waist"] is None: + relevant_params.discard("laser_waist") + relevant_param_vals = { p: param_vals[p] for p in param_vals diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index 7a39d4c4d..619466205 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -353,6 +353,12 @@ def _construct_hamiltonian(self, update: bool = True) -> None: Also builds qutip.Qobjs related to the Sequence if not built already, and refreshes potential noise parameters by drawing new at random. + Warning: + The refreshed noise parameters (when update=True) are only those + that change from shot to shot (ie doppler and state preparation). + Amplitude fluctuations change from pulse to pulse and are always + applied in `_extract_samples()`. + Args: update: Whether to update the noise parameters. """ diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 889f1e469..8f4bf94e0 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -136,8 +136,10 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: relevant_params = {"noise_types"} for nt in noise_model.noise_types: relevant_params.update(NOISE_TYPE_PARAMS[nt]) - if nt in ("doppler", "amplitude") or ( - nt == "SPAM" and noise_model.state_prep_error != 0.0 + if ( + nt == "doppler" + or (nt == "amplitude" and noise_model.amp_sigma != 0.0) + or (nt == "SPAM" and noise_model.state_prep_error != 0.0) ): relevant_params.update(("runs", "samples_per_run")) for param in relevant_params: @@ -161,9 +163,12 @@ def to_noise_model(self) -> NoiseModel: self, custom_param_map.get(param, param) ) if any( - noise_type in ("doppler", "amplitude") - or (noise_type == "SPAM" and self.eta != 0.0) - for noise_type in self.noise + ( + nt == "doppler" + or (nt == "amplitude" and self.amp_sigma != 0.0) + or (nt == "SPAM" and self.eta != 0.0) + ) + for nt in self.noise ): kwargs["runs"] = self.runs kwargs["samples_per_run"] = self.samples_per_run diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index aa28123ef..6599daba6 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -587,7 +587,18 @@ def _run_solver() -> CoherentResults: # Check if noises ask for averaging over multiple runs: if set(self.config.noise).issubset( - {"dephasing", "relaxation", "SPAM", "depolarizing", "eff_noise"} + { + "dephasing", + "relaxation", + "SPAM", + "depolarizing", + "eff_noise", + "amplitude", + } + ) and ( + # If amplitude is in noise, not resampling needs amp_sigma=0. + "amplitude" not in self.config.noise + or self.config.amp_sigma == 0.0 ): # If there is "SPAM", the preparation errors must be zero if "SPAM" not in self.config.noise or self.config.eta == 0: From 08a705cb943919d17b9c871c105bfe381db53fa8 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Tue, 16 Jul 2024 17:08:34 +0200 Subject: [PATCH 04/13] Isolating code to find relevant noise parameters --- pulser-core/pulser/noise_model.py | 104 ++++++++++-------- .../pulser_simulation/simconfig.py | 43 +++----- 2 files changed, 74 insertions(+), 73 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 3338ff0d5..2c7c371a3 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -15,9 +15,9 @@ from __future__ import annotations import json -from collections.abc import Sequence +from collections.abc import Collection, Sequence from dataclasses import asdict, dataclass -from typing import Any, Literal, cast, get_args +from typing import Any, Literal, Union, cast, get_args import numpy as np from numpy.typing import ArrayLike @@ -38,7 +38,7 @@ "eff_noise", ] -NOISE_TYPE_PARAMS = { +_NOISE_TYPE_PARAMS = { "doppler": ("temperature",), "amplitude": ("laser_waist", "amp_sigma"), "SPAM": ("p_false_pos", "p_false_neg", "state_prep_error"), @@ -50,7 +50,7 @@ _PARAM_TO_NOISE_TYPE = { param: noise_type - for noise_type, params in NOISE_TYPE_PARAMS.items() + for noise_type, params in _NOISE_TYPE_PARAMS.items() for param in params } @@ -222,27 +222,21 @@ def to_tuple(obj: tuple) -> tuple: eff_noise_rates=to_tuple(eff_noise_rates), eff_noise_opers=to_tuple(eff_noise_opers), ) - relevant_params: set[str] = set() + if noise_types is not None: # TODO: Deprecate self._check_noise_types(noise_types) - for nt in noise_types: - relevant_params.update(NOISE_TYPE_PARAMS[nt]) - for p_ in relevant_params: - # Replace undefined relevant params by the legacy default - if param_vals[p_] is None: - param_vals[p_] = _LEGACY_DEFAULTS[p_] - - # Get rid of unnecessary None's - for p_ in _POSITIVE | _PROBABILITY_LIKE: - param_vals[p_] = param_vals[p_] or 0.0 - - true_noise_types = set() - for param_ in param_vals: - if param_vals[param_] and param_ in _PARAM_TO_NOISE_TYPE: - noise_type_ = _PARAM_TO_NOISE_TYPE[param_] - true_noise_types.add(noise_type_) - relevant_params.update(NOISE_TYPE_PARAMS[noise_type_]) + for nt_ in noise_types: + for p_ in _NOISE_TYPE_PARAMS[nt_]: + # Replace undefined relevant params by the legacy default + if param_vals[p_] is None: + param_vals[p_] = _LEGACY_DEFAULTS[p_] + + true_noise_types: set[NoiseTypes] = { + cast(NoiseTypes, _PARAM_TO_NOISE_TYPE[p_]) + for p_ in param_vals + if param_vals[p_] and p_ in _PARAM_TO_NOISE_TYPE + } self._check_eff_noise( cast(tuple, param_vals["eff_noise_rates"]), @@ -250,31 +244,30 @@ def to_tuple(obj: tuple) -> tuple: "eff_noise" in (noise_types or true_noise_types), ) - if noise_types is not None and true_noise_types != set(noise_types): - raise ValueError( # TODO: Write better - "Explicitly defining noise parameters without using the noise" - ) + # Get rid of unnecessary None's + for p_ in _POSITIVE | _PROBABILITY_LIKE: + param_vals[p_] = param_vals[p_] or 0.0 - if any( - n_ == "doppler" - or ( - n_ == "amplitude" - and cast(float, param_vals["amp_sigma"]) > 0.0 - ) - or ( - n_ == "SPAM" - and cast(float, param_vals["state_prep_error"]) > 0.0 - ) - for n_ in true_noise_types - ): - relevant_params.update(run_params := ("runs", "samples_per_run")) - if noise_types is not None: - for p_ in run_params: - param_vals[p_] = param_vals[p_] or _LEGACY_DEFAULTS[p_] + relevant_params = self._find_relevant_params( + true_noise_types, + cast(float, param_vals["state_prep_error"]), + cast(float, param_vals["amp_sigma"]), + cast(Union[float, None], param_vals["laser_waist"]), + ) - # Disregard laser_waist when not defined - if param_vals["laser_waist"] is None: - relevant_params.discard("laser_waist") + if noise_types is not None: + if true_noise_types != set(noise_types): + raise ValueError( + "The explicit definition of noise types (deprecated) is" + " not compatible with the modification of unrelated noise " + "parameters. Defining only the relevant noise parameters " + "(without specifying the noise types) is recommended." + ) + run_params_ = [ + p for p in relevant_params if p in ("runs", "samples_per_run") + ] + for p_ in run_params_: + param_vals[p_] = param_vals[p_] or _LEGACY_DEFAULTS[p_] relevant_param_vals = { p: param_vals[p] @@ -287,6 +280,27 @@ def to_tuple(obj: tuple) -> tuple: for param_ in param_vals: object.__setattr__(self, param_, param_vals[param_]) + @staticmethod + def _find_relevant_params( + noise_types: Collection[NoiseTypes], + state_prep_error: float, + amp_sigma: float, + laser_waist: float | None, + ) -> set[str]: + relevant_params: set[str] = set() + for nt_ in noise_types: + relevant_params.update(_NOISE_TYPE_PARAMS[nt_]) + if ( + nt_ == "doppler" + or (nt_ == "amplitude" and amp_sigma != 0.0) + or (nt_ == "SPAM" and state_prep_error != 0.0) + ): + relevant_params.update(("runs", "samples_per_run")) + # Disregard laser_waist when not defined + if laser_waist is None: + relevant_params.discard("laser_waist") + return relevant_params + @staticmethod def _check_noise_types(noise_types: Sequence[NoiseTypes]) -> None: for noise_type in noise_types: diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 8f4bf94e0..c64110a87 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -23,7 +23,6 @@ from pulser.noise_model import ( _LEGACY_DEFAULTS, - NOISE_TYPE_PARAMS, NoiseModel, NoiseTypes, ) @@ -127,21 +126,17 @@ class SimConfig: def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: """Creates a SimConfig from a NoiseModel.""" custom_param_map = { - "noise_types": "noise", "state_prep_error": "eta", "p_false_pos": "epsilon", "p_false_neg": "epsilon_prime", } - kwargs = {} - relevant_params = {"noise_types"} - for nt in noise_model.noise_types: - relevant_params.update(NOISE_TYPE_PARAMS[nt]) - if ( - nt == "doppler" - or (nt == "amplitude" and noise_model.amp_sigma != 0.0) - or (nt == "SPAM" and noise_model.state_prep_error != 0.0) - ): - relevant_params.update(("runs", "samples_per_run")) + kwargs = dict(noise=noise_model.noise_types) + relevant_params = NoiseModel._find_relevant_params( + noise_model.noise_types, + noise_model.state_prep_error, + noise_model.amp_sigma, + noise_model.laser_waist, + ) for param in relevant_params: kwargs[custom_param_map.get(param, param)] = getattr( noise_model, param @@ -156,23 +151,15 @@ def to_noise_model(self) -> NoiseModel: "p_false_pos": "epsilon", "p_false_neg": "epsilon_prime", } + relevant_params = NoiseModel._find_relevant_params( + cast(Tuple[NoiseTypes, ...], self.noise), + self.eta, + self.amp_sigma, + self.laser_waist, + ) kwargs = {} - for noise_type in self.noise: - for param in NOISE_TYPE_PARAMS[noise_type]: - kwargs[param] = getattr( - self, custom_param_map.get(param, param) - ) - if any( - ( - nt == "doppler" - or (nt == "amplitude" and self.amp_sigma != 0.0) - or (nt == "SPAM" and self.eta != 0.0) - ) - for nt in self.noise - ): - kwargs["runs"] = self.runs - kwargs["samples_per_run"] = self.samples_per_run - + for param in relevant_params: + kwargs[param] = getattr(self, custom_param_map.get(param, param)) if "temperature" in kwargs: kwargs["temperature"] *= 1e6 # Converts back to µK return NoiseModel(**kwargs) From 28459fe136787e4d4f7f512261d6e1035aa3083f Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Wed, 17 Jul 2024 10:31:38 +0200 Subject: [PATCH 05/13] Define a custom NoiseModel.__repr__() --- pulser-core/pulser/noise_model.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 2c7c371a3..7a605a387 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -16,7 +16,7 @@ import json from collections.abc import Collection, Sequence -from dataclasses import asdict, dataclass +from dataclasses import asdict, dataclass, fields from typing import Any, Literal, Union, cast, get_args import numpy as np @@ -88,12 +88,10 @@ "dephasing_rate": 0.05, "hyperfine_dephasing_rate": 1e-3, "depolarizing_rate": 0.05, - # "eff_noise_rates": (), - # "eff_noise_opers": (), } -@dataclass(init=False, frozen=True) +@dataclass(init=False, repr=False, frozen=True) class NoiseModel: """Specifies the noise model parameters for emulation. @@ -385,6 +383,20 @@ def _to_abstract_repr(self) -> dict[str, Any]: all_fields["eff_noise"] = list(zip(eff_noise_rates, eff_noise_opers)) return all_fields + def __repr__(self) -> str: + relevant_params = self._find_relevant_params( + self.noise_types, + self.state_prep_error, + self.amp_sigma, + self.laser_waist, + ) + relevant_params.add("noise_types") + params_list = [] + for f in fields(self): + if f.name in relevant_params: + params_list.append(f"{f.name}={getattr(self, f.name)!r}") + return f"{self.__class__.__name__}({', '.join(params_list)})" + def to_abstract_repr(self) -> str: """Serializes the noise model into an abstract JSON object.""" abstr_str = json.dumps(self, cls=AbstractReprEncoder) From 9fb6ceb3e99a7dcec95564e01875ba2954f0b78f Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Wed, 17 Jul 2024 12:45:32 +0200 Subject: [PATCH 06/13] Deprecating noise_types definition --- .../pulser/json/abstract_repr/deserializer.py | 8 +++-- pulser-core/pulser/noise_model.py | 14 +++++++- .../pulser_simulation/simconfig.py | 8 ++--- .../pulser_simulation/simulation.py | 35 +++++-------------- tests/test_abstract_repr.py | 3 -- tests/test_noise_model.py | 13 +++---- tests/test_qutip_backend.py | 8 ++++- tests/test_simconfig.py | 4 +-- .../Backends for Sequence Execution.ipynb | 2 -- 9 files changed, 43 insertions(+), 52 deletions(-) diff --git a/pulser-core/pulser/json/abstract_repr/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index e76f1a900..ff6d1b784 100644 --- a/pulser-core/pulser/json/abstract_repr/deserializer.py +++ b/pulser-core/pulser/json/abstract_repr/deserializer.py @@ -410,7 +410,7 @@ def _deserialize_register3d( def _deserialize_noise_model(noise_model_obj: dict[str, Any]) -> NoiseModel: - def convert_complex(obj: list | tuple) -> list: + def convert_complex(obj: Any) -> Any: if isinstance(obj, (list, tuple)): return [convert_complex(e) for e in obj] elif isinstance(obj, dict): @@ -423,11 +423,15 @@ def convert_complex(obj: list | tuple) -> list: for rate, oper in noise_model_obj.pop("eff_noise"): eff_noise_rates.append(rate) eff_noise_opers.append(convert_complex(oper)) - return pulser.NoiseModel( + + noise_types = noise_model_obj.pop("noise_types") + noise_model = pulser.NoiseModel( **noise_model_obj, eff_noise_rates=tuple(eff_noise_rates), eff_noise_opers=tuple(eff_noise_opers), ) + assert set(noise_model.noise_types) == set(noise_types) + return noise_model def _deserialize_device_object(obj: dict[str, Any]) -> Device | VirtualDevice: diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 7a605a387..31396d1b8 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -15,6 +15,7 @@ from __future__ import annotations import json +import warnings from collections.abc import Collection, Sequence from dataclasses import asdict, dataclass, fields from typing import Any, Literal, Union, cast, get_args @@ -222,7 +223,18 @@ def to_tuple(obj: tuple) -> tuple: ) if noise_types is not None: - # TODO: Deprecate + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "The explicit definition of noise types is deprecated; " + "doing so will use legacy default values for all relevant " + "parameters that are not given a custom value. Instead, " + "defining only the necessary parameters is reccomended; " + "doing so (when the noise types are not explicitly given) " + "will disregard all undefined parameters.", + DeprecationWarning, + stacklevel=2, + ) self._check_noise_types(noise_types) for nt_ in noise_types: for p_ in _NOISE_TYPE_PARAMS[nt_]: diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index c64110a87..ed8afea61 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -21,11 +21,7 @@ import qutip -from pulser.noise_model import ( - _LEGACY_DEFAULTS, - NoiseModel, - NoiseTypes, -) +from pulser.noise_model import _LEGACY_DEFAULTS, NoiseModel, NoiseTypes MASS = 1.45e-25 # kg KB = 1.38e-23 # J/K @@ -130,7 +126,7 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: "p_false_pos": "epsilon", "p_false_neg": "epsilon_prime", } - kwargs = dict(noise=noise_model.noise_types) + kwargs: dict[str, Any] = dict(noise=noise_model.noise_types) relevant_params = NoiseModel._find_relevant_params( noise_model.noise_types, noise_model.state_prep_error, diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index 6599daba6..b06dd350e 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -250,33 +250,16 @@ def add_config(self, config: SimConfig) -> None: diff_noise_set = new_noise_set - old_noise_set # Create temporary param_dict to add noise parameters: param_dict: dict[str, Any] = asdict(self._hamiltonian.config) - # Begin populating with added noise parameters: - param_dict["noise_types"] = tuple(new_noise_set) - if "SPAM" in diff_noise_set: - param_dict["state_prep_error"] = noise_model.state_prep_error - param_dict["p_false_pos"] = noise_model.p_false_pos - param_dict["p_false_neg"] = noise_model.p_false_neg - if "doppler" in diff_noise_set: - param_dict["temperature"] = noise_model.temperature - if "amplitude" in diff_noise_set: - param_dict["laser_waist"] = noise_model.laser_waist - param_dict["amp_sigma"] = noise_model.amp_sigma - if "dephasing" in diff_noise_set: - param_dict["dephasing_rate"] = noise_model.dephasing_rate - param_dict["hyperfine_dephasing_rate"] = ( - noise_model.hyperfine_dephasing_rate - ) - if "relaxation" in diff_noise_set: - param_dict["relaxation_rate"] = noise_model.relaxation_rate - if "depolarizing" in diff_noise_set: - param_dict["depolarizing_rate"] = noise_model.depolarizing_rate - if "eff_noise" in diff_noise_set: - param_dict["eff_noise_opers"] = noise_model.eff_noise_opers - param_dict["eff_noise_rates"] = noise_model.eff_noise_rates - # update runs: - param_dict["runs"] = noise_model.runs - param_dict["samples_per_run"] = noise_model.samples_per_run + relevant_params = NoiseModel._find_relevant_params( + diff_noise_set, + noise_model.state_prep_error, + noise_model.amp_sigma, + noise_model.laser_waist, + ) + for param in relevant_params: + param_dict[param] = getattr(noise_model, param) # set config with the new parameters: + param_dict.pop("noise_types") self._hamiltonian.set_config(NoiseModel(**param_dict)) def show_config(self, solver_options: bool = False) -> None: diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 3608bb766..3cf19b34a 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -78,10 +78,8 @@ replace(Chadoq2.dmm_objects[0], total_bottom_detuning=-2000), ), default_noise_model=NoiseModel( - noise_types=("SPAM", "relaxation", "dephasing"), p_false_pos=0.02, p_false_neg=0.01, - state_prep_error=0.0, # To avoid Hamiltonian resampling relaxation_rate=0.01, dephasing_rate=0.2, ), @@ -178,7 +176,6 @@ def test_register(reg: Register | Register3D): [ NoiseModel(), NoiseModel( - noise_types=("eff_noise",), eff_noise_rates=(0.1,), eff_noise_opers=(((0, -1j), (1j, 0)),), ), diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index 93b346235..2e8f28c1e 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -22,7 +22,8 @@ def test_bad_noise_type(self): with pytest.raises( ValueError, match="'bad_noise' is not a valid noise type." ): - NoiseModel(noise_types=("bad_noise",)) + with pytest.warns(DeprecationWarning): + NoiseModel(noise_types=("bad_noise",)) @pytest.mark.parametrize( "param", @@ -97,19 +98,17 @@ def test_eff_noise_rates(self, matrices): ValueError, match="The provided rates must be greater than 0." ): NoiseModel( - noise_types=("eff_noise",), eff_noise_opers=[matrices["I"], matrices["X"]], eff_noise_rates=[-1.0, 0.5], ) def test_eff_noise_opers(self, matrices): with pytest.raises(ValueError, match="The operators list length"): - NoiseModel(noise_types=("eff_noise",), eff_noise_rates=[1.0]) + NoiseModel(eff_noise_rates=[1.0]) with pytest.raises( TypeError, match="eff_noise_rates is a list of floats" ): NoiseModel( - noise_types=("eff_noise",), eff_noise_rates=["0.1"], eff_noise_opers=[np.eye(2)], ) @@ -117,22 +116,20 @@ def test_eff_noise_opers(self, matrices): ValueError, match="The effective noise parameters have not been filled.", ): - NoiseModel(noise_types=("eff_noise",)) + with pytest.warns(DeprecationWarning): + NoiseModel(noise_types=("eff_noise",)) with pytest.raises(TypeError, match="not castable to a Numpy array"): NoiseModel( - noise_types=("eff_noise",), eff_noise_rates=[2.0], eff_noise_opers=[{(1.0, 0), (0.0, -1)}], ) with pytest.raises(ValueError, match="is not a 2D array."): NoiseModel( - noise_types=("eff_noise",), eff_noise_opers=[2.0], eff_noise_rates=[1.0], ) with pytest.raises(NotImplementedError, match="Operator's shape"): NoiseModel( - noise_types=("eff_noise",), eff_noise_opers=[matrices["I3"]], eff_noise_rates=[1.0], ) diff --git a/tests/test_qutip_backend.py b/tests/test_qutip_backend.py index 0214f00dc..5e9dd48f0 100644 --- a/tests/test_qutip_backend.py +++ b/tests/test_qutip_backend.py @@ -70,7 +70,13 @@ def test_qutip_backend(sequence): def test_with_default_noise(sequence): - spam_noise = pulser.NoiseModel(noise_types=("SPAM",)) + spam_noise = pulser.NoiseModel( + p_false_pos=0.1, + p_false_neg=0.05, + state_prep_error=0.1, + runs=10, + samples_per_run=1, + ) new_device = dataclasses.replace( MockDevice, default_noise_model=spam_noise ) diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index cd218795e..85ccd0320 100644 --- a/tests/test_simconfig.py +++ b/tests/test_simconfig.py @@ -118,11 +118,9 @@ def test_eff_noise_opers(matrices): def test_from_noise_model(): noise_model = NoiseModel( - noise_types=("SPAM",), p_false_neg=0.4, p_false_pos=0.1, - state_prep_error=0.05, ) assert SimConfig.from_noise_model(noise_model) == SimConfig( - noise="SPAM", epsilon=0.1, epsilon_prime=0.4, eta=0.05 + noise="SPAM", epsilon=0.1, epsilon_prime=0.4, eta=0.0 ) diff --git a/tutorials/advanced_features/Backends for Sequence Execution.ipynb b/tutorials/advanced_features/Backends for Sequence Execution.ipynb index ce5b6e53b..b85ec320d 100644 --- a/tutorials/advanced_features/Backends for Sequence Execution.ipynb +++ b/tutorials/advanced_features/Backends for Sequence Execution.ipynb @@ -190,10 +190,8 @@ "config = pulser.EmulatorConfig(\n", " sampling_rate=0.1,\n", " noise_model=pulser.NoiseModel(\n", - " noise_types=(\"SPAM\",),\n", " p_false_pos=0.01,\n", " p_false_neg=0.004,\n", - " state_prep_error=0.0,\n", " ),\n", ")\n", "\n", From b7db22b1a27847ff40a7afb8dce7dca128a53b5a Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Wed, 17 Jul 2024 15:35:45 +0200 Subject: [PATCH 07/13] Improving adjacent UTs --- .../pulser_simulation/simconfig.py | 23 ++++++++----------- tests/test_abstract_repr.py | 2 ++ tests/test_simconfig.py | 6 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index ed8afea61..3e8735c20 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -47,6 +47,14 @@ }, } +# Maps the noise model parameters with a different name in SimConfig +_DIFF_NOISE_PARAMS = { + "noise_types": "noise", + "state_prep_error": "eta", + "p_false_pos": "epsilon", + "p_false_neg": "epsilon_prime", +} + def doppler_sigma(temperature: float) -> float: """Standard deviation for Doppler shifting due to thermal motion. @@ -121,11 +129,6 @@ class SimConfig: @classmethod def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: """Creates a SimConfig from a NoiseModel.""" - custom_param_map = { - "state_prep_error": "eta", - "p_false_pos": "epsilon", - "p_false_neg": "epsilon_prime", - } kwargs: dict[str, Any] = dict(noise=noise_model.noise_types) relevant_params = NoiseModel._find_relevant_params( noise_model.noise_types, @@ -134,19 +137,13 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: noise_model.laser_waist, ) for param in relevant_params: - kwargs[custom_param_map.get(param, param)] = getattr( + kwargs[_DIFF_NOISE_PARAMS.get(param, param)] = getattr( noise_model, param ) return cls(**kwargs) def to_noise_model(self) -> NoiseModel: """Creates a NoiseModel from the SimConfig.""" - custom_param_map = { - "noise_types": "noise", - "state_prep_error": "eta", - "p_false_pos": "epsilon", - "p_false_neg": "epsilon_prime", - } relevant_params = NoiseModel._find_relevant_params( cast(Tuple[NoiseTypes, ...], self.noise), self.eta, @@ -155,7 +152,7 @@ def to_noise_model(self) -> NoiseModel: ) kwargs = {} for param in relevant_params: - kwargs[param] = getattr(self, custom_param_map.get(param, param)) + kwargs[param] = getattr(self, _DIFF_NOISE_PARAMS.get(param, param)) if "temperature" in kwargs: kwargs["temperature"] *= 1e6 # Converts back to µK return NoiseModel(**kwargs) diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 3cf19b34a..3cefbb095 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -175,6 +175,8 @@ def test_register(reg: Register | Register3D): "noise_model", [ NoiseModel(), + NoiseModel(laser_waist=100), + NoiseModel(temperature=100, runs=10, samples_per_run=1), NoiseModel( eff_noise_rates=(0.1,), eff_noise_opers=(((0, -1j), (1j, 0)),), diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index 85ccd0320..765257e40 100644 --- a/tests/test_simconfig.py +++ b/tests/test_simconfig.py @@ -116,11 +116,13 @@ def test_eff_noise_opers(matrices): ) -def test_from_noise_model(): +def test_noise_model_conversion(): noise_model = NoiseModel( p_false_neg=0.4, p_false_pos=0.1, ) - assert SimConfig.from_noise_model(noise_model) == SimConfig( + expected_simconfig = SimConfig( noise="SPAM", epsilon=0.1, epsilon_prime=0.4, eta=0.0 ) + assert SimConfig.from_noise_model(noise_model) == expected_simconfig + assert expected_simconfig.to_noise_model() == noise_model From 8b0b26dcf89723978624d73206044d8de86c85dc Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Wed, 17 Jul 2024 17:35:43 +0200 Subject: [PATCH 08/13] Complete NoiseModel UTs --- pulser-core/pulser/noise_model.py | 14 +- .../pulser_simulation/simulation.py | 1 + tests/test_noise_model.py | 210 +++++++++++++++++- 3 files changed, 210 insertions(+), 15 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 31396d1b8..d5bf37ea0 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -286,9 +286,17 @@ def to_tuple(obj: tuple) -> tuple: } self._validate_parameters(relevant_param_vals) - object.__setattr__(self, "noise_types", tuple(true_noise_types)) - for param_ in param_vals: - object.__setattr__(self, param_, param_vals[param_]) + object.__setattr__( + self, "noise_types", tuple(sorted(true_noise_types)) + ) + for param_, val_ in param_vals.items(): + object.__setattr__(self, param_, val_) + if val_ and param_ not in relevant_params: + warnings.warn( + f"{param_!r} is not used by any active noise type " + f"{self.noise_types}.", + stacklevel=2, + ) @staticmethod def _find_relevant_params( diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index b06dd350e..4a7185370 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -529,6 +529,7 @@ def _run_solver() -> CoherentResults: raise ValueError("`progress_bar` must be a bool.") if ( + # TODO: Check that the relevant dephasing parameter is > 0. "dephasing" in self.config.noise or "relaxation" in self.config.noise or "depolarizing" in self.config.noise diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index 2e8f28c1e..0d06ff9e6 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -11,6 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + +import dataclasses +import re + import numpy as np import pytest @@ -18,12 +23,60 @@ class TestNoiseModel: - def test_bad_noise_type(self): - with pytest.raises( - ValueError, match="'bad_noise' is not a valid noise type." - ): - with pytest.warns(DeprecationWarning): - NoiseModel(noise_types=("bad_noise",)) + + @pytest.mark.parametrize( + "params, noise_types", + [ + ({"p_false_pos", "dephasing_rate"}, {"SPAM", "dephasing"}), + ( + { + "state_prep_error", + "relaxation_rate", + "runs", + "samples_per_run", + }, + {"SPAM", "relaxation"}, + ), + ( + { + "temperature", + "depolarizing_rate", + "runs", + "samples_per_run", + }, + {"doppler", "depolarizing"}, + ), + ( + {"amp_sigma", "runs", "samples_per_run"}, + {"amplitude"}, + ), + ( + {"laser_waist", "hyperfine_dephasing_rate"}, + {"amplitude", "dephasing"}, + ), + ], + ) + def test_init(self, params, noise_types): + noise_model = NoiseModel(**{p: 1.0 for p in params}) + assert set(noise_model.noise_types) == noise_types + relevant_params = NoiseModel._find_relevant_params( + noise_types, + noise_model.state_prep_error, + noise_model.amp_sigma, + noise_model.laser_waist, + ) + assert all(getattr(noise_model, p) == 1.0 for p in params) + assert all( + not getattr(noise_model, p) for p in relevant_params - params + ) + + @pytest.mark.parametrize( + "noise_param", ["relaxation_rate", "p_false_neg", "laser_waist"] + ) + @pytest.mark.parametrize("unused_param", ["runs", "samples_per_run"]) + def test_unused_params(self, unused_param, noise_param): + with pytest.warns(UserWarning, match=f"'{unused_param}' is not used"): + NoiseModel(**{unused_param: 100, noise_param: 1.0}) @pytest.mark.parametrize( "param", @@ -112,12 +165,6 @@ def test_eff_noise_opers(self, matrices): eff_noise_rates=["0.1"], eff_noise_opers=[np.eye(2)], ) - with pytest.raises( - ValueError, - match="The effective noise parameters have not been filled.", - ): - with pytest.warns(DeprecationWarning): - NoiseModel(noise_types=("eff_noise",)) with pytest.raises(TypeError, match="not castable to a Numpy array"): NoiseModel( eff_noise_rates=[2.0], @@ -149,3 +196,142 @@ def test_eq(self, matrices): assert set(noise_model.noise_types) == {"SPAM", "eff_noise"} for param in final_fields: assert final_fields[param] == getattr(noise_model, param) + + def test_relevant_params(self): + assert NoiseModel._find_relevant_params({"SPAM"}, 0.0, 0.5, 100) == { + "state_prep_error", + "p_false_pos", + "p_false_neg", + } + assert NoiseModel._find_relevant_params({"SPAM"}, 0.1, 0.5, 100) == { + "state_prep_error", + "p_false_pos", + "p_false_neg", + "runs", + "samples_per_run", + } + + assert NoiseModel._find_relevant_params( + {"doppler"}, 0.0, 0.0, None + ) == {"temperature", "runs", "samples_per_run"} + + assert NoiseModel._find_relevant_params( + {"amplitude"}, 0.0, 1.0, None + ) == {"amp_sigma", "runs", "samples_per_run"} + assert NoiseModel._find_relevant_params( + {"amplitude"}, 0.0, 0.0, 100.0 + ) == {"amp_sigma", "laser_waist"} + assert NoiseModel._find_relevant_params( + {"amplitude"}, 0.0, 0.5, 100.0 + ) == {"amp_sigma", "laser_waist", "runs", "samples_per_run"} + + assert NoiseModel._find_relevant_params( + {"dephasing"}, 0.0, 0.0, None + ) == {"dephasing_rate", "hyperfine_dephasing_rate"} + assert NoiseModel._find_relevant_params( + {"relaxation"}, 0.0, 0.0, None + ) == {"relaxation_rate"} + assert NoiseModel._find_relevant_params( + {"depolarizing"}, 0.0, 0.0, None + ) == {"depolarizing_rate"} + assert NoiseModel._find_relevant_params( + {"eff_noise"}, 0.0, 0.0, None + ) == {"eff_noise_rates", "eff_noise_opers"} + + def test_repr(self): + assert repr(NoiseModel()) == "NoiseModel(noise_types=())" + assert ( + repr(NoiseModel(p_false_pos=0.1, relaxation_rate=0.2)) + == "NoiseModel(noise_types=('SPAM', 'relaxation'), " + "state_prep_error=0.0, p_false_pos=0.1, p_false_neg=0.0, " + "relaxation_rate=0.2)" + ) + assert ( + repr(NoiseModel(hyperfine_dephasing_rate=0.2)) + == "NoiseModel(noise_types=('dephasing',), " + "dephasing_rate=0.0, hyperfine_dephasing_rate=0.2)" + ) + assert ( + repr(NoiseModel(amp_sigma=0.3, runs=100, samples_per_run=1)) + == "NoiseModel(noise_types=('amplitude',), " + "runs=100, samples_per_run=1, amp_sigma=0.3)" + ) + assert ( + repr(NoiseModel(laser_waist=100.0)) + == "NoiseModel(noise_types=('amplitude',), " + "laser_waist=100.0, amp_sigma=0.0)" + ) + + +class TestLegacyNoiseModel: + def test_noise_type_errors(self): + with pytest.raises( + ValueError, match="'bad_noise' is not a valid noise type." + ): + with pytest.deprecated_call(): + NoiseModel(noise_types=("bad_noise",)) + + with pytest.raises( + ValueError, + match="The effective noise parameters have not been filled.", + ): + with pytest.deprecated_call(): + NoiseModel(noise_types=("eff_noise",)) + + with pytest.raises( + ValueError, + match=re.escape( + "The explicit definition of noise types (deprecated) is" + " not compatible with the modification of unrelated noise " + "parameters" + ), + ): + with pytest.deprecated_call(): + NoiseModel(noise_types=("SPAM",), laser_waist=100.0) + + @pytest.mark.parametrize( + "noise_type", ["SPAM", "doppler", "amplitude", "dephasing"] + ) + def test_legacy_init(self, noise_type): + expected_relevant_params = dict( + SPAM={ + "state_prep_error", + "p_false_pos", + "p_false_neg", + "runs", + "samples_per_run", + }, + amplitude={"laser_waist", "amp_sigma", "runs", "samples_per_run"}, + doppler={"temperature", "runs", "samples_per_run"}, + dephasing={"dephasing_rate", "hyperfine_dephasing_rate"}, + ) + non_zero_param = tuple(expected_relevant_params[noise_type])[0] + + with pytest.warns( + DeprecationWarning, + match="The explicit definition of noise types is deprecated", + ): + noise_model = NoiseModel( + **{"noise_types": (noise_type,), non_zero_param: 1} + ) + + # Check that the parameter is not overwritten by the default + assert getattr(noise_model, non_zero_param) == 1 + + relevant_params = NoiseModel._find_relevant_params( + {noise_type}, + # These values don't matter, they just have to be > 0 + state_prep_error=0.1, + amp_sigma=0.5, + laser_waist=100.0, + ) + assert relevant_params == expected_relevant_params[noise_type] + + for f in dataclasses.fields(noise_model): + val = getattr(noise_model, f.name) + if f.name == "noise_types": + assert val == (noise_type,) + elif f.name in relevant_params: + assert val > 0.0 + else: + assert not val From cb0822e4e98772e2057055bbe85ebd57fb916722 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Wed, 17 Jul 2024 17:35:55 +0200 Subject: [PATCH 09/13] Update NoiseModel JSON schema --- .../abstract_repr/schemas/noise-schema.json | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json index 6fbaecee8..5eac37f84 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json @@ -66,7 +66,10 @@ "type": "number" }, "laser_waist": { - "type": "number" + "type": [ + "number", + "null" + ] }, "noise_types": { "items": { @@ -84,16 +87,25 @@ "type": "number" }, "runs": { - "type": "number" + "type": [ + "number", + "null" + ] }, "samples_per_run": { - "type": "number" + "type": [ + "number", + "null" + ] }, "state_prep_error": { "type": "number" }, "temperature": { - "type": "number" + "type": [ + "number", + "null" + ] } }, "required": [ From 791c280b5456df62c06656a0fc264716486e49f2 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Wed, 17 Jul 2024 18:35:33 +0200 Subject: [PATCH 10/13] Fix docstring indentation --- pulser-core/pulser/noise_model.py | 54 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index d5bf37ea0..7262f9c38 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -97,36 +97,30 @@ class NoiseModel: """Specifies the noise model parameters for emulation. Supported noise types: - - "relaxation": Noise due to a decay from the Rydberg to - the ground state (parametrized by `relaxation_rate`), commonly - characterized experimentally by the T1 time. - - - "dephasing": Random phase (Z) flip (parametrized - by `dephasing_rate`), commonly characterized experimentally - by the T2* time. - - - "depolarizing": Quantum noise where the state is - turned into the maximally mixed state with rate - `depolarizing_rate`. While it does not describe a physical - phenomenon, it is a commonly used tool to test the system - under a uniform combination of phase flip (Z) and - bit flip (X) errors. - - - "eff_noise": General effective noise channel defined by - the set of collapse operators `eff_noise_opers` - and the corresponding rates distribution - `eff_noise_rates`. - - - "doppler": Local atom detuning due to termal motion of the - atoms and Doppler effect with respect to laser frequency. - Parametrized by the `temperature` field. - - - "amplitude": Gaussian damping due to finite laser waist and - laser amplitude fluctuations. Parametrized by `laser_waist` - and `amp_sigma`. - - - "SPAM": SPAM errors. Parametrized by - `state_prep_error`, `p_false_pos` and `p_false_neg`. + + - **relaxation**: Noise due to a decay from the Rydberg to + the ground state (parametrized by ``relaxation_rate``), + commonly characterized experimentally by the T1 time. + - **dephasing**: Random phase (Z) flip (parametrized + by ``dephasing_rate``), commonly characterized + experimentally by the T2* time. + - **depolarizing**: Quantum noise where the state is + turned into the maximally mixed state with rate + ``depolarizing_rate``. While it does not describe a + physical phenomenon, it is a commonly used tool to test + the system under a uniform combination of phase flip (Z) and + bit flip (X) errors. + - **eff_noise**: General effective noise channel defined by the + set of collapse operators ``eff_noise_opers`` and the + corresponding rates distribution ``eff_noise_rates``. + - **doppler**: Local atom detuning due to termal motion of the + atoms and Doppler effect with respect to laser frequency. + Parametrized by the ``temperature`` field. + - **amplitude**: Gaussian damping due to finite laser waist and + laser amplitude fluctuations. Parametrized by ``laser_waist`` + and ``amp_sigma``. + - **SPAM**: SPAM errors. Parametrized by ``state_prep_error``, + ``p_false_pos`` and ``p_false_neg``. Args: noise_types: *Deprecated, simply define the approriate parameters From 383b65c65bd074316a10b05655ef092e88673493 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 18 Jul 2024 17:47:51 +0200 Subject: [PATCH 11/13] Implementing review suggestions --- pulser-core/pulser/noise_model.py | 19 ++++++++++--------- tests/test_noise_model.py | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 7262f9c38..72bec8702 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -39,7 +39,7 @@ "eff_noise", ] -_NOISE_TYPE_PARAMS = { +_NOISE_TYPE_PARAMS: dict[NoiseTypes, tuple[str, ...]] = { "doppler": ("temperature",), "amplitude": ("laser_waist", "amp_sigma"), "SPAM": ("p_false_pos", "p_false_neg", "state_prep_error"), @@ -49,7 +49,7 @@ "eff_noise": ("eff_noise_rates", "eff_noise_opers"), } -_PARAM_TO_NOISE_TYPE = { +_PARAM_TO_NOISE_TYPE: dict[str, NoiseTypes] = { param: noise_type for noise_type, params in _NOISE_TYPE_PARAMS.items() for param in params @@ -111,8 +111,8 @@ class NoiseModel: the system under a uniform combination of phase flip (Z) and bit flip (X) errors. - **eff_noise**: General effective noise channel defined by the - set of collapse operators ``eff_noise_opers`` and the - corresponding rates distribution ``eff_noise_rates``. + set of collapse operators ``eff_noise_opers`` and their + corresponding rates ``eff_noise_rates``. - **doppler**: Local atom detuning due to termal motion of the atoms and Doppler effect with respect to laser frequency. Parametrized by the ``temperature`` field. @@ -223,7 +223,7 @@ def to_tuple(obj: tuple) -> tuple: "The explicit definition of noise types is deprecated; " "doing so will use legacy default values for all relevant " "parameters that are not given a custom value. Instead, " - "defining only the necessary parameters is reccomended; " + "defining only the necessary parameters is recommended; " "doing so (when the noise types are not explicitly given) " "will disregard all undefined parameters.", DeprecationWarning, @@ -237,7 +237,7 @@ def to_tuple(obj: tuple) -> tuple: param_vals[p_] = _LEGACY_DEFAULTS[p_] true_noise_types: set[NoiseTypes] = { - cast(NoiseTypes, _PARAM_TO_NOISE_TYPE[p_]) + _PARAM_TO_NOISE_TYPE[p_] for p_ in param_vals if param_vals[p_] and p_ in _PARAM_TO_NOISE_TYPE } @@ -267,9 +267,10 @@ def to_tuple(obj: tuple) -> tuple: "parameters. Defining only the relevant noise parameters " "(without specifying the noise types) is recommended." ) - run_params_ = [ - p for p in relevant_params if p in ("runs", "samples_per_run") - ] + # Only now that we know the relevant_params can we determine if + # we need to use the legacy defaults for the run parameters (ie in + # case they were not provided by the user) + run_params_ = relevant_params & {"runs", "samples_per_run"} for p_ in run_params_: param_vals[p_] = param_vals[p_] or _LEGACY_DEFAULTS[p_] diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index 0d06ff9e6..1e6448ee1 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -19,7 +19,21 @@ import numpy as np import pytest -from pulser.noise_model import NoiseModel +from pulser.noise_model import ( + NoiseModel, + _NOISE_TYPE_PARAMS, + _PARAM_TO_NOISE_TYPE, +) + + +def test_constants(): + # Recreate _PARAM_TO_NOISE_TYPE and check it matches + params_dict = {} + for noise_type, params in _NOISE_TYPE_PARAMS.items(): + for p in params: + assert p not in params_dict + params_dict[p] = noise_type + assert params_dict == _PARAM_TO_NOISE_TYPE class TestNoiseModel: From 72b85c2fab8d6a3a752116982ccd82d2ab93bd37 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 18 Jul 2024 19:10:29 +0200 Subject: [PATCH 12/13] Allowing temperature to be 0 --- pulser-core/pulser/noise_model.py | 4 +- .../pulser_simulation/hamiltonian.py | 2 +- tests/test_noise_model.py | 56 ++++++++++++------- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 72bec8702..349b2a59f 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -62,11 +62,11 @@ "hyperfine_dephasing_rate", "relaxation_rate", "depolarizing_rate", + "temperature", } _STRICT_POSITIVE = { "runs", "samples_per_run", - "temperature", "laser_waist", } _PROBABILITY_LIKE = { @@ -164,7 +164,7 @@ class NoiseModel: state_prep_error: float p_false_pos: float p_false_neg: float - temperature: float | None + temperature: float laser_waist: float | None amp_sigma: float relaxation_rate: float diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index 619466205..aa39cf6d7 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -310,7 +310,7 @@ def _update_noise(self) -> None: ) self._bad_atoms = dict(zip(self._qid_index, dist)) if "doppler" in self.config.noise_types: - temp = cast(float, self.config.temperature) * 1e-6 + temp = self.config.temperature * 1e-6 detune = np.random.normal( 0, doppler_sigma(temp), size=len(self._qid_index) ) diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index 1e6448ee1..a5e411754 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -20,9 +20,9 @@ import pytest from pulser.noise_model import ( - NoiseModel, _NOISE_TYPE_PARAMS, _PARAM_TO_NOISE_TYPE, + NoiseModel, ) @@ -94,7 +94,7 @@ def test_unused_params(self, unused_param, noise_param): @pytest.mark.parametrize( "param", - ["runs", "samples_per_run", "temperature", "laser_waist"], + ["runs", "samples_per_run", "laser_waist"], ) def test_init_strict_pos(self, param): with pytest.raises( @@ -102,39 +102,58 @@ def test_init_strict_pos(self, param): ): NoiseModel(**{param: 0}) - @pytest.mark.parametrize("value", [-1e-9, 0.2, 1.0001]) + @pytest.mark.parametrize("value", [-1e-9, 0.0, 0.2, 1.0001]) @pytest.mark.parametrize( - "param", + "param, noise", [ - "dephasing_rate", - "hyperfine_dephasing_rate", - "relaxation_rate", - "depolarizing_rate", + ("dephasing_rate", "dephasing"), + ("hyperfine_dephasing_rate", "dephasing"), + ("relaxation_rate", "relaxation"), + ("depolarizing_rate", "depolarizing"), + ("temperature", "doppler"), ], ) - def test_init_rate_like(self, param, value): + def test_init_rate_like(self, param, noise, value): + kwargs = {param: value} + if param == "temperature" and value != 0: + kwargs.update(dict(runs=1, samples_per_run=1)) if value < 0: with pytest.raises( ValueError, match=f"'{param}' must be greater than " f"or equal to zero, not {value}.", ): - NoiseModel(**{param: value}) + NoiseModel(**kwargs) else: - noise_model = NoiseModel(**{param: value}) + noise_model = NoiseModel(**kwargs) assert getattr(noise_model, param) == value + if value > 0: + assert noise_model.noise_types == (noise,) + else: + assert noise_model.noise_types == () - @pytest.mark.parametrize("value", [-1e-9, 1.0001]) + @pytest.mark.parametrize("value", [-1e-9, 0.0, 0.5, 1.0, 1.0001]) @pytest.mark.parametrize( - "param", + "param, noise", [ - "state_prep_error", - "p_false_pos", - "p_false_neg", - "amp_sigma", + ("state_prep_error", "SPAM"), + ("p_false_pos", "SPAM"), + ("p_false_neg", "SPAM"), + ("amp_sigma", "amplitude"), ], ) - def test_init_prob_like(self, param, value): + def test_init_prob_like(self, param, noise, value): + if 0 <= value <= 1: + kwargs = {param: value} + if value > 0 and param in ("amp_sigma", "state_prep_error"): + kwargs.update(dict(runs=1, samples_per_run=1)) + noise_model = NoiseModel(**kwargs) + assert getattr(noise_model, param) == value + if value > 0: + assert noise_model.noise_types == (noise,) + else: + assert noise_model.noise_types == () + return with pytest.raises( ValueError, match=f"'{param}' must be greater than or equal to zero and " @@ -145,7 +164,6 @@ def test_init_prob_like(self, param, value): # absence doesn't trigger their own errors runs=1, samples_per_run=1, - laser_waist=1.0, **{param: value}, ) From b549c68bccb6b145ce60da59052bec71e2e0e21d Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Fri, 19 Jul 2024 15:43:28 +0200 Subject: [PATCH 13/13] Disallow temperature to be null in JSON schema --- .../pulser/json/abstract_repr/schemas/noise-schema.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json index 5eac37f84..7da4afad0 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json @@ -102,10 +102,7 @@ "type": "number" }, "temperature": { - "type": [ - "number", - "null" - ] + "type": "number" } }, "required": [