From 4aca16a582cb1dc7a7ccc851882523b0cc823c15 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Mon, 22 Apr 2024 15:53:59 +0200 Subject: [PATCH 01/14] Default noise model on devices --- pulser-core/pulser/backend/config.py | 1 + pulser-core/pulser/backend/noise_model.py | 6 +++--- pulser-core/pulser/channels/base_channel.py | 2 +- pulser-core/pulser/devices/_device_datacls.py | 10 +++++++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pulser-core/pulser/backend/config.py b/pulser-core/pulser/backend/config.py index 6a30f2862..a5abc983d 100644 --- a/pulser-core/pulser/backend/config.py +++ b/pulser-core/pulser/backend/config.py @@ -63,6 +63,7 @@ class EmulatorConfig(BackendConfig): - "all-ground" for all atoms in the ground state - An array of floats with a shape compatible with the system + with_modulation: Whether to emulate the sequence with the programmed input or the expected output. noise_model: An optional noise model to emulate the sequence with. diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index 9c196a405..a87adeb0f 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -30,8 +30,8 @@ class NoiseModel: 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 value which - is only taken into account when the related noise type is selected. + 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 @@ -40,7 +40,7 @@ class NoiseModel: - "dephasing": Random phase (Z) flip (parametrized by `dephasing_rate`). - "depolarizing": Quantum noise where the state is - turned into a mixed state I/2 with rate + turned into the maximally mixed state with rate `depolarizing_rate`. - "eff_noise": General effective noise channel defined by the set of collapse operators `eff_noise_opers` diff --git a/pulser-core/pulser/channels/base_channel.py b/pulser-core/pulser/channels/base_channel.py index 6bd1dc335..f0adb3adc 100644 --- a/pulser-core/pulser/channels/base_channel.py +++ b/pulser-core/pulser/channels/base_channel.py @@ -75,7 +75,7 @@ class Channel(ABC): clock_period: int = 1 # ns min_duration: int = 1 # ns max_duration: Optional[int] = int(1e8) # ns - min_avg_amp: int = 0 + min_avg_amp: float = 0 mod_bandwidth: Optional[float] = None # MHz eom_config: Optional[BaseEOM] = field(init=False, default=None) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 8e9df06ad..b5309a6af 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -18,11 +18,12 @@ from abc import ABC, abstractmethod from collections import Counter from dataclasses import dataclass, field, fields -from typing import Any, Literal, cast, get_args +from typing import Any, Literal, cast, get_args, TYPE_CHECKING import numpy as np from scipy.spatial.distance import pdist, squareform +import pulser from pulser.channels.base_channel import Channel from pulser.channels.dmm import DMM from pulser.devices.interaction_coefficients import c6_dict @@ -34,6 +35,9 @@ from pulser.register.register_layout import RegisterLayout from pulser.register.traps import COORD_PRECISION +if TYPE_CHECKING: + from pulser.backend import NoiseModel + DIMENSIONS = Literal[2, 3] ALWAYS_OPTIONAL_PARAMS = ("max_sequence_duration", "max_runs", "dmm_objects") @@ -91,6 +95,7 @@ class BaseDevice(ABC): channel_ids: tuple[str, ...] | None = None channel_objects: tuple[Channel, ...] = field(default_factory=tuple) dmm_objects: tuple[DMM, ...] = field(default_factory=tuple) + default_noise_model: NoiseModel | None = None def __post_init__(self) -> None: def type_check( @@ -218,6 +223,9 @@ def type_check( f" not '{type(self.interaction_coeff_xy)}'." ) + if self.default_noise_model is not None: + type_check("default_noise", pulser.NoiseModel) + def to_tuple(obj: tuple | list) -> tuple: if isinstance(obj, (tuple, list)): obj = tuple(to_tuple(el) for el in obj) From ea8a2233a5791a7054d640151f77ce2e8148ccf4 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Mon, 22 Apr 2024 16:30:46 +0200 Subject: [PATCH 02/14] Add `prefer_device_noise_model` option to EmulatorConfig --- pulser-core/pulser/backend/config.py | 6 ++++++ pulser-simulation/pulser_simulation/qutip_backend.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pulser-core/pulser/backend/config.py b/pulser-core/pulser/backend/config.py index a5abc983d..e9e68cced 100644 --- a/pulser-core/pulser/backend/config.py +++ b/pulser-core/pulser/backend/config.py @@ -66,13 +66,19 @@ class EmulatorConfig(BackendConfig): with_modulation: Whether to emulate the sequence with the programmed input or the expected output. + prefer_device_noise_model: If the sequence's device has a default noise + model, this option signals the backend to prefer it over the noise + model given with this configuration. noise_model: An optional noise model to emulate the sequence with. + Ignored if the sequence's device has default noise model and + `prefer_device_noise_model=True`. """ sampling_rate: float = 1.0 evaluation_times: float | Sequence[float] | EVAL_TIMES_LITERAL = "Full" initial_state: Literal["all-ground"] | Sequence[complex] = "all-ground" with_modulation: bool = False + prefer_device_noise_model: bool = False noise_model: NoiseModel = field(default_factory=NoiseModel) def __post_init__(self) -> None: diff --git a/pulser-simulation/pulser_simulation/qutip_backend.py b/pulser-simulation/pulser_simulation/qutip_backend.py index f8366253c..d93c04e98 100644 --- a/pulser-simulation/pulser_simulation/qutip_backend.py +++ b/pulser-simulation/pulser_simulation/qutip_backend.py @@ -19,6 +19,7 @@ from pulser import Sequence from pulser.backend.abc import Backend from pulser.backend.config import EmulatorConfig +from pulser.backend.noise_model import NoiseModel from pulser_simulation.simconfig import SimConfig from pulser_simulation.simresults import SimulationResults from pulser_simulation.simulation import QutipEmulator @@ -43,7 +44,12 @@ def __init__( f"not {type(config)}." ) self._config = config - simconfig = SimConfig.from_noise_model(self._config.noise_model) + noise_model: None | NoiseModel = None + if self._config.prefer_device_noise_model: + noise_model = sequence.device.default_noise_model + simconfig = SimConfig.from_noise_model( + noise_model or self._config.noise_model + ) self._sim_obj = QutipEmulator.from_sequence( sequence, sampling_rate=self._config.sampling_rate, From 961fc4fc1145deeaad7aee1f2b65e78f642e7437 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Mon, 22 Apr 2024 16:57:46 +0200 Subject: [PATCH 03/14] Docstrings --- pulser-core/pulser/backend/noise_model.py | 6 +++++- pulser-core/pulser/devices/_device_datacls.py | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index a87adeb0f..bb9d1cd6b 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -15,7 +15,7 @@ from __future__ import annotations from dataclasses import dataclass, field, fields -from typing import Literal, get_args +from typing import Any, Literal, get_args import numpy as np @@ -175,3 +175,7 @@ def _check_eff_noise(self) -> None: raise NotImplementedError( "Operator's shape must be (2,2) " f"not {operator.shape}." ) + + def _to_abstract_repr(self) -> dict[str, Any]: + # TODO: Write once the JSON schema is defined + pass diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index b5309a6af..e4e1d2c93 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -18,7 +18,7 @@ from abc import ABC, abstractmethod from collections import Counter from dataclasses import dataclass, field, fields -from typing import Any, Literal, cast, get_args, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Literal, cast, get_args import numpy as np from scipy.spatial.distance import pdist, squareform @@ -40,7 +40,12 @@ DIMENSIONS = Literal[2, 3] -ALWAYS_OPTIONAL_PARAMS = ("max_sequence_duration", "max_runs", "dmm_objects") +ALWAYS_OPTIONAL_PARAMS = ( + "max_sequence_duration", + "max_runs", + "dmm_objects", + "default_noise_model", +) PARAMS_WITH_ABSTR_REPR = ("channel_objects", "channel_ids", "dmm_objects") @@ -78,6 +83,9 @@ class BaseDevice(ABC): (in ns). max_runs: The maximum number of runs allowed on the device. Only used for backend execution. + default_noise_model: An optional noise model characterizing the default + noise of the device. Can be used by emulator backends that support + noise. """ name: str @@ -514,6 +522,9 @@ class Device(BaseDevice): (in ns). max_runs: The maximum number of runs allowed on the device. Only used for backend execution. + default_noise_model: An optional noise model characterizing the default + noise of the device. Can be used by emulator backends that support + noise. pre_calibrated_layouts: RegisterLayout instances that are already available on the Device. """ @@ -712,6 +723,9 @@ class VirtualDevice(BaseDevice): (in ns). max_runs: The maximum number of runs allowed on the device. Only used for backend execution. + default_noise_model: An optional noise model characterizing the default + noise of the device. Can be used by emulator backends that support + noise. reusable_channels: Whether each channel can be declared multiple times on the same pulse sequence. """ From 5b10685c8916f505a3101bd14d5a431e3bc49c8b Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Mon, 22 Apr 2024 17:17:04 +0200 Subject: [PATCH 04/14] Unit tests --- pulser-core/pulser/backend/noise_model.py | 2 +- pulser-core/pulser/devices/_device_datacls.py | 2 +- tests/test_qutip_backend.py | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index bb9d1cd6b..ee0693af4 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -176,6 +176,6 @@ def _check_eff_noise(self) -> None: "Operator's shape must be (2,2) " f"not {operator.shape}." ) - def _to_abstract_repr(self) -> dict[str, Any]: + def _to_abstract_repr(self) -> dict[str, Any]: # type: ignore # TODO: Write once the JSON schema is defined pass diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index e4e1d2c93..4d4124444 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -232,7 +232,7 @@ def type_check( ) if self.default_noise_model is not None: - type_check("default_noise", pulser.NoiseModel) + type_check("default_noise_model", pulser.NoiseModel) def to_tuple(obj: tuple | list) -> tuple: if isinstance(obj, (tuple, list)): diff --git a/tests/test_qutip_backend.py b/tests/test_qutip_backend.py index c45086f6f..63bbc95c5 100644 --- a/tests/test_qutip_backend.py +++ b/tests/test_qutip_backend.py @@ -13,6 +13,8 @@ # limitations under the License. from __future__ import annotations +import dataclasses + import numpy as np import pytest import qutip @@ -23,7 +25,7 @@ from pulser_simulation import SimConfig from pulser_simulation.qutip_backend import QutipBackend from pulser_simulation.qutip_result import QutipResult -from pulser_simulation.simresults import CoherentResults +from pulser_simulation.simresults import CoherentResults, NoisyResults @pytest.fixture @@ -53,3 +55,17 @@ def test_qutip_backend(sequence): final_state = final_result.get_state() assert final_state == results.get_final_state() np.testing.assert_allclose(final_state.full(), [[0], [1]], atol=1e-5) + + +def test_with_default_noise(sequence): + spam_noise = pulser.NoiseModel(noise_types=("SPAM",)) + new_device = dataclasses.replace( + MockDevice, default_noise_model=spam_noise + ) + new_seq = sequence.switch_device(new_device) + backend = QutipBackend( + new_seq, config=pulser.EmulatorConfig(prefer_device_noise_model=True) + ) + new_results = backend.run() + assert isinstance(new_results, NoisyResults) + assert backend._sim_obj.config == SimConfig.from_noise_model(spam_noise) From 233c6fc8ce144078828ef5aae7477dc0c822984b Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Tue, 23 Apr 2024 11:04:46 +0200 Subject: [PATCH 05/14] Fix formatting --- pulser-core/pulser/backend/noise_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index ff35404f1..d3116949f 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -50,10 +50,10 @@ class NoiseModel: 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 + 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` From 1fc94b4e2f9dace9241723284acfe62addf3b4a8 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 11:26:43 +0200 Subject: [PATCH 06/14] Add NoiseModel JSON schema --- pulser-core/MANIFEST.in | 1 + .../pulser/json/abstract_repr/__init__.py | 2 +- .../abstract_repr/schemas/device-schema.json | 8 ++ .../abstract_repr/schemas/noise-schema.json | 107 ++++++++++++++++++ .../pulser/json/abstract_repr/validation.py | 1 + 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json diff --git a/pulser-core/MANIFEST.in b/pulser-core/MANIFEST.in index d7fe082d2..45c00358e 100644 --- a/pulser-core/MANIFEST.in +++ b/pulser-core/MANIFEST.in @@ -5,3 +5,4 @@ include pulser/json/abstract_repr/schemas/device-schema.json include pulser/json/abstract_repr/schemas/sequence-schema.json include pulser/json/abstract_repr/schemas/register-schema.json include pulser/json/abstract_repr/schemas/layout-schema.json +include pulser/json/abstract_repr/schemas/noise-schema.json diff --git a/pulser-core/pulser/json/abstract_repr/__init__.py b/pulser-core/pulser/json/abstract_repr/__init__.py index e6dd3862b..f209fdfb8 100644 --- a/pulser-core/pulser/json/abstract_repr/__init__.py +++ b/pulser-core/pulser/json/abstract_repr/__init__.py @@ -17,7 +17,7 @@ SCHEMAS_PATH = Path(__file__).parent / "schemas" SCHEMAS = {} -for obj_type in ("device", "sequence", "register", "layout"): +for obj_type in ("device", "sequence", "register", "layout", "noise"): with open( SCHEMAS_PATH / f"{obj_type}-schema.json", "r", encoding="utf-8" ) as f: diff --git a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json index 5a07ee6af..a4421f9e3 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -123,6 +123,10 @@ }, "type": "array" }, + "default_noise_model": { + "$ref": "noise-schema.json", + "description": "An optional noise model characterizing the default noise of the device." + }, "dimensions": { "description": "The maximum dimension of the supported trap arrays.", "enum": [ @@ -234,6 +238,10 @@ }, "type": "array" }, + "default_noise_model": { + "$ref": "noise-schema.json", + "description": "An optional noise model characterizing the default noise of the device." + }, "dimensions": { "description": "The maximum dimension of the supported trap arrays.", "enum": [ diff --git a/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json new file mode 100644 index 000000000..2c8811d65 --- /dev/null +++ b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json @@ -0,0 +1,107 @@ +{ + "$id": "noise-schema.json", + "$ref": "#/definitions/NoiseModel", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "NoiseModel": { + "additionalProperties": false, + "description": "Specifies the noise model parameters for emulation.", + "properties": { + "amp_sigma": { + "type": "number" + }, + "dephasing_rate": { + "type": "number" + }, + "depolarizing_rate": { + "type": "number" + }, + "eff_noise": { + "items": { + "items": [ + { + "type": "number" + }, + { + "items": { + "items": { + "type": "number" + }, + "type": "array" + }, + "type": "array" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "type": "array" + }, + "hyperfine_dephasing_rate": { + "type": "number" + }, + "laser_waist": { + "type": "number" + }, + "noise_types": { + "items": { + "$ref": "#/definitions/NoiseType" + }, + "type": "array" + }, + "p_false_neg": { + "type": "number" + }, + "p_false_pos": { + "type": "number" + }, + "relaxation_rate": { + "type": "number" + }, + "runs": { + "type": "number" + }, + "samples_per_run": { + "type": "number" + }, + "state_prep_error": { + "type": "number" + }, + "temperature": { + "type": "number" + } + }, + "required": [ + "noise_types", + "runs", + "samples_per_run", + "state_prep_error", + "p_false_pos", + "p_false_neg", + "temperature", + "laser_waist", + "amp_sigma", + "relaxation_rate", + "dephasing_rate", + "hyperfine_dephasing_rate", + "depolarizing_rate", + "eff_noise" + ], + "type": "object" + }, + "NoiseType": { + "enum": [ + "doppler", + "amplitude", + "SPAM", + "relaxation", + "dephasing", + "depolarizing", + "leakage", + "eff_noise" + ], + "type": "string" + } + } +} diff --git a/pulser-core/pulser/json/abstract_repr/validation.py b/pulser-core/pulser/json/abstract_repr/validation.py index 8dde2e53c..f103d9567 100644 --- a/pulser-core/pulser/json/abstract_repr/validation.py +++ b/pulser-core/pulser/json/abstract_repr/validation.py @@ -28,6 +28,7 @@ ("device-schema.json", Resource.from_contents(SCHEMAS["device"])), ("layout-schema.json", Resource.from_contents(SCHEMAS["layout"])), ("register-schema.json", Resource.from_contents(SCHEMAS["register"])), + ("noise-schema.json", Resource.from_contents(SCHEMAS["noise"])), ] ) From 6c646cb97ac16c71dcc051ceaa5cc7f96c59c48f Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 12:02:04 +0200 Subject: [PATCH 07/14] Convert NoiseModel lists into tuples --- pulser-core/pulser/backend/noise_model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index 9f415f72b..9be27d04e 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -108,8 +108,8 @@ class NoiseModel: dephasing_rate: float = 0.05 hyperfine_dephasing_rate: float = 1e-3 depolarizing_rate: float = 0.05 - eff_noise_rates: list[float] = field(default_factory=list) - eff_noise_opers: list[np.ndarray] = field(default_factory=list) + eff_noise_rates: tuple[float, ...] = field(default_factory=tuple) + eff_noise_opers: tuple[np.ndarray, ...] = field(default_factory=tuple) def __post_init__(self) -> None: positive = { @@ -155,6 +155,11 @@ def __post_init__(self) -> None: self._check_noise_types() self._check_eff_noise() + # Turn lists into tuples + for f in fields(self): + if f.name == "noise_types" or "eff_noise" in f.name: + object.__setattr__(self, f.name, tuple(getattr(self, f.name))) + def _check_noise_types(self) -> None: for noise_type in self.noise_types: if noise_type not in get_args(NOISE_TYPES): From 1debca7915cdb4c727047558f833c92e84859932 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 16:00:15 +0200 Subject: [PATCH 08/14] Make NoiseModel fully immutable and comparable --- pulser-core/pulser/backend/noise_model.py | 33 ++++++++++++++----- .../pulser_simulation/hamiltonian.py | 2 +- .../pulser_simulation/simconfig.py | 4 +-- tests/test_backend.py | 24 +++++++++++++- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index 9be27d04e..64194ecc4 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -18,6 +18,7 @@ from typing import Any, Literal, get_args import numpy as np +from numpy.typing import ArrayLike NOISE_TYPES = Literal[ "doppler", @@ -109,7 +110,7 @@ class NoiseModel: hyperfine_dephasing_rate: float = 1e-3 depolarizing_rate: float = 0.05 eff_noise_rates: tuple[float, ...] = field(default_factory=tuple) - eff_noise_opers: tuple[np.ndarray, ...] = field(default_factory=tuple) + eff_noise_opers: tuple[ArrayLike, ...] = field(default_factory=tuple) def __post_init__(self) -> None: positive = { @@ -152,13 +153,20 @@ def __post_init__(self) -> None: if not is_valid: raise ValueError(f"'{param}' must be {comp}, not {value}.") - self._check_noise_types() - self._check_eff_noise() + 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 into tuples + # 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, tuple(getattr(self, f.name))) + object.__setattr__( + self, f.name, to_tuple(getattr(self, f.name)) + ) + + self._check_noise_types() + self._check_eff_noise() def _check_noise_types(self) -> None: for noise_type in self.noise_types: @@ -196,13 +204,20 @@ def _check_eff_noise(self) -> None: raise ValueError("The provided rates must be greater than 0.") # Check the validity of operators - for operator in self.eff_noise_opers: + for op in self.eff_noise_opers: # type checking - if not isinstance(operator, np.ndarray): - raise TypeError(f"{operator} is not a Numpy array.") + try: + operator = np.array(op, dtype=complex) + except Exception: + raise TypeError( + f"Operator {op!r} is not castable to a Numpy array." + ) + if operator.ndim != 2: + raise ValueError(f"Operator '{op!r}' is not a 2D array.") + if operator.shape != (2, 2): raise NotImplementedError( - "Operator's shape must be (2,2) " f"not {operator.shape}." + f"Operator's shape must be (2,2) not {operator.shape}." ) def _to_abstract_repr(self) -> dict[str, Any]: # type: ignore diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index c0ea4cb07..52c1e7f6a 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -144,7 +144,7 @@ def basis_check(noise_type: str) -> None: basis_check("effective") for id, rate in enumerate(config.eff_noise_rates): local_collapse_ops.append( - np.sqrt(rate) * config.eff_noise_opers[id] + np.sqrt(rate) * np.array(config.eff_noise_opers[id]) ) # Building collapse operators diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 22abf07ad..23ff47ffe 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -133,7 +133,7 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: hyperfine_dephasing_rate=noise_model.hyperfine_dephasing_rate, relaxation_rate=noise_model.relaxation_rate, depolarizing_rate=noise_model.depolarizing_rate, - eff_noise_rates=noise_model.eff_noise_rates, + eff_noise_rates=list(noise_model.eff_noise_rates), eff_noise_opers=list(map(qutip.Qobj, noise_model.eff_noise_opers)), ) @@ -154,7 +154,7 @@ def to_noise_model(self) -> NoiseModel: relaxation_rate=self.relaxation_rate, depolarizing_rate=self.depolarizing_rate, eff_noise_rates=self.eff_noise_rates, - eff_noise_opers=[op.full() for op in self.eff_noise_opers], + eff_noise_opers=tuple(op.full() for op in self.eff_noise_opers), ) def __post_init__(self) -> None: diff --git a/tests/test_backend.py b/tests/test_backend.py index a7da46639..06a7f3614 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -150,6 +150,7 @@ def matrices(self): matrices = {} matrices["I"] = np.eye(2) matrices["X"] = np.ones((2, 2)) - np.eye(2) + matrices["Y"] = np.array([[0, -1j], [1j, 0]]) matrices["Zh"] = 0.5 * np.array([[1, 0], [0, -1]]) matrices["ket"] = np.array([[1.0], [2.0]]) matrices["I3"] = np.eye(3) @@ -181,7 +182,13 @@ def test_eff_noise_opers(self, matrices): match="The effective noise parameters have not been filled.", ): NoiseModel(noise_types=("eff_noise",)) - with pytest.raises(TypeError, match="is not a Numpy array."): + 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], @@ -194,6 +201,21 @@ def test_eff_noise_opers(self, matrices): eff_noise_rates=[1.0], ) + def test_eq(self, matrices): + final_fields = dict( + noise_types=("SPAM", "eff_noise"), + 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"], + eff_noise_rates=[0.1, 0.4], + eff_noise_opers=[matrices["X"], matrices["Y"]], + ) + assert noise_model == NoiseModel(**final_fields) + for param in final_fields: + assert final_fields[param] == getattr(noise_model, param) + class _MockConnection(RemoteConnection): def __init__(self): From 68ec2f5a88f87cb56209090d0fb8e80189a52679 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 17:08:37 +0200 Subject: [PATCH 09/14] Support NoiseModel (de)serialization --- pulser-core/pulser/backend/noise_model.py | 43 +++++++++++++++++-- .../pulser/json/abstract_repr/deserializer.py | 39 +++++++++++++++++ .../abstract_repr/schemas/noise-schema.json | 26 ++++++++++- .../pulser/json/abstract_repr/serializer.py | 2 + .../pulser/json/abstract_repr/validation.py | 3 +- tests/test_abstract_repr.py | 27 ++++++++++++ 6 files changed, 134 insertions(+), 6 deletions(-) diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/backend/noise_model.py index 64194ecc4..7941335a0 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/backend/noise_model.py @@ -14,12 +14,17 @@ """Defines a noise model class for emulator backends.""" from __future__ import annotations -from dataclasses import dataclass, field, fields +import json +from dataclasses import asdict, dataclass, field, fields from typing import Any, Literal, get_args import numpy as np from numpy.typing import ArrayLike +import pulser.json.abstract_repr as pulser_abstract_repr +from pulser.json.abstract_repr.serializer import AbstractReprEncoder +from pulser.json.abstract_repr.validation import validate_abstract_repr + NOISE_TYPES = Literal[ "doppler", "amplitude", @@ -220,6 +225,36 @@ def _check_eff_noise(self) -> None: f"Operator's shape must be (2,2) not {operator.shape}." ) - def _to_abstract_repr(self) -> dict[str, Any]: # type: ignore - # TODO: Write once the JSON schema is defined - pass + def _to_abstract_repr(self) -> dict[str, Any]: + all_fields = asdict(self) + eff_noise_rates = all_fields.pop("eff_noise_rates") + eff_noise_opers = all_fields.pop("eff_noise_opers") + all_fields["eff_noise"] = list(zip(eff_noise_rates, eff_noise_opers)) + return all_fields + + def to_abstract_repr(self) -> str: + """Serializes the noise model into an abstract JSON object.""" + abstr_str = json.dumps(self, cls=AbstractReprEncoder) + validate_abstract_repr(abstr_str, "noise") + return abstr_str + + @staticmethod + def from_abstract_repr(obj_str: str) -> NoiseModel: + """Deserialize a noise model from an abstract JSON object. + + Args: + obj_str (str): the JSON string representing the noise model + encoded in the abstract JSON format. + """ + if not isinstance(obj_str, str): + raise TypeError( + "The serialized noise model must be given as a string. " + f"Instead, got object of type {type(obj_str)}." + ) + + # Avoids circular imports + return ( + pulser_abstract_repr.deserializer.deserialize_abstract_noise_model( + obj_str + ) + ) diff --git a/pulser-core/pulser/json/abstract_repr/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index b973f7349..00197bca2 100644 --- a/pulser-core/pulser/json/abstract_repr/deserializer.py +++ b/pulser-core/pulser/json/abstract_repr/deserializer.py @@ -56,6 +56,7 @@ ) if TYPE_CHECKING: + from pulser.backend import NoiseModel from pulser.register.base_register import BaseRegister from pulser.sequence import Sequence @@ -381,6 +382,28 @@ def _deserialize_register( return reg +def _deserialize_noise_model(noise_model_obj: dict[str, Any]) -> NoiseModel: + + def convert_complex(obj: list | tuple) -> list: + if isinstance(obj, (list, tuple)): + return [convert_complex(e) for e in obj] + elif isinstance(obj, dict): + return obj["real"] + 1j * obj["imag"] + else: + return obj + + eff_noise_rates = [] + eff_noise_opers = [] + 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_model_obj, + eff_noise_rates=tuple(eff_noise_rates), + eff_noise_opers=tuple(eff_noise_opers), + ) + + def _deserialize_device_object(obj: dict[str, Any]) -> Device | VirtualDevice: device_cls: Type[Device] | Type[VirtualDevice] = ( VirtualDevice if obj["is_virtual"] else Device @@ -412,6 +435,8 @@ def _deserialize_device_object(obj: dict[str, Any]) -> Device | VirtualDevice: params[key] = tuple( _deserialize_layout(layout) for layout in obj[key] ) + elif param.name == "default_noise_model": + params[param.name] = _deserialize_noise_model(obj[param.name]) else: params[param.name] = obj[param.name] try: @@ -565,3 +590,17 @@ def deserialize_abstract_register(obj_str: str) -> BaseRegister: obj = json.loads(obj_str) layout = _deserialize_layout(obj["layout"]) if "layout" in obj else None return _deserialize_register(qubits=obj["register"], layout=layout) + + +def deserialize_abstract_noise_model(obj_str: str) -> NoiseModel: + """Deserialize a noise model from an abstract JSON object. + + Args: + obj_str: the JSON string representing the noise model encoded + in the abstract JSON format. + + Returns: + The NoiseModel instance. + """ + validate_abstract_repr(obj_str, "noise") + return _deserialize_noise_model(json.loads(obj_str)) 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 2c8811d65..6fbaecee8 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json @@ -3,6 +3,23 @@ "$ref": "#/definitions/NoiseModel", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "ComplexNumber": { + "additionalProperties": false, + "description": "A complex number.", + "properties": { + "imag": { + "type": "number" + }, + "real": { + "type": "number" + } + }, + "required": [ + "real", + "imag" + ], + "type": "object" + }, "NoiseModel": { "additionalProperties": false, "description": "Specifies the noise model parameters for emulation.", @@ -25,7 +42,14 @@ { "items": { "items": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ComplexNumber" + } + ] }, "type": "array" }, diff --git a/pulser-core/pulser/json/abstract_repr/serializer.py b/pulser-core/pulser/json/abstract_repr/serializer.py index 4bbdaddb7..c2bc412e2 100644 --- a/pulser-core/pulser/json/abstract_repr/serializer.py +++ b/pulser-core/pulser/json/abstract_repr/serializer.py @@ -48,6 +48,8 @@ def default(self, o: Any) -> dict[str, Any] | list | int: return int(o) elif isinstance(o, set): return list(o) + elif isinstance(o, complex): + return dict(real=o.real, imag=o.imag) else: return cast(dict, json.JSONEncoder.default(self, o)) diff --git a/pulser-core/pulser/json/abstract_repr/validation.py b/pulser-core/pulser/json/abstract_repr/validation.py index f103d9567..42725aa0f 100644 --- a/pulser-core/pulser/json/abstract_repr/validation.py +++ b/pulser-core/pulser/json/abstract_repr/validation.py @@ -34,7 +34,8 @@ def validate_abstract_repr( - obj_str: str, name: Literal["sequence", "device", "layout", "register"] + obj_str: str, + name: Literal["sequence", "device", "layout", "register", "noise"], ) -> None: """Validate the abstract representation of an object. diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index a16f2531d..6adc97cd8 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -22,10 +22,12 @@ from unittest.mock import patch import jsonschema +import jsonschema.exceptions import numpy as np import pytest from pulser import Pulse, Register, Register3D, Sequence, devices +from pulser.backend import NoiseModel from pulser.channels import Rydberg from pulser.channels.eom import RydbergBeam, RydbergEOM from pulser.devices import ( @@ -135,6 +137,31 @@ def test_register(reg: Register): Register.from_abstract_repr(json.dumps(ser_reg_obj)) +@pytest.mark.parametrize( + "noise_model", + [ + NoiseModel(), + NoiseModel( + noise_types=("eff_noise",), + eff_noise_rates=(0.1,), + eff_noise_opers=(((0, -1j), (1j, 0)),), + ), + ], +) +def test_noise_model(noise_model: NoiseModel): + ser_noise_model_str = noise_model.to_abstract_repr() + re_noise_model = NoiseModel.from_abstract_repr(ser_noise_model_str) + assert noise_model == re_noise_model + + ser_noise_model_obj = json.loads(ser_noise_model_str) + with pytest.raises(TypeError, match="must be given as a string"): + NoiseModel.from_abstract_repr(ser_noise_model_obj) + + ser_noise_model_obj["noise_types"].append("foo") + with pytest.raises(jsonschema.exceptions.ValidationError): + NoiseModel.from_abstract_repr(json.dumps(ser_noise_model_obj)) + + class TestDevice: @pytest.fixture( params=[DigitalAnalogDevice, phys_Chadoq2, MockDevice, AnalogDevice] From 08bb4aa379b404c183cb323b70d5e0e8deabb98e Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 17:27:26 +0200 Subject: [PATCH 10/14] Move noise_model out of pulser.backends --- pulser-core/pulser/__init__.py | 5 +++-- pulser-core/pulser/backend/__init__.py | 3 ++- pulser-core/pulser/backend/config.py | 2 +- pulser-core/pulser/devices/_device_datacls.py | 9 +++------ pulser-core/pulser/json/abstract_repr/deserializer.py | 2 +- pulser-core/pulser/{backend => }/noise_model.py | 2 ++ pulser-simulation/pulser_simulation/__init__.py | 2 +- pulser-simulation/pulser_simulation/hamiltonian.py | 2 +- pulser-simulation/pulser_simulation/qutip_backend.py | 2 +- pulser-simulation/pulser_simulation/simconfig.py | 4 ++-- pulser-simulation/pulser_simulation/simulation.py | 2 +- tests/test_abstract_repr.py | 2 +- tests/test_backend.py | 2 +- tests/test_simconfig.py | 2 +- 14 files changed, 21 insertions(+), 20 deletions(-) rename pulser-core/pulser/{backend => }/noise_model.py (99%) diff --git a/pulser-core/pulser/__init__.py b/pulser-core/pulser/__init__.py index e7ea54b98..2c5931c8a 100644 --- a/pulser-core/pulser/__init__.py +++ b/pulser-core/pulser/__init__.py @@ -26,11 +26,11 @@ ) from pulser.pulse import Pulse from pulser.register import Register, Register3D +from pulser.noise_model import NoiseModel from pulser.devices import AnalogDevice, DigitalAnalogDevice, MockDevice from pulser.sequence import Sequence from pulser.backend import ( EmulatorConfig, - NoiseModel, QPUBackend, ) @@ -59,6 +59,8 @@ # pulser.register "Register", "Register3D", + # pulser.noise_model + "NoiseModel", # pulser.devices "AnalogDevice", "DigitalAnalogDevice", @@ -67,6 +69,5 @@ "Sequence", # pulser.backends "EmulatorConfig", - "NoiseModel", "QPUBackend", ] diff --git a/pulser-core/pulser/backend/__init__.py b/pulser-core/pulser/backend/__init__.py index 4c989e1e1..f4f9361b3 100644 --- a/pulser-core/pulser/backend/__init__.py +++ b/pulser-core/pulser/backend/__init__.py @@ -13,8 +13,9 @@ # limitations under the License. """Classes for backend execution.""" +import pulser.noise_model as noise_model # For backwards compat from pulser.backend.config import EmulatorConfig -from pulser.backend.noise_model import NoiseModel +from pulser.noise_model import NoiseModel # For backwards compat from pulser.backend.qpu import QPUBackend __all__ = ["EmulatorConfig", "NoiseModel", "QPUBackend"] diff --git a/pulser-core/pulser/backend/config.py b/pulser-core/pulser/backend/config.py index e9e68cced..2da0e6f99 100644 --- a/pulser-core/pulser/backend/config.py +++ b/pulser-core/pulser/backend/config.py @@ -19,7 +19,7 @@ import numpy as np -from pulser.backend.noise_model import NoiseModel +from pulser.noise_model import NoiseModel EVAL_TIMES_LITERAL = Literal["Full", "Minimal", "Final"] diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 4d4124444..0bd99e041 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -18,26 +18,23 @@ from abc import ABC, abstractmethod from collections import Counter from dataclasses import dataclass, field, fields -from typing import TYPE_CHECKING, Any, Literal, cast, get_args +from typing import Any, Literal, cast, get_args import numpy as np from scipy.spatial.distance import pdist, squareform -import pulser from pulser.channels.base_channel import Channel from pulser.channels.dmm import DMM from pulser.devices.interaction_coefficients import c6_dict from pulser.json.abstract_repr.serializer import AbstractReprEncoder from pulser.json.abstract_repr.validation import validate_abstract_repr from pulser.json.utils import get_dataclass_defaults, obj_to_dict +from pulser.noise_model import NoiseModel from pulser.register.base_register import BaseRegister, QubitId from pulser.register.mappable_reg import MappableRegister from pulser.register.register_layout import RegisterLayout from pulser.register.traps import COORD_PRECISION -if TYPE_CHECKING: - from pulser.backend import NoiseModel - DIMENSIONS = Literal[2, 3] ALWAYS_OPTIONAL_PARAMS = ( @@ -232,7 +229,7 @@ def type_check( ) if self.default_noise_model is not None: - type_check("default_noise_model", pulser.NoiseModel) + type_check("default_noise_model", NoiseModel) def to_tuple(obj: tuple | list) -> tuple: if isinstance(obj, (tuple, list)): diff --git a/pulser-core/pulser/json/abstract_repr/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index 00197bca2..b4d77389a 100644 --- a/pulser-core/pulser/json/abstract_repr/deserializer.py +++ b/pulser-core/pulser/json/abstract_repr/deserializer.py @@ -56,7 +56,7 @@ ) if TYPE_CHECKING: - from pulser.backend import NoiseModel + from pulser.noise_model import NoiseModel from pulser.register.base_register import BaseRegister from pulser.sequence import Sequence diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/noise_model.py similarity index 99% rename from pulser-core/pulser/backend/noise_model.py rename to pulser-core/pulser/noise_model.py index 7941335a0..524f11062 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -25,6 +25,8 @@ from pulser.json.abstract_repr.serializer import AbstractReprEncoder from pulser.json.abstract_repr.validation import validate_abstract_repr +__all__ = ["NoiseModel"] + NOISE_TYPES = Literal[ "doppler", "amplitude", diff --git a/pulser-simulation/pulser_simulation/__init__.py b/pulser-simulation/pulser_simulation/__init__.py index 884400923..9494e5d38 100644 --- a/pulser-simulation/pulser_simulation/__init__.py +++ b/pulser-simulation/pulser_simulation/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. """Classes for classical emulation of a Sequence.""" -from pulser.backend import EmulatorConfig, NoiseModel +from pulser import EmulatorConfig, NoiseModel from pulser_simulation._version import __version__ as __version__ from pulser_simulation.qutip_backend import QutipBackend diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index 52c1e7f6a..ab356e13e 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -23,8 +23,8 @@ import numpy as np import qutip -from pulser.backend.noise_model import NoiseModel from pulser.devices._device_datacls import BaseDevice +from pulser.noise_model import NoiseModel from pulser.register.base_register import QubitId from pulser.sampler.samples import SequenceSamples, _PulseTargetSlot from pulser_simulation.simconfig import SUPPORTED_NOISES, doppler_sigma diff --git a/pulser-simulation/pulser_simulation/qutip_backend.py b/pulser-simulation/pulser_simulation/qutip_backend.py index d93c04e98..7a85f0472 100644 --- a/pulser-simulation/pulser_simulation/qutip_backend.py +++ b/pulser-simulation/pulser_simulation/qutip_backend.py @@ -19,7 +19,7 @@ from pulser import Sequence from pulser.backend.abc import Backend from pulser.backend.config import EmulatorConfig -from pulser.backend.noise_model import NoiseModel +from pulser.noise_model import NoiseModel from pulser_simulation.simconfig import SimConfig from pulser_simulation.simresults import SimulationResults from pulser_simulation.simulation import QutipEmulator diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 23ff47ffe..d05767663 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -21,7 +21,7 @@ import qutip -from pulser.backend.noise_model import NOISE_TYPES, NoiseModel +from pulser.noise_model import NOISE_TYPES, NoiseModel MASS = 1.45e-25 # kg KB = 1.38e-23 # J/K @@ -153,7 +153,7 @@ def to_noise_model(self) -> NoiseModel: hyperfine_dephasing_rate=self.hyperfine_dephasing_rate, relaxation_rate=self.relaxation_rate, depolarizing_rate=self.depolarizing_rate, - eff_noise_rates=self.eff_noise_rates, + eff_noise_rates=tuple(self.eff_noise_rates), eff_noise_opers=tuple(op.full() for op in self.eff_noise_opers), ) diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index 827acc97b..aa28123ef 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -28,8 +28,8 @@ import pulser.sampler as sampler from pulser import Sequence -from pulser.backend.noise_model import NoiseModel from pulser.devices._device_datacls import BaseDevice +from pulser.noise_model import NoiseModel from pulser.register.base_register import BaseRegister from pulser.result import SampledResult from pulser.sampler.samples import ChannelSamples, SequenceSamples diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 6adc97cd8..54385b6af 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -27,7 +27,6 @@ import pytest from pulser import Pulse, Register, Register3D, Sequence, devices -from pulser.backend import NoiseModel from pulser.channels import Rydberg from pulser.channels.eom import RydbergBeam, RydbergEOM from pulser.devices import ( @@ -48,6 +47,7 @@ ) from pulser.json.abstract_repr.validation import validate_abstract_repr from pulser.json.exceptions import AbstractReprError, DeserializeDeviceError +from pulser.noise_model import NoiseModel from pulser.parametrized.decorators import parametrize from pulser.parametrized.paramobj import ParamObj from pulser.parametrized.variable import Variable, VariableItem diff --git a/tests/test_backend.py b/tests/test_backend.py index 06a7f3614..4da4acc1b 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -23,7 +23,6 @@ import pulser from pulser.backend.abc import Backend from pulser.backend.config import EmulatorConfig -from pulser.backend.noise_model import NoiseModel from pulser.backend.qpu import QPUBackend from pulser.backend.remote import ( RemoteConnection, @@ -32,6 +31,7 @@ SubmissionStatus, ) from pulser.devices import DigitalAnalogDevice, MockDevice +from pulser.noise_model import NoiseModel from pulser.result import Result, SampledResult diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index 90d480376..5a48ccfb7 100644 --- a/tests/test_simconfig.py +++ b/tests/test_simconfig.py @@ -15,7 +15,7 @@ import pytest from qutip import Qobj, qeye, sigmax, sigmaz -from pulser.backend.noise_model import NoiseModel +from pulser.noise_model import NoiseModel from pulser_simulation.simconfig import SimConfig, doppler_sigma From 1bcd550e80ab756030b661657cd580a1e8647b8a Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 17:54:39 +0200 Subject: [PATCH 11/14] Finish UTs --- tests/test_abstract_repr.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 54385b6af..a9cb5b698 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -76,6 +76,14 @@ dmm_objects=( 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, + ), ) @@ -1196,14 +1204,10 @@ def test_deserialize_device_and_channels(self, is_phys_Chadoq2) -> None: if is_phys_Chadoq2: kwargs["device"] = json.loads(phys_Chadoq2.to_abstract_repr()) s = _get_serialized_seq(**kwargs) - if not is_phys_Chadoq2: - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) - deserialized_device = deserialize_device(json.dumps(s["device"])) - else: - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) - deserialized_device = deserialize_device(json.dumps(s["device"])) + + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) + deserialized_device = deserialize_device(json.dumps(s["device"])) # Check device assert seq._device == deserialized_device From a73ce61808ec800bac7b77dd93de954481b6adc9 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 25 Apr 2024 18:00:05 +0200 Subject: [PATCH 12/14] Fix UT in Python 3.8 --- pulser-core/pulser/noise_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 524f11062..0af199500 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -161,7 +161,7 @@ def __post_init__(self) -> None: raise ValueError(f"'{param}' must be {comp}, not {value}.") def to_tuple(obj: tuple) -> tuple: - if isinstance(obj, (tuple | list | np.ndarray)): + if isinstance(obj, (tuple, list, np.ndarray)): obj = tuple(to_tuple(el) for el in obj) return obj From 0a69416bcc682d27a04ae84d7acc56fa53d1e667 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Fri, 26 Apr 2024 12:23:58 +0200 Subject: [PATCH 13/14] Update device JSON schema --- .../json/abstract_repr/schemas/device-schema.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json index a4421f9e3..8c8370da3 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -116,6 +116,10 @@ "$schema": { "type": "string" }, + "accepts_new_layouts": { + "description": "Whether registers built from register layouts that are not already calibrated are accepted. Only enforced in QPU execution.", + "type": "boolean" + }, "channels": { "description": "The available channels on the device.", "items": { @@ -189,6 +193,10 @@ }, "type": "array" }, + "requires_layout": { + "description": "Whether the register used in the sequence must be created from a register layout. Only enforced in QPU execution.", + "type": "boolean" + }, "reusable_channels": { "const": false, "description": "Whether each channel can be declared multiple times on the same pulse sequence.", @@ -303,6 +311,10 @@ "description": "A unique name for the device.", "type": "string" }, + "requires_layout": { + "description": "Whether the register used in the sequence must be created from a register layout. Only enforced in QPU execution.", + "type": "boolean" + }, "reusable_channels": { "description": "Whether each channel can be declared multiple times on the same pulse sequence.", "type": "boolean" From 7e8b4b9f4c72cb675f8ebdf37b4f433c35fd4f62 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Fri, 26 Apr 2024 16:47:44 +0200 Subject: [PATCH 14/14] Add NoiseModel and EmulatorConfig to API reference --- docs/source/apidoc/backend.rst | 5 +++++ docs/source/apidoc/core.rst | 5 ++++- docs/source/apidoc/simulation.rst | 2 +- pulser-core/pulser/noise_model.py | 18 ++++++++++++------ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/source/apidoc/backend.rst b/docs/source/apidoc/backend.rst index 263b35ce2..5a84baecc 100644 --- a/docs/source/apidoc/backend.rst +++ b/docs/source/apidoc/backend.rst @@ -12,6 +12,11 @@ QPU Emulators ---------- +Configuration +^^^^^^^^^^^^^^ +.. autoclass:: pulser.EmulatorConfig + :members: + Local ^^^^^^^ .. autoclass:: pulser_simulation.QutipBackend diff --git a/docs/source/apidoc/core.rst b/docs/source/apidoc/core.rst index 83984dfb3..fd6e1714d 100644 --- a/docs/source/apidoc/core.rst +++ b/docs/source/apidoc/core.rst @@ -110,7 +110,10 @@ which when associated with a :class:`pulser.Sequence` condition its development. .. autodata:: pulser.devices.DigitalAnalogDevice - +Noise Model +-------------- +.. automodule:: pulser.noise_model + :members: Channels --------------------- diff --git a/docs/source/apidoc/simulation.rst b/docs/source/apidoc/simulation.rst index 74d233924..0ce479890 100644 --- a/docs/source/apidoc/simulation.rst +++ b/docs/source/apidoc/simulation.rst @@ -21,7 +21,7 @@ in favour of :class:`QutipEmulator`. SimConfig ---------------------- -.. automodule:: pulser_simulation.simconfig +.. autoclass:: pulser_simulation.SimConfig :members: Simulation Results diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index 0af199500..53585344c 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -48,33 +48,39 @@ class NoiseModel: are only taken into account when the related noise type is selected. Args: - noise_types: Noise types to include in the emulation. Available - options: + noise_types: Noise types to include in the emulation. + Available options: - "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`. + 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`. runs: Number of runs needed (each run draws a new random noise). samples_per_run: Number of samples per noisy run. Useful for