Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional default noise models to devices #676

Merged
merged 17 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

HGSilveri marked this conversation as resolved.
Show resolved Hide resolved
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
a-corni marked this conversation as resolved.
Show resolved Hide resolved
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
HGSilveri marked this conversation as resolved.
Show resolved Hide resolved
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),
)
HGSilveri marked this conversation as resolved.
Show resolved Hide resolved


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