diff --git a/.flake8 b/.flake8 index e5b0a13bc..32b17d17f 100644 --- a/.flake8 +++ b/.flake8 @@ -14,4 +14,5 @@ per-file-ignores = # F401 Module imported but unused tests/*: D100, D101, D102, D103 __init__.py: F401 + pulser-core/pulser/backends.py: F401 setup.py: D100 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 109486753..1429c5549 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pulser + flake8 install uses: ./.github/workflows/pulser-setup with: @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pulser + black install uses: ./.github/workflows/pulser-setup with: @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pulser + isort install uses: ./.github/workflows/pulser-setup with: @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pulser + mypy install uses: ./.github/workflows/pulser-setup with: @@ -61,7 +61,7 @@ jobs: python-version: ["3.8", "3.11"] steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pulser + pytest install uses: ./.github/workflows/pulser-setup with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a44b5ccc0..b9f5ba27a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,11 +13,11 @@ jobs: id-token: write steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install Python dependencies @@ -69,11 +69,11 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Pulser from PyPI diff --git a/.github/workflows/pulser-setup/action.yml b/.github/workflows/pulser-setup/action.yml index acfc7dfcb..ba4677ba1 100644 --- a/.github/workflows/pulser-setup/action.yml +++ b/.github/workflows/pulser-setup/action.yml @@ -4,19 +4,19 @@ inputs: python-version: description: Python version required: false - default: '3.9' + default: "3.9" extra-packages: description: Extra packages to install (give to grep) required: false - default: '' + default: "" runs: - using: 'composite' - steps: + using: "composite" + steps: - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - cache: 'pip' + cache: "pip" - name: Install Pulser shell: bash run: | @@ -28,4 +28,4 @@ runs: run: | grep -e ${{ inputs.extra-packages }} dev_requirements.txt \ | sed 's/ //g' \ - | xargs pip install \ No newline at end of file + | xargs pip install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 270785a45..21f0184d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,11 +15,22 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + # Python 3.8 and 3.9 does not run on macos-latest (14) + # Uses macos-13 for 3.8 and 3.9 and macos-latest for >=3.10 + os: [ubuntu-latest, macos-13, macos-latest, windows-latest] python-version: ["3.8", "3.9", "3.10", "3.11"] + exclude: + - os: macos-latest + python-version: "3.8" + - os: macos-latest + python-version: "3.9" + - os: macos-13 + python-version: "3.10" + - os: macos-13 + python-version: "3.11" steps: - name: Check out Pulser - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Pulser + pytest setup uses: ./.github/workflows/pulser-setup with: diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 02dd74d36..1fa819fbc 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out base branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} - name: Get old version @@ -19,7 +19,7 @@ jobs: echo "Old version: $old_version" echo "old_version=$old_version" >> $GITHUB_ENV - name: Check out head branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get new version run: | new_version="$(head -1 VERSION.txt)" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e61c31aa..3acd15772 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 24.4.0 hooks: - id: black-jupyter diff --git a/VERSION.txt b/VERSION.txt index 44e33a411..66333910a 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.17.4 +0.18.0 diff --git a/dev_requirements.txt b/dev_requirements.txt index 37188585f..4057dc9d6 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,5 +1,5 @@ # tests -black[jupyter] ~= 23.1 +black[jupyter] ~= 24.3 flake8 flake8-docstrings isort 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/docs/source/index.rst b/docs/source/index.rst index 564f4ce8b..bdccc5b32 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -75,7 +75,7 @@ computers and simulators, check the pages in :doc:`review`. tutorials/noisy_sim tutorials/spam tutorials/laser_noise - tutorials/kraus_ops + tutorials/effective_noise .. toctree:: :maxdepth: 1 diff --git a/docs/source/tutorials/kraus_ops.nblink b/docs/source/tutorials/effective_noise.nblink similarity index 100% rename from docs/source/tutorials/kraus_ops.nblink rename to docs/source/tutorials/effective_noise.nblink 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/__init__.py b/pulser-core/pulser/__init__.py index 645dfaced..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, ) @@ -42,6 +42,7 @@ devices as devices, sampler as sampler, backend as backend, + backends as backends, ) __all__ = [ @@ -58,6 +59,8 @@ # pulser.register "Register", "Register3D", + # pulser.noise_model + "NoiseModel", # pulser.devices "AnalogDevice", "DigitalAnalogDevice", @@ -66,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 6a30f2862..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"] @@ -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/backends.py b/pulser-core/pulser/backends.py new file mode 100644 index 000000000..c32a915aa --- /dev/null +++ b/pulser-core/pulser/backends.py @@ -0,0 +1,52 @@ +# Copyright 2024 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. +"""A module gathering all available backends.""" +from __future__ import annotations + +import importlib +from typing import TYPE_CHECKING, Type + +from pulser.backend.abc import Backend + +if TYPE_CHECKING: + from pulser.backend import QPUBackend as QPUBackend + from pulser_pasqal import EmuFreeBackend as EmuFreeBackend + from pulser_pasqal import EmuTNBackend as EmuTNBackend + from pulser_simulation import QutipBackend as QutipBackend + +_BACKENDS = { + "QPUBackend": "pulser.backend", + "QutipBackend": "pulser_simulation", + "EmuFreeBackend": "pulser_pasqal", + "EmuTNBackend": "pulser_pasqal", +} + + +# This prevents * imports to attempt importing unavailable backends +__all__: list[str] = [] + + +def __getattr__(name: str) -> Type[Backend]: + if name not in _BACKENDS: + raise AttributeError(f"Module {__name__!r} has no attribute {name!r}.") + try: + return getattr( # type: ignore + importlib.import_module(_BACKENDS[name]), + name, + ) + except ModuleNotFoundError: + raise AttributeError( + f"{name!r} requires the {_BACKENDS[name]!r} package. To install " + f"it, run `pip install {_BACKENDS[name]}`." + ) 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 f7c81a8aa..8244948c9 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,16 @@ DIMENSIONS = Literal[2, 3] -ALWAYS_OPTIONAL_PARAMS = ("max_sequence_duration", "max_runs", "dmm_objects") +ALWAYS_OPTIONAL_PARAMS = ("max_sequence_duration", "max_runs") +OPTIONAL_IN_ABSTR_REPR = tuple( + list(ALWAYS_OPTIONAL_PARAMS) + + [ + "dmm_objects", + "default_noise_model", + "requires_layout", + "accepts_new_layouts", + ] +) PARAMS_WITH_ABSTR_REPR = ("channel_objects", "channel_ids", "dmm_objects") @@ -55,7 +65,7 @@ class BaseDevice(ABC): dmm_objects: The DMM subclass instances specifying each channel in the device. They are referenced by their order in the list, with the ID "dmm_[index in dmm_objects]". - rybderg_level: The value of the principal quantum number :math:`n` + rydberg_level: The value of the principal quantum number :math:`n` when the Rydberg level used is of the form :math:`|nS_{1/2}, m_j = +1/2\rangle`. max_atom_num: Maximum number of atoms supported in an array. @@ -74,7 +84,13 @@ 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. + requires_layout: Whether the register used in the sequence must be + created from a register layout. Only enforced in QPU execution. """ + name: str dimensions: DIMENSIONS rydberg_level: int @@ -86,10 +102,12 @@ class BaseDevice(ABC): max_layout_filling: float = 0.5 max_sequence_duration: int | None = None max_runs: int | None = None + requires_layout: bool = False reusable_channels: bool = field(default=False, init=False) 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( @@ -217,6 +235,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) @@ -450,8 +471,8 @@ def _to_dict(self) -> dict[str, Any]: def _to_abstract_repr(self) -> dict[str, Any]: defaults = get_dataclass_defaults(fields(self)) params = self._params() - for p in ALWAYS_OPTIONAL_PARAMS: - if params[p] == defaults[p]: + for p in OPTIONAL_IN_ABSTR_REPR: + if p in params and params[p] == defaults[p]: params.pop(p, None) # Delete parameters of PARAMS_WITH_ABSTR_REPR in params for p in PARAMS_WITH_ABSTR_REPR: @@ -486,7 +507,15 @@ class Device(BaseDevice): Attributes: name: The name of the device. dimensions: Whether it supports 2D or 3D arrays. - rybderg_level : The value of the principal quantum number :math:`n` + channel_objects: The Channel subclass instances specifying each + channel in the device. + channel_ids: Custom IDs for each channel object. When defined, + an ID must be given for each channel. If not defined, the IDs are + generated internally based on the channels' names and addressing. + dmm_objects: The DMM subclass instances specifying each channel in the + device. They are referenced by their order in the list, with the ID + "dmm_[index in dmm_objects]". + rydberg_level: The value of the principal quantum number :math:`n` when the Rydberg level used is of the form :math:`|nS_{1/2}, m_j = +1/2\rangle`. max_atom_num: Maximum number of atoms supported in an array. @@ -505,14 +534,25 @@ 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. + requires_layout: Whether the register used in the sequence must be + created from a register layout. Only enforced in QPU execution. pre_calibrated_layouts: RegisterLayout instances that are already available on the Device. + accepts_new_layouts: Whether registers built from register layouts + that are not already calibrated are accepted. Only enforced in + QPU execution. """ + max_atom_num: int max_radial_distance: int + requires_layout: bool = True pre_calibrated_layouts: tuple[RegisterLayout, ...] = field( default_factory=tuple ) + accepts_new_layouts: bool = True def __post_init__(self) -> None: super().__post_init__() @@ -630,15 +670,17 @@ def _specs(self, for_docs: bool = False) -> str: + f" {ch.max_amp:.4g} rad/µs" ), ( - "\t" - + r"- Maximum :math:`|\delta|`:" - + f" {ch.max_abs_detuning:.4g} rad/µs" - ) - if not isinstance(ch, DMM) - else ( - "\t" - + r"- Bottom :math:`|\delta|`:" - + f" {ch.bottom_detuning:.4g} rad/µs" + ( + "\t" + + r"- Maximum :math:`|\delta|`:" + + f" {ch.max_abs_detuning:.4g} rad/µs" + ) + if not isinstance(ch, DMM) + else ( + "\t" + + r"- Bottom :math:`|\delta|`:" + + f" {ch.bottom_detuning:.4g} rad/µs" + ) ), f"\t- Minimum average amplitude: {ch.min_avg_amp} rad/µs", ] @@ -681,7 +723,15 @@ class VirtualDevice(BaseDevice): Attributes: name: The name of the device. dimensions: Whether it supports 2D or 3D arrays. - rybderg_level : The value of the principal quantum number :math:`n` + channel_objects: The Channel subclass instances specifying each + channel in the device. + channel_ids: Custom IDs for each channel object. When defined, + an ID must be given for each channel. If not defined, the IDs are + generated internally based on the channels' names and addressing. + dmm_objects: The DMM subclass instances specifying each channel in the + device. They are referenced by their order in the list, with the ID + "dmm_[index in dmm_objects]". + rydberg_level: The value of the principal quantum number :math:`n` when the Rydberg level used is of the form :math:`|nS_{1/2}, m_j = +1/2\rangle`. max_atom_num: Maximum number of atoms supported in an array. @@ -700,9 +750,15 @@ 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. + requires_layout: Whether the register used in the sequence must be + created from a register layout. Only enforced in QPU execution. reusable_channels: Whether each channel can be declared multiple times on the same pulse sequence. """ + min_atom_distance: float = 0 max_atom_num: int | None = None max_radial_distance: int | None = None 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/deserializer.py b/pulser-core/pulser/json/abstract_repr/deserializer.py index 96e773f26..b4d77389a 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 @@ -209,11 +210,15 @@ def _deserialize_operation(seq: Sequence, op: dict, vars: dict) -> None: channel=op["channel"], ) elif op["op"] == "align": - seq.align(*op["channels"]) + seq.align( + *op["channels"], + at_rest=op.get("at_rest", True), + ) elif op["op"] == "delay": seq.delay( duration=_deserialize_parameter(op["time"], vars), channel=op["channel"], + at_rest=op.get("at_rest", False), ) elif op["op"] == "phase_shift": seq.phase_shift_index( @@ -377,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 @@ -408,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: @@ -561,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 3ef690267..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": { @@ -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": [ @@ -181,10 +189,14 @@ "pre_calibrated_layouts": { "description": "Register layouts already calibrated on the device.", "items": { - "$ref": "#/definitions/Layout" + "$ref": "layout-schema.json" }, "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" @@ -355,7 +375,56 @@ "type": "null" }, { - "$ref": "#/definitions/RydbergEOM" + "additionalProperties": false, + "properties": { + "controlled_beams": { + "description": "The beams that can be switched on/off with an EOM.", + "items": { + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "type": "array" + }, + "custom_buffer_time": { + "description": "A custom wait time to enforce during EOM buffers.", + "type": "number" + }, + "intermediate_detuning": { + "description": "The detuning between the two beams, in rad/µs.", + "type": "number" + }, + "limiting_beam": { + "description": "The beam with the smallest amplitude range.", + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "max_limiting_amp": { + "description": "The maximum amplitude the limiting beam can reach, in rad/µs.", + "type": "number" + }, + "mod_bandwidth": { + "description": "The EOM modulation bandwidth at -3dB (50% reduction), in MHz.", + "type": "number" + }, + "multiple_beam_control": { + "description": "Whether both EOMs can be used simultaneously or not.", + "type": "boolean" + } + }, + "required": [ + "mod_bandwidth", + "limiting_beam", + "max_limiting_amp", + "intermediate_detuning", + "controlled_beams" + ], + "type": "object" } ], "description": "Configuration of an associated EOM." @@ -632,7 +701,56 @@ "type": "null" }, { - "$ref": "#/definitions/RydbergEOM" + "additionalProperties": false, + "properties": { + "controlled_beams": { + "description": "The beams that can be switched on/off with an EOM.", + "items": { + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "type": "array" + }, + "custom_buffer_time": { + "description": "A custom wait time to enforce during EOM buffers.", + "type": "number" + }, + "intermediate_detuning": { + "description": "The detuning between the two beams, in rad/µs.", + "type": "number" + }, + "limiting_beam": { + "description": "The beam with the smallest amplitude range.", + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "max_limiting_amp": { + "description": "The maximum amplitude the limiting beam can reach, in rad/µs.", + "type": "number" + }, + "mod_bandwidth": { + "description": "The EOM modulation bandwidth at -3dB (50% reduction), in MHz.", + "type": "number" + }, + "multiple_beam_control": { + "description": "Whether both EOMs can be used simultaneously or not.", + "type": "boolean" + } + }, + "required": [ + "mod_bandwidth", + "limiting_beam", + "max_limiting_amp", + "intermediate_detuning", + "controlled_beams" + ], + "type": "object" } ], "description": "Configuration of an associated EOM." @@ -899,9 +1017,6 @@ ], "description": "A Channel that can be physical or virtual." }, - "Layout": { - "$ref": "layout-schema.json" - }, "PhysicalChannel": { "anyOf": [ { @@ -926,7 +1041,56 @@ "type": "null" }, { - "$ref": "#/definitions/RydbergEOM" + "additionalProperties": false, + "properties": { + "controlled_beams": { + "description": "The beams that can be switched on/off with an EOM.", + "items": { + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "type": "array" + }, + "custom_buffer_time": { + "description": "A custom wait time to enforce during EOM buffers.", + "type": "number" + }, + "intermediate_detuning": { + "description": "The detuning between the two beams, in rad/µs.", + "type": "number" + }, + "limiting_beam": { + "description": "The beam with the smallest amplitude range.", + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "max_limiting_amp": { + "description": "The maximum amplitude the limiting beam can reach, in rad/µs.", + "type": "number" + }, + "mod_bandwidth": { + "description": "The EOM modulation bandwidth at -3dB (50% reduction), in MHz.", + "type": "number" + }, + "multiple_beam_control": { + "description": "Whether both EOMs can be used simultaneously or not.", + "type": "boolean" + } + }, + "required": [ + "mod_bandwidth", + "limiting_beam", + "max_limiting_amp", + "intermediate_detuning", + "controlled_beams" + ], + "type": "object" } ], "description": "Configuration of an associated EOM." @@ -1176,7 +1340,56 @@ "type": "null" }, { - "$ref": "#/definitions/RydbergEOM" + "additionalProperties": false, + "properties": { + "controlled_beams": { + "description": "The beams that can be switched on/off with an EOM.", + "items": { + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "type": "array" + }, + "custom_buffer_time": { + "description": "A custom wait time to enforce during EOM buffers.", + "type": "number" + }, + "intermediate_detuning": { + "description": "The detuning between the two beams, in rad/µs.", + "type": "number" + }, + "limiting_beam": { + "description": "The beam with the smallest amplitude range.", + "enum": [ + "RED", + "BLUE" + ], + "type": "string" + }, + "max_limiting_amp": { + "description": "The maximum amplitude the limiting beam can reach, in rad/µs.", + "type": "number" + }, + "mod_bandwidth": { + "description": "The EOM modulation bandwidth at -3dB (50% reduction), in MHz.", + "type": "number" + }, + "multiple_beam_control": { + "description": "Whether both EOMs can be used simultaneously or not.", + "type": "boolean" + } + }, + "required": [ + "mod_bandwidth", + "limiting_beam", + "max_limiting_amp", + "intermediate_detuning", + "controlled_beams" + ], + "type": "object" } ], "description": "Configuration of an associated EOM." @@ -1499,57 +1712,6 @@ "mod_bandwidth" ], "type": "object" - }, - "RydbergBeam": { - "enum": [ - "RED", - "BLUE" - ], - "type": "string" - }, - "RydbergEOM": { - "additionalProperties": false, - "properties": { - "controlled_beams": { - "description": "The beams that can be switched on/off with an EOM.", - "items": { - "$ref": "#/definitions/RydbergBeam" - }, - "type": "array" - }, - "custom_buffer_time": { - "description": "A custom wait time to enforce during EOM buffers.", - "type": "number" - }, - "intermediate_detuning": { - "description": "The detuning between the two beams, in rad/µs.", - "type": "number" - }, - "limiting_beam": { - "$ref": "#/definitions/RydbergBeam", - "description": "The beam with the smallest amplitude range." - }, - "max_limiting_amp": { - "description": "The maximum amplitude the limiting beam can reach, in rad/µs.", - "type": "number" - }, - "mod_bandwidth": { - "description": "The EOM modulation bandwidth at -3dB (50% reduction), in MHz.", - "type": "number" - }, - "multiple_beam_control": { - "description": "Whether both EOMs can be used simultaneously or not.", - "type": "boolean" - } - }, - "required": [ - "mod_bandwidth", - "limiting_beam", - "max_limiting_amp", - "intermediate_detuning", - "controlled_beams" - ], - "type": "object" } } } 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..6fbaecee8 --- /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/schemas/register-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json index 7619d03db..0595f5f81 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/register-schema.json @@ -26,9 +26,6 @@ ], "type": "object" }, - "Layout": { - "$ref": "layout-schema.json" - }, "QubitId": { "description": "Name for a qubit.", "type": "string" @@ -37,7 +34,7 @@ "additionalProperties": false, "properties": { "layout": { - "$ref": "#/definitions/Layout", + "$ref": "layout-schema.json", "description": "The trap layout underlying the register." }, "register": { diff --git a/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json index 3df5e08b3..01509615e 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/sequence-schema.json @@ -1,4 +1,5 @@ { + "$id": "sequence-schema.json", "$ref": "#/definitions/PulserSequence", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { @@ -156,9 +157,6 @@ ], "type": "object" }, - "Device": { - "$ref": "device-schema.json" - }, "ExprArgument": { "anyOf": [ { @@ -354,9 +352,6 @@ ], "type": "object" }, - "Layout": { - "$ref": "layout-schema.json" - }, "MappableQubit": { "additionalProperties": false, "properties": { @@ -411,6 +406,9 @@ "additionalProperties": false, "description": "Aligns multiple channels in time.\n\nIntroduces delays that align the provided channels with the one that finished the latest, such that the next action added to any of them will start right after the latest channel has finished.", "properties": { + "at_rest": { + "type": "boolean" + }, "channels": { "items": { "$ref": "#/definitions/ChannelName" @@ -481,6 +479,10 @@ "additionalProperties": false, "description": "Adds extra fixed delay before starting the pulse.", "properties": { + "at_rest": { + "description": "Whether to wait for the previous pulse to finish before applying the delay.", + "type": "boolean" + }, "channel": { "$ref": "#/definitions/ChannelName", "description": "Channel on which to insert a delay" @@ -806,13 +808,13 @@ "$ref": "#/definitions/HardcodedDevice" }, { - "$ref": "#/definitions/Device" + "$ref": "device-schema.json" } ], "description": "A valid device in which to execute the Sequence" }, "layout": { - "$ref": "#/definitions/Layout", + "$ref": "layout-schema.json", "description": "The trap layout underlying the register." }, "magnetic_field": { @@ -903,13 +905,13 @@ "$ref": "#/definitions/HardcodedDevice" }, { - "$ref": "#/definitions/Device" + "$ref": "device-schema.json" } ], "description": "A valid device in which to execute the Sequence" }, "layout": { - "$ref": "#/definitions/Layout", + "$ref": "layout-schema.json", "description": "The trap layout underlying the register." }, "magnetic_field": { diff --git a/pulser-core/pulser/json/abstract_repr/serializer.py b/pulser-core/pulser/json/abstract_repr/serializer.py index 0bc2e8ac7..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)) @@ -269,16 +271,21 @@ def remove_kwarg_if_default( } ) elif call.name == "align": - operations.append({"op": "align", "channels": list(call.args)}) - elif call.name == "delay": - data = get_all_args(("duration", "channel"), call) + optional = remove_kwarg_if_default(call.kwargs, "align", "at_rest") operations.append( - { - "op": "delay", - "channel": data["channel"], - "time": data["duration"], - } + {"op": "align", "channels": list(call.args), **optional} ) + elif call.name == "delay": + data = get_all_args(("duration", "channel", "at_rest"), call) + data = remove_kwarg_if_default(data, "delay", "at_rest") + op_dict = { + "op": "delay", + "channel": data["channel"], + "time": data["duration"], + } + if "at_rest" in data: + op_dict["at_rest"] = data["at_rest"] + operations.append(op_dict) elif call.name == "measure": data = get_all_args(("basis",), call) res["measurement"] = data["basis"] diff --git a/pulser-core/pulser/json/abstract_repr/validation.py b/pulser-core/pulser/json/abstract_repr/validation.py index 8dde2e53c..42725aa0f 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/json/supported.py b/pulser-core/pulser/json/supported.py index 9e7ad9383..597fbcb26 100644 --- a/pulser-core/pulser/json/supported.py +++ b/pulser-core/pulser/json/supported.py @@ -66,6 +66,7 @@ "pulser.register.register3d": ("Register3D",), "pulser.register.register_layout": ("RegisterLayout",), "pulser.register.special_layouts": ( + "RectangularLatticeLayout", "SquareLatticeLayout", "TriangularLatticeLayout", ), diff --git a/pulser-core/pulser/backend/noise_model.py b/pulser-core/pulser/noise_model.py similarity index 56% rename from pulser-core/pulser/backend/noise_model.py rename to pulser-core/pulser/noise_model.py index cf76ab8de..53585344c 100644 --- a/pulser-core/pulser/backend/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -14,14 +14,27 @@ """Defines a noise model class for emulator backends.""" from __future__ import annotations -import warnings -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 + +__all__ = ["NoiseModel"] NOISE_TYPES = Literal[ - "doppler", "amplitude", "SPAM", "dephasing", "depolarizing", "eff_noise" + "doppler", + "amplitude", + "SPAM", + "dephasing", + "relaxation", + "depolarizing", + "eff_noise", ] @@ -31,30 +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`). + 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`. + 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 @@ -68,17 +94,18 @@ class NoiseModel: pulses. amp_sigma: Dictates the fluctuations in amplitude as a standard deviation of a normal distribution centered in 1. - dephasing_rate: The rate of a dephasing error occuring (in rad/µs). - dephasing_prob: (Deprecated) The rate of a dephasing error occuring - (in rad/µs). Use `dephasing_rate` instead. - depolarizing_rate: The rate (in rad/µs) at which a depolarizing + relaxation_rate: The rate of relaxation from the Rydberg to the + ground state (in 1/µs). Corresponds to 1/T1. + dephasing_rate: The rate of a dephasing occuring (in 1/µs) in a + Rydberg state superpostion. Only used if a Rydberg state is + involved. Corresponds to 1/T2*. + hyperfine_dephasing_rate: The rate of dephasing occuring (in 1/µs) + between hyperfine ground states. Only used if the hyperfine + state is involved. + depolarizing_rate: The rate (in 1/µs) at which a depolarizing error occurs. - depolarizing_prob: (Deprecated) The rate (in rad/µs) at which a - depolarizing error occurs. Use `depolarizing_rate` instead. eff_noise_rates: The rate associated to each effective noise operator - (in rad/µs). - eff_noise_probs: (Deprecated) The rate associated to each effective - noise operator (in rad/µs). Use `eff_noise_rate` instead. + (in 1/µs). eff_noise_opers: The operators for the effective noise model. """ @@ -91,50 +118,18 @@ class NoiseModel: temperature: float = 50.0 laser_waist: float = 175.0 amp_sigma: float = 5e-2 + relaxation_rate: float = 0.01 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) - dephasing_prob: float | None = None - depolarizing_prob: float | None = None - eff_noise_probs: list[float] = 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: - default_field_value = { - field.name: field.default for field in fields(self) - } - for noise in ["dephasing", "depolarizing", "eff_noise"]: - # Probability and rates should be the same - prob_name = f"{noise}_prob{'s' if noise=='eff_noise' else ''}" - rate_name = f"{noise}_rate{'s' if noise=='eff_noise' else ''}" - prob, rate = (getattr(self, prob_name), getattr(self, rate_name)) - if len(prob) > 0 if noise == "eff_noise" else prob is not None: - warnings.warn( - f"{prob_name} is deprecated. Use {rate_name} instead.", - DeprecationWarning, - ) - if prob != rate: - if ( - len(rate) > 0 - if noise == "eff_noise" - else rate != default_field_value[rate_name] - ): - raise ValueError( - f"If both defined, `{rate_name}` and `{prob_name}`" - " must be equal." - ) - warnings.warn( - f"Setting {rate_name} with the value from " - f"{prob_name}.", - UserWarning, - ) - self._change_attribute(rate_name, prob) - self._change_attribute(prob_name, getattr(self, rate_name)) - assert self.dephasing_prob == self.dephasing_rate - assert self.depolarizing_prob == self.depolarizing_rate - assert self.eff_noise_probs == self.eff_noise_rates positive = { "dephasing_rate", + "hyperfine_dephasing_rate", + "relaxation_rate", "depolarizing_rate", } strict_positive = { @@ -171,12 +166,21 @@ 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() - def _change_attribute(self, attr_name: str, new_value: Any) -> None: - object.__setattr__(self, attr_name, new_value) - def _check_noise_types(self) -> None: for noise_type in self.noise_types: if noise_type not in get_args(NOISE_TYPES): @@ -185,15 +189,6 @@ def _check_noise_types(self) -> None: + "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_rates): @@ -222,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-core/pulser/pulse.py b/pulser-core/pulser/pulse.py index e61914bf7..6da051306 100644 --- a/pulser-core/pulser/pulse.py +++ b/pulser-core/pulser/pulse.py @@ -208,6 +208,30 @@ def fall_time(self, channel: Channel, in_eom_mode: bool = False) -> int: ) return aligned_start_extra_time + end_extra_time + def get_full_duration( + self, channel: Channel, in_eom_mode: bool = False + ) -> int: + """Calculates the pulse's full duration after output modulation. + + The full duration of a pulse is the total time between the start of + the input signal and the end of the output signal, as shown in + the sequence. + + Args: + channel: The pulse executing the channel. + in_eom_mode: Whether the pulse is executed in EOM mode. + """ + if not isinstance(channel, pulser.channels.base_channel.Channel): + raise TypeError( + "'channel' must be a channel object instance, not " + f"{type(channel)}." + ) + if in_eom_mode and not channel.supports_eom(): + raise ValueError( + "The given channel does not support EOM mode operation." + ) + return self.duration + self.fall_time(channel, in_eom_mode) + def _to_dict(self) -> dict[str, Any]: return obj_to_dict( self, diff --git a/pulser-core/pulser/register/__init__.py b/pulser-core/pulser/register/__init__.py index 4be93b421..5eb20397b 100644 --- a/pulser-core/pulser/register/__init__.py +++ b/pulser-core/pulser/register/__init__.py @@ -20,6 +20,7 @@ from pulser.register.special_layouts import ( SquareLatticeLayout, TriangularLatticeLayout, + RectangularLatticeLayout, ) __all__ = [ @@ -29,4 +30,5 @@ "RegisterLayout", "SquareLatticeLayout", "TriangularLatticeLayout", + "RectangularLatticeLayout", ] diff --git a/pulser-core/pulser/register/_coordinates.py b/pulser-core/pulser/register/_coordinates.py index ac104396e..575e65cdd 100644 --- a/pulser-core/pulser/register/_coordinates.py +++ b/pulser-core/pulser/register/_coordinates.py @@ -1,4 +1,5 @@ """Defines a collection of coordinates.""" + from __future__ import annotations import hashlib diff --git a/pulser-core/pulser/register/_patterns.py b/pulser-core/pulser/register/_patterns.py index 6b622e783..ccffe98a1 100644 --- a/pulser-core/pulser/register/_patterns.py +++ b/pulser-core/pulser/register/_patterns.py @@ -118,9 +118,11 @@ def triangular_hex(n_points: int) -> np.ndarray: for side in range(6) for atom in range( 1, - min_atoms_per_side + 2 - if points_left > sides_order[side] - else min_atoms_per_side + 1, + ( + min_atoms_per_side + 2 + if points_left > sides_order[side] + else min_atoms_per_side + 1 + ), ) ], dtype=float, diff --git a/pulser-core/pulser/register/register.py b/pulser-core/pulser/register/register.py index 026550a82..260608e0b 100644 --- a/pulser-core/pulser/register/register.py +++ b/pulser-core/pulser/register/register.py @@ -89,7 +89,7 @@ def rectangle( spacing: float = 4.0, prefix: Optional[str] = None, ) -> Register: - """Initializes the register with the qubits in a rectangular array. + """Creates a rectangular array of qubits on a square lattice. Args: rows: Number of rows. @@ -102,6 +102,32 @@ def rectangle( Returns: A register with qubits placed in a rectangular array. """ + return cls.rectangular_lattice(rows, columns, spacing, spacing, prefix) + + @classmethod + def rectangular_lattice( + cls, + rows: int, + columns: int, + row_spacing: float = 4.0, + col_spacing: float = 2.0, + prefix: Optional[str] = None, + ) -> Register: + """Creates a rectangular array of qubits on a rectangular lattice. + + Args: + rows: Number of rows. + columns: Number of columns. + row_spacing: The distance between rows in μm. + col_spacing: The distance between columns in μm. + prefix: The prefix for the qubit ids. If defined, each qubit + id starts with the prefix, followed by an int from 0 to N-1 + (e.g. prefix='q' -> IDs: 'q0', 'q1', 'q2', ...) + + Returns: + Register with qubits placed in a rectangular array on a + rectangular lattice. + """ # Check rows if rows < 1: raise ValueError( @@ -117,13 +143,12 @@ def rectangle( ) # Check spacing - if spacing <= 0.0: - raise ValueError( - f"Spacing between atoms (`spacing` = {spacing})" - " must be greater than 0." - ) + if row_spacing <= 0.0 or col_spacing <= 0.0: + raise ValueError("Spacing between atoms must be greater than 0.") - coords = patterns.square_rect(rows, columns) * spacing + coords = patterns.square_rect(rows, columns) + coords[:, 0] = coords[:, 0] * col_spacing + coords[:, 1] = coords[:, 1] * row_spacing return cls.from_coordinates(coords, center=True, prefix=prefix) @@ -281,35 +306,6 @@ def max_connectivity( return cls.from_coordinates(coords, center=False, prefix=prefix) - def rotate(self, degrees: float) -> None: - """Rotates the array around the origin by the given angle. - - Warning: - Deprecated in v0.17 in favour of `Register.rotated()`. To be - removed in v0.18. - - Args: - degrees: The angle of rotation in degrees. - """ - with warnings.catch_warnings(): - warnings.simplefilter("always") - warnings.warn( - "'Register.rotate()' has been deprecated and will be " - "removed in v0.18. Consider using `Register.rotated()` " - "instead.", - category=DeprecationWarning, - stacklevel=2, - ) - if self.layout is not None: - raise TypeError( - "A register defined from a RegisterLayout cannot be rotated." - ) - theta = np.deg2rad(degrees) - rot = np.array( - [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]] - ) - object.__setattr__(self, "_coords", [rot @ v for v in self._coords]) - def rotated(self, degrees: float) -> Register: """Makes a new rotated register. diff --git a/pulser-core/pulser/register/special_layouts.py b/pulser-core/pulser/register/special_layouts.py index 510eb44c2..d285f3286 100644 --- a/pulser-core/pulser/register/special_layouts.py +++ b/pulser-core/pulser/register/special_layouts.py @@ -26,26 +26,33 @@ from pulser.register import Register -class SquareLatticeLayout(RegisterLayout): - """A RegisterLayout with a square lattice pattern in a rectangular shape. +class RectangularLatticeLayout(RegisterLayout): + """RegisterLayout with rectangular lattice pattern in a rectangular shape. Args: rows: The number of rows of traps. columns: The number of columns of traps. - spacing: The distance between neighbouring traps (in µm). + col_spacing: Horizontal distance between neighbouring traps (in µm). + row_spacing: Vertical distance between neighbouring traps (in µm) """ - def __init__(self, rows: int, columns: int, spacing: float): - """Initializes a SquareLatticeLayout.""" + def __init__( + self, rows: int, columns: int, col_spacing: float, row_spacing: float + ): + """Initializes a RectangularLatticeLayout.""" self._rows = int(rows) self._columns = int(columns) - self._spacing = float(spacing) + self._col_spacing = float(col_spacing) + self._row_spacing = float(row_spacing) slug = ( - f"SquareLatticeLayout({self._rows}x{self._columns}, " - f"{self._spacing}µm)" + f"RectangularLatticeLayout({self._rows}x{self._columns}, " + f"{self._col_spacing}x{self._row_spacing}µm)" ) + self._traps = patterns.square_rect(self._rows, self._columns) + self._traps[:, 0] = self._traps[:, 0] * self._col_spacing + self._traps[:, 1] = self._traps[:, 1] * self._row_spacing super().__init__( - patterns.square_rect(self._rows, self._columns) * self._spacing, + trap_coordinates=self._traps, slug=slug, ) @@ -84,9 +91,11 @@ def rectangular_register( if rows > self._rows or columns > self._columns: raise ValueError( f"A '{rows}x{columns}' array doesn't fit a " - f"{self._rows}x{self._columns} SquareLatticeLayout." + f"{self._rows}x{self._columns} RectangularLatticeLayout." ) - points = patterns.square_rect(rows, columns) * self._spacing + points = patterns.square_rect(rows, columns) + points[:, 0] = points[:, 0] * self._col_spacing + points[:, 1] = points[:, 1] * self._row_spacing trap_ids = self.get_traps_from_coordinates(*points) qubit_ids = [f"{prefix}{i}" for i in range(len(trap_ids))] return cast( @@ -94,6 +103,41 @@ def rectangular_register( self.define_register(*trap_ids, qubit_ids=qubit_ids), ) + def _to_dict(self) -> dict[str, Any]: + return obj_to_dict( + self, + self._rows, + self._columns, + self._col_spacing, + self._row_spacing, + ) + + +class SquareLatticeLayout(RectangularLatticeLayout): + """A RegisterLayout with a square lattice pattern in a rectangular shape. + + Args: + rows: The number of rows of traps. + columns: The number of columns of traps. + spacing: The distance between neighbouring traps (in µm). + """ + + def __init__(self, rows: int, columns: int, spacing: float): + """Initializes a SquareLatticeLayout.""" + self._rows = int(rows) + self._columns = int(columns) + self._spacing = float(spacing) + self._col_spacing = self._spacing + self._row_spacing = self._spacing + super().__init__( + self._rows, self._columns, self._spacing, self._spacing + ) + slug = ( + f"SquareLatticeLayout({self._rows}x{self._columns}, " + f"{self._spacing}µm)" + ) + object.__setattr__(self, "slug", slug) + def _to_dict(self) -> dict[str, Any]: return obj_to_dict(self, self._rows, self._columns, self._spacing) diff --git a/pulser-core/pulser/sampler/sampler.py b/pulser-core/pulser/sampler/sampler.py index 6c5c6b2ea..c46ded256 100644 --- a/pulser-core/pulser/sampler/sampler.py +++ b/pulser-core/pulser/sampler/sampler.py @@ -1,4 +1,5 @@ """The main function for sequence sampling.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Optional diff --git a/pulser-core/pulser/sampler/samples.py b/pulser-core/pulser/sampler/samples.py index 00517f4f2..e88e3c676 100644 --- a/pulser-core/pulser/sampler/samples.py +++ b/pulser-core/pulser/sampler/samples.py @@ -1,4 +1,5 @@ """Dataclasses for storing and processing the samples.""" + from __future__ import annotations import itertools diff --git a/pulser-core/pulser/sequence/_seq_drawer.py b/pulser-core/pulser/sequence/_seq_drawer.py index c123ce8e5..b7d72fb52 100644 --- a/pulser-core/pulser/sequence/_seq_drawer.py +++ b/pulser-core/pulser/sequence/_seq_drawer.py @@ -325,9 +325,11 @@ def gather_qubit_data( if sub_target != set(): pad_times = ( 0 if times == "initial" else target[0], - 0 - if times == "initial" - else total_duration - target[1], + ( + 0 + if times == "initial" + else total_duration - target[1] + ), ) qubit_data[i][tuple(sub_target)] = ( w @@ -935,11 +937,13 @@ def _draw_qubit_content( draw_data = {"input": draw_input, "modulated": draw_modulation} n_data = sum(list(draw_data.values())) qubit_data = [ - gather_qubit_data( - sampled_seq, data, register, (data_name == "modulated") + ( + gather_qubit_data( + sampled_seq, data, register, (data_name == "modulated") + ) + if to_draw + else None ) - if to_draw - else None for data_name, to_draw in draw_data.items() ] # Figure composed of 2 subplots (input, modulated) each composed diff --git a/pulser-core/pulser/sequence/_seq_str.py b/pulser-core/pulser/sequence/_seq_str.py index 8cbd95391..af578a421 100644 --- a/pulser-core/pulser/sequence/_seq_str.py +++ b/pulser-core/pulser/sequence/_seq_str.py @@ -63,10 +63,12 @@ def seq_to_str(sequence: Sequence) -> str: full += dmm_det_line.format( ts.ti, ts.tf, - ts.type.detuning - if not seq.is_detuned_delay(ts.type) - else "{:.3g} rad/µs".format( - cast(float, ts.type.detuning[0]) + ( + ts.type.detuning + if not seq.is_detuned_delay(ts.type) + else "{:.3g} rad/µs".format( + cast(float, ts.type.detuning[0]) + ) ), tgt_txt, ) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 1bed95b34..1aaae7e85 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -318,9 +318,11 @@ def available_channels(self) -> dict[str, Channel]: return all_channels else: occupied_ch_ids = [ - self._schedule[ch_name].channel_id - if ch_name in self._schedule - else _dmm_id_from_name(ch_name) + ( + self._schedule[ch_name].channel_id + if ch_name in self._schedule + else _dmm_id_from_name(ch_name) + ) for ch_name in self.declared_channels.keys() ] return { @@ -1316,6 +1318,11 @@ def add( block_eom_mode=True, block_if_slm=channel.startswith("dmm_"), ) + if isinstance(self.declared_channels[channel], DMM): + raise ValueError( + "`Sequence.add()` can't be used on a DMM channel. " + "Use `Sequence.add_dmm_detuning()` instead." + ) self._add(pulse, channel, protocol) @seq_decorators.store @@ -1327,11 +1334,11 @@ def add_dmm_detuning( dmm_name: str, protocol: PROTOCOLS = "no-delay", ) -> None: - """Add a waveform to the detuning of a dmm. + """Add a waveform to the detuning of a DMM. Args: - waveform: The waveform to add to the detuning of the dmm. - dmm_name: The id of the dmm to modulate. + waveform: The waveform to add to the detuning of the DMM. + dmm_name: The name of the DMM channel to modulate. protocol: Stipulates how to deal with eventual conflicts with other channels, specifically in terms of having multiple channels act on the same target @@ -1346,6 +1353,8 @@ def add_dmm_detuning( latest pulse. """ self._validate_channel(dmm_name, block_if_slm=True) + if not isinstance(self.declared_channels[dmm_name], DMM): + raise ValueError(f"'{dmm_name}' is not the name of a DMM channel.") self._add( Pulse.ConstantAmplitude(0, waveform, 0), dmm_name, @@ -1399,14 +1408,22 @@ def delay( self, duration: Union[int, Parametrized], channel: str, + at_rest: bool = False, ) -> None: """Idles a given channel for a specific duration. Args: duration: Time to delay (in ns). channel: The channel's name provided when declared. + at_rest: Whether to wait until the previous pulse on the + channel has finished (including output modulation) before + starting the delay. + + Note: + Delays added automatically by other instructions will generally + take into account the output modulation. """ - self._delay(duration, channel) + self._delay(duration, channel, at_rest) @seq_decorators.store @seq_decorators.block_if_measured @@ -1502,7 +1519,7 @@ def phase_shift_index( @seq_decorators.store @seq_decorators.block_if_measured - def align(self, *channels: str) -> None: + def align(self, *channels: str, at_rest: bool = True) -> None: """Aligns multiple channels in time. Introduces delays that align the provided channels with the one that @@ -1512,6 +1529,8 @@ def align(self, *channels: str) -> None: Args: channels: The names of the channels to align, as given upon declaration. + at_rest: Whether to consider the output modulation of a channel's + contents when determining that it has finished. """ ch_set = set(channels) # channels have to be a subset of the declared channels @@ -1529,7 +1548,7 @@ def align(self, *channels: str) -> None: return last_ts = { - id: self.get_duration(id, include_fall_time=True) + id: self.get_duration(id, include_fall_time=at_rest) for id in channels } tf = max(last_ts.values()) @@ -2116,10 +2135,19 @@ def _check_qubits_give_ids( return ids @seq_decorators.block_if_measured - def _delay(self, duration: Union[int, Parametrized], channel: str) -> None: + def _delay( + self, + duration: Union[int, Parametrized], + channel: str, + at_rest: bool = False, + ) -> None: self._validate_channel(channel, block_if_slm=True) if self.is_parametrized(): return + if at_rest: + self._schedule.wait_for_fall(channel) + if not duration: + return self._schedule.add_delay(cast(int, duration), channel) def _phase_shift( diff --git a/pulser-pasqal/requirements.txt b/pulser-pasqal/requirements.txt index 3d5108f84..db2d9520d 100644 --- a/pulser-pasqal/requirements.txt +++ b/pulser-pasqal/requirements.txt @@ -1,2 +1,2 @@ -pasqal-cloud ~= 0.4.0 +pasqal-cloud ~= 0.8.1 backoff ~= 2.2 \ No newline at end of file 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 73d964c42..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 @@ -107,21 +107,34 @@ def config(self) -> NoiseModel: def _build_collapse_operators(self, config: NoiseModel) -> None: def basis_check(noise_type: str) -> None: """Checks if the basis allows for the use of noise.""" - if self.basis_name == "digital" or self.basis_name == "all": + if self.basis_name == "all": # Go back to previous config raise NotImplementedError( - f"Cannot include {noise_type} " - + "noise in digital- or all-basis." + f"Cannot include {noise_type} noise in all-basis." ) local_collapse_ops = [] if "dephasing" in config.noise_types: basis_check("dephasing") - coeff = np.sqrt(config.dephasing_rate / 2) - local_collapse_ops.append(coeff * qutip.sigmaz()) + rate = ( + config.hyperfine_dephasing_rate + if self.basis_name == "digital" + else config.dephasing_rate + ) + local_collapse_ops.append(np.sqrt(rate / 2) * qutip.sigmaz()) + + if "relaxation" in config.noise_types: + coeff = np.sqrt(config.relaxation_rate) + try: + local_collapse_ops.append(coeff * self.op_matrix["sigma_gr"]) + except KeyError: + raise ValueError( + "'relaxation' noise requires addressing of the" + " 'ground-rydberg' basis." + ) if "depolarizing" in config.noise_types: - basis_check("dephasing") + basis_check("depolarizing") coeff = np.sqrt(config.depolarizing_rate / 4) local_collapse_ops.append(coeff * qutip.sigmax()) local_collapse_ops.append(coeff * qutip.sigmay()) @@ -131,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 @@ -176,7 +189,7 @@ def _extract_samples(self) -> None: """Populates samples dictionary with every pulse in the sequence.""" local_noises = True if set(self.config.noise_types).issubset( - {"dephasing", "SPAM", "depolarizing", "eff_noise"} + {"dephasing", "relaxation", "SPAM", "depolarizing", "eff_noise"} ): local_noises = ( "SPAM" in self.config.noise_types diff --git a/pulser-simulation/pulser_simulation/qutip_backend.py b/pulser-simulation/pulser_simulation/qutip_backend.py index 6118dca95..7a85f0472 100644 --- a/pulser-simulation/pulser_simulation/qutip_backend.py +++ b/pulser-simulation/pulser_simulation/qutip_backend.py @@ -14,12 +14,12 @@ """Defines the QutipBackend class.""" from __future__ import annotations -import warnings from typing import Any 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 @@ -44,9 +44,12 @@ def __init__( f"not {type(config)}." ) self._config = config - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - 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 eac1993aa..d05767663 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -17,15 +17,12 @@ from dataclasses import dataclass, field from math import sqrt -from typing import Any, Literal, Optional, Tuple, Type, TypeVar, Union, cast +from typing import Any, Optional, Tuple, Type, TypeVar, Union, cast import qutip -from pulser.backend.noise_model import NoiseModel +from pulser.noise_model import NOISE_TYPES, NoiseModel -NOISE_TYPES = Literal[ - "doppler", "amplitude", "SPAM", "dephasing", "depolarizing", "eff_noise" -] MASS = 1.45e-25 # kg KB = 1.38e-23 # J/K KEFF = 8.7 # µm^-1 @@ -34,14 +31,20 @@ SUPPORTED_NOISES: dict = { "ising": { + "amplitude", "dephasing", + "relaxation", + "depolarizing", "doppler", - "amplitude", + "eff_noise", "SPAM", + }, + "XY": { + "dephasing", "depolarizing", "eff_noise", + "SPAM", }, - "XY": {"SPAM"}, } @@ -67,6 +70,7 @@ class SimConfig: simulation. You may specify just one, or a tuple of the allowed noise types: + - "relaxation": Relaxation from the Rydberg to the ground state. - "dephasing": Random phase (Z) flip. - "depolarizing": Quantum noise where the state (rho) is turned into a mixed state I/2 at a rate gamma (in rad/µs). @@ -104,14 +108,13 @@ class SimConfig: eta: float = 0.005 epsilon: float = 0.01 epsilon_prime: float = 0.05 + relaxation_rate: float = 0.01 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, repr=False) eff_noise_opers: list[qutip.Qobj] = field(default_factory=list, repr=False) solver_options: Optional[qutip.Options] = None - dephasing_prob: float | None = None - depolarizing_prob: float | None = None - eff_noise_probs: list[float] = field(default_factory=list, repr=False) @classmethod def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: @@ -127,12 +130,11 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: epsilon=noise_model.p_false_pos, epsilon_prime=noise_model.p_false_neg, dephasing_rate=noise_model.dephasing_rate, + 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)), - dephasing_prob=noise_model.dephasing_prob, - depolarizing_prob=noise_model.depolarizing_prob, - eff_noise_probs=noise_model.eff_noise_probs, ) def to_noise_model(self) -> NoiseModel: @@ -148,12 +150,11 @@ def to_noise_model(self) -> NoiseModel: laser_waist=self.laser_waist, amp_sigma=self.amp_sigma, dephasing_rate=self.dephasing_rate, + 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], - dephasing_prob=self.dephasing_prob, - depolarizing_prob=self.depolarizing_prob, - eff_noise_probs=self.eff_noise_probs, + 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: @@ -174,12 +175,7 @@ def __post_init__(self) -> None: self._check_eff_noise_opers_type() # Runs the noise model checks - noise_model = self.to_noise_model() - # Update rates and probs - for noise in ["dephasing", "depolarizing", "eff_noise"]: - for qty in ["prob", "rate"]: - attr = f"{noise}_{qty}{'s' if noise=='eff_noise' else ''}" - self._change_attribute(attr, getattr(noise_model, attr)) + self.to_noise_model() @property def spam_dict(self) -> dict[str, float]: @@ -218,8 +214,13 @@ def __str__(self, solver_options: bool = False) -> str: if "amplitude" in self.noise: lines.append(f"Laser waist: {self.laser_waist}μm") lines.append(f"Amplitude standard dev.: {self.amp_sigma}") + if "relaxation" in self.noise: + lines.append(f"Relaxation rate: {self.relaxation_rate}") if "dephasing" in self.noise: - lines.append(f"Dephasing rate: {self.dephasing_rate}") + lines.append( + f"Dephasing rate: {self.dephasing_rate} (Rydberg), " + f"{self.hyperfine_dephasing_rate} (Hyperfine)" + ) if "depolarizing" in self.noise: lines.append(f"Depolarizing rate: {self.depolarizing_rate}") if solver_options: diff --git a/pulser-simulation/pulser_simulation/simresults.py b/pulser-simulation/pulser_simulation/simresults.py index 9552414b4..ec22169b9 100644 --- a/pulser-simulation/pulser_simulation/simresults.py +++ b/pulser-simulation/pulser_simulation/simresults.py @@ -52,7 +52,7 @@ def __init__( size: The number of atoms in the register. basis_name: The basis indicating the addressed atoms after the pulse sequence ('ground-rydberg', 'digital' or 'all'). - sim_times: Array of times (µs) when simulation results are + sim_times: Array of times (in µs) when simulation results are returned. """ self._dim = 3 if basis_name == "all" else 2 @@ -132,7 +132,7 @@ def sample_state( """Returns the result of multiple measurements at time t. Args: - t: Time at which the state is sampled. + t: Time at which the state is sampled (in µs). n_samples: Number of samples to return. t_tol: Tolerance for the difference between t and closest time. @@ -291,7 +291,7 @@ def get_state(self, t: float, t_tol: float = 1.0e-3) -> qutip.Qobj: way of computing expectation values of observables. Args: - t: Time (µs) at which to return the state. + t: Time (in µs) at which to return the state. t_tol: Tolerance for the difference between t and closest time. @@ -423,7 +423,7 @@ def get_state( """Get the state at time t of the simulation. Args: - t: Time (µs) at which to return the state. + t: Time (in µs) at which to return the state. reduce_to_basis: Reduces the full state vector to the given basis ("ground-rydberg" or "digital"), if the population of the states to be ignored is negligible. Doesn't @@ -515,7 +515,7 @@ def sample_state( """Returns the result of multiple measurements at time t. Args: - t: Time at which the state is sampled. + t: Time (in µs) at which the state is sampled. n_samples: Number of samples to return. t_tol: Tolerance for the difference between t and closest time. diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index 50ad3397b..aa28123ef 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -28,11 +28,11 @@ 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 SequenceSamples +from pulser.sampler.samples import ChannelSamples, SequenceSamples from pulser.sequence._seq_drawer import draw_samples, draw_sequence from pulser_simulation.hamiltonian import Hamiltonian from pulser_simulation.qutip_result import QutipResult @@ -146,13 +146,9 @@ def __init__( "`sampling_rate` is too small, less than 4 data points." ) # Sets the config as well as builds the hamiltonian - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - noise_model: NoiseModel = ( - config.to_noise_model() - if config - else SimConfig().to_noise_model() - ) + noise_model: NoiseModel = ( + config.to_noise_model() if config else SimConfig().to_noise_model() + ) self._hamiltonian = Hamiltonian( self.samples_obj, self._register.qubits, @@ -201,9 +197,7 @@ def basis(self) -> dict[str, Any]: @property def config(self) -> SimConfig: """The current configuration, as a SimConfig instance.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - return SimConfig.from_noise_model(self._hamiltonian.config) + return SimConfig.from_noise_model(self._hamiltonian.config) def set_config(self, cfg: SimConfig) -> None: """Sets current config to cfg and updates simulation parameters. @@ -223,9 +217,7 @@ def set_config(self, cfg: SimConfig) -> None: " support simulation of noise types:" f"{', '.join(not_supported)}." ) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - self._hamiltonian.set_config(cfg.to_noise_model()) + self._hamiltonian.set_config(cfg.to_noise_model()) def add_config(self, config: SimConfig) -> None: """Updates the current configuration with parameters of another one. @@ -252,9 +244,7 @@ def add_config(self, config: SimConfig) -> None: " support simulation of noise types: " f"{', '.join(not_supported)}." ) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - noise_model = config.to_noise_model() + noise_model = config.to_noise_model() old_noise_set = set(self._hamiltonian.config.noise_types) new_noise_set = old_noise_set.union(noise_model.noise_types) diff_noise_set = new_noise_set - old_noise_set @@ -272,22 +262,22 @@ def add_config(self, config: SimConfig) -> None: param_dict["laser_waist"] = noise_model.laser_waist param_dict["amp_sigma"] = noise_model.amp_sigma if "dephasing" in diff_noise_set: - param_dict["dephasing_prob"] = noise_model.dephasing_prob param_dict["dephasing_rate"] = noise_model.dephasing_rate + param_dict["hyperfine_dephasing_rate"] = ( + noise_model.hyperfine_dephasing_rate + ) + if "relaxation" in diff_noise_set: + param_dict["relaxation_rate"] = noise_model.relaxation_rate if "depolarizing" in diff_noise_set: - param_dict["depolarizing_prob"] = noise_model.depolarizing_prob param_dict["depolarizing_rate"] = noise_model.depolarizing_rate if "eff_noise" in diff_noise_set: param_dict["eff_noise_opers"] = noise_model.eff_noise_opers param_dict["eff_noise_rates"] = noise_model.eff_noise_rates - param_dict["eff_noise_probs"] = noise_model.eff_noise_probs # update runs: param_dict["runs"] = noise_model.runs param_dict["samples_per_run"] = noise_model.samples_per_run # set config with the new parameters: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - self._hamiltonian.set_config(NoiseModel(**param_dict)) + self._hamiltonian.set_config(NoiseModel(**param_dict)) def show_config(self, solver_options: bool = False) -> None: """Shows current configuration.""" @@ -295,9 +285,7 @@ def show_config(self, solver_options: bool = False) -> None: def reset_config(self) -> None: """Resets configuration to default.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - self._hamiltonian.set_config(SimConfig().to_noise_model()) + self._hamiltonian.set_config(SimConfig().to_noise_model()) @property def initial_state(self) -> qutip.Qobj: @@ -492,19 +480,39 @@ def run( .. _docs: https://bit.ly/3il9A2u """ - if "max_step" not in options: - pulse_durations = [ - slot.tf - slot.ti - for ch_sample in self.samples_obj.samples_list - for slot in ch_sample.slots - if not ( - np.all(np.isclose(ch_sample.amp[slot.ti : slot.tf], 0)) - and np.all(np.isclose(ch_sample.det[slot.ti : slot.tf], 0)) + + def get_min_variation(ch_sample: ChannelSamples) -> int: + end_point = ch_sample.duration - 1 + min_variations: list[int] = [] + for sample in (ch_sample.amp, ch_sample.det): + min_variations.append( + int( + np.min( + np.diff( + np.nonzero(np.diff(sample)), + prepend=-1, + append=end_point, + ) + ) + ) ) - ] - if pulse_durations: - options["max_step"] = 0.5 * min(pulse_durations) / 1000 + return min(min_variations) + + if "max_step" not in options: + options["max_step"] = ( + min( + [ + get_min_variation(ch_sample) + for ch_sample in self.samples_obj.samples_list + ] + ) + / 1000 + ) + if "nsteps" not in options: + options["nsteps"] = max( + 1000, self._tot_duration // options["max_step"] + ) solv_ops = qutip.Options(**options) meas_errors: Optional[Mapping[str, float]] = None @@ -539,6 +547,7 @@ def _run_solver() -> CoherentResults: if ( "dephasing" in self.config.noise + or "relaxation" in self.config.noise or "depolarizing" in self.config.noise or "eff_noise" in self.config.noise ): @@ -578,7 +587,7 @@ def _run_solver() -> CoherentResults: # Check if noises ask for averaging over multiple runs: if set(self.config.noise).issubset( - {"dephasing", "SPAM", "depolarizing", "eff_noise"} + {"dephasing", "relaxation", "SPAM", "depolarizing", "eff_noise"} ): # If there is "SPAM", the preparation errors must be zero if "SPAM" not in self.config.noise or self.config.eta == 0: diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index b8c9173e6..64075c8fb 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] @@ -272,9 +307,18 @@ def check_error_raised( ) assert isinstance(prev_err.__cause__, ValueError) - @pytest.mark.parametrize("field", ["max_sequence_duration", "max_runs"]) - def test_optional_device_fields(self, field): - device = replace(MockDevice, **{field: 1000}) + @pytest.mark.parametrize( + "og_device, field, value", + [ + (MockDevice, "max_sequence_duration", 1000), + (MockDevice, "max_runs", 100), + (MockDevice, "requires_layout", True), + (AnalogDevice, "requires_layout", False), + (AnalogDevice, "accepts_new_layouts", False), + ], + ) + def test_optional_device_fields(self, og_device, field, value): + device = replace(og_device, **{field: value}) dev_str = device.to_abstract_repr() assert device == deserialize_device(dev_str) @@ -805,8 +849,11 @@ def test_mappable_register(self, triangular_lattice): ] assert abstract["variables"]["var"] == dict(type="int", value=[0]) + @pytest.mark.parametrize("delay_at_rest", (False, True)) @pytest.mark.parametrize("correct_phase_drift", (False, True)) - def test_eom_mode(self, triangular_lattice, correct_phase_drift): + def test_eom_mode( + self, triangular_lattice, correct_phase_drift, delay_at_rest + ): reg = triangular_lattice.hexagonal_register(7) seq = Sequence(reg, AnalogDevice) seq.declare_channel("ryd", "rydberg_global") @@ -822,18 +869,22 @@ def test_eom_mode(self, triangular_lattice, correct_phase_drift): seq.add_eom_pulse( "ryd", duration, 0.0, correct_phase_drift=correct_phase_drift ) - seq.delay(duration, "ryd") + seq.delay(duration, "ryd", at_rest=delay_at_rest) seq.disable_eom_mode("ryd", correct_phase_drift) abstract = json.loads(seq.to_abstract_repr()) validate_schema(abstract) - extra_kwargs = ( + extra_eom_kwargs = ( dict(correct_phase_drift=correct_phase_drift) if correct_phase_drift else {} ) + extra_delay_kwargs = ( + dict(at_rest=delay_at_rest) if delay_at_rest else {} + ) + assert abstract["operations"][0] == { **{ "op": "enable_eom_mode", @@ -846,7 +897,7 @@ def test_eom_mode(self, triangular_lattice, correct_phase_drift): "rhs": 0, }, }, - **extra_kwargs, + **extra_eom_kwargs, } ser_duration = { @@ -863,7 +914,16 @@ def test_eom_mode(self, triangular_lattice, correct_phase_drift): "post_phase_shift": 0.0, "protocol": "min-delay", }, - **extra_kwargs, + **extra_eom_kwargs, + } + + assert abstract["operations"][2] == { + **{ + "op": "delay", + "channel": "ryd", + "time": ser_duration, + }, + **extra_delay_kwargs, } assert abstract["operations"][3] == { @@ -871,7 +931,7 @@ def test_eom_mode(self, triangular_lattice, correct_phase_drift): "op": "disable_eom_mode", "channel": "ryd", }, - **extra_kwargs, + **extra_eom_kwargs, } @pytest.mark.parametrize("use_default", [True, False]) @@ -1153,14 +1213,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 @@ -1364,7 +1420,13 @@ def test_deserialize_variables(self, without_default): {"op": "target", "target": 2, "channel": "digital"}, {"op": "target", "target": [1, 2], "channel": "digital"}, {"op": "delay", "time": 500, "channel": "global"}, + {"op": "delay", "time": 500, "channel": "global", "at_rest": True}, {"op": "align", "channels": ["digital", "global"]}, + { + "op": "align", + "channels": ["digital", "global"], + "at_rest": False, + }, { "op": "phase_shift", "phi": 42, @@ -1413,10 +1475,12 @@ def test_deserialize_non_parametrized_op(self, op): elif op["op"] == "align": assert c.name == "align" assert c.args == tuple(op["channels"]) + assert c.kwargs.get("at_rest", True) == op.get("at_rest", True) elif op["op"] == "delay": assert c.name == "delay" assert c.kwargs["duration"] == op["time"] assert c.kwargs["channel"] == op["channel"] + assert c.kwargs.get("at_rest", False) == op.get("at_rest", False) elif op["op"] == "phase_shift": assert c.name == "phase_shift_index" assert c.args == tuple([op["phi"], *op["targets"]]) @@ -1583,6 +1647,12 @@ def test_deserialize_measurement(self): "channel": "digital", }, {"op": "delay", "time": var2, "channel": "global"}, + { + "op": "delay", + "time": var2, + "channel": "global", + "at_rest": True, + }, { "op": "phase_shift", "phi": var1, @@ -1642,6 +1712,7 @@ def test_deserialize_parametrized_op(self, op): assert c.name == "delay" assert c.kwargs["channel"] == op["channel"] assert isinstance(c.kwargs["duration"], VariableItem) + assert c.kwargs.get("at_rest", False) == op.get("at_rest", False) elif op["op"] == "phase_shift": assert c.name == "phase_shift_index" # phi is variable diff --git a/tests/test_backend.py b/tests/test_backend.py index f0aff5d08..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 @@ -110,54 +110,22 @@ def test_init_strict_pos(self, param): "param", [ "dephasing_rate", + "hyperfine_dephasing_rate", + "relaxation_rate", "depolarizing_rate", - "dephasing_prob", - "depolarizing_prob", ], ) def test_init_rate_like(self, param, value): - def create_noise_model(param, value): - if "prob" in param: - if value > 0: - with pytest.raises( - ValueError, match=f"{param}` must be equal." - ): - with pytest.warns( - DeprecationWarning, - match=f"{param} is deprecated.", - ): - NoiseModel( - **{ - param: value, - "dephasing_rate": value * 10, - "depolarizing_rate": value * 10, - } - ) - with pytest.warns( - (UserWarning, DeprecationWarning), - match=f"{param}", - ): - return NoiseModel(**{param: value}) - return NoiseModel(**{param: value}) - if value < 0: - param_mess = ( - "depolarizing_rate" - if "depolarizing" in param - else "dephasing_rate" - ) with pytest.raises( ValueError, - match=f"'{param_mess}' must be None or greater " + match=f"'{param}' must be None or greater " f"than or equal to zero, not {value}.", ): - create_noise_model(param, value) + NoiseModel(**{param: value}) else: - noise_model = create_noise_model(param, value) - if "depolarizing" in param: - assert noise_model.depolarizing_rate == value - elif "dephasing" in param: - assert noise_model.dephasing_rate == value + noise_model = NoiseModel(**{param: value}) + assert getattr(noise_model, param) == value @pytest.mark.parametrize("value", [-1e-9, 1.0001]) @pytest.mark.parametrize( @@ -177,33 +145,18 @@ def test_init_prob_like(self, param, value): ): NoiseModel(**{param: value}) - @pytest.mark.parametrize( - "noise_sample,", - [ - ("dephasing", "depolarizing"), - ("eff_noise", "depolarizing"), - ("eff_noise", "dephasing"), - ("depolarizing", "eff_noise", "dephasing"), - ], - ) - def test_eff_noise_init(self, noise_sample): - with pytest.raises( - NotImplementedError, - match="Depolarizing, dephasing and effective noise channels", - ): - NoiseModel(noise_types=noise_sample) - @pytest.fixture 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) return matrices - def test_eff_noise_probs(self, matrices): + def test_eff_noise_rates(self, matrices): with pytest.raises( ValueError, match="The provided rates must be greater than 0." ): @@ -212,38 +165,6 @@ def test_eff_noise_probs(self, matrices): eff_noise_opers=[matrices["I"], matrices["X"]], eff_noise_rates=[-1.0, 0.5], ) - with pytest.warns( - (UserWarning, DeprecationWarning), match="eff_noise_probs" - ): - NoiseModel( - noise_types=("eff_noise",), - eff_noise_opers=[matrices["I"], matrices["X"]], - eff_noise_probs=[1.2, 0.5], - ) - - with pytest.warns( - DeprecationWarning, match="eff_noise_probs is deprecated." - ): - NoiseModel( - noise_types=("eff_noise",), - eff_noise_opers=[matrices["I"], matrices["X"]], - eff_noise_rates=[1.2, 0.5], - eff_noise_probs=[1.2, 0.5], - ) - - with pytest.raises( - ValueError, - match="If both defined, `eff_noise_rates` and `eff_noise_probs`", - ): - with pytest.warns( - DeprecationWarning, match="eff_noise_probs is deprecated." - ): - NoiseModel( - noise_types=("eff_noise",), - eff_noise_opers=[matrices["I"], matrices["X"]], - eff_noise_probs=[1.4, 0.5], - eff_noise_rates=[1.2, 0.5], - ) def test_eff_noise_opers(self, matrices): with pytest.raises(ValueError, match="The operators list length"): @@ -261,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], @@ -274,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_backends.py b/tests/test_backends.py new file mode 100644 index 000000000..48485b41e --- /dev/null +++ b/tests/test_backends.py @@ -0,0 +1,46 @@ +# Copyright 2024 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. + +import sys + +import pytest + +import pulser +from pulser.backend.abc import Backend +from pulser.backends import _BACKENDS + + +@pytest.mark.parametrize("backend, missing_package", list(_BACKENDS.items())) +def test_missing_package(monkeypatch, backend, missing_package): + monkeypatch.setitem(sys.modules, missing_package, None) + with pytest.raises( + AttributeError, + match=f"{backend!r} requires the {missing_package!r} package. " + f"To install it, run `pip install {missing_package}`", + ): + getattr(pulser.backends, backend) + + +def test_missing_backend(): + with pytest.raises( + AttributeError, + match="Module 'pulser.backends' has no attribute 'SpecialBackend'", + ): + pulser.backends.SpecialBackend + + +@pytest.mark.parametrize("backend_name", list(_BACKENDS)) +def test_succesful_imports(backend_name): + backend = getattr(pulser.backends, backend_name) + assert issubclass(backend, Backend) diff --git a/tests/test_devices.py b/tests/test_devices.py index f47a31edc..5264d0b65 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -419,6 +419,7 @@ def test_convert_to_virtual(): ).to_virtual() == VirtualDevice( supports_slm_mask=False, reusable_channels=False, + requires_layout=True, dmm_objects=(), **params, ) @@ -434,7 +435,8 @@ def test_device_params(): init_virtual_params = virtual_DigitalAnalogDevice._params(init_only=True) assert all_virtual_params == init_virtual_params assert set(all_params) - set(all_virtual_params) == { - "pre_calibrated_layouts" + "pre_calibrated_layouts", + "accepts_new_layouts", } diff --git a/tests/test_json.py b/tests/test_json.py index 5ccff2f70..5db5ee9aa 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -26,6 +26,7 @@ from pulser.parametrized.decorators import parametrize from pulser.register.register_layout import RegisterLayout from pulser.register.special_layouts import ( + RectangularLatticeLayout, SquareLatticeLayout, TriangularLatticeLayout, ) @@ -92,6 +93,11 @@ def test_layout(): assert new_square_layout == square_layout assert type(new_square_layout) is SquareLatticeLayout + rectangular_layout = RectangularLatticeLayout(8, 10, 6, 5) + new_rectangular_layout = encode_decode(rectangular_layout) + assert new_rectangular_layout == rectangular_layout + assert type(new_rectangular_layout) is RectangularLatticeLayout + def test_register_from_layout(): layout = RegisterLayout([[0, 0], [1, 1], [1, 0], [0, 1]]) diff --git a/tests/test_pulse.py b/tests/test_pulse.py index 4a9209d2d..292d181eb 100644 --- a/tests/test_pulse.py +++ b/tests/test_pulse.py @@ -11,6 +11,8 @@ # 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. +import dataclasses + import numpy as np import pytest @@ -76,7 +78,8 @@ def test_draw(patch_plt_show): pls_.draw() -def test_fall_time(): +@pytest.fixture +def eom_channel(): eom_config = RydbergEOM( mod_bandwidth=24, max_limiting_amp=100, @@ -84,12 +87,35 @@ def test_fall_time(): intermediate_detuning=700, controlled_beams=tuple(RydbergBeam), ) - assert eom_config.rise_time == 20 - channel = Rydberg.Global( - None, None, mod_bandwidth=4, eom_config=eom_config - ) - assert channel.rise_time == 120 + return Rydberg.Global(None, None, mod_bandwidth=4, eom_config=eom_config) + + +def test_fall_time(eom_channel): + assert eom_channel.eom_config.rise_time == 20 + assert eom_channel.rise_time == 120 pulse = Pulse.ConstantPulse(1000, 1, 0, 0) - assert pulse.fall_time(channel, in_eom_mode=False) == 240 - assert pulse.fall_time(channel, in_eom_mode=True) == 40 + assert pulse.fall_time(eom_channel, in_eom_mode=False) == 240 + assert pulse.fall_time(eom_channel, in_eom_mode=True) == 40 + + +def test_full_duration(eom_channel): + with pytest.raises(TypeError, match="must be a channel object instance"): + pls.get_full_duration("eom_channel") + + channel1 = Rydberg.Global(None, None) + assert not channel1.supports_eom() + with pytest.raises( + ValueError, match="does not support EOM mode operation" + ): + pls.get_full_duration(channel1, in_eom_mode=True) + + assert pls.get_full_duration(channel1) == pls.duration + channel2 = dataclasses.replace(channel1, mod_bandwidth=4) + assert pls.get_full_duration(channel2) == pls.duration + pls.fall_time( + channel2 + ) + + assert pls.get_full_duration( + eom_channel, in_eom_mode=True + ) == pls.duration + pls.fall_time(eom_channel, in_eom_mode=True) 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) diff --git a/tests/test_register.py b/tests/test_register.py index 429480b32..03b571ec8 100644 --- a/tests/test_register.py +++ b/tests/test_register.py @@ -11,13 +11,11 @@ # 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. -import re from unittest.mock import patch import numpy as np import pytest -import pulser from pulser import Register, Register3D from pulser.devices import DigitalAnalogDevice, MockDevice @@ -86,6 +84,24 @@ def test_creation(): Register(qubits, spacing=10, layout="square", trap_ids=(0, 1, 3)) +def test_rectangular_lattice(): + # Check rows + with pytest.raises(ValueError, match="The number of rows"): + Register.rectangular_lattice(0, 2, 3, 4) + + # Check columns + with pytest.raises(ValueError, match="The number of columns"): + Register.rectangular_lattice(2, 0, 3, 4) + + # Check row spacing + with pytest.raises(ValueError, match="Spacing"): + Register.rectangular_lattice(2, 2, 0.0, 5) + + # Check col spacing + with pytest.raises(ValueError, match="Spacing"): + Register.rectangular_lattice(2, 2, 3, 0.0) + + def test_rectangle(): # Check rows with pytest.raises(ValueError, match="The number of rows"): @@ -277,18 +293,8 @@ def test_rotation(): rot_reg = reg.rotated(45) new_coords_ = np.array([(0, -1), (1, 0), (-1, 0), (0, 1)], dtype=float) np.testing.assert_allclose(rot_reg._coords, new_coords_, atol=1e-15) - assert rot_reg != reg - assert pulser.__version__ <= "0.18", "Remove 'Register.rotate()'." - with pytest.warns( - DeprecationWarning, - match=re.escape("'Register.rotate()' has been deprecated"), - ): - reg.rotate(45) - assert np.all(np.isclose(reg._coords, new_coords_)) - assert reg == rot_reg - draw_params = [ dict(), diff --git a/tests/test_register_layout.py b/tests/test_register_layout.py index 5d28cad5e..5909fe278 100644 --- a/tests/test_register_layout.py +++ b/tests/test_register_layout.py @@ -22,6 +22,7 @@ from pulser.register import Register, Register3D from pulser.register.register_layout import RegisterLayout from pulser.register.special_layouts import ( + RectangularLatticeLayout, SquareLatticeLayout, TriangularLatticeLayout, ) @@ -112,13 +113,6 @@ def test_register_definition(layout, layout3d): ): reg2d._validate_layout(layout, (0, 1)) - with pytest.raises(TypeError, match="cannot be rotated"): - with pytest.warns( - DeprecationWarning, - match=re.escape("'Register.rotate()' has been deprecated"), - ): - reg2d.rotate(30) - with pytest.warns( UserWarning, match="won't have an associated 'RegisterLayout'" ): @@ -197,6 +191,22 @@ def test_square_lattice_layout(): square.rectangular_register(10, 3) +def test_rectangular_lattice_layout(): + rectangle = RectangularLatticeLayout(9, 7, 2, 4) + assert str(rectangle) == "RectangularLatticeLayout(9x7, 2.0x4.0µm)" + assert rectangle.square_register(3) == Register.rectangular_lattice( + 3, 3, col_spacing=2, row_spacing=4, prefix="q" + ) + # An even number of atoms on the side won't align the center with an atom + assert rectangle.square_register(4) != Register.rectangular_lattice( + 4, 4, col_spacing=2, row_spacing=4, prefix="q" + ) + with pytest.raises(ValueError, match="'8x8' array doesn't fit"): + rectangle.square_register(8) + with pytest.raises(ValueError, match="'10x3' array doesn't fit"): + rectangle.rectangular_register(10, 3) + + def test_triangular_lattice_layout(): tri = TriangularLatticeLayout(50, 5) assert str(tri) == "TriangularLatticeLayout(50, 5.0µm)" diff --git a/tests/test_sequence.py b/tests/test_sequence.py index a228072cc..5402655df 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -1041,7 +1041,8 @@ def test_target(reg, device): seq2.target({"q3", "q1", "q2"}, "ch0") -def test_delay(reg, device): +@pytest.mark.parametrize("at_rest", [True, False]) +def test_delay(reg, device, at_rest): seq = Sequence(reg, device) seq.declare_channel("ch0", "raman_local") with pytest.raises(ValueError, match="Use the name of a declared channel"): @@ -1049,8 +1050,37 @@ def test_delay(reg, device): with pytest.raises(ValueError, match="channel has no target"): seq.delay(100, "ch0") seq.target("q19", "ch0") - seq.delay(388, "ch0") - assert seq._last("ch0") == _TimeSlot("delay", 0, 388, {"q19"}) + seq.add(Pulse.ConstantPulse(100, 1, 0, 0), "ch0") + # At rest will have no effect + assert seq.declared_channels["ch0"].mod_bandwidth is None + seq.delay(388, "ch0", at_rest) + assert seq._last("ch0") == ( + last_slot := _TimeSlot("delay", 100, 488, {"q19"}) + ) + seq.delay(0, "ch0", at_rest) + # A delay of 0 is not added to the schedule + assert seq._last("ch0") == last_slot + + +@pytest.mark.parametrize("delay_duration", [200, 0]) +@pytest.mark.parametrize("at_rest", [True, False]) +@pytest.mark.parametrize("in_eom", [True, False]) +def test_delay_at_rest(in_eom, at_rest, delay_duration): + seq = Sequence(Register.square(2, 5), AnalogDevice) + seq.declare_channel("ryd", "rydberg_global") + assert (ch_obj := seq.declared_channels["ryd"]).mod_bandwidth is not None + pulse = Pulse.ConstantPulse(100, 1, 0, 0) + assert pulse.duration == 100 + if in_eom: + seq.enable_eom_mode("ryd", 1, 0, 0) + seq.add_eom_pulse("ryd", pulse.duration, 0) + else: + seq.add(pulse, "ryd") + assert (extra_delay := pulse.fall_time(ch_obj, in_eom_mode=in_eom)) > 0 + seq.delay(delay_duration, "ryd", at_rest=at_rest) + assert seq.get_duration() == pulse.duration + delay_duration + ( + extra_delay * at_rest + ) def test_delay_min_duration(reg, device): @@ -1530,13 +1560,11 @@ def test_draw_slm_mask_in_ising( ) seq1.draw(mode, draw_qubit_det=draw_qubit_det, draw_interp_pts=False) seq1.add_dmm_detuning(RampWaveform(300, -10, 0), "dmm_0") - # Same function with add is longer - seq1.add(Pulse.ConstantAmplitude(0, RampWaveform(300, -10, 0), 0), "dmm_0") # pulse is added on rydberg global with a delay (protocol is "min-delay") seq1.add(pulse1, "ryd_glob") # slm pulse between 0 and 400 seq1.add(pulse2, "ryd_glob") seq1.config_slm_mask(targets) - mask_time = 700 + 2 * mymockdevice.channels["rydberg_global"].rise_time + mask_time = 400 + 2 * mymockdevice.channels["rydberg_global"].rise_time assert seq1._slm_mask_time == [0, mask_time] assert seq1._schedule["dmm_0_1"].slots[1].type == Pulse.ConstantPulse( mask_time, 0, -100, 0 @@ -1669,7 +1697,8 @@ def test_draw_register_det_maps(reg, ch_name, patch_plt_show): seq3d.draw(draw_register=True, draw_detuning_maps=True) -def test_hardware_constraints(reg, patch_plt_show): +@pytest.mark.parametrize("align_at_rest", [True, False]) +def test_hardware_constraints(reg, align_at_rest, patch_plt_show): rydberg_global = Rydberg.Global( 2 * np.pi * 20, 2 * np.pi * 2.5, @@ -1750,10 +1779,10 @@ def test_hardware_constraints(reg, patch_plt_show): assert seq._schedule["ch0"][-1].ti == seq._schedule["ch0"][-2].tf tf_ = seq.get_duration("ch0") - seq.align("ch0", "ch1") + seq.align("ch0", "ch1", at_rest=align_at_rest) fall_time = black_pls.fall_time(rydberg_global) assert seq.get_duration() == seq._schedule["ch0"].adjust_duration( - tf_ + fall_time + tf_ + fall_time * align_at_rest ) with pytest.raises(ValueError, match="'mode' must be one of"): @@ -2237,3 +2266,15 @@ def test_max_duration(reg, mod_device): seq.delay(16, "ch0") with catch_statement: seq.add(Pulse.ConstantPulse(100, 1, 0, 0), "ch0") + + +def test_add_to_dmm_fails(reg, device, det_map): + seq = Sequence(reg, device) + seq.config_detuning_map(det_map, "dmm_0") + pulse = Pulse.ConstantPulse(100, 0, -1, 0) + with pytest.raises(ValueError, match="can't be used on a DMM"): + seq.add(pulse, "dmm_0") + + seq.declare_channel("ryd", "rydberg_global") + with pytest.raises(ValueError, match="not the name of a DMM channel"): + seq.add_dmm_detuning(pulse.detuning, "ryd") diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index d9eb225f9..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 @@ -49,12 +49,13 @@ def test_init(): and "100" in str_config and "Solver Options" in str_config ) - config = SimConfig(noise="depolarizing") + config = SimConfig(noise=("depolarizing", "relaxation")) assert config.temperature == 5e-5 - with pytest.warns(DeprecationWarning, match="is deprecated"): - assert config.to_noise_model().temperature == 50 + assert config.to_noise_model().temperature == 50 str_config = config.__str__(True) - assert "depolarizing" in str_config + assert "depolarizing" in str_config and "relaxation" in str_config + assert f"Depolarizing rate: {config.depolarizing_rate}" in str_config + assert f"Relaxation rate: {config.relaxation_rate}" in str_config config = SimConfig( noise="eff_noise", eff_noise_opers=[qeye(2), sigmax()], @@ -122,7 +123,6 @@ def test_from_noise_model(): p_false_pos=0.1, state_prep_error=0.05, ) - with pytest.warns(DeprecationWarning, match="is deprecated"): - assert SimConfig.from_noise_model(noise_model) == SimConfig( - noise="SPAM", epsilon=0.1, epsilon_prime=0.4, eta=0.05 - ) + assert SimConfig.from_noise_model(noise_model) == SimConfig( + noise="SPAM", epsilon=0.1, epsilon_prime=0.4, eta=0.05 + ) diff --git a/tests/test_simresults.py b/tests/test_simresults.py index c360f0386..9943232ed 100644 --- a/tests/test_simresults.py +++ b/tests/test_simresults.py @@ -19,9 +19,13 @@ import qutip from qutip.piqs import isdiagonal -from pulser import Pulse, Register, Sequence +from pulser import AnalogDevice, Pulse, Register, Sequence from pulser.devices import DigitalAnalogDevice, MockDevice -from pulser.waveforms import BlackmanWaveform +from pulser.waveforms import ( + BlackmanWaveform, + CompositeWaveform, + ConstantWaveform, +) from pulser_simulation import QutipEmulator, SimConfig from pulser_simulation.simresults import CoherentResults, NoisyResults @@ -213,7 +217,7 @@ def test_get_state_float_time(results): [0.76522907 + 0.0j], [0.08339973 - 0.39374219j], [0.08339973 - 0.39374219j], - [-0.27977623 - 0.1103308j], + [-0.27977172 - 0.11031832j], ] ), ).all() @@ -386,3 +390,23 @@ def test_results_xy(reg, pi_pulse): # Check that measurement projectors are correct assert results_._meas_projector(0) == qutip.basis(2, 0).proj() assert results_._meas_projector(1) == qutip.basis(2, 1).proj() + + +def test_false_positive(): + """Breaks for pulser version < v0.18.""" + seq = Sequence(Register.square(2, 5), AnalogDevice) + seq.declare_channel("ryd_glob", "rydberg_global") + seq.add( + Pulse.ConstantDetuning( + CompositeWaveform( + ConstantWaveform(2500, 0.0), + BlackmanWaveform(1000, np.pi), + ConstantWaveform(500, 0.0), + ), + 0, + 0, + ), + channel="ryd_glob", + ) + sim = QutipEmulator.from_sequence(seq) + assert sim.run().get_final_state() != sim.initial_state diff --git a/tests/test_simulation.py b/tests/test_simulation.py index b8cb2f02c..29bc0a62a 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -37,38 +37,48 @@ def reg(): return Register(q_dict) +duration = 1000 +pi_pulse = Pulse.ConstantDetuning(BlackmanWaveform(duration, np.pi), 0.0, 0) +twopi_pulse = Pulse.ConstantDetuning( + BlackmanWaveform(duration, 2 * np.pi), 0.0, 0 +) +pi_Y_pulse = Pulse.ConstantDetuning( + BlackmanWaveform(duration, np.pi), 0.0, -np.pi / 2 +) + + @pytest.fixture -def seq(reg): - duration = 1000 - pi = Pulse.ConstantDetuning(BlackmanWaveform(duration, np.pi), 0.0, 0) - twopi = Pulse.ConstantDetuning( - BlackmanWaveform(duration, 2 * np.pi), 0.0, 0 - ) - pi_Y = Pulse.ConstantDetuning( - BlackmanWaveform(duration, np.pi), 0.0, -np.pi / 2 - ) +def seq_digital(reg): seq = Sequence(reg, DigitalAnalogDevice) # Declare Channels - seq.declare_channel("ryd", "rydberg_local", "control1") seq.declare_channel("raman", "raman_local", "control1") # Prepare state 'hhh': - seq.add(pi_Y, "raman") + seq.add(pi_Y_pulse, "raman") seq.target("target", "raman") - seq.add(pi_Y, "raman") + seq.add(pi_Y_pulse, "raman") seq.target("control2", "raman") - seq.add(pi_Y, "raman") + seq.add(pi_Y_pulse, "raman") + return seq + +@pytest.fixture +def seq(seq_digital): # Write CCZ sequence: - seq.add(pi, "ryd", protocol="wait-for-all") + with pytest.warns( + UserWarning, match="Building a non-parametrized sequence" + ): + seq = seq_digital.build() + seq.declare_channel("ryd", "rydberg_local", "control1") + seq.add(pi_pulse, "ryd", protocol="wait-for-all") seq.target("control2", "ryd") - seq.add(pi, "ryd") + seq.add(pi_pulse, "ryd") seq.target("target", "ryd") - seq.add(twopi, "ryd") + seq.add(twopi_pulse, "ryd") seq.target("control2", "ryd") - seq.add(pi, "ryd") + seq.add(pi_pulse, "ryd") seq.target("control1", "ryd") - seq.add(pi, "ryd") + seq.add(pi_pulse, "ryd") # Add a ConstantWaveform part to testout the drawing procedure seq.add(Pulse.ConstantPulse(duration, 1, 0, 0), "ryd") @@ -480,9 +490,7 @@ def test_run(seq, patch_plt_show): good_initial_qobj = qutip.tensor( [qutip.basis(sim.dim, 0) for _ in range(sim._hamiltonian._size)] ) - good_initial_qobj_no_dims = qutip.basis( - sim.dim**sim._hamiltonian._size, 2 - ) + good_initial_qobj_no_dims = qutip.basis(sim.dim**sim._hamiltonian._size, 2) with pytest.raises( ValueError, match="Incompatible shape of initial state" @@ -738,13 +746,17 @@ def test_noise_with_zero_epsilons(seq, matrices): "noise, result, n_collapse_ops", [ ("dephasing", {"0": 595, "1": 405}, 1), + ("relaxation", {"0": 595, "1": 405}, 1), ("eff_noise", {"0": 595, "1": 405}, 1), ("depolarizing", {"0": 587, "1": 413}, 3), + (("dephasing", "depolarizing", "relaxation"), {"0": 587, "1": 413}, 5), + (("eff_noise", "dephasing"), {"0": 595, "1": 405}, 2), ], ) -def test_dephasing(matrices, noise, result, n_collapse_ops): +def test_noises_rydberg(matrices, noise, result, n_collapse_ops): np.random.seed(123) reg = Register.from_coordinates([(0, 0)], prefix="q") + # Test with Rydberg Sequence seq = Sequence(reg, DigitalAnalogDevice) seq.declare_channel("ch0", "rydberg_global") duration = 2500 @@ -767,6 +779,87 @@ def test_dephasing(matrices, noise, result, n_collapse_ops): assert np.trace(trace_2) < 1 and not np.isclose(np.trace(trace_2), 1) +def test_relaxation_noise(): + seq = Sequence(Register({"q0": (0, 0)}), MockDevice) + seq.declare_channel("ryd", "rydberg_global") + seq.add(Pulse.ConstantDetuning(BlackmanWaveform(1000, np.pi), 0, 0), "ryd") + seq.delay(10000, "ryd") + + sim = QutipEmulator.from_sequence(seq) + sim.add_config(SimConfig(noise="relaxation", relaxation_rate=0.1)) + res = sim.run() + start_samples = res.sample_state(1) + ryd_pop = start_samples["1"] + assert ryd_pop > start_samples.get("0", 0) + # The Rydberg state population gradually decays + for t_ in range(2, 10): + new_ryd_pop = res.sample_state(t_)["1"] + assert new_ryd_pop < ryd_pop + ryd_pop = new_ryd_pop + + +depo_res = { + "111": 821, + "110": 61, + "011": 59, + "101": 48, + "100": 5, + "001": 3, + "010": 3, +} +deph_depo_res = { + "111": 806, + "110": 65, + "011": 63, + "101": 52, + "100": 6, + "001": 4, + "010": 3, + "000": 1, +} +eff_deph_res = {"111": 958, "110": 19, "011": 12, "101": 11} + + +@pytest.mark.parametrize( + "noise, result, n_collapse_ops", + [ + ("dephasing", {"111": 978, "110": 11, "011": 6, "101": 5}, 1), + ("eff_noise", {"111": 978, "110": 11, "011": 6, "101": 5}, 1), + ("depolarizing", depo_res, 3), + (("dephasing", "depolarizing"), deph_depo_res, 4), + (("eff_noise", "dephasing"), eff_deph_res, 2), + ], +) +def test_noises_digital(matrices, noise, result, n_collapse_ops, seq_digital): + np.random.seed(123) + # Test with Digital Sequence + sim = QutipEmulator.from_sequence( + seq_digital, # resulting state should be hhh + sampling_rate=0.01, + config=SimConfig( + noise=noise, + hyperfine_dephasing_rate=0.05, + eff_noise_opers=[matrices["Z"]], + eff_noise_rates=[0.025], + ), + ) + + with pytest.raises( + ValueError, + match="'relaxation' noise requires addressing of the 'ground-rydberg'", + ): + sim.set_config(SimConfig(noise="relaxation")) + + res = sim.run() + res_samples = res.sample_final_state() + assert res_samples == Counter(result) + assert len(sim._hamiltonian._collapse_ops) == n_collapse_ops * len( + seq_digital.register.qubits + ) + trace_2 = res.states[-1] ** 2 + assert np.trace(trace_2) < 1 and not np.isclose(np.trace(trace_2), 1) + + def test_add_config(matrices): reg = Register.from_coordinates([(0, 0)], prefix="q") seq = Sequence(reg, DigitalAnalogDevice) @@ -913,58 +1006,35 @@ def test_run_xy(): assert sim.samples_obj._measurement == "XY" +res1 = {"0000": 892, "1000": 47, "0100": 25, "0001": 19, "0010": 17} +res2 = {"0000": 962, "0010": 13, "1000": 13, "0100": 12} +res3 = {"0000": 904, "0100": 43, "0010": 24, "1000": 19, "0001": 10} +res4 = {"0000": 969, "0001": 18, "1000": 13} + + @pytest.mark.parametrize( - "masked_qubit, result", + "masked_qubit, noise, result, n_collapse_ops", [ - ( - None, - { - "0000": 837, - "0100": 62, - "0001": 42, - "0010": 28, - "1000": 19, - "0101": 12, - }, - ), - ( - "atom0", - { - "0000": 792, - "0001": 79, - "0100": 50, - "0010": 29, - "0110": 27, - "1000": 13, - "0101": 10, - }, - ), - ( - "atom1", - { - "0000": 648, - "0001": 214, - "0010": 78, - "0011": 24, - "1001": 23, - "1000": 13, - }, - ), + (None, "dephasing", res1, 1), + (None, "eff_noise", res1, 1), + (None, "depolarizing", res2, 3), + ("atom0", "dephasing", res3, 1), + ("atom1", "dephasing", res4, 1), ], ) -def test_noisy_xy(matrices, masked_qubit, result): +def test_noisy_xy(matrices, masked_qubit, noise, result, n_collapse_ops): np.random.seed(15092021) simple_reg = Register.square(2, prefix="atom") detun = 1.0 amp = 3.0 - rise = Pulse.ConstantPulse(1500, amp, detun, 0.0) + rise = Pulse.ConstantPulse(100, amp, detun, 0.0) seq = Sequence(simple_reg, MockDevice) seq.declare_channel("ch0", "mw_global") if masked_qubit is not None: seq.config_slm_mask([masked_qubit]) seq.add(rise, "ch0") - sim = QutipEmulator.from_sequence(seq, sampling_rate=0.01) + sim = QutipEmulator.from_sequence(seq, sampling_rate=0.1) with pytest.raises( NotImplementedError, match="mode 'XY' does not support simulation of" ): @@ -974,43 +1044,33 @@ def test_noisy_xy(matrices, masked_qubit, result): with pytest.raises( NotImplementedError, match="mode 'XY' does not support simulation of" ): - with pytest.warns(DeprecationWarning, match="is deprecated"): - sim._hamiltonian.set_config( - SimConfig(("SPAM", "doppler")).to_noise_model() - ) + sim._hamiltonian.set_config( + SimConfig(("SPAM", "doppler")).to_noise_model() + ) with pytest.raises( NotImplementedError, match="simulation of noise types: amplitude" ): sim.add_config(SimConfig("amplitude")) - with pytest.raises( - NotImplementedError, match="simulation of noise types: dephasing" - ): - sim.add_config(SimConfig("dephasing")) - - with pytest.raises( - NotImplementedError, match="simulation of noise types: depolarizing" - ): - sim.add_config(SimConfig("depolarizing")) - - with pytest.raises( - NotImplementedError, match="simulation of noise types: eff_noise" - ): - sim.add_config( - config=SimConfig( - noise="eff_noise", - eff_noise_opers=[matrices["Z"]], - eff_noise_rates=[0.025], - ), - ) # SPAM simulation is implemented: - sim.set_config(SimConfig("SPAM", eta=0.4)) + sim.set_config( + SimConfig( + ("SPAM", noise), + eta=0.4, + eff_noise_opers=[matrices["Z"]], + eff_noise_rates=[0.025], + ) + ) assert sim._hamiltonian._bad_atoms == { "atom0": True, "atom1": False, "atom2": True, "atom3": False, } + assert ( + len(sim._hamiltonian._collapse_ops) // len(simple_reg.qubits) + == n_collapse_ops + ) assert sim.run().sample_final_state() == Counter(result) diff --git a/tutorials/advanced_features/Backends for Sequence Execution.ipynb b/tutorials/advanced_features/Backends for Sequence Execution.ipynb index e614e3036..453136c41 100644 --- a/tutorials/advanced_features/Backends for Sequence Execution.ipynb +++ b/tutorials/advanced_features/Backends for Sequence Execution.ipynb @@ -1,461 +1,452 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "6f230abe", - "metadata": {}, - "source": [ - "# Backend Execution of Pulser Sequences" - ] - }, - { - "cell_type": "markdown", - "id": "ae508ab2", - "metadata": {}, - "source": [ - "When the time comes to execute a Pulser sequence, there are many options: one can choose to execute it on a QPU or on an emulator, which might happen locally or remotely. All these options are accessible through an unified interface we call a `Backend`. \n", - "\n", - "This tutorial is a step-by-step guide on how to use the different backends for Pulser sequence execution." - ] - }, - { - "cell_type": "markdown", - "id": "a7601ae9", - "metadata": {}, - "source": [ - "## 1. Choosing the type of backend\n", - "\n", - "Although the backend interface nearly doesn't change between backends, some will unavoidably enforce more restrictions on the sequence being executed or require extra steps. In particular, there are two questions to answer:\n", - "\n", - "1. **Is it local or remote?** Execution on remote backends requires a working remote connection. For now, this is only available through `pulser_pasqal.PasqalCloud`.\n", - "2. **Is it a QPU or an Emulator?** For QPU execution, there are extra constraints on the sequence to take into account.\n", - "\n", - "### 1.1. Starting a remote connection\n", - "\n", - "For remote backend execution, start by ensuring that you have access and start a remote connection. For `PasqalCloud`, we could start one by running:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "ef3cc2eb", - "metadata": {}, - "outputs": [], - "source": [ - "from pulser_pasqal import PasqalCloud\n", - "\n", - "connection = PasqalCloud(\n", - " username=USERNAME, # Your username or email address for the Pasqal Cloud Platform\n", - " project_id=PROJECT_ID, # The ID of the project associated to your account\n", - " password=PASSWORD, # The password for your Pasqal Cloud Platform account\n", - " **kwargs\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "29cff577", - "metadata": {}, - "source": [ - "### 1.2. Preparation for execution on `QPUBackend`\n", - "\n", - "Sequence execution on a QPU is done through the `QPUBackend`, which is a remote backend. Therefore, it requires a remote backend connection, which should be open from the start due to two additional QPU constraints:\n", - "\n", - "1. The `Device` must be chosen among the options available at the moment, which can be found through `connection.fetch_available_devices()`.\n", - "2. The `Register` must be defined from one of the register layouts calibrated for the chosen `Device`, which are found under `Device.calibrated_register_layouts`. Check out [this tutorial](reg_layouts.nblink) for more information on how to define a `Register` from a `RegisterLayout`.\n", - "\n", - "On the contrary, execution on emulator backends imposes no further restriction on the device and the register. We will stick to emulator backends in this tutorial, so we will forego the requirements of QPU backends in the following steps." - ] - }, - { - "cell_type": "markdown", - "id": "35a4f10c", - "metadata": {}, - "source": [ - "## 2. Creating the Pulse Sequence" - ] - }, - { - "cell_type": "markdown", - "id": "122a3c37", - "metadata": {}, - "source": [ - "The next step is to create the sequence that we want to execute. Here, we make a sequence with a variable duration combining a Blackman waveform in amplitude and a ramp in detuning. Since it will be executed on an emulator, we can create the register we want and choose a `VirtualDevice` that does not impose hardware restrictions (like the `MockDevice`)." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4548fedd", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pulser" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "57e088c6", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABp0AAAF8CAYAAAA9/HMGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8WgzjOAAAACXBIWXMAAA9hAAAPYQGoP6dpAADJFElEQVR4nOzdd3jb5b3+8bf2sizJezt770kIISFsCITRNARaxoEOuuF3WqDnlDIP7enpbg+0FCinpaxAwp4hAwgjezlxth3vLdmyLQ/p94dikUAgCbYjO7lf16VL0lePvs9HwXjo1vN5DJFIJIKIiIiIiIiIiIiIiIhINxjjXYCIiIiIiIiIiIiIiIj0fwqdREREREREREREREREpNsUOomIiIiIiIiIiIiIiEi3KXQSERERERERERERERGRblPoJCIiIiIiIiIiIiIiIt2m0ElERERERERERERERES6TaGTiIiIiIiIiIiIiIiIdJtCJxEREREREREREREREek2hU4iIiIiIiIiIiIiIiLSbQqdREREREREREREREREpNvM8S5A4qehoYFNmzbh9/uJRCLdPp/RaMTr9TJp0iRcLlcPVCgiIiIiIiIiIiIiIv2FQqdT0Icffsh9997Dm2+9RXt7R4+f32G3cdFFF3PX3XczZsyYHj+/iIiIiIiIiIiIiIj0PYZITyxxkX7jvffe44Lzz2Vwajs3ntnJuWMhOQGMPdBosTMM1QF4dSM8vNJEQ1si7yxfydixY7t/chERERERERERERER6dMUOp0CwuEwZWVlOBwORgwfyug0P6/8exiXvffmrGuCuf9lotU2gI8+XofBYOi9yURERERERERERERE5JhFIhEaGxvJysrC2BOrUg5S6HQKKCkpITc3N3Z/7X0weWDvz/vKBpj3P70/j4iIiIiIiIiIiIiIHL8DBw6Qk5PTY+fTnk6nALfbDcBNN93Emy/8nUkDen4fpyM5dyy47EZ+csfP+dGPfnTMz3vuuee48sore68wEREREREREREREZFTWCAQIDc3N5Yf9BSFTqeArtZ2gUCAASkRTlSnO6sZspJMBAIBEhMTj/l5drv9uMaLiIiIiIiIiIiIiMjx6+mtcXquUZ/0eR0dHVjNJ7abos0M7e3tx/WcwYMH91I1IiIiIiIiIiIiIiLSWxQ6SZ+TnZ0d7xJEREREREREREREROQ4KXSSPmfVqlXxLkFERERERERERERERI6TQic5bn9ZBqN+HO8qRERERERERERERESkL1HoJMetphEKy3vv/HPmzOm9k4uIiIiIiIiIiIiISK9Q6CR9TnFxcbxLEBERERERERERERGR46TQSfqcvXv3xrsEERERERERERERERE5TgqdpM8xm83xLkFERERERERERERERI6TQifpcxYsWBDvEkRERERERERERERE5DhpSYkA8JtXj33s+zt7rw6A5557jiuvvLJ3JxERERERERERERERkR6l0Kkf6Ozs5K677uKf//wnFRUVZGVlcf311/Of//mfGAyGHpnj3/91fON7ZtYja2tr68Wzi4iIiIiIiIiIiIhIb1Do1A/88pe/5MEHH+Txxx9n9OjRrF27lhtuuAGPx8MPfvCDHplj+X/0yGl6RH5+frxLEBERERERERERERGR46TQqR9YvXo18+fP5+KLLwZgwIABPPnkk3z88cc9NsfskT12qm4bPHhwvEsQEREREREREREREZHjZIx3AXJ0p59+OsuWLWPnzuhmSps2beK9997jwgsvPOL4UChEIBA47NKTPt4D336kR095mHfeeaf3Ti4iIiIiIiIiIiIiIr1CK536gdtvv51AIMCIESMwmUx0dnZy//33c8011xxx/AMPPMDdd9/dozXUNsL/vQeProCC0uixh27s0SlERERERERERERERKQfU+jUDzzzzDM88cQT/Otf/2L06NFs3LiRH/3oR2RlZXHdddd9Zvwdd9zBrbfeGrsfCATIzc097nkjEXh9EzyyAl7eCG0dMCAFbr0IrpzajRd0FGeccUbvnVxERERERERERERERHqFQqd+4Mc//jG33347V111FQBjx46lqKiIBx544Iihk81mw2azfen59lZFVzQ9/i6U1UOCHdo74E/XwXfO/dKnPWZVVVVfKiQTEREREREREREREZH4UejUDzQ3N2M0Hr79lslkIhwO99gcrW3w7Efw6EpYtQPMJrh4Alx/JgzNgNG3QYa3x6b7Qjt37mTy5MknZjIREREREREREREREekRCp36gUsuuYT777+fvLw8Ro8ezYYNG/jNb37Dv/3bv/XYHBnfhcYWmJAPv/s6XH06JLujj+2p7LFpRERERERERERERETkJKXQqR/44x//yM9+9jO+853vUFVVRVZWFt/6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SqAlQFFtEYWVheyr2ceuql00tzUzOW8ys4fP5vxR53Pa4NMUQomIyOfqrdBJH4M+Ao/HQ3V19TGPX7x4MQUFBdx55529WNUn/H4/wOcGMw888AB33333CamlN7zxxhtqrycip5yG5gbe2/UeywuX886Od9hSuoXUhFSGpA1heMZwzht1Hqnu1FjIZLPY4l2yiIiIiIjEgd1ix26xk5aYxtSBU2lua8bf4mdP1R52Ve7izW1v8uCKB2lpa2FC7gTOHHYm5406j5lDZuK0ffE2EiIiIt2llU5HcN5557Fjxw727NmDxWL5wrEffPABZ555JuFwuNf2dDpUOBzm0ksvpaGhgffee++IY/r7Sift6SQip4LaplpW7VzF8h3LWV64nILyAjI9mQxJG0J+Uj45STmkJqTitrtJdCTqE4oiIiIiInJUkUiElvYW/M3+6F5QVXvYX7ufPdV7aGxtZGzOWM4ceibnjT6PWUNnaU8oEZFTWJ9or2cy9cwm5AaDgY6Ojh45V2947LHHuPHGG7nlllv49a9//bnjVq1axYIFC6iursZgMJyQ0Onmm2/mtdde47333iMnJ+eYntPf9nTau3cvgwYNincZIiI9qjJQycrClSwvXM6KwhUUVhSSl5THoLRB0ZDJl0OyKxm33Y3b7sZhdcS7ZBERERER6ecODaGKaovYXb2botoi9lTvoS5Yx+is0cwaOotzR53LnOFz8Dq98S5ZREROkD4ROhmNxp6Z9AQFNF9WZ2cnp59+OmvXrmX+/Pn853/+J5MmTQKiK402b97M//7v//L4449jtVqZOXMmb731Vq+/pu9973u88MILrFq1ioEDBx7z8/pb6LR582bGjRsX7zJERL60SCRCYUUhq/esZuXOlby/+332Vu9lYMpABqUOIteXS15yHj6nLxYyaSWTiIiIiIj0tq4QqrG1kQO1B9hZtZOiuiL2Vu+lurGaYenDOG3QacwePpuzhp9FXlIeBoMh3mWLiEgv6BOh08qVK3ts4tmzZ/fYuXpDRUUFF198MRs2bMBgMOB0OvF4PFRVVdHZ2UkkEiErK4tnnnmGt956i3vuuafXQqdIJML3v/99lixZwooVKxg6dOhxPb+/hU5qryci/U1reytr96/lvd3vsXLnSj7a+xHBUJDBaYPJT8on25dNXtLhIZPVbI132SIiIiIiIrS2t9LY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ERGR7vv0Fi7d1e3QacWKFbHbXcV9evHUkY4bDAbuvPNOzj77bB5//HEyMzO7W8pJy2q1MnnyZJYtW8Zll10GRIOZZcuW8b3vfS++xfWCwYMHx7sEERERERERERERERE5Tt0OnX7+85/T3t7Ogw8+SH19PXl5ecyePZvs7GwASktLWbVqFUVFRSQlJfHtb3+bpqYm1q5dy+rVq1m2bBnnn38+H3/8MXa7vdsv6GR16623ct111zFlyhSmTZvG7373O4LBIDfccEO8S+txXV87IiIiIiIiIiIiIiLSf3Q7dPqP//gPzjnnHFpbW/n73//Otddee8Rx//jHP/j2t7/Nxx9/zOuvv47RaGTlypVcdtllbNu2jYcffpjvf//73S3npLVw4UKqq6u58847qaioYMKECbz++uukp6fHu7Qet2rVKhYtWhTvMkRERERERERERERE5DgYu3uC3/72t7z77rv84Q9/+NzACeDrX/86f/jDH1i2bBm///3vAZg9eza/+MUviEQiPPfcc90t5aT3ve99j6KiIkKhEB999BHTp0+Pd0kiIiIiIiIiIiIiIiIAGCKf3oDpOE2YMIHt27fT2NiI1Wr9wrGhUIjExERGjRrFhg0bAPD7/SQlJZGUlER1dXV3SpHPEQgE8Hg8+P1+EhMT413OUZWXl2uPLxERERERERERERGRXtJbuUG3Vzrt2bOHhISEowZOADabjYSEBHbv3h075vF48Hq9BAKB7pYiJ4ni4uJ4lyAiIiIiIiIiIiIiIsep26GT2WymoaGB8vLyo44tLy+noaEBs/nwraSam5vxeDzdLUVOEnv37o13CSIiIiIiIiIiIiIicpy6HTpNmjQJgNtuu+2oY2+//XYikUjsOQCVlZWEQiHS09O7W4qcJD4dSoqIiIiIiIiIiIiISN/X7dDpBz/4AZFIhCeeeIILL7yQd999l46OjtjjHR0drFq1iosuuoh//vOfGAwGfvCDH8Qef/311wGYPn16d0uRk8SCBQviXYKIiIiIiIiIiIiIiBynbodO8+fP59ZbbyUSifDmm28yZ84cEhISyM7OJicnh4SEBM466yzeeOMNIpEIP/rRj5g/f37s+WvWrGH8+PFcdtll3S1FThLPPfdcvEsQEREREREREREREZHj1CN9zP7nf/6HKVOmcOedd7J7927a2to+s8fTkCFDuPvuu1m0aNFhx//0pz/1RAlyEmlra4t3CSIiIiIiIiIiIiIicpx6bPOcq666iquuuoqNGzeyfv16qqurAUhNTWXSpElMmDChp6aSk1x+fn68SxARERERERERERERkePU7dDpnnvuAeCGG24gNzeXCRMmKGD6AnfddRd33333YceGDx/Ojh07Pvc5zz77LD/72c/Yv38/Q4cO5Ze//CUXXXRRb5caN4MHD453CSIiIiIiIiIiIiIicpy6vafT3XffzX333UdGRkZP1HNKGD16NOXl5bHLe++997ljV69ezaJFi7jxxhvZsGEDl112GZdddhlbt249gRWfWO+88068SxARERERERERERERkePU7dApJSWFxMRELBZLT9RzSjCbzWRkZMQuKSkpnzv297//PRdccAE//vGPGTlyJPfeey+TJk3SXlgiIiIiIiIiIiIiItKndLu93vjx43nnnXeora0lOTm5J2o66e3atYusrCzsdjszZszggQceIC8v74hjP/jgA2699dbDjp1//vksXbr0c88fCoUIhUKx+4FAoEfqPlHOOOOMeJcgIiIiIiIiIiIiIv1AJBKhpamNhsog/qog9ZVBGiqaqCtrpK68iRt/cy6uRHu8yzxldDt0+ta3vsXbb7/Nb37zG+6///6eqOmkNn36dP7+978zfPhwysvLufvuu5k1axZbt27F7XZ/ZnxFRQXp6emHHUtPT6eiouJz53jggQc+s29Uf1JVVUVubm68yxARERERERERERGROIhEIgQbWqMBUmWQhsom6iuD1JU1Ul/eRH1FkIaqJvzVzQRqWmhv7cBkNuLy2nAm2rAnWHG4rdgTrOxZX8G4OQPi/ZJOGd0Ona688kpuvfVWfvGLX9De3s5PfvKTL2wXd6q78MILY7fHjRvH9OnTyc/P55lnnuHGG2/skTnuuOOOw1ZHBQKBfhXi7Ny5k8mTJ8e7DBERERERERERERHpIeFwhMba5sOCpLryJurKmmioiIZKDVVBAtVBGmtb6GgPY7GZcHntONxWHF1BksuKI8GCJy3tk2MJVuwuC2arCZPFhNlixGQ2Ul/ehMXW7RhEjkO3/7Xnzp0LgMvl4te//jW//e1vGTJkCGlpaZhMpiM+x2AwsGzZsu5OfVLwer0MGzaM3bt3H/HxjIwMKisrDztWWVlJRkbG557TZrNhs9l6tE4RERER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DI4+h07jxo3TihUrlMlk5HQ6t3puOp1We3u7xo8f39fL9puf/vSnev/99zVv3rxBud4zzzzT4/H999+v2tpaLV26VNOmTdvi6ywWi+rr6we6vCHhySef1IknnljuMgAAAAAAAPqsI5NTdF0hQOoMkwpj7doLQdLauCLr44ptSCgeTsk0VQiNgl0hUqH7yKFgrVd1OwTl8hvyFMMlp8fRswup244km4P9SACAwdXn0Omkk07SZZddpgcffFBnnnnmVs/93//9X3V0dOikk07q62U/l6uvvnqrz2ezWc2bN08tLS2DUk9votGoJKmysnKr57W3t2vMmDHK5/Paa6+99Itf/EK77bZbr+em02ml0+nS41gs1n8FAwAAAAAAbOdSiQ5F1rYrui6hyNp2hVvaix1Jhd9H1sYVXR9XdENCyVhGFovkDhjyBgy5A065fIbcPocMj0PVzX417lJV2I9UDJmcLnuvIZLdYZXVTpAEABi6LKZpmn15g46ODs2cOVOvvvqq7rzzTp122mm9nvfggw/q7LPP1j777KMFCxbIbu9z3vWZrNatz5bt/D/oE088UX/4wx8GvJ5N5fN5HXXUUYpEIlq8ePEWz1uyZInee+897b777opGo7rxxhu1aNEivf3222pqatrs/CuvvFJXXXXVZsej0agCgUC/foaBsHz58iHVDQcAAAAAAEY20zSVbMv0GGsXWRvXxuJYu3BL12i72Iak0okOWW0WeYKGPAGjML7O37UfyfA4ZHidpeNun0N2w94tPOq5J8lmZz8SAAyET99r1fipTZowdfOfo2/vYrGYgsFgv+cG2xQ6balzKJ1O64477lAsFlNzc7NmzJhR2vG0evVqLVy4UKtWrVIwGNT3vvc9OZ1OXXHFFf3zCbZi4cKFvR5PpVL697//rdtvv13ZbFb/+Mc/VFdXN+D1bOqcc87RX//6Vy1evLjX8GhLOjo6NGHCBJ144om65pprNnu+t06n5uZmQicAAAAAALDdME1T7eFUoROpGCKFW9q1cXVb6feRdXHF1icU25BQRzonm90qb4VLnkBnWGTI8Dnk8jrk8jjk8hbH3fmdcnmdshu2UoBU2pNU7FCy2giSAKDcCJ22bEiETlbr1tt3O99q03N6O57L5bap0IEQi8U0adIk7bfffnrkkUcG9drnnXeeHn/8cS1atEhjx47d5tcf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cqVmz55dlvoAAAAAANumI5NTdH28W5AUL461K+xHCre0K7q+MNauPZySmTfl8jrkCRpy+41C55HPKZfPoUCNWzVjAqUuJbfPKafHUQiRunUhdd+TRJAEAAAAbFmfO506ZbNZPfHEE7r77rv197//Xfl8vuzdT/l8XmeddZbuu+++0uJUq9WqfD4vqTCH+4wzztDvfve7Eb1Ydbh1Om3cuFFVVVXlLgMAAADAIEknO7pCpM79SGvaS6PtImvjpSApEU1LkjwBoxgkdYZIhf1Ihtchl8dRDJIMufxOOV32HgFS9z1JNrt1RP99EAAAYHtGp9OWDVRu0G+hU3erVq3SPffco/vvv1+rV68uXMhiKVv308KFC/XAAw/ozTffVDQaVTAY1B577KFTTjlFM2bMGLQ6ymW4hU4vvPCCDjrooHKXAQAAAOALMk1TyfZMj7F2XR1Jhf1IkXVxRdcVgqRUvEMWq0XeYDFI8hWCJKMzSOo21s7tM+T2O2Q37KXuox57korHAAAAAEKnLRtWoVOnfD6vv/zlL/rd736np59+Wrlcrkf309lnn60DDjhgoC4/pN1+++264YYb1NLSoj322EO33Xab9t133y2e/+ijj+ryyy/XypUrtfPOO+v666/XYYcd9rmuNdxCJ3Y6AQAAAEOPaZqKR1IKdwuSwi3xUjdSuCWuyLrO0XZJdaSystmt8gQNeQOFjiO3v9iR5OkeJBnFgMkhh9PeNcquW4hkd1hltREkAQAAYNsQOm3ZkN3ptDVWq1VHHnmkHA6HNm7cqCVLlsg0TaVSKT300EOaM2eOpkyZoltuuUX77LPPQJYypDz88MO6+OKLddddd2m//fbTLbfcolmzZmnFihWqra3d7PwXX3xRJ554ombPnq0jjjhCc+bM0dFHH63XXntNEydOLMMnGFgej6fcJQAAAADbhXzeVNvGRI8gqTDWrl3hNW0Kd461Wx9X28aksh152Z02eSsMeQJGaReSy+uU2+dQsLZW7mKQ5PIXupTsTlup+6h7iGSzEyQBAAAAI82AdTqtWbNG9957r37/+9/ro48+UudlDjzwQB1//PH629/+pr/+9a/K5/NyOBz6v//7vwEddZfL5bRhwwal0+lenx89evSAXXtT++23n/bZZx/95je/kVToCGtubtb555+vSy65ZLPzv/nNbyoej+upp54qHZsyZYr23HNP3XXXXZ95veHW6QQAAADgi8tl84quL4RI4R5BUpvCa4qj7tbFFduQUFtrSmbelOFxyBM05Cl2InUGSZ37kUrHfE4ZHrvsTrvsdmuPPUmdo+4sVvYjAQAAYGig02nLhkWnk2maevrpp3XPPfeUxumZpqlAIKBTTjlFZ599tnbbbTdJ0vnnn68PPvhA55xzjubPn6/LL79cL7zwQn+WI0launSpLrvsMi1atEiZTKbXcywWi7LZbL9fuzeZTEZLly7VpZdeWjpmtVp18MEHa8mSJb2+ZsmSJbr44ot7HJs1a5Yee+yxXs9Pp9M9wrVYLNb3wgfR3Llz9Y3jv1nuMgAAAIAhoyOdLe1Hiq4vhEndu5E6j8c2JBWPpCRJbp+zsB+p21g7t9epUINP9eNCMrx2uXyGPD6nnB67bHZbr6PtbHarLJ8zRzJNU2ZuwCa4AwAAANtkALcLYQv6JXT65JNPSl1Nn3zySel/yL322ktnn322TjrppF5Hpo0bN06PPvqoamtr9eabb/ZHKT28/vrrOuigg2S323XIIYfoySef1B577KH6+nq99tprWr9+vWbMmKExY8b0+7W3ZMOGDcrlcqqrq+txvK6uTsuXL+/1NS0tLb2e39LS0uv5s2fP1lVXXbXZ8UcffXRYjK776MNVuv2me8pdBgAAADCgzFxeuWzhK9vR9ftcNq9c6XFOuY688vnC37FsdmtxTF2xw8htlXUnqyp3sarabpXN5pLF6pHVZtkkKMoUv4rXlpQqfkUkKTlIHxoAAAAYZBtfX67XV7rKXcaQk0gkBuR9+xw6HXHEEfq///s/5fN5maYpj8ejb37zmzr77LM/156mQCCg+vp6ffzxx30tZTPXXHONJOnll1/WhAkTZLVadcwxx+iKK65QMpnUD37wA/3xj3/U73//+36/djldeumlPTqjYrGYmpubdfzxxw+L8Xp/vO9p2cIh+SuHfkAGAAAAdDJNU5lkVolYuvAVTSseSykeTikeTSseTSsZSyvRllaqPaNsJi+LRXL5Cl1InoAhT8Apb9AlX4VLvpBLvjq3/JUuBaq88lcacrodchh2OQybHIZddsNW+r3Nzn4kAAAAYFPeCpdcHke5yxhyYrGYzjzzzH5/3z6HTk8//bQkacKECfrud7+r0047TcFgcJve47//+7+1cePGvpaymcWLF+uoo47ShAkTSsc6u7Dcbrd+85vf6MUXX9Rll12mOXPm9Pv1e1NdXS2bzaa1a9f2OL527VrV19f3+pr6+vptOt8wDBmG0T8Fl4HfHVRH0iF/lbvcpQAAAGA7Z+ZNJdszikdSikdSao+kFQ8n1daaVHs4pfZwSolYqhgoZZTL5mW1WQoj7XyF0XaegCFvhaGqRr98IbcCNR6F6n2qbvQp1OCX4bb3CJIchk12J0ESAAAAgOGnz6FTXV2dHn74YU2aNEmhUOgLvceNN97Y1zJ6FY1GNW7cuNJjh8Oh9vb20mOr1aoZM2Zo7ty5A3L93jidTk2ePFkLFizQ0UcfLUnK5/NasGCBzjvvvF5fM3XqVC1YsEAXXnhh6dj8+fM1derUQah48H3Q8q6anXuWuwwAAACMUPlcXolYWvFIuhgkpdQeTqq9NaW2cLIYMBW6lZJtGZl5U3anrbAXyVvYj+QNGvJWuFQ5yi9fpUsVtd5CkNQUULDWs1mI1P1Xq/VzLkgCAAAAgGGmz6HT2rVrNXPmTK1bt64/6ulXtbW1CofDpcf19fV67733epyTSqUGbHbhllx88cU67bTTtPfee2vffffVLbfcong8rjPOOEOSdOqpp6qxsVGzZ8+WJF1wwQWaPn26fv3rX+vwww/XvHnz9Oqrr+ruu+8e1LoBAACAoSrXkSuMsCuGSIloWm0bk4UQKdx1LB5NKxXPSKbkdNnl8jvl9hWCJE8xSKrboUL+SreCdV5VjfKruskvf6V7i91IdqeNIAkAAAAA1A+hUzAYlM1m+8JdTgNp11131YoVK0qPDzjgAD322GNasmSJpk6dqnfffVePPPKIxo8fP6h1ffOb39T69et1xRVXqKWlRXvuuaeeeeYZ1dXVSZJWrVolq7VrjMb++++vOXPm6Gc/+5kuu+wy7bzzznrsscc0ceLEQa17sHypcXcl15e7CgAAAJRbRzrbsxspUuhG6hxrF4+klIimlIhllE50SJIMj6M42s4pd6DQkRSo8WjUlyrlr/IoVOdVZaNfVaP88lW4eu5GcvYMlCwWgiQAAAAA2BYWs3PJ0Re0zz776M0331QsFhtye4Ruu+02XXTRRfr444/V0NCgN954Q1OmTFEmk1FlZaXC4bDy+bz+3//7fzrmmGPKXe6AicViCgaDikajCgQC5S7nMz187+PyphtV0zz0awUAAMDnZ5qmMslsz/1IkeJ+pNZCsBSPpJSIFUbbdaRyslgkl68QIrk69yMVO5J8IZf8lW6FGvyqGuVTVaNfLp9zi6Pt7A5bub8FAAAAADAkDFRu0OdOpxNOOEFLly7VI488olNOOaU/auo3Z599tr7xjW+UurD22GMPLViwQNddd50++OADTZ48Weeff74OP/zwMleK7lrb1svrbCx3GQAAAPgcTNNUKt7RLUhKKV7cjdTemiwGS4XRdsm2tLIdeVltllKQ5O4Mkipcqh9XIV+FW8Eaj0INPlU1+hSq98vwOLa4H8lmt352kQAAAACAQdHnTqdsNqvp06dr2bJlmjt3rg477LD+qg39ZLh1Ot196/0aZZ9IpxMAAECZ5HN5JdsypT1I7ZFUIUBqTaqtONYuHk2XgqR8zpTNYS2ESN33IwVdhY6kSpeCtV5V1vtU1RRQqM4rh6tbgOS0FTqRimGSzUaQBAAAAAADach2Ov3iF7/QtGnT9NZbb+nII4/UbrvtpgMOOEC1tbWy2bY8vuKKK67o66U/k81m0wknnKCHHnpowK+F/rPnuP21blWs3GUAAACMKLlsvhQglbqSwim1tSYVDxc7kqKF0XaptoxMU3IYtq79SH5DnmDhq2Z0QL6QS6FarypG+VXTFJC/yi2ny95rN5LdaZPVyn4kAAAAABjp+hw6XXnllbJYLOpsmFq2bJnefvvtz3zdYIROgUBAzc3NA34d9K9/vb9YjY7dy10GAADAkJftyPXcj1QMkdrDSbWHU4pH0kp0BknxDkmS023vCpKK+5H8VR417BiSr8qjijqvKht8qm4OyFfh2vJ+JKdNFgtBEgAAAACgS59Dp2nTpg3Zv2zuu+++euONN8pdBrZR3syXuwQAAICyyaSyPfYjtYeLo+3CqWKQVAiRErG0MsmsZJFcHofcfqdcfkMev1PeoKHKBp9G71ojf5VboTqvqhr9qmryy+03SiPt7Jt2JDmsQ/bP9gAAAACAoa/PodPzzz/fD2UMjCuvvFIzZszQgw8+qFNPPbXc5eBzqgrUS6lyVwEAANA/TNNUOt7RYz9SPJxUW2tKbeGk4p0dSbGUErGMspmcLFaL3D6nXD5HYaxdwClv0KXaMUH59qxToNqjUL1PVY1+hRp8cvucvY60cxg22R1bHnkNAAAAAEB/6nPoNJTNnz9fM2bM0BlnnKHbbrtN++yzj+rq6jb715sWi0WXX355marEpkK+aqUJnQAAwBBm5k0l2zPFACmleDRdDJK6dSRFCyFTsi2jXDYvm90ql8/Zc7RdhaFRO1fKF3IrWONRqKEYJNX7ZLg3349kL3Yo2ezWcn8LAAAAAADYjMXsXMY0Almtn+8v4xaLRblcboCrKZ9YLKZgMKhoNKpAIFDucj7Tnbf8Ts3OPVXTPPRrBQAAI0c+l1ciVuxEiqQL4+1ak2oLp9QeThZH3hXG2iXbMjLzpuxOW1eI5HfKEzTkDbrkrXDJV+lSqNarigafqhsDCtZ65HR1diBtvifJamWsHQAAAABgcAxUbjCiO52ee+65cpcAAACAMsp15ApdSMX9SPFISm2tXTuS4tGuICkVz0im5HTZ5SoGSZ6AU56AIV+FS3U7VMhf6VawzquqUX5VN/nlr3RvMUSyO20ESQAAAACA7cqIDp2mT59e7hLwBezYsJsyG8tdBQAAGKo60tmuTqRISu2RpNp7CZKSbRmlEx2SJMPj6DbWzilv0FBFnUdNu1TKX+VRRZ1XlY1+VY3yy1fh6gqONg2SHNbNRjUDAAAAAICCER06YXiKxjfKrfpylwEAAAaJaZrKJLOlTqRCoFTcj9Ta1aEUj6aVjKXVkc7JYlFhP5LPKZe/0I3kDRqqbg5oh91rFSgGSVWNflU1+uXyOXvtRnIYNtkdtnJ/CwAAAAAAGBFGVOj0ta99Tddcc4322WefbX5tPB7XbbfdJr/fr3PPPXcAqsPntSHWomYnoRMAAMOZaZpKtWfUXuxIikdSiodThSApnFR7OK14NKVENK1EW1q5jrysNkspRCqMtjPkrXCpflyFfBVuBWs8CjX4VNXkV2WDX05X7yGSw7DLZv98uz0BAAAAAED/GVGh0/r16zVlyhRNmzZNp556qo499lgFg8Gtvuall17SH/7wB82bN0/JZFIPPPDAIFWLLWFkDQAAQ1M+l1eyLdOtIyml9taU2sLFsXbFbqRENK1kW1r5nCmbw1oYaed3yu03ikGSoabxPvkqXQrWelVZ71NVU0ChOq8cWwiS7E6bbDaCJAAAAAAAhjKLaZpmuYvoTw888ICuuuoqrVy5UlarVbvssosmT56suro6VVRUKJVKqbW1VStWrNCrr76qtrY22Ww2nXDCCbr22ms1evTocn+EfheLxRQMBhWNRhUIBMpdzmf651P/1rpVMdU0D/1aAQAY7nLZvBLRdI8gqa24G6k9nOwKkmJppdoyMk3JYdi69iP5DXmChY4kX8glX4VLFbUeVYzyq6YpIH+Vu9iRtHmYZHfaZLXyj00AAAAAABhsA5UbjLjQSSqMc3n66ad133336fnnn1dra+tm51itVu2+++465phjdOaZZ6qhoaEMlQ6O4RY63XPb/WqwTSR0AgDgC8pmcmqPpJSIpgrj7XqMtUv1CJLS8Q5JktNt7wqSivuRvMFCkOSvdKui3qfK4mg7X4Vry/uRnDa6lgEAAAAAGOIGKjcYUeP1OlksFh1++OE6/PDDJUnvvvuuPvnkE23cuFFut1s1NTXabbfdPnP0Hsojm8tK7PMGAKCHTLKjx36k9khK7T06ktKKR9NKxtLKpLKSRXJ5nXL7HHL5DHkCTnmDhiobfBq9a438VW6F6n2qavSrqtEnt98oBEdOm+ybdiQ5rARJAAAAAADgM43I0GlTEyZM0IQJE8pdBj6nkK9GypS7CgAABpZpmkrHO7qNtUurPZwsdCRFUoqHC8cSsZQSsYyymZwsVku3/UiFL2/QpdoxFfLt6VKg2lMKkipH+eTyOrfYjWR38C88AAAAAABA/9ouQicMLzXBUUquL3cVAABsOzNvKtme6RYapdTebbRdoRsppUQ0rURbWvmsKZvdKle3IMkTKIy1G7VzpfwhtwK1HlU2+FQ5yq9QvU+Ge/P9SPbirzabtdzfAgAAAAAAsB0jdMKQ8+/Vb6jZuWe5ywAAQJKUz+WViKWLHUmF8XbtrcWOpE32IyXbMjLzpuxOW9d+JL9TnuJ+pOqmgHwhlypqvapo8Km6KaBgjUdOl1125+YdSQ7DLquVsXYAAAAAAGB4IHQCAADbnWxHrhQgxaOF4KittWtHUufxRCytVLxDMiWn217oSPI55QkUgiRfyKX6cRXyhdyqqPeqapRfVY1++SvdvQZIhWDJxn4kAAAAAAAwIhE6YcgZWz9B2dZyVwEAGG460tmuwCiSVnukECCVgqRo546ktDLJrGSRXB7HJkGSSxV1HjWNr5K/snuQFJA3aHSFR5uGSQ4rQRIAAAAAANjuEToNopUrV+qaa67Rs88+q5aWFo0aNUrf+ta39NOf/lROp3OLr5sxY4YWLlzY49h3v/td3XXXXQNdclnEUzEZqil3GQCAMjNNU5lktjjWbtP9SF3H4tG0krG0OtI5WawWubyOrtF2AUPeoKHq5oB22L1WgSq3QvV+VTb6VNUYkMvr6LUjyWHYZHfYyv0tAAAAAAAAGFYInQbR8uXLlc/n9dvf/lY77bSTli1bprPOOkvxeFw33njjVl971lln6eqrry499ng8A11u2ayLrFazk9AJAEYi0zSVas+ovXO0XSSl9nBSbRuThXApXNyPFE0r0ZZWriMvq80it88pl98pt9+QJ+CUN+hS/Y4h+YIuBWs8CjX4VNXkV2WDX4bb3utIO4dhl81uLfe3AAAAAAAAYMQidBpEX/va1/S1r32t9HjcuHFasWKF7rzzzs8MnTwej+rr6we6RAAAtlk+l1cilintRopHUmpvTaktXOxIKo62KwRJGZl5UzaHtdCJ1BkkBQsdSaEJ1fKH3ArWehSq96mqKaBQnVcO1ybdSE6b7MXf22wESQAAAAAAAEPBiAudbrnlFi1evFhz586Vw+GQVOgwuu+++9TS0qKGhgYdcMABmjVr1lZH2g2WaDSqysrKzzzvoYce0h/+8AfV19fryCOP1OWXX77Fbqd0Oq10Ol16HIvF+q3ewbDXjgdq/cft5S4DALZruWy+W4iU7upIai382nk8EUsr1Z6RaUoOl60QJPm6BUkVLtWOCcpX4VJFnVeVo/yqbgrIV+mS02XvdbSd3WmT1cp+JAAAAAAAgOFmxIVOv//979Xc3FwKnN566y3tt99+SqVSpXMsFovq6up0ww036OSTTy5XqfrPf/6j22677TO7nE466SSNGTNGo0aN0ptvvqmf/OQnWrFihf70pz/1ev7s2bN11VVXDUTJg2LZR6+ozjqh3GUAwIiTzeR67EeKR1KlEKm0IylaCJLS8Q5JkuFxdBtt55Q3aChQ5dGonSrlr3Srot6nquJ+JF+Fq7ALydnLfiSnTRYLQRIAAAAAAMBIZjFN0yx3Ef3J6/XqnHPOKQU506dPVyKR0KWXXqrKykp98MEHeuKJJ/TMM8+oo6ND1157rS699NI+XfOSSy7R9ddfv9Vz3n33XY0fP770ePXq1Zo+fbpmzJih3/3ud9t0vWeffVYzZ87Uf/7zH+24446bPd9bp1Nzc7Oi0agCgcA2Xasc7rzld2p27qma5qFfKwCUWybZsfl+pHBK7a2FICkRTSseTSsZSyuTyspikQxvcaydzylPwClP0CVv0JAv5Ja/yl0Ya9foV1WjT26/UQiOnDbZN+1IclgJkgAAAAAAAIahWCymYDDY77nBiOt0yuVy8nq9kqTW1latXLlSb775poLBoCRpxowZ+va3v61ly5bppJNO0uWXX66vfvWr2m+//b7wNX/wgx/o9NNP3+o548aNK/3+008/1Ve+8hXtv//+uvvuu7f5ep21bil0MgxDhmFs8/sOFUFvpdRR7ioAoDxM01Qq3tGtGyldHGvXtR+pvbgfKdmWUTaTk8Vq6bYfySl3oLAfqW5shXwVLgWqPaps8KlylF+Vo3xyeZ2bjbTr/NVmZz8SAAAAAAAAvpgRFzrV1NQoEolIkhYuXKgjjjiiFDh1N3HiRC1YsEC77rqrbrrpJj388MN9umZNTc3nOnf16tX6yle+osmTJ+u+++6T1brtP9x7/fXXJUkNDQ3b/NrhoKFyjOJrR1QDHoDtnJk3lWhLb7IfKVUMkpKlY/FYWsm2tPJZUza7Va5uQZInYMgbdKlxlyr5Qi4Fa70K1XlV2ehXqN4nw735fiR78VebjSAJAAAAAAAAA2/EhU6TJk3S3//+d+VyOc2dO1fTpk3b4rk1NTX67//+bz322GODUtvq1as1Y8YMjRkzRjfeeKPWr19feq6+vr50zsyZM/Xggw9q33331fvvv685c+bosMMOU1VVld58801ddNFFmjZtmnbfffdBqXuwLf/4X2p27lnuMgBgq/K5vOLRbmPtIoUupM6OpPZIoRspEU0r2Z6RmTflMGyFIKkYJnmChSCpuikgX8ililqvQqP8qm7yK1DtkdNl73U/ksOwy2plrB0AAAAAAACGlhEXOp1xxhn65je/qfr6erW2tmrFihU644wzSiP3NhUIBBQOhweltvnz5+s///mP/vOf/6ipqanHc52rtTo6OrRixQolEglJktPp1N///nfdcsstisfjam5u1nHHHaef/exng1IzAGxPsh25rq6j4ldba2G0Xel4NKVELK1UvEMyJafb3m20nSFP0ClfyKX6cRXyhdyqqPeqapRfVU0B+UOuXgOkQrBkYz8SAAAAAAAAhjWL2Zl2jCA33nijbrzxRtXU1OjWW2/Vj3/8Y91+++3ad999e5yXSqW05557KpFIaNWqVWWqduAN1EKwgfL0nEXKRzyqaR76tQIY+jrS2cIupM79SJGk2luTamvt6lBKRNNKxNLKJLOSRXJ5HKXRdp6AIU/QkK/CJW+FS/4qt0J1PlWO8qm6KSBPwOgKjzYNkxxWgiQAAAAAAAAMOQOVG4y4TidJ+uEPf6gf/vCHpcfnnXee9t9/f33pS1/SwQcfrLq6OoXDYT3++OP64IMP9L3vfa+M1WJT6Y6kHPKUuwwAQ5RpmkonOnqOtus+1i6cVCKSVjxa2I/Ukc7JYrXI5XUUupF8TrkDhrxBQ9WjA9phj1oFqjwK1ftU2ehX1Si/XF5Hrx1JDsMmu8NW7m8BAAAAAAAAMCSNyNBpU6eddpqampp0/vnn6ze/+U2P56ZOnaprr722TJWhNy3hj9XsrCp3GQAGkWmaSrZlusbaRdNqDyfVtrH7fqRUMUjKKNeRl9VmkdvnlKsYJHXuR2rYqVK+CpeCNR6FGvyqavSpssEvw23vdaSdw7DLZreW+1sAAAAAAAAADHvbRegkSTNnztQ777yj1157TW+99ZaSyaQmTJig6dOnl7s0ABiR8rm8ErGuIKk9UuhCKnQjpRQPF/cjRdNKtGVk5k3ZHVa5/YZcPkdxP1IhSBo9yidfyK1gjUeVDT5VNQZVUeeRw7WFbiSnTTYbQRIAAAAAAAAwmLab0KnTXnvtpb322qvcZWAr9hy3vzZ+kih3GQB6kcvmFY927UeKR1Jq25hUWzjZ1aUUKexHSrVnZJqSw2WT22cURtsVdyR5K1yqHROUL+RSRZ1XVaP8qmoMyF/l7jVE6uxMslrZjwQAAAAAAAAMVdtd6IShb/knr6tGXyp3GcB2I5vJqb0UGBW6kNpbk2rr7EiKpJSIFYKkdLxDkmR4HKXRdp5AYbRdoNqjUTtVyl/lVkWdT1WNPlU1BuSrcHUFR4ZNDmfPjiSLhSAJAAAAAAAAGAkInTDkpDIJyVnuKoDhyzRNZZLZ0m6keHGsXVsxTNo0SOpI5WSxSIa30Ink9hW7kYKGqhv9GjOxRoFSkFTYkeT2G712IzkMm+wOW7m/BQAAAAAAAADKgNAJQ47fXSHlyl0FMLSYpqlUvKPHfqR4OKW2zhApnFJ7cT9SMpZWtiMvi9VSCpFcPmdxP5KhurEV8ofc8le5FWooBEmVDT65vM4tjraz2dmPBAAAAAAAAGDrCJ0w5DTX7KS2lmy5ywAGnJk3lWhLl/YgtUeKnUityeK4u+JzsbSSbWnls6Zsdqvc/kKIVNqPFHSpcZcq+UIuBWu9qqz3qqopoFC9Tw5XtwDJaSt0IhXDJJuNIAkAAAAAAABA/yF0wpDzzqpX1ezcs9xlAF9IPpcvjbTr7Ehqby10JMXDSbVHCiPtEtG0ku0ZmXlTDsNWCpHcfkPegCFP0FB1U0C+kEsVtV6FRvlV3eRXoNojp8u++Ui74p4kq5X9SAAAAAAAAADKg9AJAD5DtiPX1XVU/GprTXaNtouklYimlGhLKxXvkEzJ6bbL3T1IChryVXlUv2NIvpBbFfVeVY3yq6opIH/I1etIO3uxM8liIUgCAAAAAAAAMPQROmHIaa7ZUYqWuwqMdJlUtluIlFZ7pDDWrq21q0MpES10JWWSWckiuTyOHmPtPEFDlQ0+NU+oVqDarVCdT6EGv2qa/XIHjOI4O7vsm4ZJDitBEgAAAAAAAIARh9AJQ04un5Ot3EVg2DFNU+lER6kjqT2SUjycVFs4pfZwSu3hpBKRtOLFICmbyclitcjlcxQ6knxOuQOFjqTq0QHtsEetAlUehRp8qmoMqLLBJ7fPWeo+2my8nYP/agEAAAAAAABs3widMOR8unElO50gSTLzppLtmW77kdKFIKk41q7QjZRSPJpWMpZRLpuX1WYpjLTzGYVfA055gy417FQpX4VLwRqPQg1+VTcWupIMt73XkXYOwy6b3VrubwEAAAAAAAAADBuETgAGVT6XVyLWPUgqdCG1t6bUFk6Wxt0lY2kl2jIy86bsDqtc/mKI5HPKGzTkrXCpcpRPvpBbwVqPKhv8qhoVUEWdRw7X5vuROn+1WhlrBwAAAAAAAAADgdAJQ86kHfZT5NN0ucvANsh15BSPphWPpkrj7do2JgshUrhrP1I8mlYqnpFMyeGydXUj+Z3yFIOkuh0q5Au5VFHnVWWDX9VNAfmr3FsMkexOG0ESAAAAAAAAAAwBhE4Yct5f87aqtFO5y9judaSzPfcjRVJqb+3akRQvjrZLxDJKJzokSYansB/J5XfKEygESYEaj0Z9qVL+So9C9V5VjirsSPJVuLqCI8Mmh7PbfiSnTRYLQRIAAAAAAAAADCeEThhyEul2VTnLXcXIY5qmMslsz/1IkW77kTqDpFhaiVhaHamcLBbJ5SuMtHP5nPIEDHmDhqobfRozsUaBKrdC9T5VjfKrqskvl8+5xW4ku8NKkAQAAAAAAAAAIxihE4Ycr8sv5ctdxfBgmqZS8Y4e+5Hixd1I7a3JQrAULYy2S8bSynbkZbVZSkGS218IkjxBQ3VjK+QPuRWoKXQkVTX6Far3yeV1bnG0nc1uLfe3AAAAAAAAAAAwRBA6YcgZV7/rdr3TycybSsS69iO1F8fadY62i0dSikfThSCpLa18zpTNbi3sRvJ1248UdKlpF598IZeCtV5V1ntV1RRUqN4rh6tbgOS0FUbaFcMkm40gCQAAAAAAAACw7QidMOS8tfJlNTv3LHcZ/SqXzSsRLQRIiWiqGCSl1NaaVDzcrSMpllaqLSPTlByGrasjKWDIGzDkqTBU0xyQr9KlihqvQqP8qm7yK1DtkdNl3+JoO6uVsXYAAAAAAAAAgIFF6AR8QdmOXM/9SOGU2sNdO5LikbQSnUFSvEOS5HTbS91Ibn9hP5K/yqOGHUPyVboVavAVdiQ1BeQPuXoNkRyGTXanjf1IAAAAAAAAAIAhhdAJQ05j1ViprTzXzqSyPfYjtYeLo+3Chd937kdKxNLKJLOSRXJ5HHL7nXL5uvYjVTb41DyhWoFqt0J1PlU1+lXZ6JfHb3R1IG3akeSwEiQBAAAAAAAAAIYtQqdBtsMOO+ijjz7qcWz27Nm65JJLtviaVCqlH/zgB5o3b57S6bRmzZqlO+64Q3V1dQNdbllYLP23U8g0TaUTHYpH0qUgKR5Oqq01pbZwUvHOjqRY4SubyclitcjlcxQ7kgx5AoUwqWZMQGNDtQpUeRRq8KmqMaDKBp/cPudmXUil3zts/fZZAAAAAAAAAAAYygidyuDqq6/WWWedVXrs9/u3ev5FF12kv/zlL3r00UcVDAZ13nnn6dhjj9U//vGPgS61LD7Z8P5WdzqZeVPJ9kxhP1JptF3XWLvuHUnJtoxy2bysNkthpJ3PkNtfCJG8FYZG7Vwpb4VLwRqPQg1+VTf6FGrwy3Bvvh/JXvzVZuu/UAwAAAAAAAAAgJGC0KkM/H6/6uvrP9e50WhU9957r+bMmaOvfvWrkqT77rtPEyZM0EsvvaQpU6Zs9pp0Oq10Ol16HIvF+qfwQRCPprR+eVrJ1Gq998pqtbUW9iQVRt4VupGSbRmZeVN2h1Uuv1EMk5zyBg15K1yqHOWXr9KlYI1XlfVeVTUGVVHnkcPV+34kh2GX1cpYOwAAAAAAAAAA+oLQqQx++ctf6pprrtHo0aN10kkn6aKLLpLd3vv/FEuXLlVHR4cOPvjg0rHx48dr9OjRWrJkSa+h0+zZs3XVVVcNWP0Dac37Yb33RFpO4yO5i/uRvBUu1e1QIX+lW8E6r6pG+VXd6Je/yr15N1JxtJ3daSNIAgAAAAAAAABgEBE6DbLvf//72muvvVRZWakXX3xRl156qdasWaObbrqp1/NbWlrkdDpVUVHR43hdXZ1aWlp6fc2ll16qiy++uPQ4Foupubm53z7DQNpprwadcNcETZ44RXbDJoezZ6BksRAkAQAAAAAAAAAwFBE69YNLLrlE119//VbPeffddzV+/PgeYdDuu+8up9Op7373u5o9e7YMw+iXegzD6Lf3KodYPKL6caFylwEAAAAAAAAAALYBoVM/+MEPfqDTTz99q+eMGzeu1+P77befstmsVq5cqV122WWz5+vr65XJZBSJRHp0O61du/Zz74UaboLBYLlLAAAAAAAAAAAA24jQqR/U1NSopqbmC7329ddfl9VqVW1tba/PT548WQ6HQwsWLNBxxx0nSVqxYoVWrVqlqVOnfuGah7KZM2eWuwQAAAAAAAAAALCNrOUuYHuyZMkS3XLLLXrjjTf0wQcf6KGHHtJFF12kb33rWwqFCuPkVq9erfHjx+uf//ynpELXz//8z//o4osv1nPPPaelS5fqjDPO0NSpUzVlypRyfpwB86c//ancJQAAAAAAAAAAgG1Ep9MgMgxD8+bN05VXXql0Oq2xY8fqoosu6rHnqaOjQytWrFAikSgdu/nmm2W1WnXccccpnU5r1qxZuuOOOz73dU3TlCTFYrH++zADKJFIDJtaAQAAAAAAAAAYbjp/Bt+ZH/QXi9nf74gh55NPPlFzc3O5ywAAAAAAAAAAAEPIxx9/rKampn57P0Kn7UA+n9enn34qv98vi8VS7nK2KhaLqbm5WR9//LECgUC5ywGwHeH+A6AcuPcAKAfuPQDKhfsPgHLg3tM70zTV1tamUaNGyWrtv01MjNfbDlit1n5NKgdDIBDgBgCgLLj/ACgH7j0AyoF7D4By4f4DoBy492wuGAz2+3v2X3wFAAAAAAAAAACA7RahEwAAAAAAAAAAAPqM0AlDimEY+vnPfy7DMMpdCoDtDPcfAOXAvQdAOXDvAVAu3H8AlAP3nsFlMU3TLHcRAAAAAAAAAAAAGN7odAIAAAAAAAAAAECfEToBAAAAAAAAAACgzwidAAAAAAAAAAAA0GeETgAAAAAAAAAAAOgzQicMKbfffrt22GEHuVwu7bfffvrnP/9Z7pIADGOzZ8/WPvvsI7/fr9raWh199NFasWJFj3NSqZTOPfdcVVVVyefz6bjjjtPatWt7nLNq1Sodfvjh8ng8qq2t1Y9+9CNls9nB/CgAhrFf/vKXslgsuvDCC0vHuPcAGAirV6/Wt771LVVVVcntdmvSpEl69dVXS8+bpqkrrrhCDQ0NcrvdOvjgg/Xee+/1eI/W1ladfPLJCgQCqqio0P/8z/+ovb19sD8KgGEil8vp8ssv19ixY+V2u7XjjjvqmmuukWmapXO49wDoD4sWLdKRRx6pUaNGyWKx6LHHHuvxfH/da958800ddNBBcrlcam5u1q9+9auB/mgjDqEThoyHH35YF198sX7+85/rtdde0x577KFZs2Zp3bp15S4NwDC1cOFCnXvuuXrppZc0f/58dXR06JBDDlE8Hi+dc9FFF+nJJ5/Uo48+qoULF+rTTz/VscceW3o+l8vp8MMPVyaT0YsvvqgHHnhA999/v6644opyfCQAw8wrr7yi3/72t9p99917HOfeA6C/hcNhHXDAAXI4HPrrX/+qd955R7/+9a8VCoVK5/zqV7/Srbfeqrvuuksvv/yyvF6vZs2apVQqVTrn5JNP1ttvv6358+frqaee0qJFi/Sd73ynHB8JwDBw/fXX684779RvfvMbvfvuu7r++uv1q1/9SrfddlvpHO49APpDPB7XHnvsodtvv73X5/vjXhOLxXTIIYdozJgxWrp0qW644QZdeeWVuvvuuwf8840oJjBE7Lvvvua5555bepzL5cxRo0aZs2fPLmNVAEaSdevWmZLMhQsXmqZpmpFIxHQ4HOajjz5aOufdd981JZlLliwxTdM0n376adNqtZotLS2lc+68804zEAiY6XR6cD8AgGGlra3N3Hnnnc358+eb06dPNy+44ALTNLn3ABgYP/nJT8wDDzxwi8/n83mzvr7evOGGG0rHIpGIaRiGOXfuXNM0TfOdd94xJZmvvPJK6Zy//vWvpsViMVevXj1wxQMYtg4//HDz29/+do9jxx57rHnyySebpsm9B8DAkGT++c9/Lj3ur3vNHXfcYYZCoR5/5/rJT35i7rLLLgP8iUYWOp0wJGQyGS1dulQHH3xw6ZjVatXBBx+sJUuWlLEyACNJNBqVJFVWVkqSli5dqo6Ojh73nvHjx2v06NGle8+SJUs0adIk1dXVlc6ZNWuWYrGY3n777UGsHsBwc+655+rwww/vcY+RuPcAGBhPPPGE9t57bx1//PGqra3Vl7/8Zd1zzz2l5z/88EO1tLT0uPcEg0Htt99+Pe49FRUV2nvvvUvnHHzwwbJarXr55ZcH78MAGDb2339/LViwQP/+978lSW+88YYWL16sQw89VBL3HgCDo7/uNUuWLNG0adPkdDpL58yaNUsrVqxQOBwepE8z/NnLXQAgSRs2bFAul+vxgxVJqqur0/Lly8tUFYCRJJ/P68ILL9QBBxygiRMnSpJaWlrkdDpVUVHR49y6ujq1tLSUzunt3tT5HAD0Zt68eXrttdf0yiuvbPYc9x4AA+GDDz7QnXfeqYsvvliXXXaZXnnlFX3/+9+X0+nUaaedVrp39HZv6X7vqa2t7fG83W5XZWUl9x4AvbrkkksUi8U0fvx42Ww25XI5XXfddTr55JMliXsPgEHRX/ealpYWjR07drP36Hyu+9hibBmhEwBgu3Duuedq2bJlWrx4cblLATDCffzxx7rgggs0f/58uVyucpcDYDuRz+e199576xe/+IUk6ctf/rKWLVumu+66S6eddlqZqwMwUj3yyCN66KGHNGfOHO222256/fXXdeGFF2rUqFHcewBgO8V4PQwJ1dXVstlsWrt2bY/ja9euVX19fZmqAjBSnHfeeXrqqaf03HPPqampqXS8vr5emUxGkUikx/nd7z319fW93ps6nwOATS1dulTr1q3TXnvtJbvdLrvdroULF+rWW2+V3W5XXV0d9x4A/a6hoUG77rprj2MTJkzQqlWrJHXdO7b2d676+nqtW7eux/PZbFatra3cewD06kc/+pEuueQSnXDCCZo0aZJOOeUUXXTRRZo9e7Yk7j0ABkd/3Wv4e1j/IHTCkOB0OjV58mQtWLCgdCyfz2vBggWaOnVqGSsDMJyZpqnzzjtPf/7zn/Xss89u1iI9efJkORyOHveeFStWaNWqVaV7z9SpU/XWW2/1+IPJ/PnzFQgENvvBDgBI0syZM/XWW2/p9ddfL33tvffeOvnkk0u/594DoL8dcMABWrFiRY9j//73vzVmzBhJ0tixY1VfX9/j3hOLxfTyyy/3uPdEIhEtXbq0dM6zzz6rfD6v/fbbbxA+BYDhJpFIyGrt+eNFm82mfD4viXsPgMHRX/eaqVOnatGiRero6CidM3/+fO2yyy6M1tsWJjBEzJs3zzQMw7z//vvNd955x/zOd75jVlRUmC0tLeUuDcAwdc4555jBYNB8/vnnzTVr1pS+EolE6Zyzzz7bHD16tPnss8+ar776qjl16lRz6tSppeez2aw5ceJE85BDDjFff/1185lnnjFramrMSy+9tBwfCcAwNX36dPOCCy4oPebeA6C//fOf/zTtdrt53XXXme+995750EMPmR6Px/zDH/5QOueXv/ylWVFRYT7++OPmm2++aX796183x44dayaTydI5X/va18wvf/nL5ssvv2wuXrzY3Hnnnc0TTzyxHB8JwDBw2mmnmY2NjeZTTz1lfvjhh+af/vQns7q62vzxj39cOod7D4D+0NbWZv7rX/8y//Wvf5mSzJtuusn817/+ZX700UemafbPvSYSiZh1dXXmKaecYi5btsycN2+e6fF4zN/+9reD/nmHM0InDCm33XabOXr0aNPpdJr77ruv+dJLL5W7JADDmKRev+67777SOclk0vze975nhkIh0+PxmMccc4y5Zs2aHu+zcuVK89BDDzXdbrdZXV1t/uAHPzA7OjoG+dMAGM42DZ249wAYCE8++aQ5ceJE0zAMc/z48ebdd9/d4/l8Pm9efvnlZl1dnWkYhjlz5kxzxYoVPc7ZuHGjeeKJJ5o+n88MBALmGWecYba1tQ3mxwAwjMRiMfOCCy4wR48ebbpcLnPcuHHmT3/6UzOdTpfO4d4DoD8899xzvf6M57TTTjNNs//uNW+88YZ54IEHmoZhmI2NjeYvf/nLwfqII4bFNE2zPD1WAAAAAAAAAAAAGCnY6QQAAAAAAAAAAIA+I3QCAAAAAAAAAABAnxE6AQAAAAAAAAAAoM8InQAAAAAAAAAAANBnhE4AAAAAAAAAAADoM0InAAAAAAAAAAAA9BmhEwAAAAAAAAAAAPqM0AkAAAAAAAAAAAB9RugEAAAAACPMYYcdprPOOmtA3nvjxo3yer16+umnB+T9AQAAAAxfFtM0zXIXAQAAAADY3Isvvqi//e1vuvDCC1VRUfG5XvOPf/xD06dP1/Lly7XTTjsNSF0XXHCBFi9erKVLlw7I+wMAAAAYnuh0AgAAAIAh6sUXX9RVV12lSCTyuV9zww03aObMmQMWOEnS2Wefrddee03PPvvsgF0DAAAAwPBD6AQAAAAAI8S6dev0l7/8Rd/4xjcG9DoTJkzQxIkTdf/99w/odQAAAAAML4ROAAAAADAEXXnllfrRj34kSRo7dqwsFossFotWrly5xdf85S9/UTab1cEHH9zj+P333y+LxaJ//OMfuvjii1VTUyOv16tjjjlG69ev73Huq6++qlmzZqm6ulput1tjx47Vt7/97c2u9V//9V968sknxcR2AAAAAJ3s5S4AAAAAALC5Y489Vv/+9781d+5c3XzzzaqurpYk1dTUbPE1L774oqqqqjRmzJhenz///PMVCoX085//XCtXrtQtt9yi8847Tw8//LCkQqfUIYccopqaGl1yySWqqKjQypUr9ac//Wmz95o8ebJuvvlmvf3225o4cWI/fGIAAAAAwx2hEwAAAAAMQbvvvrv22msvzZ07V0cffbR22GGHz3zN8uXLt3peVVWV/va3v8lisUiS8vm8br31VkWjUQWDQb344osKh8P629/+pr333rv0umuvvXaz9xo3bpwk6Z133iF0AgAAACCJ8XoAAAAAMGJs3LhRoVBoi89/5zvfKQVOknTQQQcpl8vpo48+kiRVVFRIkp566il1dHRs9Vqd19mwYUMfqwYAAAAwUhA6AQAAAMAIsrUdS6NHj+7xuDM4CofDkqTp06fruOOO01VXXaXq6mp9/etf13333ad0Or3F63QPsQAAAABs3widAAAAAGCEqKqqKgVIvbHZbL0e7x4g/fGPf9SSJUt03nnnafXq1fr2t7+tyZMnq729vcdrOq/TuWsKAAAAAAidAAAAAGCI2tYuovHjx+vDDz/s83WnTJmi6667Tq+++qoeeughvf3225o3b16PczqvM2HChD5fDwAAAMDIQOgEAAAAAEOU1+uVJEUikc91/tSpUxUOh/XBBx98oeuFw+HNxvPtueeekrTZiL2lS5cqGAxqt912+0LXAgAAADDy2MtdAAAAAACgd5MnT5Yk/fSnP9UJJ5wgh8OhI488shRGberwww+X3W7X3//+d33nO9/Z5us98MADuuOOO3TMMcdoxx13VFtbm+655x4FAgEddthhPc6dP3++jjzySHY6AQAAACghdAIAAACAIWqfffbRNddco7vuukvPPPOM8vm8Pvzwwy2GTnV1dTrssMP0yCOPfKHQafr06frnP/+pefPmae3atQoGg9p333310EMPaezYsaXzli9frmXLlumWW275oh8NAAAAwAhkMTednQAAAAAAGLZeeOEFzZgxQ8uXL9fOO+88INe48MILtWjRIi1dupROJwAAAAAlhE4AAAAAMMIceuihampq0j333NPv771x40aNGTNGjzzyyGYj9wAAAABs3widAAAAAAAAAAAA0GfWchcAAAAAAAAAAACA4Y/QCQAAAAAAAAAAAH1G6AQAAAAAAAAAAIA+I3QCAAAAAAAAAABAnxE6AQAAAAAAAAAAoM8InQAAAAAAAAAAANBnhE4AAAAAAAAAAADoM0InAAAAAAAAAAAA9BmhEwAAAAAAAAAAAPqM0AkAAAAAAAAAAAB99v8BxnBRMMkPEOIAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "reg = pulser.Register({\"q0\": (-5, 0), \"q1\": (5, 0)})\n", - "\n", - "seq = pulser.Sequence(reg, pulser.MockDevice)\n", - "seq.declare_channel(\"rydberg_global\", \"rydberg_global\")\n", - "t = seq.declare_variable(\"t\", dtype=int)\n", - "\n", - "amp_wf = pulser.BlackmanWaveform(t, np.pi)\n", - "det_wf = pulser.RampWaveform(t, -5, 5)\n", - "seq.add(pulser.Pulse(amp_wf, det_wf, 0), \"rydberg_global\")\n", - "\n", - "# We build with t=1000 so that we can draw it\n", - "seq.build(t=1000).draw()" - ] - }, - { - "cell_type": "markdown", - "id": "deb625b6", - "metadata": {}, - "source": [ - "## 3. Starting the backend" - ] - }, - { - "cell_type": "markdown", - "id": "953eab2e", - "metadata": {}, - "source": [ - "It is now time to select and initialize the backend. Currently, these are the available backends (but bear in mind that the list may grow in the future):\n", - "\n", - " - **Local**: \n", - " - `QutipBackend` (from `pulser_simulation`): Uses `QutipEmulator` to emulate the sequence execution locally.\n", - " - **Remote**:\n", - " - `QPUBackend` (from `pulser`): Executes on a QPU through a remote connection.\n", - " - `EmuFreeBackend` (from `pulser_pasqal`): Emulates the sequence execution using free Hamiltonian time evolution (similar to `QutipBackend`, but runs remotely). \n", - " - `EmuTNBackend` (from `pulser_pasqal`): Emulates the sequence execution using a tensor network simulator." - ] - }, - { - "cell_type": "markdown", - "id": "438c3cca", - "metadata": {}, - "source": [ - "Instead of choosing one, here we will import the three emulator backends so that we can compare them." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "c508a2d8", - "metadata": {}, - "outputs": [], - "source": [ - "from pulser_simulation import QutipBackend\n", - "from pulser_pasqal import EmuFreeBackend, EmuTNBackend" - ] - }, - { - "cell_type": "markdown", - "id": "365ed331", - "metadata": {}, - "source": [ - "Upon creation, all backends require the sequence they will execute. Emulator backends also accept, optionally, a configuration given as an instance of the `EmulatorConfig` class. This class allows for setting all the parameters available in `QutipEmulator` and is forward looking, meaning that it envisions that these options will at some point be availabe on other emulator backends. This also means that trying to change parameters in the configuration of a backend that does not support them yet will raise an error.\n", - "\n", - "Even so, `EmulatorConfig` also has a dedicated `backend_options` for options specific to each backend, which are detailed in the [backends' docstrings](../apidoc/backend.rst)." - ] - }, - { - "cell_type": "markdown", - "id": "21f506c5", - "metadata": {}, - "source": [ - "With `QutipBackend`, we have free reign over the configuration. In this example, we will:\n", - " \n", - "- Change the `sampling_rate`\n", - "- Include measurement errors using a custom `NoiseModel`\n", - "\n", - "On the other hand, `QutipBackend` does not support parametrized sequences. Since it is running locally, they can always be built externally before being given to the backend. Therefore, we will build the sequence (with `t=2000`) before we give it to the backend." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "6f64a5af", - "metadata": {}, - "outputs": [], - "source": [ - "config = pulser.EmulatorConfig(\n", - " sampling_rate=0.1,\n", - " noise_model=pulser.NoiseModel(\n", - " noise_types=(\"SPAM\",),\n", - " p_false_pos=0.01,\n", - " p_false_neg=0.004,\n", - " state_prep_error=0.0,\n", - " ),\n", - ")\n", - "\n", - "qutip_bknd = QutipBackend(seq.build(t=2000), config=config)" - ] - }, - { - "cell_type": "markdown", - "id": "e74755e3", - "metadata": {}, - "source": [ - "Currently, the remote emulator backends are still quite limited in the number of parameters they allow to be changed. Furthermore, the default configuration of a given backend does not necessarily match that of `EmulatorConfig()`, so it's important to start from the correct default configuration. Here's how to do that for the `EmuTNBackend`:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "0889e0ba", - "metadata": {}, - "outputs": [], - "source": [ - "import dataclasses\n", - "\n", - "emu_tn_default = EmuTNBackend.default_config\n", - "# This will create a new config with a different sampling rate\n", - "# All other parameters remain the same\n", - "emu_tn_config = dataclasses.replace(emu_tn_default, sampling_rate=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "21f4ee21", - "metadata": {}, - "source": [ - "We will stick to the default configuration for `EmuFreeBackend`, but the process to create a custom configuration would be identical. To know which parameters can be changed, consult the backend's docstring." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "59d5e3ca", - "metadata": {}, - "outputs": [], - "source": [ - "free_bknd = EmuFreeBackend(seq, connection=connection)\n", - "tn_bknd = EmuTNBackend(seq, connection=connection, config=emu_tn_config)" - ] - }, - { - "cell_type": "markdown", - "id": "50729b54", - "metadata": {}, - "source": [ - "Note also that the remote backends require an open connection upon initialization. This would also be the case for `QPUBackend`." - ] - }, - { - "cell_type": "markdown", - "id": "51cce28c", - "metadata": {}, - "source": [ - "## 4. Executing the Sequence" - ] - }, - { - "cell_type": "markdown", - "id": "f4590ab7", - "metadata": {}, - "source": [ - "Once the backend is created, executing the sequence is always done through the backend's `run()` method.\n", - "\n", - "For the `QutipBackend`, all arguments are optional and are the same as the ones in `QutipEmulator`. On the other hand, remote backends all require `job_params` to be specified. `job_params` are given as a list of dictionaries, each containing the number of runs and the values for the variables of the parametrized sequence (if any). The sequence is then executed with the parameters specified within each entry of `job_params`." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "22e8f95b", - "metadata": {}, - "outputs": [], - "source": [ - "# Local execution, returns the same results as QutipEmulator\n", - "qutip_results = qutip_bknd.run()\n", - "\n", - "# Remote execution, requires job_params\n", - "job_params = [\n", - " {\"runs\": 100, \"variables\": {\"t\": 1000}},\n", - " {\"runs\": 50, \"variables\": {\"t\": 2000}},\n", - "]\n", - "free_results = free_bknd.run(job_params=job_params)\n", - "tn_results = tn_bknd.run(job_params=job_params)" - ] - }, - { - "cell_type": "markdown", - "id": "4421eb27", - "metadata": {}, - "source": [ - "## 5. Retrieving the Results" - ] - }, - { - "cell_type": "markdown", - "id": "8289b06f", - "metadata": {}, - "source": [ - "For the `QutipBackend` the results are identical to those of `QutipEmulator`: a sequence of individual `QutipResult` objects, one for each evaluation time. As usual we can, for example, get the final state:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "c920679c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket $ \\\\ \\left(\\begin{matrix}(-0.380-0.157j)\\\\(0.035+0.593j)\\\\(0.035+0.593j)\\\\(-0.235-0.263j)\\\\\\end{matrix}\\right)$" - ], - "text/plain": [ - "Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket\n", - "Qobj data =\n", - "[[-0.38024396-0.15656328j]\n", - " [ 0.03529282+0.59329452j]\n", - " [ 0.03529282+0.59329452j]\n", - " [-0.23481812-0.26320141j]]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qutip_results[-1].state" - ] - }, - { - "cell_type": "markdown", - "id": "2618a789", - "metadata": {}, - "source": [ - "For remote backends, the object returned is a `RemoteResults` instance, which uses the connection to fetch the results once they are ready. To check the status of the submission, we can run:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "d24593f4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "free_results.get_status()" - ] - }, - { - "cell_type": "markdown", - "id": "763e011c", - "metadata": {}, - "source": [ - "When the submission states shows as `DONE`, the results can be accessed. In this case, they are a sequence of `SampledResult` objects, one for each entry in `job_params` in the same order. For example, we can retrieve the bitstring counts or even plot an histogram with the results:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "738de317", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'00': 13, '01': 13, '10': 8, '11': 66}\n" - ] - }, - { - "data": { - "image/png": "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KipSXV2dSkpKtHHjRmVkZPS73ebNm3X11VfrhBNOiOchAAAAQ8X9PjfBYFAtLS265557tGPHDklScnJyzHGzZMkSVVZWqqKiQlOmTFF9fb1Gjx6thoaGfrfx+/264IILtGjRIh1++OHxPgQAAGCguOLmvffe0zHHHKNZs2apqqpKnZ2dkj59uurqq6+Oej99fX1qaWmRy+X6fCC7XS6XS01NTf1ud8MNNygjI0Nz5879wu/R29srn88XdgMAAOaKK27mz5+vgoICbdu2Tfvvv39o+VlnnSWPxxP1frq6uuT3+5WZmRm2PDMzU16vN+I2r732mn7zm99o2bJlUX2P2tpaORyO0C0nJyfq+QAAwPATV9y8+uqruu6665ScnBy23Ol06oMPPkjIYJHs2LFDF110kZYtW6b09PSotlm4cKG6u7tDt7a2tgGbDwAAWC+uC4oDgYD8fv8ey99//32NGTMm6v2kp6crKSlJHR0dYcs7OjqUlZW1x/r//ve/tXnzZp1xxhlhs0jSqFGjtHHjRh1xxBFh26SkpITekwcAAJgvrjM33/zmN1VXVxf62maz6aOPPlJNTY1OO+20qPeTnJys/Pz8sKeyAoGAPB6PiouL91h/8uTJev3117V27drQ7cwzz9RJJ52ktWvX8pQTAACI78zNL37xC5WUlGjKlCn65JNPdP755+udd95Renq6fv/738e0L7fbrfLychUUFKiwsFB1dXXq6elRRUWFJKmsrEzZ2dmqra1Vamqqpk6dGrb92LFjJWmP5QAAYGSKK24OPfRQrVu3To2NjVq3bp0++ugjzZ07VxdccEHYBcbRKC0tVWdnp6qrq+X1epWXl6cVK1aELjJubW2V3R73K9YBAMAIYwsGg8FYN1q1apWmT5+uUaPC22j37t1avXq1TjzxxIQNmGg+n08Oh0Pd3d1KS0tL+P6dC5YnfJ+IzubFp1s9AgBggMTy+zuuUyInnXSStm7dusfy7u5unXTSSfHsEgAAICHiiptgMCibzbbH8v/+97864IAD9nkoAACAeMV0zc13vvMdSZ++Ouriiy8Oe4m13+/X+vXrNX369MROCAAAEIOY4sbhcEj69MzNmDFjwi4eTk5O1vHHH6/KysrETggAABCDmOLmvvvuk/TpOxFfffXVPAUFAACGnLheCl5TU5PoOQAAABIi6riZNm2aPB6P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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "cells": [ + { + "cell_type": "markdown", + "id": "6f230abe", + "metadata": {}, + "source": [ + "# Backend Execution of Pulser Sequences" + ] + }, + { + "cell_type": "markdown", + "id": "ae508ab2", + "metadata": {}, + "source": [ + "When the time comes to execute a Pulser sequence, there are many options: one can choose to execute it on a QPU or on an emulator, which might happen locally or remotely. All these options are accessible through an unified interface we call a `Backend`. \n", + "\n", + "This tutorial is a step-by-step guide on how to use the different backends for Pulser sequence execution." + ] + }, + { + "cell_type": "markdown", + "id": "a7601ae9", + "metadata": {}, + "source": [ + "## 1. Choosing the type of backend\n", + "\n", + "Although the backend interface nearly doesn't change between backends, some will unavoidably enforce more restrictions on the sequence being executed or require extra steps. In particular, there are two questions to answer:\n", + "\n", + "1. **Is it local or remote?** Execution on remote backends requires a working remote connection. For now, this is only available through `pulser_pasqal.PasqalCloud`.\n", + "2. **Is it a QPU or an Emulator?** For QPU execution, there are extra constraints on the sequence to take into account.\n", + "\n", + "### 1.1. Starting a remote connection\n", + "\n", + "For remote backend execution, start by ensuring that you have access and start a remote connection. For `PasqalCloud`, we could start one by running:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ef3cc2eb", + "metadata": {}, + "outputs": [], + "source": [ + "from pulser_pasqal import PasqalCloud\n", + "\n", + "connection = PasqalCloud(\n", + " username=USERNAME, # Your username or email address for the Pasqal Cloud Platform\n", + " project_id=PROJECT_ID, # The ID of the project associated to your account\n", + " password=PASSWORD, # The password for your Pasqal Cloud Platform account\n", + " **kwargs\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "29cff577", + "metadata": {}, + "source": [ + "### 1.2. Preparation for execution on `QPUBackend`\n", + "\n", + "Sequence execution on a QPU is done through the `QPUBackend`, which is a remote backend. Therefore, it requires a remote backend connection, which should be open from the start due to two additional QPU constraints:\n", + "\n", + "1. The `Device` must be chosen among the options available at the moment, which can be found through `connection.fetch_available_devices()`.\n", + "2. The `Register` must be defined from one of the register layouts calibrated for the chosen `Device`, which are found under `Device.calibrated_register_layouts`. Check out [this tutorial](reg_layouts.nblink) for more information on how to define a `Register` from a `RegisterLayout`.\n", + "\n", + "On the contrary, execution on emulator backends imposes no further restriction on the device and the register. We will stick to emulator backends in this tutorial, so we will forego the requirements of QPU backends in the following steps." + ] + }, + { + "cell_type": "markdown", + "id": "35a4f10c", + "metadata": {}, + "source": [ + "## 2. Creating the Pulse Sequence" + ] + }, + { + "cell_type": "markdown", + "id": "122a3c37", + "metadata": {}, + "source": [ + "The next step is to create the sequence that we want to execute. Here, we make a sequence with a variable duration combining a Blackman waveform in amplitude and a ramp in detuning. Since it will be executed on an emulator, we can create the register we want and choose a `VirtualDevice` that does not impose hardware restrictions (like the `MockDevice`)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4548fedd", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pulser" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "57e088c6", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABp0AAAF8CAYAAAA9/HMGAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8WgzjOAAAACXBIWXMAAA9hAAAPYQGoP6dpAADJFElEQVR4nOzdd3jb5b3+8bf2sizJezt770kIISFsCITRNARaxoEOuuF3WqDnlDIP7enpbg+0FCinpaxAwp4hAwgjezlxth3vLdmyLQ/p94dikUAgCbYjO7lf16VL0lePvs9HwXjo1vN5DJFIJIKIiIiIiIiIiIiIiIhINxjjXYCIiIiIiIiIiIiIiIj0fwqdREREREREREREREREpNsUOomIiIiIiIiIiIiIiEi3KXQSERERERERERERERGRblPoJCIiIiIiIiIiIiIiIt2m0ElERERERERERERERES6TaGTiIiIiIiIiIiIiIiIdJtCJxEREREREREREREREek2hU4iIiIiIiIiIiIiIiLSbQqdREREREREREREREREpNvM8S5A4qehoYFNmzbh9/uJRCLdPp/RaMTr9TJp0iRcLlcPVCgiIiIiIiIiIiIiIv2FQqdT0Icffsh9997Dm2+9RXt7R4+f32G3cdFFF3PX3XczZsyYHj+/iIiIiIiIiIiIiIj0PYZITyxxkX7jvffe44Lzz2Vwajs3ntnJuWMhOQGMPdBosTMM1QF4dSM8vNJEQ1si7yxfydixY7t/chERERERERERERER6dMUOp0CwuEwZWVlOBwORgwfyug0P6/8exiXvffmrGuCuf9lotU2gI8+XofBYOi9yURERERERERERERE5JhFIhEaGxvJysrC2BOrUg5S6HQKKCkpITc3N3Z/7X0weWDvz/vKBpj3P70/j4iIiIiIiIiIiIiIHL8DBw6Qk5PTY+fTnk6nALfbDcBNN93Emy/8nUkDen4fpyM5dyy47EZ+csfP+dGPfnTMz3vuuee48sore68wEREREREREREREZFTWCAQIDc3N5Yf9BSFTqeArtZ2gUCAASkRTlSnO6sZspJMBAIBEhMTj/l5drv9uMaLiIiIiIiIiIiIiMjx6+mtcXquUZ/0eR0dHVjNJ7abos0M7e3tx/WcwYMH91I1IiIiIiIiIiIiIiLSWxQ6SZ+TnZ0d7xJEREREREREREREROQ4KXSSPmfVqlXxLkFERERERERERERERI6TQic5bn9ZBqN+HO8qRERERERERERERESkL1HoJMetphEKy3vv/HPmzOm9k4uIiIiIiIiIiIiISK9Q6CR9TnFxcbxLEBERERERERERERGR46TQSfqcvXv3xrsEERERERERERERERE5TgqdpM8xm83xLkFERERERERERERERI6TQifpcxYsWBDvEkRERERERERERERE5DhpSYkA8JtXj33s+zt7rw6A5557jiuvvLJ3JxERERERERERERERkR6l0Kkf6Ozs5K677uKf//wnFRUVZGVlcf311/Of//mfGAyGHpnj3/91fON7ZtYja2tr68Wzi4iIiIiIiIiIiIhIb1Do1A/88pe/5MEHH+Txxx9n9OjRrF27lhtuuAGPx8MPfvCDHplj+X/0yGl6RH5+frxLEBERERERERERERGR46TQqR9YvXo18+fP5+KLLwZgwIABPPnkk3z88cc9NsfskT12qm4bPHhwvEsQEREREREREREREZHjZIx3AXJ0p59+OsuWLWPnzuhmSps2beK9997jwgsvPOL4UChEIBA47NKTPt4D336kR095mHfeeaf3Ti4iIiIiIiIiIiIiIr1CK536gdtvv51AIMCIESMwmUx0dnZy//33c8011xxx/AMPPMDdd9/dozXUNsL/vQeProCC0uixh27s0SlERERERERERERERKQfU+jUDzzzzDM88cQT/Otf/2L06NFs3LiRH/3oR2RlZXHdddd9Zvwdd9zBrbfeGrsfCATIzc097nkjEXh9EzyyAl7eCG0dMCAFbr0IrpzajRd0FGeccUbvnVxERERERERERERERHqFQqd+4Mc//jG33347V111FQBjx46lqKiIBx544Iihk81mw2azfen59lZFVzQ9/i6U1UOCHdo74E/XwXfO/dKnPWZVVVVfKiQTEREREREREREREZH4UejUDzQ3N2M0Hr79lslkIhwO99gcrW3w7Efw6EpYtQPMJrh4Alx/JgzNgNG3QYa3x6b7Qjt37mTy5MknZjIREREREREREREREekRCp36gUsuuYT777+fvLw8Ro8ezYYNG/jNb37Dv/3bv/XYHBnfhcYWmJAPv/s6XH06JLujj+2p7LFpRERERERERERERETkJKXQqR/44x//yM9+9jO+853vUFVVRVZWFt/6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SqAlQFFtEYWVheyr2ceuql00tzUzOW8ys4fP5vxR53Pa4NMUQomIyOfqrdBJH4M+Ao/HQ3V19TGPX7x4MQUFBdx55529WNUn/H4/wOcGMw888AB33333CamlN7zxxhtqrycip5yG5gbe2/UeywuX886Od9hSuoXUhFSGpA1heMZwzht1Hqnu1FjIZLPY4l2yiIiIiIjEgd1ix26xk5aYxtSBU2lua8bf4mdP1R52Ve7izW1v8uCKB2lpa2FC7gTOHHYm5406j5lDZuK0ffE2EiIiIt2llU5HcN5557Fjxw727NmDxWL5wrEffPABZ555JuFwuNf2dDpUOBzm0ksvpaGhgffee++IY/r7Sift6SQip4LaplpW7VzF8h3LWV64nILyAjI9mQxJG0J+Uj45STmkJqTitrtJdCTqE4oiIiIiInJUkUiElvYW/M3+6F5QVXvYX7ufPdV7aGxtZGzOWM4ceibnjT6PWUNnaU8oEZFTWJ9or2cy9cwm5AaDgY6Ojh45V2947LHHuPHGG7nlllv49a9//bnjVq1axYIFC6iursZgMJyQ0Onmm2/mtdde47333iMnJ+eYntPf9nTau3cvgwYNincZIiI9qjJQycrClSwvXM6KwhUUVhSSl5THoLRB0ZDJl0OyKxm33Y3b7sZhdcS7ZBERERER6ecODaGKaovYXb2botoi9lTvoS5Yx+is0cwaOotzR53LnOFz8Dq98S5ZREROkD4ROhmNxp6Z9AQFNF9WZ2cnp59+OmvXrmX+/Pn853/+J5MmTQKiK402b97M//7v//L4449jtVqZOXMmb731Vq+/pu9973u88MILrFq1ioEDBx7z8/pb6LR582bGjRsX7zJERL60SCRCYUUhq/esZuXOlby/+332Vu9lYMpABqUOIteXS15yHj6nLxYyaSWTiIiIiIj0tq4QqrG1kQO1B9hZtZOiuiL2Vu+lurGaYenDOG3QacwePpuzhp9FXlIeBoMh3mWLiEgv6BOh08qVK3ts4tmzZ/fYuXpDRUUFF198MRs2bMBgMOB0OvF4PFRVVdHZ2UkkEiErK4tnnnmGt956i3vuuafXQqdIJML3v/99lixZwooVKxg6dOhxPb+/hU5qryci/U1reytr96/lvd3vsXLnSj7a+xHBUJDBaYPJT8on25dNXtLhIZPVbI132SIiIiIiIrS2t9LY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ERGR7vv0Fi7d1e3QacWKFbHbXcV9evHUkY4bDAbuvPNOzj77bB5//HEyMzO7W8pJy2q1MnnyZJYtW8Zll10GRIOZZcuW8b3vfS++xfWCwYMHx7sEERERERERERERERE5Tt0OnX7+85/T3t7Ogw8+SH19PXl5ecyePZvs7GwASktLWbVqFUVFRSQlJfHtb3+bpqYm1q5dy+rVq1m2bBnnn38+H3/8MXa7vdsv6GR16623ct111zFlyhSmTZvG7373O4LBIDfccEO8S+txXV87IiIiIiIiIiIiIiLSf3Q7dPqP//gPzjnnHFpbW/n73//Otddee8Rx//jHP/j2t7/Nxx9/zOuvv47RaGTlypVcdtllbNu2jYcffpjvf//73S3npLVw4UKqq6u58847qaioYMKECbz++uukp6fHu7Qet2rVKhYtWhTvMkRERERERERERERE5DgYu3uC3/72t7z77rv84Q9/+NzACeDrX/86f/jDH1i2bBm///3vAZg9eza/+MUviEQiPPfcc90t5aT3ve99j6KiIkKhEB999BHTp0+Pd0kiIiIiIiIiIiIiIiIAGCKf3oDpOE2YMIHt27fT2NiI1Wr9wrGhUIjExERGjRrFhg0bAPD7/SQlJZGUlER1dXV3SpHPEQgE8Hg8+P1+EhMT413OUZWXl2uPLxERERERERERERGRXtJbuUG3Vzrt2bOHhISEowZOADabjYSEBHbv3h075vF48Hq9BAKB7pYiJ4ni4uJ4lyAiIiIiIiIiIiIiIsep26GT2WymoaGB8vLyo44tLy+noaEBs/nwraSam5vxeDzdLUVOEnv37o13CSIiIiIiIiIiIiIicpy6HTpNmjQJgNtuu+2oY2+//XYikUjsOQCVlZWEQiHS09O7W4qcJD4dSoqIiIiIiIiIiIiISN/X7dDpBz/4AZFIhCeeeIILL7yQd999l46OjtjjHR0drFq1iosuuoh//vOfGAwGfvCDH8Qef/311wGYPn16d0uRk8SCBQviXYKIiIiIiIiIiIiIiBynbodO8+fP59ZbbyUSifDmm28yZ84cEhISyM7OJicnh4SEBM466yzeeOMNIpEIP/rRj5g/f37s+WvWrGH8+PFcdtll3S1FThLPPfdcvEsQEREREREREREREZHj1CN9zP7nf/6HKVOmcOedd7J7927a2to+s8fTkCFDuPvuu1m0aNFhx//0pz/1RAlyEmlra4t3CSIiIiIiIiIiIiIicpx6bPOcq666iquuuoqNGzeyfv16qqurAUhNTWXSpElMmDChp6aSk1x+fn68SxARERERERERERERkePU7dDpnnvuAeCGG24gNzeXCRMmKGD6AnfddRd33333YceGDx/Ojh07Pvc5zz77LD/72c/Yv38/Q4cO5Ze//CUXXXRRb5caN4MHD453CSIiIiIiIiIiIiIicpy6vafT3XffzX333UdGRkZP1HNKGD16NOXl5bHLe++997ljV69ezaJFi7jxxhvZsGEDl112GZdddhlbt249gRWfWO+88068SxARERERERERERERkePU7dApJSWFxMRELBZLT9RzSjCbzWRkZMQuKSkpnzv297//PRdccAE//vGPGTlyJPfeey+TJk3SXlgiIiIiIiIiIiIiItKndLu93vjx43nnnXeora0lOTm5J2o66e3atYusrCzsdjszZszggQceIC8v74hjP/jgA2699dbDjp1//vksXbr0c88fCoUIhUKx+4FAoEfqPlHOOOOMeJcgIiIiIiIiIiIiIv1AJBKhpamNhsog/qog9ZVBGiqaqCtrpK68iRt/cy6uRHu8yzxldDt0+ta3vsXbb7/Nb37zG+6///6eqOmkNn36dP7+978zfPhwysvLufvuu5k1axZbt27F7XZ/ZnxFRQXp6emHHUtPT6eiouJz53jggQc+s29Uf1JVVUVubm68yxARERERERERERGROIhEIgQbWqMBUmWQhsom6iuD1JU1Ul/eRH1FkIaqJvzVzQRqWmhv7cBkNuLy2nAm2rAnWHG4rdgTrOxZX8G4OQPi/ZJOGd0Ona688kpuvfVWfvGLX9De3s5PfvKTL2wXd6q78MILY7fHjRvH9OnTyc/P55lnnuHGG2/skTnuuOOOw1ZHBQKBfhXi7Ny5k8mTJ8e7DBERERERERERERHpIeFwhMba5sOCpLryJurKmmioiIZKDVVBAtVBGmtb6GgPY7GZcHntONxWHF1BksuKI8GCJy3tk2MJVuwuC2arCZPFhNlixGQ2Ul/ehMXW7RhEjkO3/7Xnzp0LgMvl4te//jW//e1vGTJkCGlpaZhMpiM+x2AwsGzZsu5OfVLwer0MGzaM3bt3H/HxjIwMKisrDztWWVlJRkbG557TZrNhs9l6tE4RERER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DI4+h07jxo3TihUrlMlk5HQ6t3puOp1We3u7xo8f39fL9puf/vSnev/99zVv3rxBud4zzzzT4/H999+v2tpaLV26VNOmTdvi6ywWi+rr6we6vCHhySef1IknnljuMgAAAAAAAPqsI5NTdF0hQOoMkwpj7doLQdLauCLr44ptSCgeTsk0VQiNgl0hUqH7yKFgrVd1OwTl8hvyFMMlp8fRswup244km4P9SACAwdXn0Omkk07SZZddpgcffFBnnnnmVs/93//9X3V0dOikk07q62U/l6uvvnqrz2ezWc2bN08tLS2DUk9votGoJKmysnKr57W3t2vMmDHK5/Paa6+99Itf/EK77bZbr+em02ml0+nS41gs1n8FAwAAAAAAbOdSiQ5F1rYrui6hyNp2hVvaix1Jhd9H1sYVXR9XdENCyVhGFovkDhjyBgy5A065fIbcPocMj0PVzX417lJV2I9UDJmcLnuvIZLdYZXVTpAEABi6LKZpmn15g46ODs2cOVOvvvqq7rzzTp122mm9nvfggw/q7LPP1j777KMFCxbIbu9z3vWZrNatz5bt/D/oE088UX/4wx8GvJ5N5fN5HXXUUYpEIlq8ePEWz1uyZInee+897b777opGo7rxxhu1aNEivf3222pqatrs/CuvvFJXXXXVZsej0agCgUC/foaBsHz58iHVDQcAAAAAAEY20zSVbMv0GGsXWRvXxuJYu3BL12i72Iak0okOWW0WeYKGPAGjML7O37UfyfA4ZHidpeNun0N2w94tPOq5J8lmZz8SAAyET99r1fipTZowdfOfo2/vYrGYgsFgv+cG2xQ6balzKJ1O64477lAsFlNzc7NmzJhR2vG0evVqLVy4UKtWrVIwGNT3vvc9OZ1OXXHFFf3zCbZi4cKFvR5PpVL697//rdtvv13ZbFb/+Mc/VFdXN+D1bOqcc87RX//6Vy1evLjX8GhLOjo6NGHCBJ144om65pprNnu+t06n5uZmQicAAAAAALDdME1T7eFUoROpGCKFW9q1cXVb6feRdXHF1icU25BQRzonm90qb4VLnkBnWGTI8Dnk8jrk8jjk8hbH3fmdcnmdshu2UoBU2pNU7FCy2giSAKDcCJ22bEiETlbr1tt3O99q03N6O57L5bap0IEQi8U0adIk7bfffnrkkUcG9drnnXeeHn/8cS1atEhjx47d5tcf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cqVmz55dlvoAAAAAANumI5NTdH28W5AUL461K+xHCre0K7q+MNauPZySmTfl8jrkCRpy+41C55HPKZfPoUCNWzVjAqUuJbfPKafHUQiRunUhdd+TRJAEAAAAbFmfO506ZbNZPfHEE7r77rv197//Xfl8vuzdT/l8XmeddZbuu+++0uJUq9WqfD4vqTCH+4wzztDvfve7Eb1Ydbh1Om3cuFFVVVXlLgMAAADAIEknO7pCpM79SGvaS6PtImvjpSApEU1LkjwBoxgkdYZIhf1Ihtchl8dRDJIMufxOOV32HgFS9z1JNrt1RP99EAAAYHtGp9OWDVRu0G+hU3erVq3SPffco/vvv1+rV68uXMhiKVv308KFC/XAAw/ozTffVDQaVTAY1B577KFTTjlFM2bMGLQ6ymW4hU4vvPCCDjrooHKXAQAAAOALMk1TyfZMj7F2XR1Jhf1IkXVxRdcVgqRUvEMWq0XeYDFI8hWCJKMzSOo21s7tM+T2O2Q37KXuox57korHAAAAAEKnLRtWoVOnfD6vv/zlL/rd736np59+Wrlcrkf309lnn60DDjhgoC4/pN1+++264YYb1NLSoj322EO33Xab9t133y2e/+ijj+ryyy/XypUrtfPOO+v666/XYYcd9rmuNdxCJ3Y6AQAAAEOPaZqKR1IKdwuSwi3xUjdSuCWuyLrO0XZJdaSystmt8gQNeQOFjiO3v9iR5OkeJBnFgMkhh9PeNcquW4hkd1hltREkAQAAYNsQOm3ZkN3ptDVWq1VHHnmkHA6HNm7cqCVLlsg0TaVSKT300EOaM2eOpkyZoltuuUX77LPPQJYypDz88MO6+OKLddddd2m//fbTLbfcolmzZmnFihWqra3d7PwXX3xRJ554ombPnq0jjjhCc+bM0dFHH63XXntNEydOLMMnGFgej6fcJQAAAADbhXzeVNvGRI8gqTDWrl3hNW0Kd461Wx9X28aksh152Z02eSsMeQJGaReSy+uU2+dQsLZW7mKQ5PIXupTsTlup+6h7iGSzEyQBAAAAI82AdTqtWbNG9957r37/+9/ro48+UudlDjzwQB1//PH629/+pr/+9a/K5/NyOBz6v//7vwEddZfL5bRhwwal0+lenx89evSAXXtT++23n/bZZx/95je/kVToCGtubtb555+vSy65ZLPzv/nNbyoej+upp54qHZsyZYr23HNP3XXXXZ95veHW6QQAAADgi8tl84quL4RI4R5BUpvCa4qj7tbFFduQUFtrSmbelOFxyBM05Cl2InUGSZ37kUrHfE4ZHrvsTrvsdmuPPUmdo+4sVvYjAQAAYGig02nLhkWnk2maevrpp3XPPfeUxumZpqlAIKBTTjlFZ599tnbbbTdJ0vnnn68PPvhA55xzjubPn6/LL79cL7zwQn+WI0launSpLrvsMi1atEiZTKbXcywWi7LZbL9fuzeZTEZLly7VpZdeWjpmtVp18MEHa8mSJb2+ZsmSJbr44ot7HJs1a5Yee+yxXs9Pp9M9wrVYLNb3wgfR3Llz9Y3jv1nuMgAAAIAhoyOdLe1Hiq4vhEndu5E6j8c2JBWPpCRJbp+zsB+p21g7t9epUINP9eNCMrx2uXyGPD6nnB67bHZbr6PtbHarLJ8zRzJNU2ZuwCa4AwAAANtkALcLYQv6JXT65JNPSl1Nn3zySel/yL322ktnn322TjrppF5Hpo0bN06PPvqoamtr9eabb/ZHKT28/vrrOuigg2S323XIIYfoySef1B577KH6+nq99tprWr9+vWbMmKExY8b0+7W3ZMOGDcrlcqqrq+txvK6uTsuXL+/1NS0tLb2e39LS0uv5s2fP1lVXXbXZ8UcffXRYjK776MNVuv2me8pdBgAAADCgzFxeuWzhK9vR9ftcNq9c6XFOuY688vnC37FsdmtxTF2xw8htlXUnqyp3sarabpXN5pLF6pHVZtkkKMoUv4rXlpQqfkUkKTlIHxoAAAAYZBtfX67XV7rKXcaQk0gkBuR9+xw6HXHEEfq///s/5fN5maYpj8ejb37zmzr77LM/156mQCCg+vp6ffzxx30tZTPXXHONJOnll1/WhAkTZLVadcwxx+iKK65QMpnUD37wA/3xj3/U73//+36/djldeumlPTqjYrGYmpubdfzxxw+L8Xp/vO9p2cIh+SuHfkAGAAAAdDJNU5lkVolYuvAVTSseSykeTikeTSseTSsZSyvRllaqPaNsJi+LRXL5Cl1InoAhT8Apb9AlX4VLvpBLvjq3/JUuBaq88lcacrodchh2OQybHIZddsNW+r3Nzn4kAAAAYFPeCpdcHke5yxhyYrGYzjzzzH5/3z6HTk8//bQkacKECfrud7+r0047TcFgcJve47//+7+1cePGvpaymcWLF+uoo47ShAkTSsc6u7Dcbrd+85vf6MUXX9Rll12mOXPm9Pv1e1NdXS2bzaa1a9f2OL527VrV19f3+pr6+vptOt8wDBmG0T8Fl4HfHVRH0iF/lbvcpQAAAGA7Z+ZNJdszikdSikdSao+kFQ8n1daaVHs4pfZwSolYqhgoZZTL5mW1WQoj7XyF0XaegCFvhaGqRr98IbcCNR6F6n2qbvQp1OCX4bb3CJIchk12J0ESAAAAgOGnz6FTXV2dHn74YU2aNEmhUOgLvceNN97Y1zJ6FY1GNW7cuNJjh8Oh9vb20mOr1aoZM2Zo7ty5A3L93jidTk2ePFkLFizQ0UcfLUnK5/NasGCBzjvvvF5fM3XqVC1YsEAXXnhh6dj8+fM1derUQah48H3Q8q6anXuWuwwAAACMUPlcXolYWvFIuhgkpdQeTqq9NaW2cLIYMBW6lZJtGZl5U3anrbAXyVvYj+QNGvJWuFQ5yi9fpUsVtd5CkNQUULDWs1mI1P1Xq/VzLkgCAAAAgGGmz6HT2rVrNXPmTK1bt64/6ulXtbW1CofDpcf19fV67733epyTSqUGbHbhllx88cU67bTTtPfee2vffffVLbfcong8rjPOOEOSdOqpp6qxsVGzZ8+WJF1wwQWaPn26fv3rX+vwww/XvHnz9Oqrr+ruu+8e1LoBAACAoSrXkSuMsCuGSIloWm0bk4UQKdx1LB5NKxXPSKbkdNnl8jvl9hWCJE8xSKrboUL+SreCdV5VjfKruskvf6V7i91IdqeNIAkAAAAA1A+hUzAYlM1m+8JdTgNp11131YoVK0qPDzjgAD322GNasmSJpk6dqnfffVePPPKIxo8fP6h1ffOb39T69et1xRVXqKWlRXvuuaeeeeYZ1dXVSZJWrVolq7VrjMb++++vOXPm6Gc/+5kuu+wy7bzzznrsscc0ceLEQa17sHypcXcl15e7CgAAAJRbRzrbsxspUuhG6hxrF4+klIimlIhllE50SJIMj6M42s4pd6DQkRSo8WjUlyrlr/IoVOdVZaNfVaP88lW4eu5GcvYMlCwWgiQAAAAA2BYWs3PJ0Re0zz776M0331QsFhtye4Ruu+02XXTRRfr444/V0NCgN954Q1OmTFEmk1FlZaXC4bDy+bz+3//7fzrmmGPKXe6AicViCgaDikajCgQC5S7nMz187+PyphtV0zz0awUAAMDnZ5qmMslsz/1IkeJ+pNZCsBSPpJSIFUbbdaRyslgkl68QIrk69yMVO5J8IZf8lW6FGvyqGuVTVaNfLp9zi6Pt7A5bub8FAAAAADAkDFRu0OdOpxNOOEFLly7VI488olNOOaU/auo3Z599tr7xjW+UurD22GMPLViwQNddd50++OADTZ48Weeff74OP/zwMleK7lrb1svrbCx3GQAAAPgcTNNUKt7RLUhKKV7cjdTemiwGS4XRdsm2tLIdeVltllKQ5O4Mkipcqh9XIV+FW8Eaj0INPlU1+hSq98vwOLa4H8lmt352kQAAAACAQdHnTqdsNqvp06dr2bJlmjt3rg477LD+qg39ZLh1Ot196/0aZZ9IpxMAAECZ5HN5JdsypT1I7ZFUIUBqTaqtONYuHk2XgqR8zpTNYS2ESN33IwVdhY6kSpeCtV5V1vtU1RRQqM4rh6tbgOS0FTqRimGSzUaQBAAAAAADach2Ov3iF7/QtGnT9NZbb+nII4/UbrvtpgMOOEC1tbWy2bY8vuKKK67o66U/k81m0wknnKCHHnpowK+F/rPnuP21blWs3GUAAACMKLlsvhQglbqSwim1tSYVDxc7kqKF0XaptoxMU3IYtq79SH5DnmDhq2Z0QL6QS6FarypG+VXTFJC/yi2ny95rN5LdaZPVyn4kAAAAABjp+hw6XXnllbJYLOpsmFq2bJnefvvtz3zdYIROgUBAzc3NA34d9K9/vb9YjY7dy10GAADAkJftyPXcj1QMkdrDSbWHU4pH0kp0BknxDkmS023vCpKK+5H8VR417BiSr8qjijqvKht8qm4OyFfh2vJ+JKdNFgtBEgAAAACgS59Dp2nTpg3Zv2zuu+++euONN8pdBrZR3syXuwQAAICyyaSyPfYjtYeLo+3CqWKQVAiRErG0MsmsZJFcHofcfqdcfkMev1PeoKHKBp9G71ojf5VboTqvqhr9qmryy+03SiPt7Jt2JDmsQ/bP9gAAAACAoa/PodPzzz/fD2UMjCuvvFIzZszQgw8+qFNPPbXc5eBzqgrUS6lyVwEAANA/TNNUOt7RYz9SPJxUW2tKbeGk4p0dSbGUErGMspmcLFaL3D6nXD5HYaxdwClv0KXaMUH59qxToNqjUL1PVY1+hRp8cvucvY60cxg22R1bHnkNAAAAAEB/6nPoNJTNnz9fM2bM0BlnnKHbbrtN++yzj+rq6jb715sWi0WXX355marEpkK+aqUJnQAAwBBm5k0l2zPFACmleDRdDJK6dSRFCyFTsi2jXDYvm90ql8/Zc7RdhaFRO1fKF3IrWONRqKEYJNX7ZLg3349kL3Yo2ezWcn8LAAAAAADYjMXsXMY0Almtn+8v4xaLRblcboCrKZ9YLKZgMKhoNKpAIFDucj7Tnbf8Ts3OPVXTPPRrBQAAI0c+l1ciVuxEiqQL4+1ak2oLp9QeThZH3hXG2iXbMjLzpuxOW1eI5HfKEzTkDbrkrXDJV+lSqNarigafqhsDCtZ65HR1diBtvifJamWsHQAAAABgcAxUbjCiO52ee+65cpcAAACAMsp15ApdSMX9SPFISm2tXTuS4tGuICkVz0im5HTZ5SoGSZ6AU56AIV+FS3U7VMhf6VawzquqUX5VN/nlr3RvMUSyO20ESQAAAACA7cqIDp2mT59e7hLwBezYsJsyG8tdBQAAGKo60tmuTqRISu2RpNp7CZKSbRmlEx2SJMPj6DbWzilv0FBFnUdNu1TKX+VRRZ1XlY1+VY3yy1fh6gqONg2SHNbNRjUDAAAAAICCER06YXiKxjfKrfpylwEAAAaJaZrKJLOlTqRCoFTcj9Ta1aEUj6aVjKXVkc7JYlFhP5LPKZe/0I3kDRqqbg5oh91rFSgGSVWNflU1+uXyOXvtRnIYNtkdtnJ/CwAAAAAAGBFGVOj0ta99Tddcc4322WefbX5tPB7XbbfdJr/fr3PPPXcAqsPntSHWomYnoRMAAMOZaZpKtWfUXuxIikdSiodThSApnFR7OK14NKVENK1EW1q5jrysNkspRCqMtjPkrXCpflyFfBVuBWs8CjX4VNXkV2WDX05X7yGSw7DLZv98uz0BAAAAAED/GVGh0/r16zVlyhRNmzZNp556qo499lgFg8Gtvuall17SH/7wB82bN0/JZFIPPPDAIFWLLWFkDQAAQ1M+l1eyLdOtIyml9taU2sLFsXbFbqRENK1kW1r5nCmbw1oYaed3yu03ikGSoabxPvkqXQrWelVZ71NVU0ChOq8cWwiS7E6bbDaCJAAAAAAAhjKLaZpmuYvoTw888ICuuuoqrVy5UlarVbvssosmT56suro6VVRUKJVKqbW1VStWrNCrr76qtrY22Ww2nXDCCbr22ms1evTocn+EfheLxRQMBhWNRhUIBMpdzmf651P/1rpVMdU0D/1aAQAY7nLZvBLRdI8gqa24G6k9nOwKkmJppdoyMk3JYdi69iP5DXmChY4kX8glX4VLFbUeVYzyq6YpIH+Vu9iRtHmYZHfaZLXyj00AAAAAABhsA5UbjLjQSSqMc3n66ad133336fnnn1dra+tm51itVu2+++465phjdOaZZ6qhoaEMlQ6O4RY63XPb/WqwTSR0AgDgC8pmcmqPpJSIpgrj7XqMtUv1CJLS8Q5JktNt7wqSivuRvMFCkOSvdKui3qfK4mg7X4Vry/uRnDa6lgEAAAAAGOIGKjcYUeP1OlksFh1++OE6/PDDJUnvvvuuPvnkE23cuFFut1s1NTXabbfdPnP0Hsojm8tK7PMGAKCHTLKjx36k9khK7T06ktKKR9NKxtLKpLKSRXJ5nXL7HHL5DHkCTnmDhiobfBq9a438VW6F6n2qavSrqtEnt98oBEdOm+ybdiQ5rARJAAAAAADgM43I0GlTEyZM0IQJE8pdBj6nkK9GypS7CgAABpZpmkrHO7qNtUurPZwsdCRFUoqHC8cSsZQSsYyymZwsVku3/UiFL2/QpdoxFfLt6VKg2lMKkipH+eTyOrfYjWR38C88AAAAAABA/9ouQicMLzXBUUquL3cVAABsOzNvKtme6RYapdTebbRdoRsppUQ0rURbWvmsKZvdKle3IMkTKIy1G7VzpfwhtwK1HlU2+FQ5yq9QvU+Ge/P9SPbirzabtdzfAgAAAAAAsB0jdMKQ8+/Vb6jZuWe5ywAAQJKUz+WViKWLHUmF8XbtrcWOpE32IyXbMjLzpuxOW9d+JL9TnuJ+pOqmgHwhlypqvapo8Km6KaBgjUdOl1125+YdSQ7DLquVsXYAAAAAAGB4IHQCAADbnWxHrhQgxaOF4KittWtHUufxRCytVLxDMiWn217oSPI55QkUgiRfyKX6cRXyhdyqqPeqapRfVY1++SvdvQZIhWDJxn4kAAAAAAAwIhE6YcgZWz9B2dZyVwEAGG460tmuwCiSVnukECCVgqRo546ktDLJrGSRXB7HJkGSSxV1HjWNr5K/snuQFJA3aHSFR5uGSQ4rQRIAAAAAANjuEToNopUrV+qaa67Rs88+q5aWFo0aNUrf+ta39NOf/lROp3OLr5sxY4YWLlzY49h3v/td3XXXXQNdclnEUzEZqil3GQCAMjNNU5lktjjWbtP9SF3H4tG0krG0OtI5WawWubyOrtF2AUPeoKHq5oB22L1WgSq3QvV+VTb6VNUYkMvr6LUjyWHYZHfYyv0tAAAAAAAAGFYInQbR8uXLlc/n9dvf/lY77bSTli1bprPOOkvxeFw33njjVl971lln6eqrry499ng8A11u2ayLrFazk9AJAEYi0zSVas+ovXO0XSSl9nBSbRuThXApXNyPFE0r0ZZWriMvq80it88pl98pt9+QJ+CUN+hS/Y4h+YIuBWs8CjX4VNXkV2WDX4bb3utIO4dhl81uLfe3AAAAAAAAYMQidBpEX/va1/S1r32t9HjcuHFasWKF7rzzzs8MnTwej+rr6we6RAAAtlk+l1cilintRopHUmpvTaktXOxIKo62KwRJGZl5UzaHtdCJ1BkkBQsdSaEJ1fKH3ArWehSq96mqKaBQnVcO1ybdSE6b7MXf22wESQAAAAAAAEPBiAudbrnlFi1evFhz586Vw+GQVOgwuu+++9TS0qKGhgYdcMABmjVr1lZH2g2WaDSqysrKzzzvoYce0h/+8AfV19fryCOP1OWXX77Fbqd0Oq10Ol16HIvF+q3ewbDXjgdq/cft5S4DALZruWy+W4iU7upIai382nk8EUsr1Z6RaUoOl60QJPm6BUkVLtWOCcpX4VJFnVeVo/yqbgrIV+mS02XvdbSd3WmT1cp+JAAAAAAAgOFmxIVOv//979Xc3FwKnN566y3tt99+SqVSpXMsFovq6up0ww036OSTTy5XqfrPf/6j22677TO7nE466SSNGTNGo0aN0ptvvqmf/OQnWrFihf70pz/1ev7s2bN11VVXDUTJg2LZR6+ozjqh3GUAwIiTzeR67EeKR1KlEKm0IylaCJLS8Q5JkuFxdBtt55Q3aChQ5dGonSrlr3Srot6nquJ+JF+Fq7ALydnLfiSnTRYLQRIAAAAAAMBIZjFN0yx3Ef3J6/XqnHPOKQU506dPVyKR0KWXXqrKykp98MEHeuKJJ/TMM8+oo6ND1157rS699NI+XfOSSy7R9ddfv9Vz3n33XY0fP770ePXq1Zo+fbpmzJih3/3ud9t0vWeffVYzZ87Uf/7zH+24446bPd9bp1Nzc7Oi0agCgcA2Xasc7rzld2p27qma5qFfKwCUWybZsfl+pHBK7a2FICkRTSseTSsZSyuTyspikQxvcaydzylPwClP0CVv0JAv5Ja/yl0Ya9foV1WjT26/UQiOnDbZN+1IclgJkgAAAAAAAIahWCymYDDY77nBiOt0yuVy8nq9kqTW1latXLlSb775poLBoCRpxowZ+va3v61ly5bppJNO0uWXX66vfvWr2m+//b7wNX/wgx/o9NNP3+o548aNK/3+008/1Ve+8hXtv//+uvvuu7f5ep21bil0MgxDhmFs8/sOFUFvpdRR7ioAoDxM01Qq3tGtGyldHGvXtR+pvbgfKdmWUTaTk8Vq6bYfySl3oLAfqW5shXwVLgWqPaps8KlylF+Vo3xyeZ2bjbTr/NVmZz8SAAAAAAAAvpgRFzrV1NQoEolIkhYuXKgjjjiiFDh1N3HiRC1YsEC77rqrbrrpJj388MN9umZNTc3nOnf16tX6yle+osmTJ+u+++6T1brtP9x7/fXXJUkNDQ3b/NrhoKFyjOJrR1QDHoDtnJk3lWhLb7IfKVUMkpKlY/FYWsm2tPJZUza7Va5uQZInYMgbdKlxlyr5Qi4Fa70K1XlV2ehXqN4nw735fiR78VebjSAJAAAAAAAAA2/EhU6TJk3S3//+d+VyOc2dO1fTpk3b4rk1NTX67//+bz322GODUtvq1as1Y8YMjRkzRjfeeKPWr19feq6+vr50zsyZM/Xggw9q33331fvvv685c+bosMMOU1VVld58801ddNFFmjZtmnbfffdBqXuwLf/4X2p27lnuMgBgq/K5vOLRbmPtIoUupM6OpPZIoRspEU0r2Z6RmTflMGyFIKkYJnmChSCpuikgX8ililqvQqP8qm7yK1DtkdNl73U/ksOwy2plrB0AAAAAAACGlhEXOp1xxhn65je/qfr6erW2tmrFihU644wzSiP3NhUIBBQOhweltvnz5+s///mP/vOf/6ipqanHc52rtTo6OrRixQolEglJktPp1N///nfdcsstisfjam5u1nHHHaef/exng1IzAGxPsh25rq6j4ldba2G0Xel4NKVELK1UvEMyJafb3m20nSFP0ClfyKX6cRXyhdyqqPeqapRfVU0B+UOuXgOkQrBkYz8SAAAAAAAAhjWL2Zl2jCA33nijbrzxRtXU1OjWW2/Vj3/8Y91+++3ad999e5yXSqW05557KpFIaNWqVWWqduAN1EKwgfL0nEXKRzyqaR76tQIY+jrS2cIupM79SJGk2luTamvt6lBKRNNKxNLKJLOSRXJ5HKXRdp6AIU/QkK/CJW+FS/4qt0J1PlWO8qm6KSBPwOgKjzYNkxxWgiQAAAAAAAAMOQOVG4y4TidJ+uEPf6gf/vCHpcfnnXee9t9/f33pS1/SwQcfrLq6OoXDYT3++OP64IMP9L3vfa+M1WJT6Y6kHPKUuwwAQ5RpmkonOnqOtus+1i6cVCKSVjxa2I/Ukc7JYrXI5XUUupF8TrkDhrxBQ9WjA9phj1oFqjwK1ftU2ehX1Si/XF5Hrx1JDsMmu8NW7m8BAAAAAAAAMCSNyNBpU6eddpqampp0/vnn6ze/+U2P56ZOnaprr722TJWhNy3hj9XsrCp3GQAGkWmaSrZlusbaRdNqDyfVtrH7fqRUMUjKKNeRl9VmkdvnlKsYJHXuR2rYqVK+CpeCNR6FGvyqavSpssEvw23vdaSdw7DLZreW+1sAAAAAAAAADHvbRegkSTNnztQ777yj1157TW+99ZaSyaQmTJig6dOnl7s0ABiR8rm8ErGuIKk9UuhCKnQjpRQPF/cjRdNKtGVk5k3ZHVa5/YZcPkdxP1IhSBo9yidfyK1gjUeVDT5VNQZVUeeRw7WFbiSnTTYbQRIAAAAAAAAwmLab0KnTXnvtpb322qvcZWAr9hy3vzZ+kih3GQB6kcvmFY927UeKR1Jq25hUWzjZ1aUUKexHSrVnZJqSw2WT22cURtsVdyR5K1yqHROUL+RSRZ1XVaP8qmoMyF/l7jVE6uxMslrZjwQAAAAAAAAMVdtd6IShb/knr6tGXyp3GcB2I5vJqb0UGBW6kNpbk2rr7EiKpJSIFYKkdLxDkmR4HKXRdp5AYbRdoNqjUTtVyl/lVkWdT1WNPlU1BuSrcHUFR4ZNDmfPjiSLhSAJAAAAAAAAGAkInTDkpDIJyVnuKoDhyzRNZZLZ0m6keHGsXVsxTNo0SOpI5WSxSIa30Ink9hW7kYKGqhv9GjOxRoFSkFTYkeT2G712IzkMm+wOW7m/BQAAAAAAAADKgNAJQ47fXSHlyl0FMLSYpqlUvKPHfqR4OKW2zhApnFJ7cT9SMpZWtiMvi9VSCpFcPmdxP5KhurEV8ofc8le5FWooBEmVDT65vM4tjraz2dmPBAAAAAAAAGDrCJ0w5DTX7KS2lmy5ywAGnJk3lWhLl/YgtUeKnUityeK4u+JzsbSSbWnls6Zsdqvc/kKIVNqPFHSpcZcq+UIuBWu9qqz3qqopoFC9Tw5XtwDJaSt0IhXDJJuNIAkAAAAAAABA/yF0wpDzzqpX1ezcs9xlAF9IPpcvjbTr7Ehqby10JMXDSbVHCiPtEtG0ku0ZmXlTDsNWCpHcfkPegCFP0FB1U0C+kEsVtV6FRvlV3eRXoNojp8u++Ui74p4kq5X9SAAAAAAAAADKg9AJAD5DtiPX1XVU/GprTXaNtouklYimlGhLKxXvkEzJ6bbL3T1IChryVXlUv2NIvpBbFfVeVY3yq6opIH/I1etIO3uxM8liIUgCAAAAAAAAMPQROmHIaa7ZUYqWuwqMdJlUtluIlFZ7pDDWrq21q0MpES10JWWSWckiuTyOHmPtPEFDlQ0+NU+oVqDarVCdT6EGv2qa/XIHjOI4O7vsm4ZJDitBEgAAAAAAAIARh9AJQ04un5Ot3EVg2DFNU+lER6kjqT2SUjycVFs4pfZwSu3hpBKRtOLFICmbyclitcjlcxQ6knxOuQOFjqTq0QHtsEetAlUehRp8qmoMqLLBJ7fPWeo+2my8nYP/agEAAAAAAABs3widMOR8unElO50gSTLzppLtmW77kdKFIKk41q7QjZRSPJpWMpZRLpuX1WYpjLTzGYVfA055gy417FQpX4VLwRqPQg1+VTcWupIMt73XkXYOwy6b3VrubwEAAAAAAAAADBuETgAGVT6XVyLWPUgqdCG1t6bUFk6Wxt0lY2kl2jIy86bsDqtc/mKI5HPKGzTkrXCpcpRPvpBbwVqPKhv8qhoVUEWdRw7X5vuROn+1WhlrBwAAAAAAAAADgdAJQ86kHfZT5NN0ucvANsh15BSPphWPpkrj7do2JgshUrhrP1I8mlYqnpFMyeGydXUj+Z3yFIOkuh0q5Au5VFHnVWWDX9VNAfmr3FsMkexOG0ESAAAAAAAAAAwBhE4Yct5f87aqtFO5y9judaSzPfcjRVJqb+3akRQvjrZLxDJKJzokSYansB/J5XfKEygESYEaj0Z9qVL+So9C9V5VjirsSPJVuLqCI8Mmh7PbfiSnTRYLQRIAAAAAAAAADCeEThhyEul2VTnLXcXIY5qmMslsz/1IkW77kTqDpFhaiVhaHamcLBbJ5SuMtHP5nPIEDHmDhqobfRozsUaBKrdC9T5VjfKrqskvl8+5xW4ku8NKkAQAAAAAAAAAIxihE4Ycr8sv5ctdxfBgmqZS8Y4e+5Hixd1I7a3JQrAULYy2S8bSynbkZbVZSkGS218IkjxBQ3VjK+QPuRWoKXQkVTX6Far3yeV1bnG0nc1uLfe3AAAAAAAAAAAwRBA6YcgZV7/rdr3TycybSsS69iO1F8fadY62i0dSikfThSCpLa18zpTNbi3sRvJ1248UdKlpF598IZeCtV5V1ntV1RRUqN4rh6tbgOS0FUbaFcMkm40gCQAAAAAAAACw7QidMOS8tfJlNTv3LHcZ/SqXzSsRLQRIiWiqGCSl1NaaVDzcrSMpllaqLSPTlByGrasjKWDIGzDkqTBU0xyQr9KlihqvQqP8qm7yK1DtkdNl3+JoO6uVsXYAAAAAAAAAgIFF6AR8QdmOXM/9SOGU2sNdO5LikbQSnUFSvEOS5HTbS91Ibn9hP5K/yqOGHUPyVboVavAVdiQ1BeQPuXoNkRyGTXanjf1IAAAAAAAAAIAhhdAJQ05j1ViprTzXzqSyPfYjtYeLo+3Chd937kdKxNLKJLOSRXJ5HHL7nXL5uvYjVTb41DyhWoFqt0J1PlU1+lXZ6JfHb3R1IG3akeSwEiQBAAAAAAAAAIYtQqdBtsMOO+ijjz7qcWz27Nm65JJLtviaVCqlH/zgB5o3b57S6bRmzZqlO+64Q3V1dQNdbllYLP23U8g0TaUTHYpH0qUgKR5Oqq01pbZwUvHOjqRY4SubyclitcjlcxQ7kgx5AoUwqWZMQGNDtQpUeRRq8KmqMaDKBp/cPudmXUil3zts/fZZAAAAAAAAAAAYygidyuDqq6/WWWedVXrs9/u3ev5FF12kv/zlL3r00UcVDAZ13nnn6dhjj9U//vGPgS61LD7Z8P5WdzqZeVPJ9kxhP1JptF3XWLvuHUnJtoxy2bysNkthpJ3PkNtfCJG8FYZG7Vwpb4VLwRqPQg1+VTf6FGrwy3Bvvh/JXvzVZuu/UAwAAAAAAAAAgJGC0KkM/H6/6uvrP9e50WhU9957r+bMmaOvfvWrkqT77rtPEyZM0EsvvaQpU6Zs9pp0Oq10Ol16HIvF+qfwQRCPprR+eVrJ1Gq998pqtbUW9iQVRt4VupGSbRmZeVN2h1Uuv1EMk5zyBg15K1yqHOWXr9KlYI1XlfVeVTUGVVHnkcPV+34kh2GX1cpYOwAAAAAAAAAA+oLQqQx++ctf6pprrtHo0aN10kkn6aKLLpLd3vv/FEuXLlVHR4cOPvjg0rHx48dr9OjRWrJkSa+h0+zZs3XVVVcNWP0Dac37Yb33RFpO4yO5i/uRvBUu1e1QIX+lW8E6r6pG+VXd6Je/yr15N1JxtJ3daSNIAgAAAAAAAABgEBE6DbLvf//72muvvVRZWakXX3xRl156qdasWaObbrqp1/NbWlrkdDpVUVHR43hdXZ1aWlp6fc2ll16qiy++uPQ4Foupubm53z7DQNpprwadcNcETZ44RXbDJoezZ6BksRAkAQAAAAAAAAAwFBE69YNLLrlE119//VbPeffddzV+/PgeYdDuu+8up9Op7373u5o9e7YMw+iXegzD6Lf3KodYPKL6caFylwEAAAAAAAAAALYBoVM/+MEPfqDTTz99q+eMGzeu1+P77befstmsVq5cqV122WWz5+vr65XJZBSJRHp0O61du/Zz74UaboLBYLlLAAAAAAAAAAAA24jQqR/U1NSopqbmC7329ddfl9VqVW1tba/PT548WQ6HQwsWLNBxxx0nSVqxYoVWrVqlqVOnfuGah7KZM2eWuwQAAAAAAAAAALCNrOUuYHuyZMkS3XLLLXrjjTf0wQcf6KGHHtJFF12kb33rWwqFCuPkVq9erfHjx+uf//ynpELXz//8z//o4osv1nPPPaelS5fqjDPO0NSpUzVlypRyfpwB86c//ancJQAAAAAAAAAAgG1Ep9MgMgxD8+bN05VXXql0Oq2xY8fqoosu6rHnqaOjQytWrFAikSgdu/nmm2W1WnXccccpnU5r1qxZuuOOOz73dU3TlCTFYrH++zADKJFIDJtaAQAAAAAAAAAYbjp/Bt+ZH/QXi9nf74gh55NPPlFzc3O5ywAAAAAAAAAAAEPIxx9/rKampn57P0Kn7UA+n9enn34qv98vi8VS7nK2KhaLqbm5WR9//LECgUC5ywGwHeH+A6AcuPcAKAfuPQDKhfsPgHLg3tM70zTV1tamUaNGyWrtv01MjNfbDlit1n5NKgdDIBDgBgCgLLj/ACgH7j0AyoF7D4By4f4DoBy492wuGAz2+3v2X3wFAAAAAAAAAACA7RahEwAAAAAAAAAAAPqM0AlDimEY+vnPfy7DMMpdCoDtDPcfAOXAvQdAOXDvAVAu3H8AlAP3nsFlMU3TLHcRAAAAAAAAAAAAGN7odAIAAAAAAAAAAECfEToBAAAAAAAAAACgzwidAAAAAAAAAAAA0GeETgAAAAAAAAAAAOgzQicMKbfffrt22GEHuVwu7bfffvrnP/9Z7pIADGOzZ8/WPvvsI7/fr9raWh199NFasWJFj3NSqZTOPfdcVVVVyefz6bjjjtPatWt7nLNq1Sodfvjh8ng8qq2t1Y9+9CNls9nB/CgAhrFf/vKXslgsuvDCC0vHuPcAGAirV6/Wt771LVVVVcntdmvSpEl69dVXS8+bpqkrrrhCDQ0NcrvdOvjgg/Xee+/1eI/W1ladfPLJCgQCqqio0P/8z/+ovb19sD8KgGEil8vp8ssv19ixY+V2u7XjjjvqmmuukWmapXO49wDoD4sWLdKRRx6pUaNGyWKx6LHHHuvxfH/da958800ddNBBcrlcam5u1q9+9auB/mgjDqEThoyHH35YF198sX7+85/rtdde0x577KFZs2Zp3bp15S4NwDC1cOFCnXvuuXrppZc0f/58dXR06JBDDlE8Hi+dc9FFF+nJJ5/Uo48+qoULF+rTTz/VscceW3o+l8vp8MMPVyaT0YsvvqgHHnhA999/v6644opyfCQAw8wrr7yi3/72t9p99917HOfeA6C/hcNhHXDAAXI4HPrrX/+qd955R7/+9a8VCoVK5/zqV7/Srbfeqrvuuksvv/yyvF6vZs2apVQqVTrn5JNP1ttvv6358+frqaee0qJFi/Sd73ynHB8JwDBw/fXX684779RvfvMbvfvuu7r++uv1q1/9SrfddlvpHO49APpDPB7XHnvsodtvv73X5/vjXhOLxXTIIYdozJgxWrp0qW644QZdeeWVuvvuuwf8840oJjBE7Lvvvua5555bepzL5cxRo0aZs2fPLmNVAEaSdevWmZLMhQsXmqZpmpFIxHQ4HOajjz5aOufdd981JZlLliwxTdM0n376adNqtZotLS2lc+68804zEAiY6XR6cD8AgGGlra3N3Hnnnc358+eb06dPNy+44ALTNLn3ABgYP/nJT8wDDzxwi8/n83mzvr7evOGGG0rHIpGIaRiGOXfuXNM0TfOdd94xJZmvvPJK6Zy//vWvpsViMVevXj1wxQMYtg4//HDz29/+do9jxx57rHnyySebpsm9B8DAkGT++c9/Lj3ur3vNHXfcYYZCoR5/5/rJT35i7rLLLgP8iUYWOp0wJGQyGS1dulQHH3xw6ZjVatXBBx+sJUuWlLEyACNJNBqVJFVWVkqSli5dqo6Ojh73nvHjx2v06NGle8+SJUs0adIk1dXVlc6ZNWuWYrGY3n777UGsHsBwc+655+rwww/vcY+RuPcAGBhPPPGE9t57bx1//PGqra3Vl7/8Zd1zzz2l5z/88EO1tLT0uPcEg0Htt99+Pe49FRUV2nvvvUvnHHzwwbJarXr55ZcH78MAGDb2339/LViwQP/+978lSW+88YYWL16sQw89VBL3HgCDo7/uNUuWLNG0adPkdDpL58yaNUsrVqxQOBwepE8z/NnLXQAgSRs2bFAul+vxgxVJqqur0/Lly8tUFYCRJJ/P68ILL9QBBxygiRMnSpJaWlrkdDpVUVHR49y6ujq1tLSUzunt3tT5HAD0Zt68eXrttdf0yiuvbPYc9x4AA+GDDz7QnXfeqYsvvliXXXaZXnnlFX3/+9+X0+nUaaedVrp39HZv6X7vqa2t7fG83W5XZWUl9x4AvbrkkksUi8U0fvx42Ww25XI5XXfddTr55JMliXsPgEHRX/ealpYWjR07drP36Hyu+9hibBmhEwBgu3Duuedq2bJlWrx4cblLATDCffzxx7rgggs0f/58uVyucpcDYDuRz+e199576xe/+IUk6ctf/rKWLVumu+66S6eddlqZqwMwUj3yyCN66KGHNGfOHO222256/fXXdeGFF2rUqFHcewBgO8V4PQwJ1dXVstlsWrt2bY/ja9euVX19fZmqAjBSnHfeeXrqqaf03HPPqampqXS8vr5emUxGkUikx/nd7z319fW93ps6nwOATS1dulTr1q3TXnvtJbvdLrvdroULF+rWW2+V3W5XXV0d9x4A/a6hoUG77rprj2MTJkzQqlWrJHXdO7b2d676+nqtW7eux/PZbFatra3cewD06kc/+pEuueQSnXDCCZo0aZJOOeUUXXTRRZo9e7Yk7j0ABkd/3Wv4e1j/IHTCkOB0OjV58mQtWLCgdCyfz2vBggWaOnVqGSsDMJyZpqnzzjtPf/7zn/Xss89u1iI9efJkORyOHveeFStWaNWqVaV7z9SpU/XWW2/1+IPJ/PnzFQgENvvBDgBI0syZM/XWW2/p9ddfL33tvffeOvnkk0u/594DoL8dcMABWrFiRY9j//73vzVmzBhJ0tixY1VfX9/j3hOLxfTyyy/3uPdEIhEtXbq0dM6zzz6rfD6v/fbbbxA+BYDhJpFIyGrt+eNFm82mfD4viXsPgMHRX/eaqVOnatGiRero6CidM3/+fO2yyy6M1tsWJjBEzJs3zzQMw7z//vvNd955x/zOd75jVlRUmC0tLeUuDcAwdc4555jBYNB8/vnnzTVr1pS+EolE6Zyzzz7bHD16tPnss8+ar776qjl16lRz6tSppeez2aw5ceJE85BDDjFff/1185lnnjFramrMSy+9tBwfCcAwNX36dPOCCy4oPebeA6C//fOf/zTtdrt53XXXme+995750EMPmR6Px/zDH/5QOueXv/ylWVFRYT7++OPmm2++aX796183x44dayaTydI5X/va18wvf/nL5ssvv2wuXrzY3Hnnnc0TTzyxHB8JwDBw2mmnmY2NjeZTTz1lfvjhh+af/vQns7q62vzxj39cOod7D4D+0NbWZv7rX/8y//Wvf5mSzJtuusn817/+ZX700UemafbPvSYSiZh1dXXmKaecYi5btsycN2+e6fF4zN/+9reD/nmHM0InDCm33XabOXr0aNPpdJr77ruv+dJLL5W7JADDmKRev+67777SOclk0vze975nhkIh0+PxmMccc4y5Zs2aHu+zcuVK89BDDzXdbrdZXV1t/uAHPzA7OjoG+dMAGM42DZ249wAYCE8++aQ5ceJE0zAMc/z48ebdd9/d4/l8Pm9efvnlZl1dnWkYhjlz5kxzxYoVPc7ZuHGjeeKJJ5o+n88MBALmGWecYba1tQ3mxwAwjMRiMfOCCy4wR48ebbpcLnPcuHHmT3/6UzOdTpfO4d4DoD8899xzvf6M57TTTjNNs//uNW+88YZ54IEHmoZhmI2NjeYvf/nLwfqII4bFNE2zPD1WAAAAAAAAAAAAGCnY6QQAAAAAAAAAAIA+I3QCAAAAAAAAAABAnxE6AQAAAAAAAAAAoM8InQAAAAAAAAAAANBnhE4AAAAAAAAAAADoM0InAAAAAAAAAAAA9BmhEwAAAAAAAAAAAPqM0AkAAAAAAAAAAAB9RugEAAAAACPMYYcdprPOOmtA3nvjxo3yer16+umnB+T9AQAAAAxfFtM0zXIXAQAAAADY3Isvvqi//e1vuvDCC1VRUfG5XvOPf/xD06dP1/Lly7XTTjsNSF0XXHCBFi9erKVLlw7I+wMAAAAYnuh0AgAAAIAh6sUXX9RVV12lSCTyuV9zww03aObMmQMWOEnS2Wefrddee03PPvvsgF0DAAAAwPBD6AQAAAAAI8S6dev0l7/8Rd/4xjcG9DoTJkzQxIkTdf/99w/odQAAAAAML4ROAAAAADAEXXnllfrRj34kSRo7dqwsFossFotWrly5xdf85S9/UTab1cEHH9zj+P333y+LxaJ//OMfuvjii1VTUyOv16tjjjlG69ev73Huq6++qlmzZqm6ulput1tjx47Vt7/97c2u9V//9V968sknxcR2AAAAAJ3s5S4AAAAAALC5Y489Vv/+9781d+5c3XzzzaqurpYk1dTUbPE1L774oqqqqjRmzJhenz///PMVCoX085//XCtXrtQtt9yi8847Tw8//LCkQqfUIYccopqaGl1yySWqqKjQypUr9ac//Wmz95o8ebJuvvlmvf3225o4cWI/fGIAAAAAwx2hEwAAAAAMQbvvvrv22msvzZ07V0cffbR22GGHz3zN8uXLt3peVVWV/va3v8lisUiS8vm8br31VkWjUQWDQb344osKh8P629/+pr333rv0umuvvXaz9xo3bpwk6Z133iF0AgAAACCJ8XoAAAAAMGJs3LhRoVBoi89/5zvfKQVOknTQQQcpl8vpo48+kiRVVFRIkp566il1dHRs9Vqd19mwYUMfqwYAAAAwUhA6AQAAAMAIsrUdS6NHj+7xuDM4CofDkqTp06fruOOO01VXXaXq6mp9/etf13333ad0Or3F63QPsQAAAABs3widAAAAAGCEqKqqKgVIvbHZbL0e7x4g/fGPf9SSJUt03nnnafXq1fr2t7+tyZMnq729vcdrOq/TuWsKAAAAAAidAAAAAGCI2tYuovHjx+vDDz/s83WnTJmi6667Tq+++qoeeughvf3225o3b16PczqvM2HChD5fDwAAAMDIQOgEAAAAAEOU1+uVJEUikc91/tSpUxUOh/XBBx98oeuFw+HNxvPtueeekrTZiL2lS5cqGAxqt912+0LXAgAAADDy2MtdAAAAAACgd5MnT5Yk/fSnP9UJJ5wgh8OhI488shRGberwww+X3W7X3//+d33nO9/Z5us98MADuuOOO3TMMcdoxx13VFtbm+655x4FAgEddthhPc6dP3++jjzySHY6AQAAACghdAIAAACAIWqfffbRNddco7vuukvPPPOM8vm8Pvzwwy2GTnV1dTrssMP0yCOPfKHQafr06frnP/+pefPmae3atQoGg9p333310EMPaezYsaXzli9frmXLlumWW275oh8NAAAAwAhkMTednQAAAAAAGLZeeOEFzZgxQ8uXL9fOO+88INe48MILtWjRIi1dupROJwAAAAAlhE4AAAAAMMIceuihampq0j333NPv771x40aNGTNGjzzyyGYj9wAAAABs3widAAAAAAAAAAAA0GfWchcAAAAAAAAAAACA4Y/QCQAAAAAAAAAAAH1G6AQAAAAAAAAAAIA+I3QCAAAAAAAAAABAnxE6AQAAAAAAAAAAoM8InQAAAAAAAAAAANBnhE4AAAAAAAAAAADoM0InAAAAAAAAAAAA9BmhEwAAAAAAAAAAAPqM0AkAAAAAAAAAAAB99v8BxnBRMMkPEOIAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "reg = pulser.Register({\"q0\": (-5, 0), \"q1\": (5, 0)})\n", + "\n", + "seq = pulser.Sequence(reg, pulser.MockDevice)\n", + "seq.declare_channel(\"rydberg_global\", \"rydberg_global\")\n", + "t = seq.declare_variable(\"t\", dtype=int)\n", + "\n", + "amp_wf = pulser.BlackmanWaveform(t, np.pi)\n", + "det_wf = pulser.RampWaveform(t, -5, 5)\n", + "seq.add(pulser.Pulse(amp_wf, det_wf, 0), \"rydberg_global\")\n", + "\n", + "# We build with t=1000 so that we can draw it\n", + "seq.build(t=1000).draw()" + ] + }, + { + "cell_type": "markdown", + "id": "deb625b6", + "metadata": {}, + "source": [ + "## 3. Starting the backend" + ] + }, + { + "cell_type": "markdown", + "id": "953eab2e", + "metadata": {}, + "source": [ + "It is now time to select and initialize the backend. Currently, these are the available backends (but bear in mind that the list may grow in the future):\n", + "\n", + " - **Local**: \n", + " - `QutipBackend` (from `pulser_simulation`): Uses `QutipEmulator` to emulate the sequence execution locally.\n", + " - **Remote**:\n", + " - `QPUBackend` (from `pulser`): Executes on a QPU through a remote connection.\n", + " - `EmuFreeBackend` (from `pulser_pasqal`): Emulates the sequence execution using free Hamiltonian time evolution (similar to `QutipBackend`, but runs remotely). \n", + " - `EmuTNBackend` (from `pulser_pasqal`): Emulates the sequence execution using a tensor network simulator." + ] + }, + { + "cell_type": "markdown", + "id": "438c3cca", + "metadata": {}, + "source": [ + "If the appropriate packages are installed, all backends should be available via the `pulser.backends` module so we don't need to explicitly import them." + ] + }, + { + "cell_type": "markdown", + "id": "365ed331", + "metadata": {}, + "source": [ + "Upon creation, all backends require the sequence they will execute. Emulator backends also accept, optionally, a configuration given as an instance of the `EmulatorConfig` class. This class allows for setting all the parameters available in `QutipEmulator` and is forward looking, meaning that it envisions that these options will at some point be availabe on other emulator backends. This also means that trying to change parameters in the configuration of a backend that does not support them yet will raise an error.\n", + "\n", + "Even so, `EmulatorConfig` also has a dedicated `backend_options` for options specific to each backend, which are detailed in the [backends' docstrings](../apidoc/backend.rst)." + ] + }, + { + "cell_type": "markdown", + "id": "21f506c5", + "metadata": {}, + "source": [ + "With `QutipBackend`, we have free reign over the configuration. In this example, we will:\n", + " \n", + "- Change the `sampling_rate`\n", + "- Include measurement errors using a custom `NoiseModel`\n", + "\n", + "On the other hand, `QutipBackend` does not support parametrized sequences. Since it is running locally, they can always be built externally before being given to the backend. Therefore, we will build the sequence (with `t=2000`) before we give it to the backend." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6f64a5af", + "metadata": {}, + "outputs": [], + "source": [ + "config = pulser.EmulatorConfig(\n", + " sampling_rate=0.1,\n", + " noise_model=pulser.NoiseModel(\n", + " noise_types=(\"SPAM\",),\n", + " p_false_pos=0.01,\n", + " p_false_neg=0.004,\n", + " state_prep_error=0.0,\n", + " ),\n", + ")\n", + "\n", + "qutip_bknd = pulser.backends.QutipBackend(seq.build(t=2000), config=config)" + ] + }, + { + "cell_type": "markdown", + "id": "e74755e3", + "metadata": {}, + "source": [ + "Currently, the remote emulator backends are still quite limited in the number of parameters they allow to be changed. Furthermore, the default configuration of a given backend does not necessarily match that of `EmulatorConfig()`, so it's important to start from the correct default configuration. Here's how to do that for the `EmuTNBackend`:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0889e0ba", + "metadata": {}, + "outputs": [], + "source": [ + "import dataclasses\n", + "\n", + "emu_tn_default = pulser.backends.EmuTNBackend.default_config\n", + "# This will create a new config with a different sampling rate\n", + "# All other parameters remain the same\n", + "emu_tn_config = dataclasses.replace(emu_tn_default, sampling_rate=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "21f4ee21", + "metadata": {}, + "source": [ + "We will stick to the default configuration for `EmuFreeBackend`, but the process to create a custom configuration would be identical. To know which parameters can be changed, consult the backend's docstring." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "59d5e3ca", + "metadata": {}, + "outputs": [], + "source": [ + "free_bknd = pulser.backends.EmuFreeBackend(seq, connection=connection)\n", + "tn_bknd = pulser.backends.EmuTNBackend(\n", + " seq, connection=connection, config=emu_tn_config\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "50729b54", + "metadata": {}, + "source": [ + "Note also that the remote backends require an open connection upon initialization. This would also be the case for `QPUBackend`." + ] + }, + { + "cell_type": "markdown", + "id": "51cce28c", + "metadata": {}, + "source": [ + "## 4. Executing the Sequence" + ] + }, + { + "cell_type": "markdown", + "id": "f4590ab7", + "metadata": {}, + "source": [ + "Once the backend is created, executing the sequence is always done through the backend's `run()` method.\n", + "\n", + "For the `QutipBackend`, all arguments are optional and are the same as the ones in `QutipEmulator`. On the other hand, remote backends all require `job_params` to be specified. `job_params` are given as a list of dictionaries, each containing the number of runs and the values for the variables of the parametrized sequence (if any). The sequence is then executed with the parameters specified within each entry of `job_params`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "22e8f95b", + "metadata": {}, + "outputs": [], + "source": [ + "# Local execution, returns the same results as QutipEmulator\n", + "qutip_results = qutip_bknd.run()\n", + "\n", + "# Remote execution, requires job_params\n", + "job_params = [\n", + " {\"runs\": 100, \"variables\": {\"t\": 1000}},\n", + " {\"runs\": 50, \"variables\": {\"t\": 2000}},\n", + "]\n", + "free_results = free_bknd.run(job_params=job_params)\n", + "tn_results = tn_bknd.run(job_params=job_params)" + ] + }, + { + "cell_type": "markdown", + "id": "4421eb27", + "metadata": {}, + "source": [ + "## 5. Retrieving the Results" + ] + }, + { + "cell_type": "markdown", + "id": "8289b06f", + "metadata": {}, + "source": [ + "For the `QutipBackend` the results are identical to those of `QutipEmulator`: a sequence of individual `QutipResult` objects, one for each evaluation time. As usual we can, for example, get the final state:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c920679c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/latex": [ + "Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket $ \\\\ \\left(\\begin{matrix}(-0.380-0.157j)\\\\(0.035+0.593j)\\\\(0.035+0.593j)\\\\(-0.235-0.263j)\\\\\\end{matrix}\\right)$" ], - "source": [ - "print(free_results[0].bitstring_counts)\n", - "free_results[0].plot_histogram()" - ] - }, - { - "cell_type": "markdown", - "id": "579c9417", - "metadata": {}, - "source": [ - "The same could be done with the results from `EmuTNBackend` or even from `QPUBackend`, as they all share the same format." - ] - }, - { - "cell_type": "markdown", - "id": "d960fbe6", - "metadata": {}, - "source": [ - "## 6. Alternative user interfaces for using remote backends" - ] - }, - { - "cell_type": "markdown", - "id": "93891a39", - "metadata": {}, - "source": [ - "Once you have created a Pulser sequence, you can also use specialized Python SDKs to send it for execution:\n", - "\n", - "- the [pasqal-cloud](https://github.com/pasqal-io/pasqal-cloud/) Python SDK, developed by PASQAL and used under-the-hood by Pulser's remote backends.\n", - "- Azure's Quantum Development Kit (QDK) which you can use by creating an [Azure Quantum workspace](https://learn.microsoft.com/en-gb/azure/quantum/provider-pasqal) directly integrated with PASQAL emulators and QPU." - ] + "text/plain": [ + "Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket\n", + "Qobj data =\n", + "[[-0.38024334-0.15655643j]\n", + " [ 0.03529034+0.59329597j]\n", + " [ 0.03529034+0.59329597j]\n", + " [-0.23481746-0.2632011j ]]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" + ], + "source": [ + "qutip_results[-1].state" + ] + }, + { + "cell_type": "markdown", + "id": "2618a789", + "metadata": {}, + "source": [ + "For remote backends, the object returned is a `RemoteResults` instance, which uses the connection to fetch the results once they are ready. To check the status of the submission, we can run:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d24593f4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "free_results.get_status()" + ] + }, + { + "cell_type": "markdown", + "id": "763e011c", + "metadata": {}, + "source": [ + "When the submission states shows as `DONE`, the results can be accessed. In this case, they are a sequence of `SampledResult` objects, one for each entry in `job_params` in the same order. For example, we can retrieve the bitstring counts or even plot an histogram with the results:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "738de317", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'00': 4, '01': 19, '10': 22, '11': 55}\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGhCAYAAACd/5VtAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8WgzjOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAik0lEQVR4nO3df1iV9f3H8ReggCggRkIq6+jcJWMWKCTiVtrEcLnSfpLZIDJaS8p21rYsk9R1oWZGXWmYC11lk7Xs96bVKbMlG4X5IxNXKwMjUEo5inVQON8/ujrF16Nxjgdu+Ph8XNe5Ls597nN4030FT+/7PvcJcrvdbgEAABgi2OoBAAAAAom4AQAARiFuAACAUYgbAABgFOIGAAAYhbgBAABGIW4AAIBRiBsAAGCUHlYP0NlaW1tVW1uryMhIBQUFWT0OAABoB7fbrYMHD2rAgAEKDj7xvplTLm5qa2uVkJBg9RgAAMAPNTU1GjRo0AnXOeXiJjIyUtLX/3GioqIsngYAALSH0+lUQkKC5+/4iZxycfPNoaioqCjiBgCAbqY9p5RwQjEAADAKcQMAAIxC3AAAAKMQNwAAwCjEDQAAMApxAwAAjELcAAAAoxA3AADAKMQNAAAwCnEDAACMQtwAAACjEDcAAMAoxA0AADAKcQMAAIxC3AAAAKP0sHoA09huf8nqEU5ZuxdMsnoEAEAXwJ4bAABgFOIGAAAYhbgBAABGIW4AAIBRiBsAAGAU4gYAABiFuAEAAEYhbgAAgFGIGwAAYBTiBgAAGIW4AQAARiFuAACAUYgbAABgFOIGAAAYhbgBAABGIW4AAIBRiBsAAGAU4gYAABiFuAEAAEYhbgAAgFGIGwAAYBTiBgAAGIW4AQAARiFuAACAUYgbAABgFOIGAAAYpUvEzdKlS2Wz2RQeHq709HRVVFQcd91Vq1YpKCiozS08PLwTpwUAAF2Z5XFTVlYmu92uwsJCbd68WcnJycrKytLevXuP+5yoqCh99tlnntsnn3zSiRMDAICuzPK4WbJkifLz85WXl6ekpCSVlJQoIiJCpaWlx31OUFCQ4uPjPbe4uLhOnBgAAHRllsZNc3OzKisrlZmZ6VkWHByszMxMlZeXH/d5hw4d0plnnqmEhARNnjxZO3bs6IxxAQBAN2Bp3DQ0NKilpeWYPS9xcXGqq6vz+pxhw4aptLRUzz33nJ544gm1trZqzJgx2rNnj9f1XS6XnE5nmxsAADCX5YelfJWRkaGcnBylpKRo7NixWrt2rU4//XQtX77c6/pFRUWKjo723BISEjp5YgAA0JksjZvY2FiFhISovr6+zfL6+nrFx8e36zV69uypESNG6MMPP/T6+KxZs9TY2Oi51dTUnPTcAACg67I0bkJDQ5WamiqHw+FZ1traKofDoYyMjHa9RktLi7Zv364zzjjD6+NhYWGKiopqcwMAAObqYfUAdrtdubm5SktL06hRo1RcXKympibl5eVJknJycjRw4EAVFRVJkubNm6fRo0dr6NChOnDggO6991598sknuv766638MQAAQBdhedxkZ2dr3759mjNnjurq6pSSkqJ169Z5TjKurq5WcPC3O5j279+v/Px81dXVKSYmRqmpqdq0aZOSkpKs+hEAAEAXEuR2u91WD9GZnE6noqOj1djY2CGHqGy3vxTw10T77F4wyeoRAAAdxJe/393u3VIAAAAnQtwAAACjEDcAAMAoxA0AADAKcQMAAIxC3AAAAKMQNwAAwCjEDQAAMApxAwAAjELcAAAAoxA3AADAKMQNAAAwCnEDAACMQtwAAACjEDcAAMAoxA0AADAKcQMAAIxC3AAAAKMQNwAAwCjEDQAAMApxAwAAjELcAAAAoxA3AADAKMQNAAAwCnEDAACMQtwAAACjEDcAAMAoxA0AADAKcQMAAIxC3AAAAKMQNwAAwCjEDQAAMApxAwAAjELcAAAAoxA3AADAKMQNAAAwCnEDAACMQtwAAACjEDcAAMAoxA0AADAKcQMAAIxC3AAAAKMQNwAAwCjEDQAAMApxAwAAjELcAAAAoxA3AADAKMQNAAAwCnEDAACMQtwAAACjEDcAAMAoxA0AADAKcQMAAIzSJeJm6dKlstlsCg8PV3p6uioqKtr1vDVr1igoKEhTpkzp2AEBAEC3YXnclJWVyW63q7CwUJs3b1ZycrKysrK0d+/eEz5v9+7duu2223Tuued20qQAAKA7sDxulixZovz8fOXl5SkpKUklJSWKiIhQaWnpcZ/T0tKiadOmae7cuRoyZEgnTgsAALo6S+OmublZlZWVyszM9CwLDg5WZmamysvLj/u8efPmqX///po+fXpnjAkAALqRHlZ+84aGBrW0tCguLq7N8ri4OFVVVXl9zr/+9S89+uij2rJl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uUcFBQUBGw4AAMBX7T4sdemll7a5/+qrr2rQoEFKTk6WJG3dulXNzc0aP358YCcEAADwQbvjJjo6us39yy67rM193goOAAC6gnbHzcqVKztyDgAAgIA4qYv47du3z/NBmcOGDdPpp58ekKEAAAD85dcJxU1NTbruuut0xhln6LzzztN5552nAQMGaPr06Tp8+HCgZwQAAGg3v+LGbrfrjTfe0AsvvKADBw7owIEDeu655/TGG2/od7/7XaBnBAAAaDe/Dks9/fTT+vvf/65x48Z5ll144YXq1auXrrzySi7iBwAALOPXnpvDhw8rLi7umOX9+/fnsBQAALCUX3GTkZGhwsJCffXVV55lX375pebOnev5rCkAAAAr+HVYqri4WBMnTjzmIn7h4eFav359QAcEAADwhV9xc9ZZZ+mDDz7Q6tWrVVVVJUmaOnWqpk2bpl69egV0QAAAAF/4HDdHjhxRYmKiXnzxReXn53fETAAAAH7z+Zybnj17tjnXBgAAoCvx64TiGTNmaOHChTp69Gig5wEAADgpfp1z8/bbb8vhcOjll1/WWWedpd69e7d5fO3atQEZDgAAwFd+xU3fvn2P+VRwAACArsCnuGltbdW9996r//73v2pubtbPf/5z3X333bxDCgAAdBk+nXNzzz336I477lCfPn00cOBAPfjgg5oxY0ZHzQYAAOAzn+Lmscce07Jly7R+/Xo9++yzeuGFF7R69Wq1trZ21HwAAAA+8SluqqurdeGFF3ruZ2ZmKigoSLW1tQEfDAAAwB8+xc3Ro0cVHh7eZlnPnj115MiRgA4FAADgL59OKHa73br22msVFhbmWfbVV1/pxhtvbPN2cN4KDgAArOJT3OTm5h6z7JprrgnYMAAAACfLp7hZuXJlR80BAAAQEH59/AIAAEBXRdwAAACj+PXxC8Cpxnb7S1aPcMravWCS1SMA6GbYcwMAAIxC3AAAAKMQNwAAwCjEDQAAMApxAwAAjELcAAAAoxA3AADAKF0ibpYuXSqbzabw8HClp6eroqLiuOuuWLFC5557rmJiYhQTE6PMzMwTrg8AAE4tlsdNWVmZ7Ha7CgsLtXnzZiUnJysrK0t79+71uv6GDRs0depUvf766yovL1dCQoIuuOACffrpp508OQAA6Iosj5slS5YoPz9feXl5SkpKUklJiSIiIlRaWup1/dWr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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } + ], + "source": [ + "print(free_results[0].bitstring_counts)\n", + "free_results[0].plot_histogram()" + ] + }, + { + "cell_type": "markdown", + "id": "579c9417", + "metadata": {}, + "source": [ + "The same could be done with the results from `EmuTNBackend` or even from `QPUBackend`, as they all share the same format." + ] + }, + { + "cell_type": "markdown", + "id": "d960fbe6", + "metadata": {}, + "source": [ + "## 6. Alternative user interfaces for using remote backends" + ] + }, + { + "cell_type": "markdown", + "id": "93891a39", + "metadata": {}, + "source": [ + "Once you have created a Pulser sequence, you can also use specialized Python SDKs to send it for execution:\n", + "\n", + "- the [pasqal-cloud](https://github.com/pasqal-io/pasqal-cloud/) Python SDK, developed by PASQAL and used under-the-hood by Pulser's remote backends.\n", + "- Azure's Quantum Development Kit (QDK) which you can use by creating an [Azure Quantum workspace](https://learn.microsoft.com/en-gb/azure/quantum/provider-pasqal) directly integrated with PASQAL emulators and QPU." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 5 + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb index c96025b36..071fde63b 100644 --- a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb +++ b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb @@ -178,6 +178,8 @@ "seq.target(\"q1\", \"raman\")\n", "seq.add(short_pulse, \"raman\")\n", "seq.delay(100, \"raman\")\n", + "seq.add(short_pulse, \"raman\")\n", + "seq.delay(0, \"raman\", at_rest=True)\n", "long_pulse = Pulse.ConstantPulse(500, 1, 0, 0)\n", "seq.add(long_pulse, \"raman\")\n", "\n", @@ -198,7 +200,7 @@ "1. Not only the amplitude but also the detuning and phase are modulated, all with the same modulation bandwidth.\n", "2. Alignment between channels takes into account the extended duration of the pulses in the other channels. Note, for instance, how the last pulse on the `rydberg` channel starts only after the output of the `raman` channel goes to zero.\n", "3. Similarly, changing the target in a local channel will also wait for the output to ramp down before starting the retargeting.\n", - "4. For consecutive pulses in the same channel, there is no automatically imposed delay between them to allow one pulse to finish before the next one starts. As such, whenever the interval between two pulses is too short, they will be \"merged\" together, as is illustrated in the `raman` channel." + "4. For consecutive pulses in the same channel, there is no automatically imposed delay between them to allow one pulse to finish before the next one starts. As such, whenever the interval between two pulses is too short, they will be \"merged\" together, as is illustrated in the `raman` channel. To avoid this merging, we can set `at_rest=True` in the `delay()` call. As shown above, when this option is combined with an applied delay time of 0 the channel will be delayed by the exact ammount necessary for the previous pulse to finish." ] }, { @@ -324,7 +326,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/tutorials/classical_simulation/Simulating Sequences with Errors and Noise.ipynb b/tutorials/classical_simulation/Simulating Sequences with Errors and Noise.ipynb index 34cc45333..e76c96f12 100644 --- a/tutorials/classical_simulation/Simulating Sequences with Errors and Noise.ipynb +++ b/tutorials/classical_simulation/Simulating Sequences with Errors and Noise.ipynb @@ -543,7 +543,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now you have the basic knowledge to tackle noise simulations. In the following tutorials, we will see in more detail simulations with [SPAM errors](spam.nblink), [laser noise](laser_noise.nblink) and [effective noise channels](kraus_ops.nblink). Enjoy!" + "Now you have the basic knowledge to tackle noise simulations. In the following tutorials, we will see in more detail simulations with [SPAM errors](spam.nblink), [laser noise](laser_noise.nblink) and [effective noise channels](effective_noise.nblink). Enjoy!" ] } ], diff --git a/tutorials/classical_simulation/Simulating with effective noise channels.ipynb b/tutorials/classical_simulation/Simulating with effective noise channels.ipynb index 91e339e60..64956e573 100644 --- a/tutorials/classical_simulation/Simulating with effective noise channels.ipynb +++ b/tutorials/classical_simulation/Simulating with effective noise channels.ipynb @@ -15591,9 +15591,7 @@ "config_dephasing = SimConfig(\n", " noise=\"eff_noise\",\n", " eff_noise_opers=[qutip.sigmaz()],\n", - " eff_noise_rates=[\n", - " 0.05,\n", - " ],\n", + " eff_noise_rates=[0.05],\n", ")\n", "clean_simu.add_config(config_dephasing)\n", "clean_simu.show_config()"