diff --git a/docs/source/apidoc/backend.rst b/docs/source/apidoc/backend.rst index 263b35ce..5a84baec 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 83984dfb..fd6e1714 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 74d23392..0ce47989 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/MANIFEST.in b/pulser-core/MANIFEST.in index d7fe082d..45c00358 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/__init__.py b/pulser-core/pulser/__init__.py index e7ea54b9..2c5931c8 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 4c989e1e..f4f9361b 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 6a30f286..2da0e6f9 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"] @@ -63,15 +63,22 @@ 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. + 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-core/pulser/channels/base_channel.py b/pulser-core/pulser/channels/base_channel.py index 6bd1dc33..f0adb3ad 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 8e9df06a..0bd99e04 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -29,6 +29,7 @@ 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 @@ -36,7 +37,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") @@ -74,6 +80,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 @@ -91,6 +100,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 +228,9 @@ def type_check( f" not '{type(self.interaction_coeff_xy)}'." ) + if self.default_noise_model is not None: + type_check("default_noise_model", NoiseModel) + def to_tuple(obj: tuple | list) -> tuple: if isinstance(obj, (tuple, list)): obj = tuple(to_tuple(el) for el in obj) @@ -506,6 +519,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. """ @@ -704,6 +720,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. """ diff --git a/pulser-core/pulser/json/abstract_repr/__init__.py b/pulser-core/pulser/json/abstract_repr/__init__.py index e6dd3862..f209fdfb 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/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index b973f734..b4d77389 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.noise_model 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/device-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json index 5a07ee6a..8c8370da 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": { @@ -123,6 +127,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": [ @@ -185,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.", @@ -234,6 +246,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": [ @@ -295,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" 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 00000000..6fbaecee --- /dev/null +++ b/pulser-core/pulser/json/abstract_repr/schemas/noise-schema.json @@ -0,0 +1,131 @@ +{ + "$id": "noise-schema.json", + "$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.", + "properties": { + "amp_sigma": { + "type": "number" + }, + "dephasing_rate": { + "type": "number" + }, + "depolarizing_rate": { + "type": "number" + }, + "eff_noise": { + "items": { + "items": [ + { + "type": "number" + }, + { + "items": { + "items": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ComplexNumber" + } + ] + }, + "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/serializer.py b/pulser-core/pulser/json/abstract_repr/serializer.py index 4bbdaddb..c2bc412e 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 8dde2e53..42725aa0 100644 --- a/pulser-core/pulser/json/abstract_repr/validation.py +++ b/pulser-core/pulser/json/abstract_repr/validation.py @@ -28,12 +28,14 @@ ("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"])), ] ) 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/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/noise_model.py similarity index 66% rename from pulser-core/pulser/backend/noise_model.py rename to pulser-core/pulser/noise_model.py index dcccb3e6..53585344 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -14,10 +14,18 @@ """Defines a noise model class for emulator backends.""" from __future__ import annotations -from dataclasses import dataclass, field, fields -from typing import Literal, get_args +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 + +__all__ = ["NoiseModel"] NOISE_TYPES = Literal[ "doppler", @@ -36,36 +44,43 @@ 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 - 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 a mixed state I/2 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. + 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 @@ -107,8 +122,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[ArrayLike, ...] = field(default_factory=tuple) def __post_init__(self) -> None: positive = { @@ -151,6 +166,18 @@ def __post_init__(self) -> None: if not is_valid: raise ValueError(f"'{param}' must be {comp}, not {value}.") + 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)) + ) + self._check_noise_types() self._check_eff_noise() @@ -190,11 +217,52 @@ 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]: + 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-simulation/pulser_simulation/__init__.py b/pulser-simulation/pulser_simulation/__init__.py index 88440092..9494e5d3 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 c0ea4cb0..ab356e13 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 @@ -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/qutip_backend.py b/pulser-simulation/pulser_simulation/qutip_backend.py index f8366253..7a85f047 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.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, diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 22abf07a..d0576766 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 @@ -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)), ) @@ -153,8 +153,8 @@ 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_opers=[op.full() for op in self.eff_noise_opers], + eff_noise_rates=tuple(self.eff_noise_rates), + eff_noise_opers=tuple(op.full() for op in self.eff_noise_opers), ) def __post_init__(self) -> None: diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index 827acc97..aa28123e 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 a16f2531..a9cb5b69 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -22,6 +22,7 @@ from unittest.mock import patch import jsonschema +import jsonschema.exceptions import numpy as np import pytest @@ -46,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 @@ -74,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, + ), ) @@ -135,6 +145,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] @@ -1169,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 diff --git a/tests/test_backend.py b/tests/test_backend.py index a7da4663..4da4acc1 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 @@ -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): diff --git a/tests/test_qutip_backend.py b/tests/test_qutip_backend.py index c45086f6..63bbc95c 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) diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index 90d48037..5a48ccfb 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