Skip to content

Commit

Permalink
Add optional default noise models to devices (#676)
Browse files Browse the repository at this point in the history
* Default noise model on devices

* Add `prefer_device_noise_model` option to EmulatorConfig

* Docstrings

* Unit tests

* Fix formatting

* Add NoiseModel JSON schema

* Convert NoiseModel lists into tuples

* Make NoiseModel fully immutable and comparable

* Support NoiseModel (de)serialization

* Move noise_model out of pulser.backends

* Finish UTs

* Fix UT in Python 3.8

* Update device JSON schema

* Add NoiseModel and EmulatorConfig to API reference
  • Loading branch information
HGSilveri authored Apr 26, 2024
1 parent 831ca04 commit 4981ca6
Show file tree
Hide file tree
Showing 25 changed files with 425 additions and 51 deletions.
5 changes: 5 additions & 0 deletions docs/source/apidoc/backend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ QPU
Emulators
----------

Configuration
^^^^^^^^^^^^^^
.. autoclass:: pulser.EmulatorConfig
:members:

Local
^^^^^^^
.. autoclass:: pulser_simulation.QutipBackend
Expand Down
5 changes: 4 additions & 1 deletion docs/source/apidoc/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/source/apidoc/simulation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ in favour of :class:`QutipEmulator`.
SimConfig
----------------------

.. automodule:: pulser_simulation.simconfig
.. autoclass:: pulser_simulation.SimConfig
:members:

Simulation Results
Expand Down
1 change: 1 addition & 0 deletions pulser-core/MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 3 additions & 2 deletions pulser-core/pulser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -59,6 +59,8 @@
# pulser.register
"Register",
"Register3D",
# pulser.noise_model
"NoiseModel",
# pulser.devices
"AnalogDevice",
"DigitalAnalogDevice",
Expand All @@ -67,6 +69,5 @@
"Sequence",
# pulser.backends
"EmulatorConfig",
"NoiseModel",
"QPUBackend",
]
3 changes: 2 additions & 1 deletion pulser-core/pulser/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
9 changes: 8 additions & 1 deletion pulser-core/pulser/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pulser-core/pulser/channels/base_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
21 changes: 20 additions & 1 deletion pulser-core/pulser/devices/_device_datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@
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

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")


Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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.
"""
Expand Down
2 changes: 1 addition & 1 deletion pulser-core/pulser/json/abstract_repr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
39 changes: 39 additions & 0 deletions pulser-core/pulser/json/abstract_repr/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
20 changes: 20 additions & 0 deletions pulser-core/pulser/json/abstract_repr/schemas/device-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,21 @@
"$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": {
"$ref": "#/definitions/PhysicalChannel"
},
"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": [
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 4981ca6

Please sign in to comment.