forked from pasqal-io/Pulser
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding backends to create a uniform interface for sequence execution (p…
…asqal-io#518) * WIP: Preliminary backend classes * Set the Backend and CloudBackend interface * Restructure the backend module * Create classes for configuration * Preliminart QutipBackend implementation * Move sequence validation to Backend * Cloud -> Remote * Defining the Results container class * Defining the RemoteResults object * Defining QPUBackend * Documenting QutipBackend * Adding helper methods to Sequence * Make PasqalCloud a RemoteConnection child class * Defining the Pasqal EMU backends * Relaxing back `Sequence.measure()` accepted bases * Import sorting * Defines NoiseModel and combines it with SimConfig * Converting EmulatorConfig into pasqal_cloud config * Move device validation and deprecate PasqalCloud methods * Typing * Add evaluation_times = "Final" option * UTs for Backend and NoiseModel * Relax return type of `Backend.run()` * Add UTs for new Sequence methods * Test new Sequence methods * Complete PasqalCloud UTs * Add tests for Pasqal Emulator backends * Tests for QPUBackend * Add tests for QutipBackend * Typing and import sorting * Add checks to EmulatorConfig * Use `QutipEmulator` in `QutipBackend` * Change type hint for job parameters * Addressing review comments * Change removal version for deprecation * Minor review suggestion
- Loading branch information
Showing
20 changed files
with
1,746 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Copyright 2023 Pulser Development Team | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Classes for backend execution.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Copyright 2023 Pulser Development Team | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Base class for the backend interface.""" | ||
from __future__ import annotations | ||
|
||
import typing | ||
from abc import ABC, abstractmethod | ||
|
||
from pulser.result import Result | ||
from pulser.sequence import Sequence | ||
|
||
Results = typing.Sequence[Result] | ||
|
||
|
||
class Backend(ABC): | ||
"""The backend abstract base class.""" | ||
|
||
def __init__(self, sequence: Sequence) -> None: | ||
"""Starts a new backend instance.""" | ||
self.validate_sequence(sequence) | ||
self._sequence = sequence | ||
|
||
@abstractmethod | ||
def run(self) -> Results | typing.Sequence[Results]: | ||
"""Executes the sequence on the backend.""" | ||
pass | ||
|
||
def validate_sequence(self, sequence: Sequence) -> None: | ||
"""Validates a sequence prior to submission.""" | ||
if not isinstance(sequence, Sequence): | ||
raise TypeError( | ||
"'sequence' should be a `Sequence` instance" | ||
f", not {type(sequence)}." | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# Copyright 2023 Pulser Development Team | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""Defines the backend configuration classes.""" | ||
from __future__ import annotations | ||
|
||
from dataclasses import dataclass, field | ||
from typing import Any, Literal, Sequence, get_args | ||
|
||
import numpy as np | ||
|
||
from pulser.backend.noise_model import NoiseModel | ||
|
||
EVAL_TIMES_LITERAL = Literal["Full", "Minimal", "Final"] | ||
|
||
|
||
@dataclass | ||
class BackendConfig: | ||
"""The base backend configuration. | ||
Attributes: | ||
backend_options: A dictionary of backend specific options. | ||
""" | ||
|
||
backend_options: dict[str, Any] = field(default_factory=dict) | ||
|
||
|
||
@dataclass | ||
class EmulatorConfig(BackendConfig): | ||
"""The configuration for emulator backends. | ||
Attributes: | ||
backend_options: A dictionary of backend-specific options. | ||
sampling_rate: The fraction of samples to extract from the pulse | ||
sequence for emulation. | ||
evaluation_times: The times at which results are returned. Choose | ||
between: | ||
- "Full": The times are set to be the ones used to define the | ||
Hamiltonian to the solver. | ||
- "Minimal": The times are set to only include initial and final | ||
times. | ||
- "Final": Returns only the result at the end of the sequence. | ||
- A list of times in µs if you wish to only include those specific | ||
times. | ||
- A float to act as a sampling rate for the resulting state. | ||
initial_state: The initial state from which emulation starts. | ||
Choose between: | ||
- "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. | ||
""" | ||
|
||
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 | ||
noise_model: NoiseModel = field(default_factory=NoiseModel) | ||
|
||
def __post_init__(self) -> None: | ||
if not (0 < self.sampling_rate <= 1.0): | ||
raise ValueError( | ||
"The sampling rate (`sampling_rate` = " | ||
f"{self.sampling_rate}) must be greater than 0 and " | ||
"less than or equal to 1." | ||
) | ||
|
||
if isinstance(self.evaluation_times, str): | ||
if self.evaluation_times not in get_args(EVAL_TIMES_LITERAL): | ||
raise ValueError( | ||
"If provided as a string, 'evaluation_times' must be one " | ||
f"of the following options: {get_args(EVAL_TIMES_LITERAL)}" | ||
) | ||
elif isinstance(self.evaluation_times, float): | ||
if not (0 < self.evaluation_times <= 1.0): | ||
raise ValueError( | ||
"If provided as a float, 'evaluation_times' must be" | ||
" greater than 0 and less than or equal to 1." | ||
) | ||
elif isinstance(self.evaluation_times, (list, tuple, np.ndarray)): | ||
if np.min(self.evaluation_times, initial=0) < 0: | ||
raise ValueError( | ||
"If provided as a sequence of values, " | ||
"'evaluation_times' must not contain negative values." | ||
) | ||
else: | ||
raise TypeError( | ||
f"'{type(self.evaluation_times)}' is not a valid" | ||
" type for 'evaluation_times'." | ||
) | ||
|
||
if isinstance(self.initial_state, str): | ||
if self.initial_state != "all-ground": | ||
raise ValueError( | ||
"If provided as a string, 'initial_state' must be" | ||
" 'all-ground'." | ||
) | ||
elif not isinstance(self.initial_state, (tuple, list, np.ndarray)): | ||
raise TypeError( | ||
f"'{type(self.initial_state)}' is not a valid type for" | ||
" 'initial_state'." | ||
) | ||
|
||
if not isinstance(self.noise_model, NoiseModel): | ||
raise TypeError( | ||
"'noise_model' must be a NoiseModel instance," | ||
f" not {type(self.noise_model)}." | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
# Copyright 2023 Pulser Development Team | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""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 numpy as np | ||
|
||
NOISE_TYPES = Literal[ | ||
"doppler", "amplitude", "SPAM", "dephasing", "depolarizing", "eff_noise" | ||
] | ||
|
||
|
||
@dataclass | ||
class NoiseModel: | ||
"""Specifies the noise model parameters for emulation. | ||
Select the desired noise types in `noise_types` and, if necessary, | ||
modifiy the default values of related parameters. | ||
Non-specified parameters will have reasonable default value which | ||
is only taken into account when the related noise type is selected. | ||
Args: | ||
noise_types: Noise types to include in the emulation. Available | ||
options: | ||
- "dephasing": Random phase (Z) flip (parametrized | ||
by `dephasing_prob`). | ||
- "depolarizing": Quantum noise where the state is | ||
turned into a mixed state I/2 with probability | ||
`depolarizing_prob`. | ||
- "eff_noise": General effective noise channel defined by | ||
the set of collapse operators `eff_noise_opers` | ||
and the corresponding probability distribution | ||
`eff_noise_probs`. | ||
- "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`. | ||
runs: Number of runs needed (each run draws a new random noise). | ||
samples_per_run: Number of samples per noisy run. Useful for | ||
cutting down on computing time, but unrealistic. | ||
state_prep_error: The state preparation error probability. | ||
p_false_pos: Probability of measuring a false positive. | ||
p_false_neg: Probability of measuring a false negative. | ||
temperature: Temperature, set in µK, of the atoms in the array. | ||
Also sets the standard deviation of the speed of the atoms. | ||
laser_waist: Waist of the gaussian laser, set in µm, for global | ||
pulses. | ||
amp_sigma: Dictates the fluctuations in amplitude as a standard | ||
deviation of a normal distribution centered in 1. | ||
dephasing_prob: The probability of a dephasing error occuring. | ||
depolarizing_prob: The probability of a depolarizing error occuring. | ||
eff_noise_probs: The probability associated to each effective noise | ||
operator. | ||
eff_noise_opers: The operators for the effective noise model. The | ||
first operator must be the identity. | ||
""" | ||
|
||
noise_types: tuple[NOISE_TYPES, ...] = () | ||
runs: int = 15 | ||
samples_per_run: int = 5 | ||
state_prep_error: float = 0.005 | ||
p_false_pos: float = 0.01 | ||
p_false_neg: float = 0.05 | ||
temperature: float = 50.0 | ||
laser_waist: float = 175.0 | ||
amp_sigma: float = 5e-2 | ||
dephasing_prob: float = 0.05 | ||
depolarizing_prob: float = 0.05 | ||
eff_noise_probs: list[float] = field(default_factory=list) | ||
eff_noise_opers: list[np.ndarray] = field(default_factory=list) | ||
|
||
def __post_init__(self) -> None: | ||
strict_positive = { | ||
"runs", | ||
"samples_per_run", | ||
"temperature", | ||
"laser_waist", | ||
} | ||
probability_like = { | ||
"state_prep_error", | ||
"p_false_pos", | ||
"p_false_neg", | ||
"dephasing_prob", | ||
"depolarizing_prob", | ||
"amp_sigma", | ||
} | ||
# The two share no common terms | ||
assert not strict_positive.intersection(probability_like) | ||
|
||
for f in fields(self): | ||
is_valid = True | ||
param = f.name | ||
value = getattr(self, param) | ||
if param in strict_positive: | ||
is_valid = value > 0 | ||
comp = "greater than zero" | ||
elif param in probability_like: | ||
is_valid = 0 <= value <= 1 | ||
comp = ( | ||
"greater than or equal to zero and smaller than " | ||
"or equal to one" | ||
) | ||
if not is_valid: | ||
raise ValueError(f"'{param}' must be {comp}, not {value}.") | ||
|
||
self._check_noise_types() | ||
self._check_eff_noise() | ||
|
||
def _check_noise_types(self) -> None: | ||
for noise_type in self.noise_types: | ||
if noise_type not in get_args(NOISE_TYPES): | ||
raise ValueError( | ||
f"'{noise_type}' is not a valid noise type. " | ||
+ "Valid noise types: " | ||
+ ", ".join(get_args(NOISE_TYPES)) | ||
) | ||
dephasing_on = "dephasing" in self.noise_types | ||
depolarizing_on = "depolarizing" in self.noise_types | ||
eff_noise_on = "eff_noise" in self.noise_types | ||
eff_noise_conflict = dephasing_on + depolarizing_on + eff_noise_on > 1 | ||
if eff_noise_conflict: | ||
raise NotImplementedError( | ||
"Depolarizing, dephasing and effective noise channels" | ||
"cannot be simultaneously selected." | ||
) | ||
|
||
def _check_eff_noise(self) -> None: | ||
if len(self.eff_noise_opers) != len(self.eff_noise_probs): | ||
raise ValueError( | ||
f"The operators list length({len(self.eff_noise_opers)}) " | ||
"and probabilities list length" | ||
f"({len(self.eff_noise_probs)}) must be equal." | ||
) | ||
for prob in self.eff_noise_probs: | ||
if not isinstance(prob, float): | ||
raise TypeError( | ||
"eff_noise_probs is a list of floats," | ||
f" it must not contain a {type(prob)}." | ||
) | ||
|
||
if "eff_noise" not in self.noise_types: | ||
# Stop here if effective noise is not selected | ||
return | ||
|
||
if not self.eff_noise_opers or not self.eff_noise_probs: | ||
raise ValueError( | ||
"The general noise parameters have not been filled." | ||
) | ||
|
||
prob_distr = np.array(self.eff_noise_probs) | ||
lower_bound = np.any(prob_distr < 0.0) | ||
upper_bound = np.any(prob_distr > 1.0) | ||
sum_p = not np.isclose(sum(prob_distr), 1.0) | ||
|
||
if sum_p or lower_bound or upper_bound: | ||
raise ValueError( | ||
"The distribution given is not a probability distribution." | ||
) | ||
|
||
# Check the validity of operators | ||
for operator in self.eff_noise_opers: | ||
# type checking | ||
if not isinstance(operator, np.ndarray): | ||
raise TypeError(f"{operator} is not a Numpy array.") | ||
if operator.shape != (2, 2): | ||
raise NotImplementedError( | ||
"Operator's shape must be (2,2) " f"not {operator.shape}." | ||
) | ||
# Identity position | ||
identity = np.eye(2) | ||
if np.any(self.eff_noise_opers[0] != identity): | ||
raise NotImplementedError( | ||
"You must put the identity matrix at the " | ||
"beginning of the operator list." | ||
) | ||
# Completeness relation checking | ||
sum_op = np.zeros((2, 2), dtype=complex) | ||
for prob, op in zip(self.eff_noise_probs, self.eff_noise_opers): | ||
sum_op += prob * op @ op.conj().transpose() | ||
|
||
if not np.all(np.isclose(sum_op, identity)): | ||
raise ValueError( | ||
"The completeness relation is not verified." | ||
f" Ended up with {sum_op} instead of {identity}." | ||
) |
Oops, something went wrong.