From 80048f3135f171a26c42bb97443489de43c2f9fb Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Fri, 18 Oct 2024 10:33:03 +0200 Subject: [PATCH 01/15] Bump version to 1.2dev0 --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 9084fa2f7..e61a19bf2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.1.0 +1.2dev0 From b59782620ed029c01278cd7c12237a91c8fb54d1 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Thu, 31 Oct 2024 14:33:02 +0100 Subject: [PATCH 02/15] Bump version to 1.2dev1 --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 524cb5524..1ae500e1f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.1.1 +1.2dev1 From cf998cbca8196ecd0ede5d0de0b3ed0dadb3a3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= <29920212+HGSilveri@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:40:22 +0100 Subject: [PATCH 03/15] Add pyproject.toml to all packages (#758) * Add pyproject.toml to all packages * Improve the publishing workflow --- .github/workflows/publish.yml | 2 ++ pulser-core/pyproject.toml | 3 +++ pulser-pasqal/pyproject.toml | 3 +++ pulser-simulation/pyproject.toml | 3 +++ pyproject.toml | 6 +++++- 5 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 pulser-core/pyproject.toml create mode 100644 pulser-pasqal/pyproject.toml create mode 100644 pulser-simulation/pyproject.toml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7b9eefb7d..8ab3b5357 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,6 +31,8 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ + skip-existing: true # Allows repeating the action + attestations: false # Otherwise an attestation is added to dist/ - name: Install from TestPyPI timeout-minutes: 5 shell: bash diff --git a/pulser-core/pyproject.toml b/pulser-core/pyproject.toml new file mode 100644 index 000000000..feb5d4d4b --- /dev/null +++ b/pulser-core/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/pulser-pasqal/pyproject.toml b/pulser-pasqal/pyproject.toml new file mode 100644 index 000000000..feb5d4d4b --- /dev/null +++ b/pulser-pasqal/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/pulser-simulation/pyproject.toml b/pulser-simulation/pyproject.toml new file mode 100644 index 000000000..feb5d4d4b --- /dev/null +++ b/pulser-simulation/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index edaf2ff0c..a4abb7d55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,4 +13,8 @@ filterwarnings = [ "error", # Except these particular warnings, which are ignored 'ignore:A duration of \d+ ns is not a multiple of:UserWarning', - ] \ No newline at end of file + ] + +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" \ No newline at end of file From e8e4f23fece5550c97b4b2fb91c1243f21552452 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 12:09:04 -0500 Subject: [PATCH 04/15] Fix typo in docstring of draw (#766) * Fix typo in docstring of draw * Fix mypy error --- pulser-core/pulser/sequence/sequence.py | 2 +- pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index be146c0b9..50daadbcc 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -1746,7 +1746,7 @@ def draw( Args: mode: The curves to draw. 'input' - draws only the programmed curves, 'output' the excepted curves + draws only the programmed curves, 'output' the expected curves after modulation. 'input+output' will draw both curves except for channels without a defined modulation bandwidth, in which case only the input is drawn. diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index 5cb8de9c0..ec7320567 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -238,6 +238,7 @@ def _query_job_progress( get_batch_fn = backoff_decorator(self._sdk_connection.get_batch) batch = get_batch_fn(id=batch_id) + assert isinstance(batch.sequence_builder, str) seq_builder = Sequence.from_abstract_repr(batch.sequence_builder) reg = seq_builder.get_register(include_mappable=True) all_qubit_ids = reg.qubit_ids From e27d45afc24f546af01d4ac5f52ccdee7614998a Mon Sep 17 00:00:00 2001 From: RafaelMALima <81188798+RafaelMALima@users.noreply.github.com> Date: Mon, 2 Dec 2024 05:43:08 -0300 Subject: [PATCH 05/15] Reverting __repr__ for Channels to default from dataclass, old __repr__ now on __str__ (#770) --- pulser-core/pulser/channels/base_channel.py | 4 ++-- pulser-core/pulser/channels/channels.py | 6 +++--- pulser-core/pulser/channels/dmm.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pulser-core/pulser/channels/base_channel.py b/pulser-core/pulser/channels/base_channel.py index 456c11577..bc0e4d625 100644 --- a/pulser-core/pulser/channels/base_channel.py +++ b/pulser-core/pulser/channels/base_channel.py @@ -54,7 +54,7 @@ def get_states_from_bases(bases: Collection[str]) -> list[States]: return [state for state in STATES_RANK if state in all_states] -@dataclass(init=True, repr=False, frozen=True) +@dataclass(init=True, frozen=True) class Channel(ABC): """Base class of a hardware channel. @@ -595,7 +595,7 @@ def _eom_buffer_mod_bandwidth(self) -> float: rise_time_us = self._eom_buffer_time / 2 * 1e-3 return MODBW_TO_TR / rise_time_us - def __repr__(self) -> str: + def __str__(self) -> str: config = ( f".{self.addressing}(Max Absolute Detuning: " f"{self.max_abs_detuning}" diff --git a/pulser-core/pulser/channels/channels.py b/pulser-core/pulser/channels/channels.py index 9a95a31e1..fcb9087b4 100644 --- a/pulser-core/pulser/channels/channels.py +++ b/pulser-core/pulser/channels/channels.py @@ -22,7 +22,7 @@ from pulser.channels.eom import RydbergEOM -@dataclass(init=True, repr=False, frozen=True) +@dataclass(init=True, frozen=True) class Raman(Channel): """Raman beam channel. @@ -36,7 +36,7 @@ def basis(self) -> Literal["digital"]: return "digital" -@dataclass(init=True, repr=False, frozen=True) +@dataclass(init=True, frozen=True) class Rydberg(Channel): """Rydberg beam channel. @@ -62,7 +62,7 @@ def basis(self) -> Literal["ground-rydberg"]: return "ground-rydberg" -@dataclass(init=True, repr=False, frozen=True) +@dataclass(init=True, frozen=True) class Microwave(Channel): """Microwave adressing channel. diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 50720d78a..330886556 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -28,7 +28,7 @@ OPTIONAL_ABSTR_DMM_FIELDS = ["total_bottom_detuning"] -@dataclass(init=True, repr=False, frozen=True) +@dataclass(init=True, frozen=True) class DMM(Channel): """Defines a Detuning Map Modulator (DMM) Channel. From 5a58264f7b639440db3cdf7cd48ae5ebaf8672c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= <29920212+HGSilveri@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:27:58 +0100 Subject: [PATCH 06/15] Fixes to the amplitude noise implementation (#768) * Include the beam's propagation direction in Channel * Apply amplitude fluctuations from run to run instead of pulse to pulse * Correct emulation of finite-waist effects on amplitude * Fix handling or laser_waist to/from SimConfig * UTs for the noise * Include propagation_dir in the JSON schema * Fix error in conversion between SimConfig and NoiseModel * Add the pulser version to serialized devices * Missing UT for Channel * Adopting review suggestions --- pulser-core/pulser/channels/base_channel.py | 25 +- pulser-core/pulser/devices/_device_datacls.py | 9 +- .../abstract_repr/schemas/device-schema.json | 224 ++++++++++++++++++ pulser-core/pulser/noise_model.py | 9 +- .../pulser_simulation/hamiltonian.py | 61 +++-- .../pulser_simulation/simconfig.py | 13 +- tests/test_abstract_repr.py | 4 + tests/test_channels.py | 7 +- tests/test_simconfig.py | 12 +- tests/test_simresults.py | 6 +- tests/test_simulation.py | 27 ++- 11 files changed, 367 insertions(+), 30 deletions(-) diff --git a/pulser-core/pulser/channels/base_channel.py b/pulser-core/pulser/channels/base_channel.py index bc0e4d625..54f27bfe0 100644 --- a/pulser-core/pulser/channels/base_channel.py +++ b/pulser-core/pulser/channels/base_channel.py @@ -34,7 +34,7 @@ ChannelType = TypeVar("ChannelType", bound="Channel") -OPTIONAL_ABSTR_CH_FIELDS = ("min_avg_amp",) +OPTIONAL_ABSTR_CH_FIELDS = ("min_avg_amp", "propagation_dir") # States ranked in decreasing order of their associated eigenenergy States = Literal["u", "d", "r", "g", "h", "x"] @@ -78,6 +78,8 @@ class Channel(ABC): min_avg_amp: The minimum average amplitude of a pulse (when not zero). mod_bandwidth: The modulation bandwidth at -3dB (50% reduction), in MHz. + propagation_dir: The propagation direction of the beam associated with + the channel, given as a vector in 3D space. Example: To create a channel targeting the 'ground-rydberg' transition globally, @@ -96,6 +98,7 @@ class Channel(ABC): min_avg_amp: float = 0 mod_bandwidth: Optional[float] = None # MHz eom_config: Optional[BaseEOM] = field(init=False, default=None) + propagation_dir: tuple[float, float, float] | None = None @property def name(self) -> str: @@ -196,7 +199,12 @@ def __post_init__(self) -> None: getattr(self, p) is None ), f"'{p}' must be left as None in a Global channel." else: + assert self.addressing == "Local" parameters += local_only + if self.propagation_dir is not None: + raise NotImplementedError( + "'propagation_dir' must be left as None in Local channels." + ) for param in parameters: value = getattr(self, param) @@ -244,6 +252,18 @@ def __post_init__(self) -> None: "modulation bandwidth." ) + if self.propagation_dir is not None: + dir_vector = np.array(self.propagation_dir, dtype=float) + if dir_vector.size != 3 or np.sum(dir_vector) == 0.0: + raise ValueError( + "'propagation_dir' must be given as a non-zero 3D vector;" + f" got {self.propagation_dir} instead." + ) + # Make sure it's stored as a tuple + object.__setattr__( + self, "propagation_dir", tuple(self.propagation_dir) + ) + @property def rise_time(self) -> int: """The rise time (in ns). @@ -340,6 +360,7 @@ def Global( cls: Type[ChannelType], max_abs_detuning: Optional[float], max_amp: Optional[float], + # TODO: Impose a default propagation_dir in pulser-core 1.3 **kwargs: Any, ) -> ChannelType: """Initializes the channel with global addressing. @@ -361,6 +382,8 @@ def Global( bandwidth at -3dB (50% reduction), in MHz. min_avg_amp: The minimum average amplitude of a pulse (when not zero). + propagation_dir: The propagation direction of the beam associated + with the channel, given as a vector in 3D space. """ # Can't initialize a channel whose addressing is determined internally for cls_field in fields(cls): diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 3a6872a37..0a2481256 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -24,6 +24,7 @@ import numpy as np from scipy.spatial.distance import squareform +import pulser import pulser.json.abstract_repr as pulser_abstract_repr import pulser.math as pm from pulser.channels.base_channel import Channel, States, get_states_from_bases @@ -565,7 +566,13 @@ def _to_abstract_repr(self) -> dict[str, Any]: for ch_name, ch_obj in self.channels.items(): ch_list.append(ch_obj._to_abstract_repr(ch_name)) # Add version and channels to params - params.update({"version": "1", "channels": ch_list}) + params.update( + { + "version": "1", + "pulser_version": pulser.__version__, + "channels": ch_list, + } + ) dmm_list = [] for dmm_name, dmm_obj in self.dmm_channels.items(): dmm_list.append(dmm_obj._to_abstract_repr(dmm_name)) 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 6d4108510..7a97fbd34 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -82,6 +82,22 @@ "null" ] }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." + }, "total_bottom_detuning": { "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", "type": [ @@ -520,6 +536,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -610,6 +642,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -700,6 +748,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -857,6 +921,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -950,6 +1030,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1043,6 +1139,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1193,6 +1305,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1274,6 +1402,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1355,6 +1499,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1500,6 +1660,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1581,6 +1757,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1662,6 +1854,22 @@ "number", "null" ] + }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." } }, "required": [ @@ -1751,6 +1959,22 @@ "null" ] }, + "propagation_dir": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "The propagation direction of the beam leaving the channel." + }, "total_bottom_detuning": { "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", "type": [ diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index e46a9533b..a79f25e00 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -144,10 +144,11 @@ class NoiseModel: p_false_neg: Probability of measuring a false negative. temperature: Temperature, set in µK, of the atoms in the array. Also sets the standard deviation of the speed of the atoms. - laser_waist: Waist of the gaussian laser, set in µm, for global - pulses. - amp_sigma: Dictates the fluctuations in amplitude as a standard - deviation of a normal distribution centered in 1. + laser_waist: Waist of the gaussian lasers, set in µm, for global + pulses. Assumed to be the same for all global channels. + amp_sigma: Dictates the fluctuations in amplitude of global pulses + from run to run as a standard deviation of a normal distribution + centered in 1. Assumed to be the same for all global channels. 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 diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index 13a83f622..c17decf96 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -15,6 +15,7 @@ from __future__ import annotations +import functools import itertools from collections import defaultdict from collections.abc import Mapping @@ -221,6 +222,30 @@ def set_config(self, cfg: NoiseModel) -> None: # Noise, samples and Hamiltonian update routine self._construct_hamiltonian() + @staticmethod + @functools.cache + def _finite_waist_amp_fraction( + coords: tuple[float, ...], + propagation_dir: tuple[float, float, float], + laser_waist: float, + ) -> float: + pos_vec = np.zeros(3, dtype=float) + pos_vec[: len(coords)] = np.array(coords, dtype=float) + u_vec = np.array(propagation_dir, dtype=float) + u_vec = u_vec / np.linalg.norm(u_vec) + # Given a line crossing the origin with normalized direction vector + # u_vec, the closest point along said line and an arbitrary point + # pos_vec is at k*u_vec, where + k = np.dot(pos_vec, u_vec) + # The distance between pos_vec and the line is then given by + dist = np.linalg.norm(pos_vec - k * u_vec) + # We assume the Rayleigh length of the gaussian beam is very large, + # resulting in a negligble drop in amplitude along the propagation + # direction. Therefore, the drop in amplitude at a given position + # is dictated solely by its distance to the optical axis (as dictated + # by the propagation direction), ie + return float(np.exp(-((dist / laser_waist) ** 2))) + def _extract_samples(self) -> None: """Populates samples dictionary with every pulse in the sequence.""" local_noises = True @@ -244,15 +269,14 @@ def add_noise( slot: _PulseTargetSlot, samples_dict: Mapping[QubitId, dict[str, np.ndarray]], is_global_pulse: bool, + amp_fluctuation: float, + propagation_dir: tuple | None, ) -> None: """Builds hamiltonian coefficients. Taking into account, if necessary, noise effects, which are local and depend on the qubit's id qid. """ - noise_amp_base = max( - 0, np.random.normal(1.0, self.config.amp_sigma) - ) for qid in slot.targets: if "doppler" in self.config.noise_types: noise_det = self._doppler_detune[qid] @@ -260,22 +284,31 @@ def add_noise( # Gaussian beam loss in amplitude for global pulses only # Noise is drawn at random for each pulse if "amplitude" in self.config.noise_types and is_global_pulse: - amp_fraction = 1.0 + amp_fraction = amp_fluctuation if self.config.laser_waist is not None: - position = self._qdict[qid] - r = np.linalg.norm(position) - w0 = self.config.laser_waist - amp_fraction = np.exp(-((r / w0) ** 2)) - noise_amp = noise_amp_base * amp_fraction - samples_dict[qid]["amp"][slot.ti : slot.tf] *= noise_amp + # Default to an optical axis along y + prop_dir = propagation_dir or (0.0, 1.0, 0.0) + amp_fraction *= self._finite_waist_amp_fraction( + tuple(self._qdict[qid]), + tuple(prop_dir), + self.config.laser_waist, + ) + samples_dict[qid]["amp"][slot.ti : slot.tf] *= amp_fraction if local_noises: for ch, ch_samples in self.samples_obj.channel_samples.items(): - addr = self.samples_obj._ch_objs[ch].addressing - basis = self.samples_obj._ch_objs[ch].basis - samples_dict = samples["Local"][basis] + _ch_obj = self.samples_obj._ch_objs[ch] + samples_dict = samples["Local"][_ch_obj.basis] for slot in ch_samples.slots: - add_noise(slot, samples_dict, addr == "Global") + add_noise( + slot, + samples_dict, + _ch_obj.addressing == "Global", + amp_fluctuation=max( + 0, np.random.normal(1.0, self.config.amp_sigma) + ), + propagation_dir=_ch_obj.propagation_dir, + ) # Delete samples for badly prepared atoms for basis in samples["Local"]: for qid in samples["Local"][basis]: diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index 5811495a7..cf229f4b4 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -15,8 +15,8 @@ from __future__ import annotations +import math from dataclasses import dataclass, field, fields -from math import sqrt from typing import Any, Optional, Tuple, Type, TypeVar, Union, cast import qutip @@ -58,7 +58,7 @@ def doppler_sigma(temperature: float) -> float: Arg: temperature: The temperature in K. """ - return KEFF * sqrt(KB * temperature / MASS) + return KEFF * math.sqrt(KB * temperature / MASS) @dataclass(frozen=True) @@ -139,16 +139,23 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: kwargs[_DIFF_NOISE_PARAMS.get(param, param)] = getattr( noise_model, param ) + # When laser_waist is None, it should be given as inf instead + # Otherwise, the legacy default laser_waist value will be taken + if "amplitude" in noise_model.noise_types: + kwargs.setdefault("laser_waist", float("inf")) kwargs.pop("with_leakage", None) return cls(**kwargs) def to_noise_model(self) -> NoiseModel: """Creates a NoiseModel from the SimConfig.""" + laser_waist_ = ( + None if math.isinf(self.laser_waist) else self.laser_waist + ) relevant_params = NoiseModel._find_relevant_params( cast(Tuple[NoiseTypes, ...], self.noise), self.eta, self.amp_sigma, - self.laser_waist, + laser_waist_, ) kwargs = {} for param in relevant_params: diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 95ca43054..0d8874e4a 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -232,6 +232,9 @@ def abstract_device(self, request): def test_device_schema(self, abstract_device): validate_abstract_repr(json.dumps(abstract_device), "device") + def test_pulser_version(self, abstract_device): + assert abstract_device["pulser_version"] == pulser.__version__ + def test_roundtrip(self, abstract_device): def _roundtrip(abstract_device): device = deserialize_device(json.dumps(abstract_device)) @@ -509,6 +512,7 @@ def test_optional_device_fields(self, og_device, field, value): "ch_obj", [ Rydberg.Global(None, None, min_avg_amp=1), + Rydberg.Global(None, None, propagation_dir=(1, 0, 0)), Rydberg.Global( None, None, diff --git a/tests/test_channels.py b/tests/test_channels.py index bbf1a3217..5f2e2cfe0 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -36,6 +36,8 @@ ("mod_bandwidth", 0), ("mod_bandwidth", MODBW_TO_TR * 1e3 + 1), ("min_avg_amp", -1e-3), + ("propagation_dir", (0, 0, 0)), + ("propagation_dir", [1, 0]), ], ) def test_bad_init_global_channel(bad_param, bad_value): @@ -63,12 +65,15 @@ def test_bad_init_global_channel(bad_param, bad_value): ("mod_bandwidth", -1e4), ("mod_bandwidth", MODBW_TO_TR * 1e3 + 1), ("min_avg_amp", -1e-3), + ("propagation_dir", (1, 0, 0)), ], ) def test_bad_init_local_channel(bad_param, bad_value): kwargs = dict(max_abs_detuning=None, max_amp=None) kwargs[bad_param] = bad_value - if bad_param == "mod_bandwidth" and bad_value > 1: + if ( + bad_param == "mod_bandwidth" and bad_value > 1 + ) or bad_param == "propagation_dir": error_type = NotImplementedError else: error_type = ValueError diff --git a/tests/test_simconfig.py b/tests/test_simconfig.py index 14c879cfc..2b414dd08 100644 --- a/tests/test_simconfig.py +++ b/tests/test_simconfig.py @@ -138,9 +138,19 @@ def test_noise_model_conversion(): noise_model = NoiseModel( p_false_neg=0.4, p_false_pos=0.1, + amp_sigma=1e-3, + runs=10, + samples_per_run=1, ) expected_simconfig = SimConfig( - noise="SPAM", epsilon=0.1, epsilon_prime=0.4, eta=0.0 + noise=("SPAM", "amplitude"), + epsilon=0.1, + epsilon_prime=0.4, + eta=0.0, + amp_sigma=1e-3, + laser_waist=float("inf"), + runs=10, + samples_per_run=1, ) assert SimConfig.from_noise_model(noise_model) == expected_simconfig assert expected_simconfig.to_noise_model() == noise_model diff --git a/tests/test_simresults.py b/tests/test_simresults.py index e366fa5af..927a37567 100644 --- a/tests/test_simresults.py +++ b/tests/test_simresults.py @@ -309,7 +309,7 @@ def test_expect_noisy(results_noisy): with pytest.raises(ValueError, match="non-diagonal"): results_noisy.expect([bad_op]) op = qutip.tensor([qutip.qeye(2), qutip.basis(2, 0).proj()]) - assert np.isclose(results_noisy.expect([op])[0][-1], 0.7333333333333334) + assert np.isclose(results_noisy.expect([op])[0][-1], 0.7466666666666667) def test_plot(results_noisy, results): @@ -326,7 +326,7 @@ def test_sim_without_measurement(seq_no_meas): ) results_no_meas = sim_no_meas.run() assert results_no_meas.sample_final_state() == Counter( - {"11": 587, "10": 165, "01": 164, "00": 84} + {"11": 580, "10": 173, "01": 167, "00": 80} ) @@ -358,7 +358,7 @@ def test_sample_final_state_three_level(seq_no_meas, pi_pulse): def test_sample_final_state_noisy(seq_no_meas, results_noisy): np.random.seed(123) assert results_noisy.sample_final_state(N_samples=1234) == Counter( - {"11": 725, "10": 265, "01": 192, "00": 52} + {"11": 676, "10": 244, "01": 218, "00": 96} ) res_3level = QutipEmulator.from_sequence( seq_no_meas, config=SimConfig(noise=("SPAM", "doppler"), runs=10) diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 5921e55a4..cd4650e94 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import dataclasses from collections import Counter from unittest.mock import patch @@ -1535,7 +1536,19 @@ def test_effective_size_disjoint(channel_type): ) -def test_simulation_with_modulation(mod_device, reg, patch_plt_show): +@pytest.mark.parametrize( + "propagation_dir", (None, (1, 0, 0), (0, 1, 0), (0, 0, 1)) +) +def test_simulation_with_modulation( + mod_device, reg, propagation_dir, patch_plt_show +): + channels = mod_device.channels + channels["rydberg_global"] = dataclasses.replace( + channels["rydberg_global"], propagation_dir=propagation_dir + ) + mod_device = dataclasses.replace( + mod_device, channel_objects=tuple(channels.values()), channel_ids=None + ) seq = Sequence(reg, mod_device) seq.declare_channel("ch0", "rydberg_global") seq.config_slm_mask({"control1"}) @@ -1589,7 +1602,17 @@ def test_simulation_with_modulation(mod_device, reg, patch_plt_show): ) def pos_factor(qid): - r = np.linalg.norm(reg.qubits[qid].as_array()) + if propagation_dir is None or propagation_dir == (0, 1, 0): + # Optical axis long y, x dicates the distance + r = reg.qubits[qid].as_array()[0] + elif propagation_dir == (1, 0, 0): + # Optical axis long x, y dicates the distance + r = reg.qubits[qid].as_array()[1] + else: + # Optical axis long z, use distance to origin + assert propagation_dir == (0, 0, 1) + r = np.linalg.norm(reg.qubits[qid].as_array()) + w0 = sim_config.laser_waist return np.exp(-((r / w0) ** 2)) From dd40da79f4d4932740158d20e30345fcda04f8dd Mon Sep 17 00:00:00 2001 From: Antoine Cornillot <61453516+a-corni@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:00:41 +0100 Subject: [PATCH 07/15] Draw the phase curve by default in Sequence.draw() (#771) * Set draw_phase_curve to True by default * Fix phase curve * Always display phase in draw_phase_area --- pulser-core/pulser/sequence/_seq_drawer.py | 24 ++++++------------- pulser-core/pulser/sequence/sequence.py | 5 ++-- .../Output Modulation and EOM Mode.ipynb | 2 +- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/pulser-core/pulser/sequence/_seq_drawer.py b/pulser-core/pulser/sequence/_seq_drawer.py index 6b922462d..15fe1880a 100644 --- a/pulser-core/pulser/sequence/_seq_drawer.py +++ b/pulser-core/pulser/sequence/_seq_drawer.py @@ -540,8 +540,7 @@ def _draw_channel_content( the solver. If present, plots the effective pulse alongside the input pulse. draw_phase_area: Whether phase and area values need to be shown - as text on the plot, defaults to False. If `draw_phase_curve=True`, - phase values are ommited. + as text on the plot, defaults to False. draw_phase_shifts: Whether phase shift and reference information should be added to the plot, defaults to False. draw_input: Draws the programmed pulses on the channels, defaults @@ -732,10 +731,6 @@ def phase_str(phi: Any) -> str: if draw_phase_area: top = False # Variable to track position of box, top or center. - print_phase = not draw_phase_curve and any( - np.any(ch_data.samples.phase[slot.ti : slot.tf] != 0) - for slot in ch_data.samples.slots - ) for slot in ch_data.samples.slots: if sampling_rate: @@ -767,11 +762,8 @@ def phase_str(phi: Any) -> str: if round(area_val, 2) == 1 else rf"A: {float(area_val):.2g}$\pi$" ) - if not print_phase: - txt = area_fmt - else: - phase_fmt = rf"$\phi$: {phase_str(phase_val)}" - txt = "\n".join([phase_fmt, area_fmt]) + phase_fmt = rf"$\phi$: {phase_str(phase_val)}" + txt = "\n".join([phase_fmt, area_fmt]) axes[0].text( x_plot, y_plot, @@ -1246,7 +1238,7 @@ def draw_samples( sampling_rate: Optional[float] = None, draw_phase_area: bool = False, draw_phase_shifts: bool = False, - draw_phase_curve: bool = False, + draw_phase_curve: bool = True, draw_detuning_maps: bool = False, draw_qubit_amp: bool = False, draw_qubit_det: bool = False, @@ -1263,8 +1255,7 @@ def draw_samples( the solver. If present, plots the effective pulse alongside the input pulse. draw_phase_area: Whether phase and area values need to be shown - as text on the plot, defaults to False. If `draw_phase_curve=True`, - phase values are ommited. + as text on the plot, defaults to False. draw_phase_shifts: Whether phase shift and reference information should be added to the plot, defaults to False. draw_phase_curve: Draws the changes in phase in its own curve (ignored @@ -1322,7 +1313,7 @@ def draw_sequence( draw_register: bool = False, draw_input: bool = True, draw_modulation: bool = False, - draw_phase_curve: bool = False, + draw_phase_curve: bool = True, draw_detuning_maps: bool = False, draw_qubit_amp: bool = False, draw_qubit_det: bool = False, @@ -1336,8 +1327,7 @@ def draw_sequence( the solver. If present, plots the effective pulse alongside the input pulse. draw_phase_area: Whether phase and area values need to be shown - as text on the plot, defaults to False. If `draw_phase_curve=True`, - phase values are ommited. + as text on the plot, defaults to False. draw_interp_pts: When the sequence has pulses with waveforms of type InterpolatedWaveform, draws the points of interpolation on top of the respective waveforms (defaults to True). diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 50daadbcc..6287dbd8b 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -1734,7 +1734,7 @@ def draw( draw_interp_pts: bool = True, draw_phase_shifts: bool = False, draw_register: bool = False, - draw_phase_curve: bool = False, + draw_phase_curve: bool = True, draw_detuning_maps: bool = False, draw_qubit_amp: bool = False, draw_qubit_det: bool = False, @@ -1754,8 +1754,7 @@ def draw( offsets, displays the equivalent phase modulation. draw_phase_area: Whether phase and area values need to be shown as text on the plot, defaults to False. Doesn't work in - 'output' mode. If `draw_phase_curve=True`, phase values are - ommited. + 'output' mode. draw_interp_pts: When the sequence has pulses with waveforms of type InterpolatedWaveform, draws the points of interpolation on top of the respective input waveforms (defaults to True). diff --git a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb index 071fde63b..944598fb6 100644 --- a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb +++ b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb @@ -287,7 +287,7 @@ "seq.disable_eom_mode(\"rydberg\")\n", "seq.add(Pulse.ConstantPulse(100, 1, 0, 0), \"rydberg\")\n", "\n", - "seq.draw(draw_phase_curve=True)" + "seq.draw()" ] }, { From 9a32a2b2c0b5b8c0a742a9fceeb1410b3c93acc7 Mon Sep 17 00:00:00 2001 From: Antoine Cornillot <61453516+a-corni@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:53:14 +0100 Subject: [PATCH 08/15] Add method to Sequence to estimate delay introduced before adding pulse (#773) * Add estimate_add_delay * Fix test coverage * Add testing of DMM * Address review comments * Fix test --- pulser-core/pulser/sequence/_schedule.py | 41 +++++++++-- pulser-core/pulser/sequence/sequence.py | 92 ++++++++++++++++++++++++ tests/test_sequence.py | 91 +++++++++++++++++++++++ 3 files changed, 217 insertions(+), 7 deletions(-) diff --git a/pulser-core/pulser/sequence/_schedule.py b/pulser-core/pulser/sequence/_schedule.py index 744040ed2..f5207d918 100644 --- a/pulser-core/pulser/sequence/_schedule.py +++ b/pulser-core/pulser/sequence/_schedule.py @@ -396,14 +396,15 @@ def disable_eom(self, channel_id: str, _skip_buffer: bool = False) -> None: else: self.wait_for_fall(channel_id) - def add_pulse( + def make_next_pulse_slot( self, pulse: Pulse, channel: str, phase_barrier_ts: list[int], protocol: str, phase_drift_params: _PhaseDriftParams | None = None, - ) -> None: + block_over_max_duration: bool = False, + ) -> _TimeSlot: def corrected_phase(tf: int) -> pm.AbstractArray: phase_drift = pm.AbstractArray( phase_drift_params.calc_phase_drift(tf) @@ -447,11 +448,10 @@ def corrected_phase(tf: int) -> pm.AbstractArray: delay_duration = max(current_max_t - t0, phase_jump_buffer) if delay_duration > 0: delay_duration = self[channel].adjust_duration(delay_duration) - self.add_delay(delay_duration, channel) ti = t0 + delay_duration tf = ti + pulse.duration - self._check_duration(tf) + self._check_duration(tf, block_over_max_duration) # dataclasses.replace() does not work on Pulse (because init=False) if phase_drift_params is not None: pulse = Pulse( @@ -460,7 +460,29 @@ def corrected_phase(tf: int) -> pm.AbstractArray: phase=corrected_phase(ti), post_phase_shift=pulse.post_phase_shift, ) - self[channel].slots.append(_TimeSlot(pulse, ti, tf, last.targets)) + return _TimeSlot(pulse, ti, tf, last.targets) + + def add_pulse( + self, + pulse: Pulse, + channel: str, + phase_barrier_ts: list[int], + protocol: str, + phase_drift_params: _PhaseDriftParams | None = None, + ) -> None: + last = self[channel][-1] + time_slot = self.make_next_pulse_slot( + pulse, + channel, + phase_barrier_ts, + protocol, + phase_drift_params, + True, + ) + delay_duration = time_slot.ti - last.tf + if delay_duration > 0: + self.add_delay(delay_duration, channel) + self[channel].slots.append(time_slot) def add_delay(self, duration: int, channel: str) -> None: last = self[channel][-1] @@ -557,9 +579,14 @@ def _get_last_pulse_phase(self, channel: str) -> pm.AbstractArray: phase = pm.AbstractArray(0.0) return phase - def _check_duration(self, t: int) -> None: + def _check_duration( + self, t: int, block_over_max_duration: bool = True + ) -> None: if self.max_duration is not None and t > self.max_duration: - raise RuntimeError( + msg = ( "The sequence's duration exceeded the maximum duration allowed" f" by the device ({self.max_duration} ns)." ) + if block_over_max_duration: + raise RuntimeError(msg) + warnings.warn(msg, UserWarning) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 6287dbd8b..11334dccd 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -1381,6 +1381,98 @@ def delay( """ self._delay(duration, channel, at_rest) + def estimate_added_delay( + self, + pulse: Union[Pulse, Parametrized], + channel: str, + protocol: PROTOCOLS = "min-delay", + ) -> int: + """Delay that will be added before the pulse when added to a channel. + + When adding a pulse to a channel of the Sequence, a delay can be added + to account for the modulation bandwidth of the channel or the protocol + chosen. This method estimates the delay that will be added before the + pulse if this pulse was added to this channel with this protocol. It + works even if the channel is in EOM mode, but to be appropriate, the + Pulse should be a ConstantPulse with amplitude and detuning + respectively the rabi_freq and detuning_on of the EOM block. + + Args: + pulse: The pulse object to add to the channel. + channel: The channel's name provided when declared. + protocol: Stipulates how to deal with + eventual conflicts with other channels, specifically in terms + of having multiple channels act on the same target + simultaneously. + + - ``'min-delay'``: Before adding the pulse, introduces the + smallest possible delay that avoids all exisiting conflicts. + - ``'no-delay'``: Adds the pulse to the channel, regardless of + existing conflicts. + - ``'wait-for-all'``: Before adding the pulse, adds a delay + that idles the channel until the end of the other channels' + latest pulse. + + Returns: + The delay that would be added before the pulse. + """ + self._validate_channel( + channel, + block_if_slm=channel.startswith("dmm_"), + ) + self._validate_add_protocol(protocol) + if self.is_parametrized() or isinstance(pulse, Parametrized): + raise ValueError( + "Can't compute the delay to add before a pulse if sequence or" + "pulse is parametrized." + ) + if self.is_in_eom_mode(channel): + eom_settings = self._schedule[channel].eom_blocks[-1] + if np.any(pulse.amplitude.samples != eom_settings.rabi_freq): + warnings.warn( + f"Channel {channel} is in EOM mode, the amplitude of the " + "pulse will be constant and equal to " + f"{eom_settings.rabi_freq}.", + UserWarning, + ) + if np.any(pulse.detuning.samples != eom_settings.detuning_on): + warnings.warn( + f"Channel {channel} is in EOM mode, the detuning of the " + "pulse will be constant and equal to " + f"{eom_settings.detuning_on}.", + UserWarning, + ) + channel_obj = self._schedule[channel].channel_obj + last = self._last(channel) + basis = channel_obj.basis + + ph_refs = { + self._basis_ref[basis][q].phase.last_phase for q in last.targets + } + if isinstance(channel_obj, DMM): + phase_ref = None + elif len(ph_refs) != 1: + raise ValueError( + "Cannot do a multiple-target pulse on qubits with different " + "phase references for the same basis." + ) + else: + phase_ref = ph_refs.pop() + + pulse = self._validate_and_adjust_pulse(pulse, channel, phase_ref) + + phase_barriers = [ + self._basis_ref[basis][q].phase.last_time for q in last.targets + ] + next_time_slot = self._schedule.make_next_pulse_slot( + pulse, + channel, + phase_barriers, + protocol, + # phase_drift_params does not impact delay between pulses + ) + return next_time_slot.ti - last.tf + @seq_decorators.store @seq_decorators.block_if_measured def measure(self, basis: str = "ground-rydberg") -> None: diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 6254ea50a..a871ab770 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -1729,6 +1729,97 @@ def test_sequence(reg, device, patch_plt_show): assert str(seq) == str(seq_) +@pytest.mark.parametrize("eom", [False, True]) +def test_estimate_added_delay(eom): + reg = Register.square(2, 5) + seq = Sequence(reg, AnalogDevice) + pulse_0 = Pulse.ConstantPulse(100, 1, 0, 0) + pulse_pi_2 = Pulse.ConstantPulse(100, 1, 0, np.pi / 2) + + with pytest.raises( + ValueError, match="Use the name of a declared channel." + ): + seq.estimate_added_delay(pulse_0, "ising", "min-delay") + seq.declare_channel("ising", "rydberg_global") + ising_obj = seq.declared_channels["ising"] + if eom: + seq.enable_eom_mode("ising", 1, 0) + with pytest.warns( + UserWarning, + match="Channel ising is in EOM mode, the amplitude", + ): + assert ( + seq.estimate_added_delay( + Pulse.ConstantPulse(100, 2, 0, 0), "ising" + ) + == 0 + ) + with pytest.warns( + UserWarning, + match="Channel ising is in EOM mode, the detuning", + ): + assert ( + seq.estimate_added_delay( + Pulse.ConstantPulse(100, 1, 1, 0), "ising" + ) + == 0 + ) + assert seq.estimate_added_delay(pulse_0, "ising", "min-delay") == 0 + seq._add(pulse_0, "ising", "min-delay") + first_pulse = seq._last("ising") + assert first_pulse.ti == 0 + delay = pulse_0.fall_time(ising_obj, eom) + ising_obj.phase_jump_time + assert seq.estimate_added_delay(pulse_pi_2, "ising") == delay + seq._add(pulse_pi_2, "ising", "min-delay") + second_pulse = seq._last("ising") + assert second_pulse.ti - first_pulse.tf == delay + assert seq.estimate_added_delay(pulse_0, "ising") == delay + seq.delay(100, "ising") + assert seq.estimate_added_delay(pulse_0, "ising") == delay - 100 + with pytest.warns( + UserWarning, + match="The sequence's duration exceeded the maximum duration", + ): + seq.estimate_added_delay( + pulser.Pulse.ConstantPulse(4000, 1, 0, np.pi), "ising" + ) + var = seq.declare_variable("var", dtype=int) + with pytest.raises( + ValueError, match="Can't compute the delay to add before a pulse" + ): + seq.estimate_added_delay(Pulse.ConstantPulse(var, 1, 0, 0), "ising") + # We shift the phase of just one qubit, which blocks addition + # of new pulses on this basis + seq.phase_shift(1.0, 0, basis="ground-rydberg") + with pytest.raises( + ValueError, + match="Cannot do a multiple-target pulse on qubits with different", + ): + seq.estimate_added_delay(pulse_0, "ising") + + +def test_estimate_added_delay_dmm(): + pulse_0 = Pulse.ConstantPulse(100, 1, 0, 0) + det_pulse = Pulse.ConstantPulse(100, 0, -1, 0) + seq = Sequence(Register.square(2, 5), DigitalAnalogDevice) + seq.declare_channel("ising", "rydberg_global") + seq.config_slm_mask([0, 1]) + with pytest.raises( + ValueError, match="You should add a Pulse to a Global Channel" + ): + seq.estimate_added_delay(det_pulse, "dmm_0") + seq.add(pulse_0, "ising") + assert seq.estimate_added_delay(det_pulse, "dmm_0") == 0 + with pytest.raises( + ValueError, match="The detuning in a DMM must not be positive." + ): + seq.estimate_added_delay(Pulse.ConstantPulse(100, 0, 1, 0), "dmm_0") + with pytest.raises( + ValueError, match="The pulse's amplitude goes over the maximum" + ): + seq.estimate_added_delay(pulse_0, "dmm_0") + + @pytest.mark.parametrize("qubit_ids", [["q0", "q1", "q2"], [0, 1, 2]]) def test_config_slm_mask(qubit_ids, device, det_map): reg: Register | MappableRegister From 4e73021a5f3e92c00b79865d2f7c244a2b615512 Mon Sep 17 00:00:00 2001 From: Antoine Cornillot <61453516+a-corni@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:57:59 +0100 Subject: [PATCH 09/15] Deprecate usage of int in QubitIds (#774) * Deprecate usage of int * Fix tutorials * Fix targets * Update warning message * Fix tutorials * Revert "Fix tutorials" This reverts commit a99d61501ba040e590059a23763833103ff1633d. * Fix tutorial * Fix typing with qubitID=str * Fix tests * Fix typing * Fix tests * Fix built in values for register generation * Fix tests * Address review comments --- docs/source/intro_rydberg_blockade.ipynb | 6 +- pulser-core/pulser/devices/_device_datacls.py | 6 +- pulser-core/pulser/register/_reg_drawer.py | 4 +- pulser-core/pulser/register/base_register.py | 13 +- .../pulser/register/register_layout.py | 2 +- pulser-core/pulser/register/weight_maps.py | 4 +- pulser-core/pulser/sequence/sequence.py | 8 +- pyproject.toml | 1 + tests/test_abstract_repr.py | 3 + tests/test_dmm.py | 40 +- tests/test_json.py | 19 +- tests/test_paramseq.py | 18 +- tests/test_sequence.py | 7 +- tests/test_sequence_sampler.py | 9 +- .../Interpolated Waveforms.ipynb | 2 +- .../Output Modulation and EOM Mode.ipynb | 2 +- .../advanced_features/Virtual Devices.ipynb | 4 +- ...QAOA and QAA to solve a QUBO problem.ipynb | 2 +- tutorials/creating_sequences.ipynb | 16 +- ...iltonians in arrays of Rydberg atoms.ipynb | 6 +- ...rromagnetic order in the Ising model.ipynb | 2 +- .../Shadow estimation for VQS.ipynb | 2801 ++++++++--------- .../Spin chain of 3 atoms in XY mode.ipynb | 16 +- 23 files changed, 1523 insertions(+), 1468 deletions(-) diff --git a/docs/source/intro_rydberg_blockade.ipynb b/docs/source/intro_rydberg_blockade.ipynb index db47534b2..35cef0446 100644 --- a/docs/source/intro_rydberg_blockade.ipynb +++ b/docs/source/intro_rydberg_blockade.ipynb @@ -51,7 +51,7 @@ "outputs": [], "source": [ "layers = 3\n", - "reg = pulser.Register.hexagon(layers)\n", + "reg = pulser.Register.hexagon(layers, prefix=\"q\")\n", "reg.draw(with_labels=False)" ] }, @@ -221,7 +221,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "pulserenv", "language": "python", "name": "python3" }, @@ -235,7 +235,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 0a2481256..203cb6cf6 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -534,7 +534,7 @@ def _validate_coords( ), kind: Literal["atoms", "traps"] = "atoms", ) -> None: - ids = list(coords_dict.keys()) + ids = [str(id) for id in list(coords_dict.keys())] coords = list(map(pm.AbstractArray, coords_dict.values())) if kind == "atoms" and not ( "max_atom_num" in self._optional_parameters @@ -763,7 +763,7 @@ def _specs(self, for_docs: bool = False) -> str: ( "\t" + r"- Maximum :math:`\Omega`:" - + f" {float(cast(float,ch.max_amp)):.4g} rad/µs" + + f" {float(cast(float, ch.max_amp)):.4g} rad/µs" ), ( ( @@ -776,7 +776,7 @@ def _specs(self, for_docs: bool = False) -> str: else ( "\t" + r"- Bottom :math:`|\delta|`:" - + f" {float(cast(float,ch.bottom_detuning)):.4g}" + + f" {float(cast(float, ch.bottom_detuning)):.4g}" + " rad/µs" ) ), diff --git a/pulser-core/pulser/register/_reg_drawer.py b/pulser-core/pulser/register/_reg_drawer.py index a50ce41d4..155c7e8c1 100644 --- a/pulser-core/pulser/register/_reg_drawer.py +++ b/pulser-core/pulser/register/_reg_drawer.py @@ -93,8 +93,8 @@ def _draw_2D( if dmm_qubits: dmm_pos = [] - for i, c in zip(ids, pos): - if i in dmm_qubits.keys(): + for id, c in zip(ids, pos): + if id in dmm_qubits.keys(): dmm_pos.append(c) dmm_arr = np.array(dmm_pos) max_weight = max(dmm_qubits.values()) diff --git a/pulser-core/pulser/register/base_register.py b/pulser-core/pulser/register/base_register.py index d01253dbd..9b30c33bd 100644 --- a/pulser-core/pulser/register/base_register.py +++ b/pulser-core/pulser/register/base_register.py @@ -16,6 +16,7 @@ from __future__ import annotations import json +import warnings from abc import ABC, abstractmethod from collections.abc import Iterable, Mapping from collections.abc import Sequence as abcSequence @@ -44,7 +45,7 @@ from pulser.register.register_layout import RegisterLayout T = TypeVar("T", bound="BaseRegister") -QubitId = Union[int, str] +QubitId = str class _LayoutInfo(NamedTuple): @@ -77,6 +78,16 @@ def __init__( [pm.AbstractArray(v, dtype=float) for v in qubits.values()] ) self._ids: tuple[QubitId, ...] = tuple(qubits.keys()) + if any(not isinstance(id, str) for id in self._ids): + warnings.simplefilter("always") + warnings.warn( + "Usage of `int`s or any non-`str`types as `QubitId`s will be " + "deprecated. Define your `QubitId`s as `str`s, prefer setting " + "`prefix='q'` when using classmethods, as that will become the" + " new default once `int` qubit IDs become invalid.", + DeprecationWarning, + stacklevel=2, + ) self._layout_info: Optional[_LayoutInfo] = None self._init_kwargs(**kwargs) diff --git a/pulser-core/pulser/register/register_layout.py b/pulser-core/pulser/register/register_layout.py index f543e7e1a..8dd6e5477 100644 --- a/pulser-core/pulser/register/register_layout.py +++ b/pulser-core/pulser/register/register_layout.py @@ -172,7 +172,7 @@ def draw( draw_graph=draw_graph, draw_half_radius=draw_half_radius, ) - ids = list(range(self.number_of_traps)) + ids = [str(i) for i in range(self.number_of_traps)] if self.dimensionality == 2: fig, ax = self._initialize_fig_axes( coords, diff --git a/pulser-core/pulser/register/weight_maps.py b/pulser-core/pulser/register/weight_maps.py index f2d146e1d..d0c67f241 100644 --- a/pulser-core/pulser/register/weight_maps.py +++ b/pulser-core/pulser/register/weight_maps.py @@ -123,7 +123,9 @@ def draw( pos = self.trap_coordinates custom_ax = custom_ax or cast(Axes, self._initialize_fig_axes(pos)[1]) - labels_ = labels if labels is not None else list(range(len(pos))) + labels_ = ( + labels if labels is not None else [str(i) for i in range(len(pos))] + ) super()._draw_2D( custom_ax, diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 11334dccd..0f1a213dd 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -1344,7 +1344,7 @@ def target_index( Args: qubits: The new target for this channel. Must correspond to a - qubit index or an collection of qubit indices, when multi-qubit + qubit index or a collection of qubit indices, when multi-qubit addressing is possible. A qubit index is a number between 0 and the number of qubits. It is then converted to a Qubit ID using the order in which @@ -2057,7 +2057,7 @@ def _add( @seq_decorators.block_if_measured def _target( self, - qubits: Union[Collection[QubitId], QubitId, Parametrized], + qubits: Union[Collection[QubitId | int], QubitId | int, Parametrized], channel: str, _index: bool = False, ) -> None: @@ -2105,7 +2105,7 @@ def _target( self._schedule.add_target(qubit_ids_set, channel) def _check_qubits_give_ids( - self, *qubits: Union[QubitId, Parametrized], _index: bool = False + self, *qubits: Union[QubitId, int, Parametrized], _index: bool = False ) -> set[QubitId]: if _index: if self.is_parametrized(): @@ -2158,7 +2158,7 @@ def _delay( def _phase_shift( self, phi: float | Parametrized, - *targets: QubitId | Parametrized, + *targets: QubitId | int | Parametrized, basis: str, _index: bool = False, ) -> None: diff --git a/pyproject.toml b/pyproject.toml index a4abb7d55..b71074177 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ filterwarnings = [ "error", # Except these particular warnings, which are ignored 'ignore:A duration of \d+ ns is not a multiple of:UserWarning', + 'ignore:Usage of `int`s or any non-`str`types as `QubitId`s:DeprecationWarning', ] [build-system] diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 0d8874e4a..f218fe77a 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -820,6 +820,9 @@ def test_exceptions(self, sequence): UserWarning, match="converts all qubit ID's to strings" ), pytest.raises( AbstractReprError, match="Name collisions encountered" + ), pytest.warns( + DeprecationWarning, + match="Usage of `int`s or any non-`str`types as `QubitId`s", ): Register({"0": (0, 0), 0: (20, 20)})._to_abstract_repr() diff --git a/tests/test_dmm.py b/tests/test_dmm.py index f03df88b6..6927765c4 100644 --- a/tests/test_dmm.py +++ b/tests/test_dmm.py @@ -14,7 +14,7 @@ from __future__ import annotations import re -from typing import cast +from typing import Union, cast from unittest.mock import patch import numpy as np @@ -37,7 +37,9 @@ def layout(self) -> RegisterLayout: @pytest.fixture def register(self, layout: RegisterLayout) -> BaseRegister: - return layout.define_register(0, 1, 2, 3, qubit_ids=(0, 1, 2, 3)) + return layout.define_register( + 0, 1, 2, 3, qubit_ids=("0", "1", "2", "3") + ) @pytest.fixture def map_reg(self, layout: RegisterLayout) -> MappableRegister: @@ -63,7 +65,7 @@ def slm_map( ) -> DetuningMap: return layout.define_detuning_map(slm_dict) - @pytest.mark.parametrize("bad_key", [{"1": 1.0}, {4: 1.0}]) + @pytest.mark.parametrize("bad_key", [{1: 1.0}, {"4": 1.0}]) def test_define_detuning_map( self, layout: RegisterLayout, @@ -72,6 +74,13 @@ def test_define_detuning_map( bad_key: dict, ): for reg in (layout, map_reg): + if type(list(bad_key.keys())[0]) == int: + with pytest.raises( + ValueError, + match="'trap_coordinates' must be an array or list", + ): + reg.define_detuning_map(bad_key) # type: ignore + continue with pytest.raises( ValueError, match=re.escape( @@ -91,7 +100,7 @@ def test_define_detuning_map( def test_qubit_weight_map(self, register): # Purposefully unsorted - qid_weight_map = {1: 1.0, 0: 0.1, 3: 0.4} + qid_weight_map = {"1": 1.0, "0": 0.1, "3": 0.4} sorted_qids = sorted(qid_weight_map) det_map = register.define_detuning_map(qid_weight_map) qubits = register.qubits @@ -104,7 +113,7 @@ def test_qubit_weight_map(self, register): # We recover the original qid_weight_map (and undefined qids show as 0) assert det_map.get_qubit_weight_map(qubits) == { **qid_weight_map, - 2: 0.0, + "2": 0.0, } tri_layout = TriangularLatticeLayout(100, spacing=5) @@ -172,8 +181,12 @@ def test_detuning_map_bad_init( ): DetuningMap([(0, 0), (1, 0)], [0]) - bad_weights = {0: -1.0, 1: 1.0, 2: 1.0} for reg in (layout, map_reg, register): + bad_weights: dict[int | str, float] + if reg == register: + bad_weights = {"0": -1.0, "1": 1.0, "2": 1.0} + else: + bad_weights = {0: -1.0, 1: 1.0, 2: 1.0} with pytest.raises( ValueError, match="All weights must be between 0 and 1." ): @@ -187,11 +200,22 @@ def test_init( det_dict: dict[int, float], slm_dict: dict[int, float], ): + for reg in (layout, map_reg, register): for detuning_map_dict in (det_dict, slm_dict): + reg_det_map_dict: dict[int | str, float] + if reg == register: + reg_det_map_dict = { + str(id): weight + for (id, weight) in detuning_map_dict.items() + } + else: + reg_det_map_dict = cast( + dict[Union[int, str], float], detuning_map_dict + ) detuning_map = cast( DetuningMap, - reg.define_detuning_map(detuning_map_dict), # type: ignore + reg.define_detuning_map(reg_det_map_dict), # type: ignore ) assert np.all( [ @@ -227,7 +251,7 @@ def test_draw(self, det_map, slm_map, patch_plt_show, with_labels): )[1], np.array(slm_map.trap_coordinates), [ - i + str(i) for i, _ in enumerate(cast(list, slm_map.trap_coordinates)) ], with_labels=True, diff --git a/tests/test_json.py b/tests/test_json.py index b211c06d1..4ee83a8c8 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -119,15 +119,24 @@ def test_detuning_map(): @pytest.mark.parametrize( - "reg", + "reg_dict", [ - Register(dict(enumerate([(2, 3), (5, 1), (10, 0)]))), - Register3D({3: (2, 3, 4), 4: (3, 4, 5), 2: (4, 5, 7)}), + dict(enumerate([(2, 3), (5, 1), (10, 0)])), + {3: (2, 3, 4), 4: (3, 4, 5), 2: (4, 5, 7)}, ], ) -def test_register_numbered_keys(reg): +def test_register_numbered_keys(reg_dict): + with pytest.warns( + DeprecationWarning, + match="Usage of `int`s or any non-`str`types as `QubitId`s", + ): + reg = (Register if len(reg_dict[2]) == 2 else Register3D)(reg_dict) j = json.dumps(reg, cls=PulserEncoder) - decoded_reg = json.loads(j, cls=PulserDecoder) + with pytest.warns( + DeprecationWarning, + match="Usage of `int`s or any non-`str`types as `QubitId`s", + ): + decoded_reg = json.loads(j, cls=PulserDecoder) assert reg == decoded_reg assert all([type(i) is int for i in decoded_reg.qubit_ids]) diff --git a/tests/test_paramseq.py b/tests/test_paramseq.py index 976e246f9..7048a20a6 100644 --- a/tests/test_paramseq.py +++ b/tests/test_paramseq.py @@ -24,7 +24,7 @@ from pulser.parametrized.variable import VariableItem from pulser.waveforms import BlackmanWaveform -reg = Register.rectangle(4, 3) +reg = Register.rectangle(4, 3, prefix="q") device = DigitalAnalogDevice @@ -50,10 +50,10 @@ def test_parametrized_channel_initial_target(): var = sb.declare_variable("var") sb.declare_channel("ch1", "rydberg_local") sb.target_index(var, "ch1") - sb.declare_channel("ch0", "raman_local", initial_target=0) + sb.declare_channel("ch0", "raman_local", initial_target="q0") assert sb._calls[-1].name == "declare_channel" assert sb._to_build_calls[-1].name == "target" - assert sb._to_build_calls[-1].args == (0, "ch0") + assert sb._to_build_calls[-1].args == ("q0", "ch0") def test_stored_calls(): @@ -125,7 +125,9 @@ def test_stored_calls(): sb.target_index(q_var, "ch1") sb2 = Sequence(reg, MockDevice) - sb2.declare_channel("ch1", "rydberg_local", initial_target={3, 4, 5}) + sb2.declare_channel( + "ch1", "rydberg_local", initial_target={"q3", "q4", "q5"} + ) q_var2 = sb2.declare_variable("q_var2", size=5, dtype=int) var2 = sb2.declare_variable("var2") assert sb2._building @@ -229,7 +231,7 @@ def test_str(): def test_screen(): sb = Sequence(reg, device) sb.declare_channel("ch1", "rydberg_global") - assert sb.current_phase_ref(4, basis="ground-rydberg") == 0 + assert sb.current_phase_ref("q4", basis="ground-rydberg") == 0 var = sb.declare_variable("var") sb.delay(var, "ch1") with pytest.raises(RuntimeError, match="can't be called in parametrized"): @@ -239,7 +241,7 @@ def test_screen(): def test_parametrized_in_eom_mode(mod_device): # Case 1: Sequence becomes parametrized while in EOM mode seq = Sequence(reg, mod_device) - seq.declare_channel("ch0", "rydberg_local", initial_target=0) + seq.declare_channel("ch0", "rydberg_local", initial_target="q0") assert not seq.is_in_eom_mode("ch0") seq.enable_eom_mode("ch0", amp_on=2.0, detuning_on=0.0) @@ -278,8 +280,8 @@ def test_parametrized_before_eom_mode(mod_device): # Case 2: Sequence is parametrized before entering EOM mode seq = Sequence(reg, mod_device) - seq.declare_channel("ch0", "rydberg_local", initial_target=0) - seq.declare_channel("raman", "raman_local", initial_target=2) + seq.declare_channel("ch0", "rydberg_local", initial_target="q0") + seq.declare_channel("raman", "raman_local", initial_target="q2") amp = seq.declare_variable("amp", dtype=float) seq.add(Pulse.ConstantPulse(200, amp, -1, 0), "ch0") diff --git a/tests/test_sequence.py b/tests/test_sequence.py index a871ab770..d13d5bc3d 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -1614,8 +1614,11 @@ def test_str(reg, device, mod_device, det_map): measure_msg = "\n\nMeasured in basis: digital" assert seq.__str__() == msg_ch0 + msg_ch1 + msg_det_map + measure_msg - - seq2 = Sequence(Register({"q0": (0, 0), 1: (5, 5)}), device) + with pytest.warns( + DeprecationWarning, + match="Usage of `int`s or any non-`str`types as `QubitId`s", + ): + seq2 = Sequence(Register({"q0": (0, 0), 1: (5, 5)}), device) seq2.declare_channel("ch1", "rydberg_global") with pytest.raises( NotImplementedError, diff --git a/tests/test_sequence_sampler.py b/tests/test_sequence_sampler.py index 8363825eb..6b27d5be8 100644 --- a/tests/test_sequence_sampler.py +++ b/tests/test_sequence_sampler.py @@ -201,8 +201,8 @@ def test_modulation(mod_seq: pulser.Sequence) -> None: def test_modulation_local(mod_device): - seq = pulser.Sequence(pulser.Register.square(2), mod_device) - seq.declare_channel("ch0", "rydberg_local", initial_target=0) + seq = pulser.Sequence(pulser.Register.square(2, prefix="q"), mod_device) + seq.declare_channel("ch0", "rydberg_local", initial_target="q0") ch_obj = seq.declared_channels["ch0"] pulse1 = Pulse.ConstantPulse(500, 1, -1, 0) pulse2 = Pulse.ConstantPulse(200, 2.5, 0, 0) @@ -210,7 +210,7 @@ def test_modulation_local(mod_device): seq.add(pulse1, "ch0") seq.delay(partial_fall, "ch0") seq.add(pulse2, "ch0") - seq.target(1, "ch0") + seq.target("q1", "ch0") seq.add(pulse1, "ch0") input_samples = sample(seq) @@ -236,7 +236,8 @@ def test_modulation_local(mod_device): samples_dict = output_samples.to_nested_dict() for qty in ("amp", "det", "phase"): combined = sum( - samples_dict["Local"]["ground-rydberg"][t][qty] for t in range(2) + samples_dict["Local"]["ground-rydberg"][f"q{t}"][qty] + for t in range(2) ) np.testing.assert_array_equal(getattr(out_ch_samples, qty), combined) diff --git a/tutorials/advanced_features/Interpolated Waveforms.ipynb b/tutorials/advanced_features/Interpolated Waveforms.ipynb index 163a71b28..fadf97ec5 100644 --- a/tutorials/advanced_features/Interpolated Waveforms.ipynb +++ b/tutorials/advanced_features/Interpolated Waveforms.ipynb @@ -158,7 +158,7 @@ }, "outputs": [], "source": [ - "reg = Register.square(1)\n", + "reg = Register.square(1, prefix=\"q\")\n", "param_seq = Sequence(reg, AnalogDevice)\n", "param_seq.declare_channel(\"rydberg_global\", \"rydberg_global\", initial_target=0)\n", "amp_vals = param_seq.declare_variable(\"amp_vals\", size=5, dtype=float)\n", diff --git a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb index 944598fb6..9f1c7b7e2 100644 --- a/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb +++ b/tutorials/advanced_features/Output Modulation and EOM Mode.ipynb @@ -276,7 +276,7 @@ "source": [ "from pulser.devices import AnalogDevice\n", "\n", - "seq = Sequence(Register.square(2, spacing=6), AnalogDevice)\n", + "seq = Sequence(Register.square(2, spacing=6, prefix=\"q\"), AnalogDevice)\n", "seq.declare_channel(\"rydberg\", \"rydberg_global\")\n", "\n", "seq.add(Pulse.ConstantPulse(100, 1, 0, 0), \"rydberg\")\n", diff --git a/tutorials/advanced_features/Virtual Devices.ipynb b/tutorials/advanced_features/Virtual Devices.ipynb index 75b80f62a..1dc783437 100644 --- a/tutorials/advanced_features/Virtual Devices.ipynb +++ b/tutorials/advanced_features/Virtual Devices.ipynb @@ -142,7 +142,9 @@ "# Enable the multiple declaration of a channel in a sequence\n", "VirtualAnalog3D = replace(VirtualAnalog3D, reusable_channels=True)\n", "# Creating a square register\n", - "reg = Register.square(4, spacing=5) # 4x4 array with atoms 5 um apart\n", + "reg = Register.square(\n", + " 4, spacing=5, prefix=\"q\"\n", + ") # 4x4 array with atoms 5 um apart\n", "# Building a sequence with the register and the virtual device\n", "seq = Sequence(reg, VirtualAnalog3D)\n", "# Declare twice the channel \"rydberg_global\"\n", diff --git a/tutorials/applications/QAOA and QAA to solve a QUBO problem.ipynb b/tutorials/applications/QAOA and QAA to solve a QUBO problem.ipynb index 624fc8948..254639ca1 100644 --- a/tutorials/applications/QAOA and QAA to solve a QUBO problem.ipynb +++ b/tutorials/applications/QAOA and QAA to solve a QUBO problem.ipynb @@ -183,7 +183,7 @@ "metadata": {}, "outputs": [], "source": [ - "qubits = dict(enumerate(coords))\n", + "qubits = {f\"q{i}\": coord for (i, coord) in enumerate(coords)}\n", "reg = Register(qubits)\n", "reg.draw(\n", " blockade_radius=DigitalAnalogDevice.rydberg_blockade_radius(1.0),\n", diff --git a/tutorials/creating_sequences.ipynb b/tutorials/creating_sequences.ipynb index ff9c86816..b5fc3aaf1 100644 --- a/tutorials/creating_sequences.ipynb +++ b/tutorials/creating_sequences.ipynb @@ -43,7 +43,7 @@ "square -= np.mean(square, axis=0)\n", "square *= 5\n", "\n", - "qubits = dict(enumerate(square))\n", + "qubits = {f\"q{i}\": point for (i, point) in enumerate(square)}\n", "reg = pulser.Register(qubits)" ] }, @@ -103,7 +103,9 @@ "metadata": {}, "outputs": [], "source": [ - "reg3 = pulser.Register.square(4, spacing=5) # 4x4 array with atoms 5 um apart\n", + "reg3 = pulser.Register.square(\n", + " 4, spacing=5, prefix=\"q\"\n", + ") # 4x4 array with atoms 5 um apart\n", "reg3.draw()" ] }, @@ -176,7 +178,7 @@ "print(\"Available channels after declaring 'ch0':\")\n", "pprint(seq.available_channels)\n", "\n", - "seq.declare_channel(\"ch1\", \"rydberg_local\", initial_target=4)\n", + "seq.declare_channel(\"ch1\", \"rydberg_local\", initial_target=\"q4\")\n", "print(\"\\nAvailable channels after declaring 'ch1':\")\n", "pprint(seq.available_channels)" ] @@ -219,7 +221,7 @@ "metadata": {}, "outputs": [], "source": [ - "seq.target(1, \"ch0\")" + "seq.target(\"q1\", \"ch0\")" ] }, { @@ -406,7 +408,7 @@ "metadata": {}, "outputs": [], "source": [ - "seq.target(4, \"ch0\")\n", + "seq.target(\"q4\", \"ch0\")\n", "seq.add(complex_pulse, \"ch0\")\n", "\n", "print(\"Current Schedule:\")\n", @@ -436,7 +438,7 @@ "metadata": {}, "outputs": [], "source": [ - "seq.target(0, \"ch1\")\n", + "seq.target(\"q0\", \"ch1\")\n", "seq.add(simple_pulse, \"ch1\", protocol=\"min-delay\")\n", "seq.add(simple_pulse, \"ch1\", protocol=\"wait-for-all\")\n", "\n", @@ -465,7 +467,7 @@ "metadata": {}, "outputs": [], "source": [ - "seq.target(0, \"ch0\")\n", + "seq.target(\"q0\", \"ch0\")\n", "seq.add(complex_pulse, \"ch0\", protocol=\"no-delay\")\n", "\n", "print(\"Current Schedule:\")\n", diff --git a/tutorials/quantum_simulation/Microwave-engineering of programmable XXZ Hamiltonians in arrays of Rydberg atoms.ipynb b/tutorials/quantum_simulation/Microwave-engineering of programmable XXZ Hamiltonians in arrays of Rydberg atoms.ipynb index a57a92ec0..2d0aad62f 100644 --- a/tutorials/quantum_simulation/Microwave-engineering of programmable XXZ Hamiltonians in arrays of Rydberg atoms.ipynb +++ b/tutorials/quantum_simulation/Microwave-engineering of programmable XXZ Hamiltonians in arrays of Rydberg atoms.ipynb @@ -108,7 +108,7 @@ "source": [ "# We take two atoms distant by 10 ums.\n", "coords = np.array([[0, 0], [10, 0]])\n", - "qubits = dict(enumerate(coords))\n", + "qubits = {f\"q{i}\": coord for (i, coord) in enumerate(coords)}\n", "reg = Register(qubits)\n", "\n", "seq = Sequence(reg, MockDevice)\n", @@ -284,7 +284,7 @@ "outputs": [], "source": [ "# Line geometry\n", - "reg = Register.rectangle(1, N_at, 10)\n", + "reg = Register.rectangle(1, N_at, 10, prefix=\"q\")\n", "magnetizations_obc = np.zeros((N_at, N_cycles), dtype=float)\n", "correl_obc = np.zeros(N_cycles, dtype=float)\n", "for m in range(N_cycles): # Runtime close to 2 min!\n", @@ -336,7 +336,7 @@ " ]\n", " )\n", ")\n", - "reg = Register.from_coordinates(coords)\n", + "reg = Register.from_coordinates(coords, prefix=\"q\")\n", "\n", "magnetizations_pbc = np.zeros((N_at, N_cycles), dtype=float)\n", "correl_pbc = np.zeros(N_cycles, dtype=float)\n", diff --git a/tutorials/quantum_simulation/Preparing state with antiferromagnetic order in the Ising model.ipynb b/tutorials/quantum_simulation/Preparing state with antiferromagnetic order in the Ising model.ipynb index 67ec882b6..de35b0067 100644 --- a/tutorials/quantum_simulation/Preparing state with antiferromagnetic order in the Ising model.ipynb +++ b/tutorials/quantum_simulation/Preparing state with antiferromagnetic order in the Ising model.ipynb @@ -467,7 +467,7 @@ " ) # To be a multiple of the clock period of AnalogDevice (4ns)\n", "\n", " R_interatomic = AnalogDevice.rydberg_blockade_radius(U)\n", - " reg = Register.rectangle(N, N, R_interatomic)\n", + " reg = Register.rectangle(N, N, R_interatomic, prefix=\"q\")\n", "\n", " # Pulse Sequence\n", " rise = Pulse.ConstantDetuning(\n", diff --git a/tutorials/quantum_simulation/Shadow estimation for VQS.ipynb b/tutorials/quantum_simulation/Shadow estimation for VQS.ipynb index 5c6a9e620..985e75e64 100644 --- a/tutorials/quantum_simulation/Shadow estimation for VQS.ipynb +++ b/tutorials/quantum_simulation/Shadow estimation for VQS.ipynb @@ -1,1408 +1,1403 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Efficient estimation techniques for Variational Quantum Simulation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Introduction" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$\\newcommand{\\ket}[1]{\\left|#1\\right>} \\newcommand{\\bra}[1]{\\left<#1\\right|}$\n", - "This notebook's purpose is to introduce the concept of classical shadow estimation, as well as its use in **VQS** (**V**ariational **Q**uantum **S**imulation). This technique, introduced in [this article by Huang, Kueng and Preskill](https://arxiv.org/abs/2002.08953), is used for efficiently estimating multiple observables, and is extremely powerful in that regard, asymptotically reaching theoretical lower bounds of quantum information theory regarding the number of required samples of a given state for estimation ([see here for details](https://arxiv.org/abs/2101.02464)). \n", - "\n", - "The primary goal of this notebook is to estimate the groundstate energy of the $H_2$ molecule, using a VQS. We will first implement the method of random classical shadows in Python. Then, we'll introduce its derandomized counterpart, which is particularly useful in our setting. We'll finally describe the VQS, and benchmark the estimation methods we introduced for computing the molecule's energy. This notebook draws some inspiration from [this PennyLane Jupyter notebook](https://pennylane.ai/qml/demos/tutorial_classical_shadows.html) on quantum machine learning and classical shadows." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Random classical shadows" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Main ideas and implementation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Classical shadow estimation relies on the fact that for a particular\n", - "choice of measurement, we can efficiently store snapshots of the state\n", - "that contain enough information to accurately predict linear functions\n", - "of observables.\n", - "\n", - "Let us consider an $n$-qubit quantum state $\\rho$ (prepared by a\n", - "pulse sequence) and apply a random unitary $U$ to the state:\n", - "\n", - "$$\\rho \\to U \\rho U^\\dagger.$$\n", - "\n", - "Next, we measure in the computational basis and obtain a bit string of\n", - "outcomes $|b\\rangle = |0011\\ldots10\\rangle$. If the unitaries $U$ are\n", - "chosen at random from a particular ensemble, then we can store the\n", - "reverse operation $U^\\dagger |b\\rangle\\langle b| U$ efficiently in\n", - "classical memory. We call this a *snapshot* of the state. Moreover, we\n", - "can view the average over these snapshots as a measurement channel:\n", - "\n", - "$$\\mathbb{E}\\left[U^\\dagger |b\\rangle\\langle b| U\\right] = \\mathcal{M}(\\rho).$$\n", - "\n", - "We restrict ourselves to unitary ensembles that define a tomographically complete set of\n", - "measurements (i.e $\\mathcal{M}$ is invertible), therefore :\n", - "\n", - "$$\\rho = \\mathbb{E}\\left[\\mathcal{M}^{-1}\\left(U^\\dagger |b\\rangle\\langle b| U \\right)\\right].$$\n", - "\n", - "If we apply the procedure outlined above $N$ times, then the collection\n", - "of inverted snapshots is what we call the *classical shadow*\n", - "\n", - "$$S(\\rho,N) = \\left\\{\\hat{\\rho}_1= \\mathcal{M}^{-1}\\left(U_1^\\dagger |b_1\\rangle\\langle b_1| U_1 \\right)\n", - ",\\ldots, \\hat{\\rho}_N= \\mathcal{M}^{-1}\\left(U_N^\\dagger |b_N\\rangle\\langle b_N| U_N \\right)\n", - "\\right\\}.$$\n", - "\n", - "Since the shadow approximates $\\rho$, we can now estimate **any**\n", - "observable with the empirical mean:\n", - "\n", - "$$\\langle O \\rangle = \\frac{1}{N}\\sum_i \\text{Tr}{\\hat{\\rho}_i O}.$$\n", - "\n", - "We will be using a median-of-means procedure in practice." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We start by defining several useful quantities, such as the unitary matrices associated with Pauli measurements : the Hadamard matrix, change of basis from $\\{\\ket{0}, \\ket{1}\\}$ to the eigenbasis of $\\sigma_X$, $\\{\\ket{+}, \\ket{-}\\}$, and its $\\sigma_Y, \\sigma_Z$ counterparts. We will then draw randomly from this tomographically complete set of $3$ unitaries.\n", - "\n", - "Note that we will need $4$ qubits for our VQS problem : we will explain the mapping from the molecule to qubits later." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import qutip\n", - "import matplotlib.pyplot as plt\n", - "from scipy.optimize import minimize\n", - "\n", - "from pulser import Register, Sequence, Pulse\n", - "from pulser.devices import DigitalAnalogDevice\n", - "from pulser_simulation import QutipEmulator" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "num_qubits = 4\n", - "zero_state = qutip.basis(2, 0).proj()\n", - "one_state = qutip.basis(2, 1).proj()\n", - "hadamard = 1 / np.sqrt(2) * qutip.Qobj([[1.0, 1.0], [1.0, -1.0]])\n", - "h_mul_phase = qutip.Qobj(np.array([[1.0, 1], [1.0j, -1.0j]])) / np.sqrt(2)\n", - "unitary_ensemble = [hadamard, h_mul_phase, qutip.qeye(2)]\n", - "\n", - "g = qutip.basis(2, 1)\n", - "r = qutip.basis(2, 0)\n", - "n = r * r.dag()\n", - "\n", - "sx = qutip.sigmax()\n", - "sy = qutip.sigmay()\n", - "sz = qutip.sigmaz()\n", - "\n", - "gggg = qutip.tensor([g, g, g, g])\n", - "ggrr = qutip.tensor([g, g, r, r])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We first define a function that spits out a random bitstring sampled from a given density matrix." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def measure_bitstring(state):\n", - " \"\"\"Auxiliary function that returns a bitstring according to the measure of a quantum state.\"\"\"\n", - " probs = np.real(state.diag())\n", - " probs /= np.sum(probs)\n", - " x = np.nonzero(np.random.multinomial(1, probs))[0][0]\n", - " bitstring = np.binary_repr(x, num_qubits)\n", - " return bitstring" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will need to compute the number of shadows needed given :\n", - "\n", - "* A list of observables $o_i$\n", - "* Desired precision on expectation values $\\epsilon$ : if $\\tilde{o}_i$ is the estimated expectation value for observable $o_i$, we wish for $|Tr(o_i \\rho) - \\tilde{o}_i| \\leq \\epsilon$\n", - "* Failure probability $\\delta$ : we wish for the above equation to be satisfied with probability $1-\\delta$\n", - "\n", - "Precise formulae are given in [Huang et al.](https://arxiv.org/abs/2002.08953)\n", - "The integer $K$ returned by the function will serve as the number of blocks in our median of means procedure afterwards." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "def compute_shadow_size(delta, epsilon, observables):\n", - " \"\"\"Helper function.\n", - "\n", - " Computes both the number of shadows needed as well as the size of blocks needed\n", - " for the median_of_means method in order to approximate the expectation value of M\n", - " (linear) observables with additive error epsilon and fail probability delta.\n", - "\n", - " Args:\n", - " delta (float): Failure probability.\n", - " epsilon (float): Additive error on expectation values.\n", - " observables (list[qutip.Qobj]): Observables the expectation value of which is to be computed.\n", - " \"\"\"\n", - " M = len(observables)\n", - " K = 2 * np.log(2 * M / delta)\n", - " shadow_norm = (\n", - " lambda op: np.linalg.norm(\n", - " op - np.trace(op) / 2 ** int(np.log2(op.shape[0])), ord=np.inf\n", - " )\n", - " ** 2\n", - " )\n", - " # Theoretical number of shadows per cluster in the median of means procedure :\n", - " # N = 34 * max(shadow_norm(o) for o in observables) / epsilon ** 2\n", - " # We use N = 20 here to allow for quick simulation\n", - " N = 20\n", - " return int(np.ceil(N * K)), int(K)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, we design a function that returns snapshots (bitstrings) of the rotated state as well as the sampled unitaries used to rotate the state $\\rho$." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "def calculate_classical_shadow(rho, shadow_size):\n", - " \"\"\"\n", - " Given a state rho, creates a collection of snapshots consisting of a bit string\n", - " and the index of a unitary operation.\n", - "\n", - " Returns:\n", - " Tuple of two numpy arrays. The first array contains measurement outcomes as bitstrings\n", - " while the second array contains the index for the sampled Pauli's (0,1,2=X,Y,Z).\n", - " \"\"\"\n", - " # sample random Pauli measurements uniformly\n", - " unitary_ids = np.random.randint(0, 3, size=(shadow_size, num_qubits))\n", - " outcomes = []\n", - " for ns in range(shadow_size):\n", - " unitmat = qutip.tensor(\n", - " [unitary_ensemble[unitary_ids[ns, i]] for i in range(num_qubits)]\n", - " )\n", - " outcomes.append(measure_bitstring(unitmat.dag() * rho * unitmat))\n", - "\n", - " # combine the computational basis outcomes and the sampled unitaries\n", - " return (outcomes, unitary_ids)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We then reconstruct an estimate of the quantum state from the sampled bitstrings, using the inverse quantum channel $\\mathcal{M}^{-1}$ defined above. In the particular case of Pauli measurements, we can actually compute the inverse channel : \n", - "\n", - "$$\\mathcal{M}^{-1} = \\otimes_{i=1}^n (3 U_i \\ket{b_i}\\bra{b_i} U^\\dagger_i - \\mathbb{1}_2)$$\n", - "\n", - "where $i$ runs over all qubits : $\\ket{b_i}$, $b_i \\in \\{0,1\\}$, is the single-bit snapshot of qubit $i$ and $U_i$ is the sampled unitary corresponding to the snapshot, acting on qubit $i$." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def snapshot_state(outcome_ns, unitary_ids_ns):\n", - " \"\"\"\n", - " Reconstructs an estimate of a state from a single snapshot in a shadow.\n", - "\n", - " Implements Eq. (S44) from https://arxiv.org/pdf/2002.08953.pdf\n", - "\n", - " Args:\n", - " outcome_ns: Bitstring at ns\n", - " unitary_ids_ns: Rotation applied at ns.\n", - "\n", - " Returns:\n", - " Reconstructed snapshot.\n", - " \"\"\"\n", - " state_list = []\n", - "\n", - " for k in range(num_qubits):\n", - " op = unitary_ensemble[unitary_ids_ns[k]]\n", - " b = zero_state if outcome_ns[k] == \"0\" else one_state\n", - " state_list.append(3 * op * b * op.dag() - qutip.qeye(2))\n", - "\n", - " return qutip.tensor(state_list)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We finally write a median of means procedure. We feed it an observable, the list of snapshots computed above and the number of blocks needed. It returns the median of the means of the observable acting on the snapshots in each block." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "def _median_of_means(obs, snap_list, K):\n", - " if K > len(snap_list): # preventing the n_blocks > n_observations\n", - " K = int(np.ceil(len(snap_list) / 2))\n", - " # dividing seq in K random blocks\n", - " indic = np.array((list(range(K)) * int(len(snap_list) / K)))\n", - " np.random.shuffle(indic)\n", - " # computing and saving mean per block\n", - " means = []\n", - " for block in range(K):\n", - " states = [snap_list[i] for i in np.where(indic == block)[0]]\n", - " exp = qutip.expect(obs, states)\n", - " means.append(np.mean(exp))\n", - " return np.median(means)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Reconstructing a given quantum state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us try out the efficiency of this method. We will reconstruct a given density matrix from classical shadows estimation, and observe the evolution of the trace distance between the original state and its reconstruction according to the number of shadows used." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "def state_reconstruction(snaps):\n", - " return sum(snaps) / len(snaps)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Original density matrix :\n", - "[[0.5+0.j 0.5+0.j 0. +0.j 0. +0.j]\n", - " [0.5+0.j 0.5+0.j 0. +0.j 0. +0.j]\n", - " [0. +0.j 0. +0.j 0. +0.j 0. +0.j]\n", - " [0. +0.j 0. +0.j 0. +0.j 0. +0.j]]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Shadow reconstruction :\n", - "[[0.49+0.j 0.51+0.j 0.01+0.j 0.02+0.01j]\n", - " [0.51-0.j 0.5 +0.j 0. -0.01j 0. -0.01j]\n", - " [0.01-0.j 0. +0.01j 0.01+0.j 0. +0.j ]\n", - " [0.02-0.01j 0. +0.01j 0. -0.j 0. +0.j ]]\n" - ] - } - ], - "source": [ - "num_qubits = 2\n", - "shadow_size = 10000\n", - "rho_1 = (\n", - " (\n", - " qutip.tensor([qutip.basis(2, 0), qutip.basis(2, 0)])\n", - " + qutip.tensor([qutip.basis(2, 0), qutip.basis(2, 1)])\n", - " )\n", - " .proj()\n", - " .unit()\n", - ")\n", - "print(\"Original density matrix :\")\n", - "print(rho_1.full())\n", - "outcomes, unitary_ids = calculate_classical_shadow(rho_1, shadow_size)\n", - "snapshots = [\n", - " snapshot_state(outcomes[ns], unitary_ids[ns]) for ns in range(shadow_size)\n", - "]\n", - "print(\"Shadow reconstruction :\")\n", - "print(np.around(state_reconstruction(snapshots).full(), 2))\n", - "\n", - "dist = np.zeros(5)\n", - "shadow_sizes = [100, 1000, 2000, 5000, 10000]\n", - "for i, shadow_size in enumerate(shadow_sizes):\n", - " outcomes, unitary_ids = calculate_classical_shadow(rho_1, shadow_size)\n", - " snapshots = [\n", - " snapshot_state(outcomes[ns], unitary_ids[ns])\n", - " for ns in range(shadow_size)\n", - " ]\n", - " dist[i] = qutip.tracedist(state_reconstruction(snapshots), rho_1)\n", - "num_qubits = 4" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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552aMGHCcfeDe+0/77//vkaOHKnrr79e8fHxGj58uF5++WXv+zt37lRxcbHPfYqKilJmZqbPvY6OjtbIkSO9bbKysmQ2m7Vq1Spvm8suu0w2m83bJjs7W5s3b9bhw4c7+jKDwsUXX6y8vDxt2bJFkvTtt9/qiy++0FVXXSWJe91ROvO++vO/KQSjTlRWVqbm5mafXxiSlJCQoOLi4gD1qmtxuVyaPXu2LrnkEp1//vmSpOLiYtlsNkVHR/u0Pfq+FhcXt3rfPe+dqE1lZaXq6uo64nKC0rJly/TNN99owYIFx73HvfafHTt26MUXX9TgwYP14YcfatasWbrnnnu0dOlSSS336kT/vSguLlZ8fLzP+1arVbGxse3699HdPfzww5o6daqGDBmikJAQDR8+XLNnz9b06dMlca87Smfe17banM59t7b7E0AA3XnnndqwYYO++OKLQHelW9q9e7fuvfdeffTRR3I4HIHuTrfmcrk0cuRI/eu//qskafjw4dqwYYOWLFmiG264IcC9617efvttvfHGG3rzzTd13nnnqbCwULNnz1ZSUhL3GsehYtSJ4uLiZLFYjlvBU1JSosTExAD1quu466679Ne//lWffPKJ+vfv7z2emJgop9Op8vJyn/ZH39fExMRW77vnvRO1iYyMVGhoqL8vJyitXbtWpaWluvDCC2W1WmW1WvXpp5/q+eefl9VqVUJCAvfaT/r27atzzz3X59g555yjoqIiSS336kT/vUhMTFRpaanP+01NTTp06FC7/n10dw888IC3anTBBRfoV7/6lX7zm994q6Lc647Rmfe1rTanc98JRp3IZrNpxIgRysvL8x5zuVzKy8vT6NGjA9iz4GYYhu666y699957+vjjjzVo0CCf90eMGKGQkBCf+7p582YVFRV57+vo0aP13Xff+fwF/OijjxQZGen95TR69Gifc3ja9KR/N5dffrm+++47FRYWel8jR47U9OnTvX/mXvvHJZdcctxjJ7Zs2aKBAwdKkgYNGqTExESf+1RZWalVq1b53Ovy8nKtXbvW2+bjjz+Wy+VSZmamt81nn32mxsZGb5uPPvpIZ599tmJiYjrs+oJJbW2tzGbfX3cWi0Uul0sS97qjdOZ99et/U9o9XRtnZNmyZYbdbjdyc3ONjRs3GrfeeqsRHR3ts4IHvmbNmmVERUUZ+fn5xv79+72v2tpab5vbb7/dGDBggPHxxx8bX3/9tTF69Ghj9OjR3vc9S8ivuOIKo7Cw0FixYoXRp0+fVpeQP/DAA8amTZuMxYsX97gl5K05elWaYXCv/WX16tWG1Wo1nnjiCWPr1q3GG2+8YYSFhRmvv/66t83ChQuN6Oho43/+53+M9evXG1dffXWrS52HDx9urFq1yvjiiy+MwYMH+yx1Li8vNxISEoxf/epXxoYNG4xly5YZYWFh3XoJ+bFuuOEGo1+/ft7l+u+++64RFxdnPPjgg9423OvTU1VVZaxbt85Yt26dIcl49tlnjXXr1hn//Oc/DcPovPu6cuVKw2q1Gk8//bSxadMmY/78+SzX70r++Mc/GgMGDDBsNpsxatQo46uvvgp0l4KapFZfr776qrdNXV2dcccddxgxMTFGWFiYcc011xj79+/3Oc+uXbuMq666yggNDTXi4uKM++67z2hsbPRp88knnxgZGRmGzWYzUlNTfb6jpzo2GHGv/ed///d/jfPPP9+w2+3GkCFDjJdeesnnfZfLZcydO9dISEgw7Ha7cfnllxubN2/2aXPw4EFj2rRpRnh4uBEZGWnMnDnTqKqq8mnz7bffGmPGjDHsdrvRr18/Y+HChR1+bcGksrLSuPfee40BAwYYDofDSE1NNX7729/6LP/mXp+eTz75pNX/Pt9www2GYXTufX377beNs846y7DZbMZ5551n/N///d9pXZPJMI569CcAAEAPxhwjAAAAN4IRAACAG8EIAADAjWAEAADgRjACAABwIxgBAAC4EYwAAADcCEYAAABuBCMAQcFkMmn58uV+P++NN96oiRMn+v28pyM3N1fR0dGB7gaAEyAYAehwBw4c0KxZszRgwADZ7XYlJiYqOztbK1euDHTXOtWUKVO0ZcuWQHcDwAlYA90BAN3ftddeK6fTqaVLlyo1NVUlJSXKy8vTwYMHA921ThUaGqrQ0NBAdwPACVAxAtChysvL9fnnn+vJJ5/Uj3/8Yw0cOFCjRo3SnDlz9Itf/MKnbVlZma655hqFhYVp8ODBev/9973vNTc366abbtKgQYMUGhqqs88+W3/4wx98Pt/c3KycnBxFR0erd+/eevDBB3XsdpANDQ265557FB8fL4fDoTFjxmjNmjXe90eOHKmnn37a+/PEiRMVEhKi6upqSdKePXtkMpm0bdu2Vq/322+/1Y9//GNFREQoMjJSI0aM0Ndffy3p+KG0lJQUmUym414eu3fv1uTJkxUdHa3Y2FhdffXV2rVr1yncdQCni2AEoEOFh4crPDxcy5cvV0NDwwnbPvbYY5o8ebLWr1+v8ePHa/r06Tp06JAkyeVyqX///nrnnXe0ceNGzZs3T4888ojefvtt7+efeeYZ5ebm6pVXXtEXX3yhQ4cO6b333vP5jgcffFD//d//raVLl+qbb75Renq6srOzvd8zduxY5efnS5IMw9Dnn3+u6OhoffHFF5KkTz/9VP369VN6enqr1zB9+nT1799fa9as0dq1a/Xwww8rJCSk1bZr1qzR/v37tX//fu3Zs0c/+tGPdOmll0qSGhsblZ2drYiICH3++edauXKlwsPDdeWVV8rpdJ7krgM4bQYAdLD/+q//MmJiYgyHw2FcfPHFxpw5c4xvv/3Wp40k43e/+5335+rqakOS8be//a3N8955553Gtdde6/25b9++xqJFi7w/NzY2Gv379zeuvvpq7zlDQkKMN954w9vG6XQaSUlJ3s+9//77RlRUlNHU1GQUFhYaiYmJxr333ms89NBDhmEYxs0332z88pe/bLNPERERRm5ubqvvvfrqq0ZUVFSr791zzz3GwIEDjdLSUsMwDOO1114zzj77bMPlcnnbNDQ0GKGhocaHH37Y5vcDODNUjAB0uGuvvVb79u3T+++/ryuvvFL5+fm68MILlZub69Nu6NCh3j/36tVLkZGRKi0t9R5bvHixRowYoT59+ig8PFwvvfSSioqKJEkVFRXav3+/MjMzve2tVqtGjhzp/Xn79u1qbGzUJZdc4j0WEhKiUaNGadOmTZKkSy+9VFVVVVq3bp0+/fRTjR07VuPGjfNWkT799FONGzeuzWvNycnRzTffrKysLC1cuFDbt28/6f156aWX9J//+Z96//331adPH0lHhuS2bdumiIgIb9UtNjZW9fX1p3ROAKeHYASgUzgcDv30pz/V3Llz9eWXX+rGG2/U/PnzfdocO+RkMpnkcrkkScuWLdP999+vm266SX//+99VWFiomTNn+n1YKTo6WsOGDVN+fr43BF122WVat26dtmzZoq1bt2rs2LFtfv7RRx/V999/rwkTJujjjz/Wueeee9xw3tE++eQT3X333frzn//sEwyrq6s1YsQIFRYW+ry2bNmiX/7yl369ZgAtCEYAAuLcc89VTU3NKbdfuXKlLr74Yt1xxx0aPny40tPTfSonUVFR6tu3r1atWuU91tTUpLVr13p/TktLk81m83lMQGNjo9asWaNzzz3Xe2zs2LH65JNP9Nlnn2ncuHGKjY3VOeecoyeeeEJ9+/bVWWeddcK+nnXWWfrNb36jv//975o0aZJeffXVVttt27ZN1113nR555BFNmjTJ570LL7xQW7duVXx8vNLT031eUVFRp3bTALQbwQhAhzp48KB+8pOf6PXXX9f69eu1c+dOvfPOO1q0aJGuvvrqUz7P4MGD9fXXX+vDDz/Uli1bNHfuXJ/VZJJ07733auHChVq+fLl++OEH3XHHHSovL/e+36tXL82aNUsPPPCAVqxYoY0bN+qWW25RbW2tbrrpJm+7cePG6cMPP5TVatWQIUO8x954440TVovq6up01113KT8/X//85z+1cuVKrVmzRuecc06rbX/+859r+PDhuvXWW1VcXOx9SUcmccfFxenqq6/W559/rp07dyo/P1/33HOP9uzZc8r3DUD78BwjAB0qPDxcmZmZ+rd/+zfvHJ/k5GTdcssteuSRR075PLfddpvWrVunKVOmyGQyadq0abrjjjv0t7/9zdvmvvvu0/79+3XDDTfIbDbr17/+ta655hpVVFR42yxcuFAul0u/+tWvVFVVpZEjR+rDDz9UTEyMt82ll14ql8vlE4LGjRunP/zhDyecX2SxWHTw4EHNmDFDJSUliouL06RJk/TYY48d17akpEQ//PCDfvjhByUlJfm8ZxiGwsLC9Nlnn+mhhx7SpEmTVFVVpX79+unyyy9XZGTkKd83AO1jMoxjHvIBAADQQzGUBgAA4EYwAgAAcCMYAQAAuBGMAAAA3AhGAAAAbgQjAAAAN4IRAACAG8EIAADAjWAEAADgRjACAABwIxgBAAC4/X88cMZysMKyMAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(shadow_sizes, dist)\n", - "plt.xlabel(\"Shadow size\")\n", - "plt.ylabel(r\"$||\\rho - \\hat{\\rho}||_1$\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can expect, the estimation gets better and better as shadow size gets larger, with about $2$% accuracy at $10000$ shadows. This mostly serves as a reality check, as we will be using classical shadows to estimate observables acting on quantum states, not to reconstruct those states." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Derandomized Paulis" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Derandomization Algorithm" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Randomized classical shadows are useful when dealing with low-weight, general observables. However, suppose, as is the case when estimating the Hamiltonian of the $H_2$ molecule written as a sum of Pauli strings, that we're dealing with Pauli observables of varying weights. In this setting, choosing wisely each Pauli measurement instead of randomly drawing a basis is particularly useful : indeed, say one wants to measure observable $\\sigma_x^1 \\otimes \\sigma_x^2 \\otimes \\dots \\otimes \\sigma_x^n$. Using random rotations in each Pauli $X,Y$ or $Z$ basis and projection in the $Z$ (computational) basis, there is a probability $\\frac{1}{3^n}$ to get each measurement basis right (i.e. rotate the system using the Hadamard matrix). This is extremely unlikely and unefficient as the number of qubits goes up. [Huang et al](https://arxiv.org/abs/2103.07510) outline an interesting greedy algorithm used for choosing suitable measurement bases for the efficient estimation of $L$ $n-$qubit Pauli strings, $\\{O_i\\}$. \n", - "\n", - "Feeding these observables and chosen Pauli measurements {P_i} as input, the algorithm aims at optimizing a certain cost function. This function, labeled $Conf_\\epsilon(O_i, P_j)$ is such that, if $Conf_\\epsilon(O_i, P_j) \\leq \\frac{\\delta}{2}$, then the empirical averages $\\tilde{\\omega_l}$ of each Pauli observable $O_l$ will be $\\epsilon$-close to its true average $Tr(\\rho O_l)$ with probability $1-\\delta$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to implement this cost function, we first need to design two auxiliary functions. The first one decides if a given Pauli measurement $p$ is compatible with (\"hits\") a Pauli observable $o$. This means that each time $o$ acts non-trivially on a qubit $q_i$ with Pauli matrix $\\sigma \\in \\{\\sigma_X, \\sigma_Y, \\sigma_Z\\}, \\sigma \\neq \\mathbb{1}$, $p$ acts on $q_i$ with $\\sigma$. We denote it by $o \\triangleright p$." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "def hits(p, o, end=-1):\n", - " \"\"\"Determines if measurement p hits observable o\n", - "\n", - " Args:\n", - " p (str): Pauli string in str format (ex \"XYZ\"), measurement\n", - " o (str): same as above, observable (ex \"11ZY\")\n", - " end (int): index before which to check if p hits o\n", - " \"\"\"\n", - " if end != -1:\n", - " o = o[:end]\n", - " for i, x in enumerate(o):\n", - " if not (x == p[i] or x == \"1\"):\n", - " return False\n", - " return True" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The second function simply computes the number of qubits observable $o$ acts non-trivially upon." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def weight(o, start=0):\n", - " o_k = o[start:]\n", - " return len(o_k) - o_k.count(\"1\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now implement the conditioned cost function using these auxiliary functions. We call it \"conditioned\", since we feed it only the first $m \\times n + k$ single-qubit Pauli measurements, and average over the others, not yet determined ones." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "def cond_conf(o, P_sharp):\n", - " \"\"\"Returns the (modified) conditionned expectation value of the cost function depending\n", - " on already chosen Paulis in P_sharp.\n", - "\n", - " Args:\n", - " o (list[str]): list of Pauli strings to be measured\n", - " P_sharp (list[str]): list of already chosen Paulis\n", - " \"\"\"\n", - " # Hyperparameters : see Huang et al. for more details\n", - " eta = 0.9\n", - " nu = 1 - np.exp(-eta / 2)\n", - " L = len(o)\n", - " m = len(P_sharp) - 1 # index of last chosen Pauli string\n", - " k = (\n", - " len(P_sharp[-1]) - 1\n", - " ) # index of last chosen Pauli matrix in mth Pauli string\n", - " result = 0\n", - " for l in range(0, L):\n", - " v = 0\n", - " for m_prime in range(0, m):\n", - " v += (eta / 2) * int(hits(P_sharp[m_prime], o[l]))\n", - " v -= np.log(\n", - " 1\n", - " - (nu / 3 ** (weight(o[l], start=k + 1)))\n", - " * hits(P_sharp[m], o[l], end=k + 1)\n", - " )\n", - " result += np.exp(-v)\n", - " return result" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we design a simple greedy algorithm which purpose is to minimize this conditioned cost function, choosing one single-qubit Pauli at a time." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "def derandomization(M, o):\n", - " \"\"\"Derandomization algorithm returning best Pauli indices according to a greedy algorithm\n", - " that aims at minimizing the cost function above.\n", - "\n", - " Args:\n", - " M (int): number of measurements\n", - " n (int): number of qubits (size of Pauli strings)\n", - " epsilon (float): desired accuracy on observable expectation values\n", - " o (list[str]): list of Pauli strings to be measured\n", - " \"\"\"\n", - " n = len(o[0])\n", - " P_sharp = []\n", - " for m in range(M):\n", - " P_sharp.append(\"\")\n", - " for k in range(n):\n", - " P_sharp_m = P_sharp[m]\n", - " P_sharp[m] += \"X\"\n", - " valmin = cond_conf(o, P_sharp)\n", - " argmin = \"X\"\n", - " for W in [\"Y\", \"Z\"]:\n", - " P_sharp[m] = P_sharp_m + W\n", - " val_W = cond_conf(o, P_sharp)\n", - " if val_W < valmin:\n", - " valmin = val_W\n", - " argmin = W\n", - " P_sharp[m] = P_sharp_m + argmin\n", - " return P_sharp" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Estimating expectation values from Pauli measurements" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we have our Pauli measurements, we proceed differently from randomized classical shadows, where we gave an estimate of the actual quantum channels. Here, we're only interested in the Pauli averages $\\tilde{\\omega}_l$, that we can infer from Pauli measurements $p$ that **hit** observable $o_l$. Indeed, we have the following formula :\n", - "\n", - "$$\\tilde{\\omega}_{l}=\\frac{1}{h\\left(\\mathbf{o}_{l} ;\\left[\\mathbf{p}_{1}, \\ldots, \\mathbf{p}_{M}\\right]\\right)} \\sum_{m: \\mathbf{o}_{l} \\triangleright \\mathbf{p}_{m}} \\prod_{j: \\mathbf{o}_{l}[j] \\neq I} \\mathbf{q}_{m}[j]$$\n", - "\n", - "where $h\\left(\\mathbf{o}_{l} ;\\left[\\mathbf{p}_{1}, \\ldots, \\mathbf{p}_{M}\\right]\\right)$ is the number of times a Pauli measurement $p_i$ is such that $o \\triangleright p_i$, and $\\mathbf{q}_m$ is the output of the measurement of Pauli string $p_m$ ($\\mathbf{q}_m \\in \\{\\pm 1\\}^n$)." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "def _pauli_index(letter):\n", - " if letter == \"X\":\n", - " return 0\n", - " elif letter == \"Y\":\n", - " return 1\n", - " else:\n", - " return 2" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "def pauli_string_value(x, sigma):\n", - " \"\"\"Returns the evaluation of a Pauli string sigma in a bitstring state $|x>$,\n", - " assuming the state is already rotated in the needed eigenbases of all single-qubit Paulis.\n", - "\n", - " NB : Faster than using qutip.measure due to not returning the eigenstates...\n", - "\n", - " Args:\n", - " x (str): input bitstring\n", - " sigma (str): input Pauli string to be measured on |x>\n", - " \"\"\"\n", - " outcomes = []\n", - " for i, q in enumerate(x):\n", - " if q == \"0\":\n", - " outcomes.append((sigma[i], 1))\n", - " else:\n", - " outcomes.append((sigma[i], -1))\n", - " return outcomes" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "def classical_shadow_derand(rho, measurements):\n", - " \"\"\"Returns the n-strings of ±1 corresponding to measurements in the input list on state rho.\n", - "\n", - " Args:\n", - " rho (qutip.Qobj): input state as a density matrix\n", - " measurements (list[str]): derandomized measurement bases in which to measure state rho\n", - "\n", - " Returns:\n", - " Tuple of two numpy arrays. The first array contains measurement outcomes as bitstrings\n", - " while the second array contains the index for the derandomized Pauli's (0,1,2=X,Y,Z).\n", - " \"\"\"\n", - " # Fill the unitary ids with derandomized measurements ids\n", - " shadow_size = len(measurements)\n", - " outcomes = []\n", - " for ns in range(shadow_size):\n", - " # multi-qubit change of basis\n", - " unitmat = qutip.tensor(\n", - " [\n", - " unitary_ensemble[_pauli_index(measurements[ns][i])]\n", - " for i in range(num_qubits)\n", - " ]\n", - " )\n", - " x = measure_bitstring(unitmat.dag() * rho * unitmat)\n", - " outcomes.append(pauli_string_value(x, measurements[ns]))\n", - " # ±1 strings\n", - " return outcomes" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "def exp_value(input_pauli, pm_strings):\n", - " \"\"\"Computes an estimation of the expectation value of a given Pauli string given multiple ±1 bitstring\n", - " outcomes.\n", - " \"\"\"\n", - " sum_product, cnt_match = 0, 0\n", - "\n", - " for single_measurement in pm_strings:\n", - " not_match = False\n", - " product = 1\n", - "\n", - " for i, pauli in enumerate(input_pauli):\n", - " if pauli != single_measurement[i][0] and pauli != \"1\":\n", - " not_match = True\n", - " break\n", - " if pauli != \"1\":\n", - " product *= single_measurement[i][1]\n", - " if not_match:\n", - " continue\n", - "\n", - " sum_product += product\n", - " cnt_match += 1\n", - " if cnt_match == 0:\n", - " return f\"No measurement given for {input_pauli}\"\n", - " return sum_product / cnt_match" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Variational Quantum Simulation for the $H_2$ molecule" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The main problem with usual variational classical algorithms, the classical counterparts of VQS, is computing the value of the $2^n \\times 2^n$ matrix on the output state vector $\\bra{\\psi}H\\ket{\\psi}$ after each loop of the algorithm, which grows exponentially in the size of the system. The purpose of VQS algorithms is to offer a solution which time complexity only grows polynomially, thanks to reading all the important properties on the quantum state. Therefore, we need accurate and efficient methods to estimate these properties, which we'll present afterwards.\n", - "\n", - "For now, let's focus on what makes a VQS algorithm, specifically for computing the groundstate energy of the $H_2$ molecule." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Jordan-Wigner Hamiltonian (cost function)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We need to write the Hamiltonian in a way that's compatible with the formalism of quantum computing. We first second-quantize the Hamiltonian, obtaining an expression in terms of fermionic operators $a, a^\\dagger$. Then, we use the Jordan-Wigner transformation, which maps the fermionic operators to Pauli matrices. We obtain the Hamiltonian below, acting on $4$ qubits, decomposed in terms of the coefficients in front of the Pauli matrices.\n", - "\n", - "[This article by Seeley et al.](https://math.berkeley.edu/~linlin/2018Spring_290/SRL12.pdf) gives us the value of \n", - "$H_{JW}$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$H_{J W}=-0.81261 \\mathbb{1}+0.171201 \\sigma_{0}^{z}+0.171201 \\sigma_{1}^{z}-0.2227965 \\sigma_{2}^{z} \\\\\n", - "-0.2227965 \\sigma_{3}^{z} +0.16862325 \\sigma_{1}^{z} \\sigma_{0}^{z}+0.12054625 \\sigma_{2}^{z} \\sigma_{0}^{z} \\\\\n", - "+0.165868 \\sigma_{2}^{z} \\sigma_{1}^{z}+0.165868 \\sigma_{3}^{z} \\sigma_{0}^{z} +0.12054625 \\sigma_{3}^{z}\\sigma_{1}^{z} \\\\\n", - "+0.17434925 \\sigma_{3}^{z} \\sigma_{2}^{z}-0.04532175 \\sigma_{3}^{x} \\sigma_{2}^{x} \\sigma_{1}^{y} \\sigma_{0}^{y}\\\\\n", - "+0.04532175 \\sigma_{3}^{x} \\sigma_{2}^{y} \\sigma_{1}^{y} \\sigma_{0}^{x}+0.04532175 \\sigma_{3}^{y} \\sigma_{2}^{x}\n", - "\\sigma_{1}^{x} \\sigma_{0}^{y} -0.04532175 \\sigma_{3}^{y} \\sigma_{2}^{y} \\sigma_{1}^{x} \\sigma_{0}^{x}$$" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "def pauli(positions=[], operators=[]):\n", - " op_list = [\n", - " operators[positions.index(j)] if j in positions else qutip.qeye(2)\n", - " for j in range(num_qubits)\n", - " ]\n", - " return qutip.tensor(op_list)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "coeff_fact = [\n", - " 0.81261,\n", - " 0.171201,\n", - " 0.2227965,\n", - " 0.16862325,\n", - " 0.174349,\n", - " 0.12054625,\n", - " 0.165868,\n", - " 0.04532175,\n", - "]\n", - "\n", - "paulis = [\n", - " pauli(),\n", - " pauli([0], [sz]) + pauli([1], [sz]),\n", - " pauli([2], [sz]) + pauli([3], [sz]),\n", - " pauli([1, 0], [sz, sz]),\n", - " pauli([3, 2], [sz, sz]),\n", - " pauli([2, 0], [sz, sz]) + pauli([3, 1], [sz, sz]),\n", - " pauli([2, 1], [sz, sz]) + pauli([3, 0], [sz, sz]),\n", - " pauli([3, 2, 1, 0], [sx, sx, sy, sy])\n", - " + pauli([3, 2, 1, 0], [sy, sy, sx, sx]),\n", - " pauli([3, 2, 1, 0], [sx, sy, sy, sx])\n", - " + pauli([3, 2, 1, 0], [sy, sx, sx, sy]),\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# H2 Molecule : 4 qubits in Jordan-Wigner mapping of the Hamiltonian\n", - "a = 10\n", - "reg = Register.from_coordinates(\n", - " [\n", - " [0, 0],\n", - " [a, 0],\n", - " [0.5 * a, a * np.sqrt(3) / 2],\n", - " [0.5 * a, -a * np.sqrt(3) / 2],\n", - " ]\n", - ")\n", - "reg.draw()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us keep the exact ground-state energy of the molecule for future reference, by diagonalizing it exactly - this is possible for such a small system, however, this quickly becomes an intractable problem for large molecules." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-1.8510459284448646\n" - ] - } - ], - "source": [ - "def cost_hamiltonian_JW():\n", - " H = (\n", - " -coeff_fact[0] * paulis[0]\n", - " + coeff_fact[1] * paulis[1]\n", - " - coeff_fact[2] * paulis[2]\n", - " + coeff_fact[3] * paulis[3]\n", - " + coeff_fact[4] * paulis[4]\n", - " + coeff_fact[5] * paulis[5]\n", - " + coeff_fact[6] * paulis[6]\n", - " - coeff_fact[7] * paulis[7]\n", - " + coeff_fact[7] * paulis[8]\n", - " )\n", - " return H\n", - "\n", - "\n", - "global H\n", - "H = cost_hamiltonian_JW()\n", - "exact_energy, ground_state = cost_hamiltonian_JW().groundstate()\n", - "print(exact_energy)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Quantum Loop (VQS)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Much like in the *Using QAOA to solve a QUBO problem* notebook, we will use a mixed classical-quantum approach for minimizing the energy. The quantum part will do the exploration in Hilbert space, according to a certain set of parameters $\\theta_i, \\tau_j$, and the classical part will find the optimal parameters given the value of the energy after each loop. For now, we will ignore sampling problems and simply compute the exact expectation value of $H_{JW}$. See [this article by Xiao Yuan et al.](https://arxiv.org/abs/1812.08767) for details about VQS algorithms." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Two mixing Hamiltonians are used for the exploration of the solution space :\n", - "$H_1 = \\hbar / 2 \\sum_i \\sigma_i^x + \\sum_{j" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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+GONGTNG06ZN07Fjx5wuzbJNmzZp0aJF2rp1q9atW6fa2lpNnTpVJ0+ebNJu/vz5Ki8v908PPfSQQxV3zIgRI5rU/9577/mX3XnnnXr11Vf14osvatOmTTp8+LCuuuoqB6u1Zvv27U32bd26dZKk2bNn+9uE2/E7efKkxowZo8cff7zF5Q899JB+//vf68knn9S2bdvUs2dPTZs2TadPn/a3mTNnjj777DOtW7dOr732mt59910tWLCgs3ahXW3t43fffaePP/5Y9957rz7++GO99NJLKikp0eWXX96s7QMPPNDk2P785z/vjPLb1d4xlKTp06c3qf0vf/lLk+XhfAwlNdm38vJyPfXUU3K5XLr66qubtAvVYxjI+aG9v5/19fWaOXOmampq9MEHH6iwsFAFBQVatmxZ8Ao1CMiFF15oFi1a5P9cX19vMjIyTH5+voNVBcexY8eMJLNp0yb/vEsuucT84he/cK6oM3TfffeZMWPGtLisoqLCdOvWzbz44ov+eV988YWRZLZs2dJJFQbXL37xCzN48GDT0NBgjAn/4yfJrF692v+5oaHBpKenm5UrV/rnVVRUmLi4OPOXv/zFGGPM559/biSZ7du3+9v8/e9/Ny6Xyxw6dKjTag/UP+5jSz788EMjyRw4cMA/Lysryzz66KP2FhcELe3fDTfcYK644opW14nEY3jFFVeYyy67rMm8cDmGxjQ/PwTy9/P11183UVFR5siRI/42TzzxhElKSjLV1dVBqYuelwDU1NRox44dmjJlin9eVFSUpkyZoi1btjhYWXB4vV5JUu/evZvM/9///V+lpqZq5MiRysvL03fffedEeR22d+9eZWRkKDs7W3PmzJHH45Ek7dixQ7W1tU2OZ25urgYMGBCWx7OmpkbPPvusfvaznzV5GWm4H78f2r9/v44cOdLkmLndbo0bN85/zLZs2aLk5GSdf/75/jZTpkxRVFSUtm3b1uk1B4PX65XL5VJycnKT+Q8++KBSUlI0duxYrVy5Mqjd8XbbuHGj+vTpo6FDh2rhwoU6fvy4f1mkHcOjR49q7dq1uummm5otC5dj+I/nh0D+fm7ZskWjRo1S3759/W2mTZsmn8+nzz77LCh1RdyLGe3wzTffqL6+vsmBkKS+ffuquLjYoaqCo6GhQXfccYcuuugijRw50j//uuuuU1ZWljIyMrRr1y7dfffdKikp0UsvveRgtYEbN26cCgoKNHToUJWXl2v58uX60Y9+pN27d+vIkSOKjY1tdkLo27evjhw54kzBZ+Dll19WRUWF5s2b558X7sfvHzUel5Z+BxuXHTlyRH369GmyPCYmRr179w7L43r69Gndfffduvbaa5u89O7222/Xueeeq969e+uDDz5QXl6eysvL9cgjjzhYbWCmT5+uq666SoMGDVJpaanuuecezZgxQ1u2bFF0dHTEHcPCwkIlJiY2uyQdLsewpfNDIH8/jxw50uLvauOyYCC8dHGLFi3S7t27m4wHkdTkGvOoUaPUr18/TZ48WaWlpRo8eHBnl2nZjBkz/D+PHj1a48aNU1ZWlv7v//5P8fHxDlYWfKtWrdKMGTOUkZHhnxfux6+rq62t1U9+8hMZY/TEE080WXbXXXf5fx49erRiY2P1b//2b8rPzw/5x9D/9Kc/9f88atQojR49WoMHD9bGjRs1efJkByuzx1NPPaU5c+aoe/fuTeaHyzFs7fwQCrhsFIDU1FRFR0c3G0199OhRpaenO1TVmbvtttv02muvacOGDcrMzGyz7bhx4yRJ+/bt64zSgi45OVlDhgzRvn37lJ6erpqaGlVUVDRpE47H88CBA3r77bd18803t9ku3I9f43Fp63cwPT292QD6uro6nThxIqyOa2NwOXDggNatW9ek16Ul48aNU11dnb766qvOKTCIsrOzlZqa6v93GSnHUJI2b96skpKSdn83pdA8hq2dHwL5+5ment7i72rjsmAgvAQgNjZW5513ntavX++f19DQoPXr12v8+PEOVtYxxhjddtttWr16td555x0NGjSo3XWKiookSf369bO5OntUVVWptLRU/fr103nnnadu3bo1OZ4lJSXyeDxhdzyffvpp9enTRzNnzmyzXbgfv0GDBik9Pb3JMfP5fNq2bZv/mI0fP14VFRXasWOHv80777yjhoYGf3gLdY3BZe/evXr77beVkpLS7jpFRUWKiopqdrklHJSVlen48eP+f5eRcAwbrVq1Suedd57GjBnTbttQOobtnR8C+fs5fvx4ffrpp02CaGMQHz58eNAKRQCef/55ExcXZwoKCsznn39uFixYYJKTk5uMpg4XCxcuNG6322zcuNGUl5f7p++++84YY8y+ffvMAw88YD766COzf/9+88orr5js7GwzceJEhysP3OLFi83GjRvN/v37zfvvv2+mTJliUlNTzbFjx4wxxtxyyy1mwIAB5p133jEfffSRGT9+vBk/frzDVVtTX19vBgwYYO6+++4m88P1+FVWVpqdO3eanTt3GknmkUceMTt37vTfafPggw+a5ORk88orr5hdu3aZK664wgwaNMicOnXKv43p06ebsWPHmm3btpn33nvPnH322ebaa691apeaaWsfa2pqzOWXX24yMzNNUVFRk9/Nxjs0PvjgA/Poo4+aoqIiU1paap599lmTlpZm5s6d6/Cefa+t/ausrDS//OUvzZYtW8z+/fvN22+/bc4991xz9tlnm9OnT/u3Ec7HsJHX6zU9evQwTzzxRLP1Q/0Ytnd+MKb9v591dXVm5MiRZurUqaaoqMi88cYbJi0tzeTl5QWtTsKLBY899pgZMGCAiY2NNRdeeKHZunWr0yV1iKQWp6efftoYY4zH4zETJ040vXv3NnFxcSYnJ8csWbLEeL1eZwu34JprrjH9+vUzsbGx5qyzzjLXXHON2bdvn3/5qVOnzK233mp69eplevToYWbNmmXKy8sdrNi6N99800gyJSUlTeaH6/HbsGFDi/8ub7jhBmPM97dL33vvvaZv374mLi7OTJ48udm+Hz9+3Fx77bUmISHBJCUlmRtvvNFUVlY6sDcta2sf9+/f3+rv5oYNG4wxxuzYscOMGzfOuN1u0717dzNs2DDzH//xH01O/k5qa/++++47M3XqVJOWlma6detmsrKyzPz585v9B2A4H8NGf/zjH018fLypqKhotn6oH8P2zg/GBPb386uvvjIzZsww8fHxJjU11SxevNjU1tYGrU7X/y8WAAAgLDDmBQAAhBXCCwAACCuEFwAAEFYILwAAIKwQXgAAQFghvAAAgLBCeAEAAGGF8AIAAMIK4QUAAIQVwgsAAAgrhBcAABBWCC8AACCs/D8m/7qANeh5qwAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(\n", - " [quantum_loop(pars, gggg) for pars in loop_ising_results.allvecs], \"k\"\n", - ")\n", - "plt.axhline(exact_energy, color=\"red\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Seems like we can cut on calculation time by only allowing $100$ iterations, since we don't get much more accurate afterwards." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Estimating Jordan-Wigner $H_2$ Hamiltonian with classical shadows" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Randomized measurements" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now consider the real-life problem where we don't have access to the exact value $\\bra{\\Psi(\\theta_i, \\tau_j)} H_{JW} \\ket{\\Psi(\\theta_i, \\tau_j)}$. It can be estimated with classical shadows.\n", - "We modify the quantum loop to add classical shadow estimation of the several Pauli strings making up the $H_{JW}$ Hamiltonian : this is the perfect setting to do so, because we have multiple Pauli strings and most of them have low weight." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "def exp_value_JW(exp_values):\n", - " return (\n", - " -coeff_fact[0] * exp_values[0]\n", - " + coeff_fact[1] * exp_values[1]\n", - " - coeff_fact[2] * exp_values[2]\n", - " + coeff_fact[3] * exp_values[3]\n", - " + coeff_fact[4] * exp_values[4]\n", - " + coeff_fact[5] * exp_values[5]\n", - " + coeff_fact[6] * exp_values[6]\n", - " - coeff_fact[7] * exp_values[7]\n", - " + coeff_fact[7] * exp_values[8]\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [], - "source": [ - "def quantum_loop_shadows(param, in_state, shadow_size=20, r=reg):\n", - " \"\"\"\n", - " Args:\n", - " param (np.array): time parameters for each mixing Hamiltonian. There are 2p time parameters in param.\n", - " in_state (qubit.Qobj): initial state.\n", - " \"\"\"\n", - " seq = Sequence(r, DigitalAnalogDevice)\n", - " seq.declare_channel(\"ch0\", \"rydberg_global\")\n", - " middle = len(param) // 2\n", - "\n", - " for tau, t in zip(param[middle:], param[:middle]):\n", - " pulse_1 = Pulse.ConstantPulse(tau, 1.0, 0, 0)\n", - " pulse_2 = Pulse.ConstantPulse(t, 1.0, 1.0, 0)\n", - " seq.add(pulse_1, \"ch0\")\n", - " seq.add(pulse_2, \"ch0\")\n", - "\n", - " seq.measure(\"ground-rydberg\")\n", - " simul = QutipEmulator.from_sequence(seq, sampling_rate=0.01)\n", - " simul.set_initial_state(in_state)\n", - "\n", - " # Classical shadow estimation\n", - " # Theoretical shadow size and number of clusters :\n", - " # shadow_size, K = compute_shadow_size(0.1, 0.5, paulis)\n", - " # We use K=4 to allow for quick simulation\n", - " K = 4\n", - " rho = simul.run().get_final_state().proj()\n", - " outcomes, unitary_ids = calculate_classical_shadow(rho, shadow_size)\n", - " snapshots = [\n", - " snapshot_state(outcomes[ns], unitary_ids[ns])\n", - " for ns in range(shadow_size)\n", - " ]\n", - " meds = [_median_of_means(obs, snapshots, K) for obs in paulis]\n", - " return exp_value_JW(meds)\n", - "\n", - "\n", - "def loop_JW_shadows(param, in_state, shadow_size=20):\n", - " res = minimize(\n", - " quantum_loop_shadows,\n", - " param,\n", - " method=\"Nelder-Mead\",\n", - " args=(in_state, shadow_size),\n", - " options={\"return_all\": True, \"maxiter\": 100, \"adaptive\": True},\n", - " )\n", - " return res" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [], - "source": [ - "shadow_sizes = [10, 20, 40, 60, 80, 100]\n", - "energies = []\n", - "for shadow_size in shadow_sizes:\n", - " energies.append(\n", - " abs(\n", - " loop_JW_shadows(param, gggg, shadow_size=shadow_size).fun\n", - " - exact_energy\n", - " )\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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VsjPh+UBAQBA4+aywL148WJ9+umnKisrU//+/TV27FhFR0e7aroqunfvru7defrZHHUJba0uIb7ac6REP+w6opF9I8wuCQAAoFYueWly7ty5mjJlimw2m4KCgvTll1+qb9++euihh1wxnSRp9+7duvXWW3Xttdfq/vvv17///W9t2rRJdrvdZXPCHJVPuVdk5ppcCQAAwLm55An3iy++qMWLF+vKK690Hlu3bp1Gjx6tyMhI3XrrrQ0+55gxY2S323XFFVdo9+7dWrp0qX766Sd5eXmpT58+Sk1NbfA5YY7knmF6PWWvvj29PGDlC7kAAACNkUsC99GjR9W7d+8qx+Lj4zVv3jzdd999LgncW7duVVpamnr16uU8VlRUpPT0dG3YsKHB54N5LuoWIi8Pqw7mn9DOnOPqEe5ndkkAAABn5ZKWksTERL333nvVjvft21d79+51xZQaOHCg8vPzqxzz8/PT8OHDNW3aNJfMCXO08rIpoWuwpFOb4AAAADRmLuvhfvTRR/XnP/9Zhw4dkiQZhqFXXnnFZS9OPvTQQ5o9e7aKi4tdcn00LvRxAwCApqLBW0rsdrvsdrv+85//6I9//KPmzp2rgIAAlZeXy9vbWx9++GFDTylJuvLKK+Xp6amePXtq9OjRSkhIUFxcnHr16kWPbzOU3DNMf//PVq356ahKyk/K18stK1wCAADUW4OnFJvNpmHDhmnjxo1av369Nm3apC1btsjPz0+JiYku2/Vx48aN2rhxozZs2KANGzZo6dKlOnDggFq1aqV+/fpp9erVLpkX5ujetrU6BLbSwfwTStl1RJf3Dje7JAAAgBq55LFgbGyscnJyFB0drX79+qlfv36umKaKvn37qm/fvho3bpzzWH5+vjIyMrRx40aXzw/3slgsSu7ZVv9evU8rt+cSuAEAQKPlksD9l7/8RQ8++KCWLVum0NBQV0xRTXBwsGJiYhQbG6uYmBjFxcWpb9++SkpKUlJSkltqgHslRZ8K3CsyWR4QAAA0Xi55afKaa67R6tWr1bNnT91222165513tHXrVhmG4YrpJElvvvmmRo4cqezsbD311FO66KKL5Ofnp969e2vs2LEXdO158+apS5cu8vHxUUJCgtasWVOn8959911ZLBaNGjXqguZHzYZGhcrTZtG+oyXac6TE7HIAAABq5JLAvW3bNr399tu6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tVL37t31yCOP6Mz1ILjnTdu3336r3/zmN2rfvr0sFouWLVtW5fu63N+jR49q/Pjx8vf3V2BgoG655RYdP37cjT8FgRstwMqVKzVt2jT9+OOP+uqrr1RRUaErrrhCxcXFzjH33HOPPvnkEy1ZskQrV67UoUOHdO2115pYNRrK2rVr9dJLLykmJqbKce5583Ps2DENGzZMnp6e+vzzz7VlyxY9/fTTCgoKco554okn9M9//lMLFizQ6tWr1bp1a40cOVKlpaUmVo7zNXfuXM2fP18vvPCCtm7dqrlz5+qJJ57Q888/7xzDPW/aiouLFRsbq3nz5tX4fV3u7/jx47V582Z99dVX+vTTT/Xtt9/q1ltvddePcIoBtDA5OTmGJGPlypWGYRhGfn6+4enpaSxZssQ5ZuvWrYYkIyUlxawy0QCKioqMHj16GF999ZWRlJRk3HXXXYZhcM+bqwceeMC4+OKLz/q9w+EwIiIijCeffNJ5LD8/3/D29jbeeecdd5SIBnbVVVcZv//976scu/baa43x48cbhsE9b24kGR9++KHz13W5v1u2bDEkGWvXrnWO+fzzzw2LxWIcPHjQbbXzhBstTkFBgSQpODhYkpSamqqKigqNGDHCOaZXr17q1KmTUlJSTKkRDWPatGm66qqrqtxbiXveXH388ceKj4/XmDFjFBYWpgEDBmjRokXO73/66SdlZWVVue8BAQFKSEjgvjdRQ4cO1fLly7V9+3ZJUkZGhr777jtdeeWVkrjnzV1d7m9KSooCAwMVHx/vHDNixAhZrVatXr3abbV6uG0moBFwOBy6++67NWzYMPXr10+SlJWVJS8vLwUGBlYZGx4erqysLBOqREN49913lZaWprVr11b7jnvePO3evVvz58/X9OnT9ec//1lr167VnXfeKS8vL02cONF5b8PDw6ucx31vuh588EEVFhaqV69estlsstvtevTRRzV+/HhJ4p43c3W5v1lZWQoLC6vyvYeHh4KDg9367wCBGy3KtGnTtGnTJn333XdmlwIX2r9/v+666y599dVX8vHxMbscuInD4VB8fLwee+wxSdKAAQO0adMmLViwQBMnTjS5OrjCe++9p3//+996++231bdvX6Wnp+vuu+9W+/btuedoVGgpQYtxxx136NNPP9U333yjjh07Oo9HRESovLxc+fn5VcZnZ2crIiLCzVWiIaSmpionJ0cDBw6Uh4eHPDw8tHLlSv3zn/+Uh4eHwsPDuefNULt27dSnT58qx3r37q19+/ZJkvPe/nI1Gu5703XffffpwQcf1Lhx49S/f3/ddNNNuueeezRnzhxJ3PPmri73NyIiQjk5OVW+P3nypI4ePerWfwcI3Gj2DMPQHXfcoQ8//FBff/21unbtWuX7QYMGydPTU8uXL3cey8zM1L59+5SYmOjuctEALr/8cm3cuFHp6enOT3x8vMaPH+/8e+558zNs2LBqS35u375dnTt3liR17dpVERERVe57YWGhVq9ezX1vokpKSmS1Vo0yNptNDodDEve8uavL/U1MTFR+fr5SU1OdY77++ms5HA4lJCS4r1i3vZ4JmGTq1KlGQECAsWLFCuPw4cPOT0lJiXPMbbfdZnTq1Mn4+uuvjXXr1hmJiYlGYmKiiVWjoZ25SolhcM+bozVr1hgeHh7Go48+auzYscP497//bfj6+hpvvfWWc8zjjz9uBAYGGh999JGxYcMG4+qrrza6du1qnDhxwsTKcb4mTpxodOjQwfj000+Nn376yVi6dKkRGhpq3H///c4x3POmraioyFi/fr2xfv16Q5LxzDPPGOvXrzf27t1rGEbd7u+vf/1rY8CAAcbq1auN7777zujRo4fxu9/9zq0/B4EbzZ6kGj//+te/nGNOnDhh3H777UZQUJDh6+trXHPNNcbhw4fNKxoN7peBm3vePH3yySdGv379DG9vb6NXr17GwoULq3zvcDiMhx56yAgPDze8vb2Nyy+/3MjMzDSpWlyowsJC46677jI6depk+Pj4GN26dTP+8pe/GGVlZc4x3POm7Ztvvqnxv+ETJ040DKNu9/fIkSPG7373O6NNmzaGv7+/MWnSJKOoqMitP4fFMM7YjgkAAABAg6KHGwAAAHAhAjcAAADgQgRuAAAAwIUI3AAAAIALEbgBAAAAFyJwAwAAAC5E4AYAAABciMANAG72zTff6LrrrlOHDh3k5eWloKAg9ezZU2PGjNELL7yggoKCKuO7dOkii8ViUrWnvPbaa7JYLHr44YdNraMhrVixQhaLRTfffLPZpQBo5jzMLgAAWpK//e1vmjVrliSpd+/eSkhIkKenpzIzM7V06VK9//77io+P10UXXWRypQCAhkLgBgA3SU1N1cMPPyxPT0+99957GjVqVJXvs7Ky9NZbbykwMNCU+lqaIUOGaOvWrQoICDC7FADNHIEbANxk6dKlMgxD119/fbWwLUkRERG699573V9YC+Xr66tevXqZXQaAFoAebgBwk9zcXElS27Ztz/saL7/8smJiYtSqVStFREToD3/4g/Lz86uN27lzpx5++GElJiYqIiJCXl5e6tixoyZMmKDt27ef9frff/+9RowYIT8/PwUGBmrkyJFavXp1rTUdOXJE9913n3r06CEfHx8FBwfr17/+tb788stqY7t06SIfHx+VlpZWOX733XfLYrEoMjKy2jmjR4+WxWLRunXraq2j0g8//KBRo0apc+fO8vb2VkREhIYMGaIHH3xQx48fd46rqYe78lhtn1/2fBuGoXfeeUeXXXaZgoKC5OPjo969e+vhhx9WSUlJnWoG0LwRuAHATSrD5AcffKCcnJx6n3///fdr2rRpateuna688koZhqGFCxfqt7/9rQzDqDL25Zdf1t/+9jcVFxdr8ODB+u1vfyt/f3+9+eabGjx4sDZs2FDt+p9++qmSk5O1fPly9enTR1deeaX279+vSy65RCkpKTXWdPDgQQ0ZMkRPPfWUysvLNWrUKA0YMED/+9//NHLkSD377LNVxiclJamsrEw//vhjlePffPONJOnAgQPauXOn87hhGFq5cqUCAgI0YMCAc/4z+uSTTzR8+HB9/PHHateuna699loNGDBAR48e1dy5c5WXl1fr+REREZo4cWKNn+7du0uSbDabc7zD4dD48eN1ww03aO3atYqLi9P//d//qbi4WLNnz9all16qEydOnLNuAM2cAQBwi127dhmtWrUyJBl+fn7GxIkTjUWLFhlpaWnGyZMnz3pe586dDUlGRESEsW3bNufx3NxcIyoqypBkLF++vMo5KSkpxu7du6td69VXXzUkGZdeemmV44WFhUbbtm0NScarr77qPO5wOIwHHnjAkGRIMmbNmlXlvP/3//6fIcm44YYbjLKyMufxVatWGb6+vobNZjPWr19fbf4zr3PkyBHDYrEYffv2NSQZixYtcn63YcMGQ5Jx1VVXnfWfz5kuueQSQ5Lx/vvvV/tuzZo1RmFhofPX33zzjSHJmDhx4jmvm5KSYnh7exuBgYFGZmam8/gTTzxhSDKSk5ONw4cPO4+XlZUZt9xyiyHJeOCBB+pUO4Dmi8ANAG70v//9z4iMjHQG2MpPYGCgMXXqVOPQoUPVzqkM3GcG0UpPPfVUjUG4NsOGDTMsFouRn5/vPFYZhC+55JJq48vLy42OHTtWm2fXrl2GJKNNmzbGkSNHqp03ffp0Q5IxefJk57Hdu3cbkoykpCTnsaVLlxqSjNdff93w9vY2xo8f7/zun//8pyHJeOKJJ+r0s/Xu3duQVOVnO5u6Bu79+/cbERERhs1mM/773/86j1dUVBihoaFG69atjaysrGrnlZSUGBEREUZQUJBht9vrVD+A5omWEgBwo8svv1w7d+7U0qVLddttt2ngwIHy8PBQfn6+5s+fr7i4OGVmZtZ47hVXXFHtWHR0tCTp8OHD1b47fvy43nnnHT3wwAOaMmWKbr75Zt188806fPiwDMPQrl27nGNXrVolSRo3bly163h6emr06NHVjn/33XeSpF//+tcKDg6u9v1NN91U5dqS1LVrV3Xq1Ek//vijs497xYoVzutcdNFFWrlypXN85XfJycnVrl+TQYMGOedeu3atHA5Hnc47mxMnTujqq69WVlaWnn766Sr3IC0tTXl5eRo6dKjCw8OrnduqVSsNGjRIx44d044dOy6oDgBNG6uUAICbeXl56ZprrtE111wjScrPz9e7776rP//5z8rJydEdd9yhr776qtp5HTt2rHbMz89PklRWVlbl+Ndff61x48Y5X9SsSVFRkfPvDx06JEnq3LlzjWO7dOlS7VjlOTV9d+bxgwcPVjmelJSkN998Uz/++KOSk5O1YsUK9enTR2FhYUpOTtbKlSu1c+dOde/eXd9++638/Pw0cODAs/4cZ3rssce0ceNGffLJJ/rkk08UFBSkiy++WL/97W914403ysfHp07XqXTzzTcrLS1Nt9xyi+66664q3+3Zs0eS9NVXX51zY6K8vDz17NmzXnMDaD4I3ABgssDAQN12221q3769rr76an3zzTcqKSmRr69vlXFWa93+UPL48eO6/vrrdfToUc2cOVPjxo1T586d1apVK1ksFt1www165513qr1o2dDOFkKTk5P15ptvasWKFYqJidHGjRs1depU53ezZ8/WihUrdOLECeXl5enKK6+s8qJibSIjI7Vu3Tp9/fXX+vTTT7Vy5Upn+H7iiSeUkpKikJCQOl3rkUce0XvvvaeLL75YL774YrXvK5+eR0VFadiwYbVeq65zAmieCNwA0EhcdtllkiS73a78/PxqgbuuVq1apSNHjmj06NGaPXt2te93795d7Vi7du0kSXv37q3xmjUdb9++fa3nVD4B7tChQ5XjSUlJkk61i8TGxsowDGfLyEUXXSRvb29n4Jbq3k5SycPDQ1dccYWz/WPv3r36/e9/r6+//lpz587VE088cc5rfPjhh5o1a5Y6d+6spUuXysvLq9qYyj9x6NWrl1577bV61QigZaGHGwDc5FxPlCuXw/Py8lJoaOh5z3Ps2DFJNbeg7Ny5U2lpadWODx8+XJL03nvvVfvu5MmT+uCDD6odv/jiiyVJX3zxRY1rgb/11ltVrl2pe/fuioyM1I8//qgvvvhCFovFGap9fHycfdz17d8+m86dO+uBBx6QJG3atOmc4zMyMnTTTTfJ19dXH3300VnXTR88eLACAgK0cuVKHT169IJqBNC8EbgBwE0eeugh3XfffVVeVqx08OBB/eEPf5Ak/fa3v63xiWpdVb5IuXTp0io93Pn5+brllltUUVFR7ZwxY8YoJCREK1as0Ouvv+48bhiGZs2apX379lU7p1u3brrqqqtUVFSku+66q8p1U1JSNH/+fNlsNk2bNq3auZXrcb/xxhvq06dPlVCbnJysAwcO6LPPPqtX/7YkPfvss8rKyqp2/LPPPpOkGjfWOVNubq6uvvpqlZSU6I033lBsbOxZx3p7e+v+++9XUVGRrr322hr/5ODgwYN6880361w/gOaJlhIAcJPjx4/rH//4h5566ilFR0erT58+8vHx0YEDB7R69WpVVFQoKipKzz333AXNEx8fr1/96lf66quvFB0d7XxCvGLFCoWGhurqq6/WRx99VOUcPz8/vfLKK7ruuut08803a/78+erWrZsyMjK0Y8cOTZkyRYsWLao210svvaThw4frjTfe0MqVK5WYmKjc3FytWLFCdrtdTz/9tOLi4qqdl5SUpLfeekulpaXVnmBX9nFXfufhUff/VM2ePVv33nuvYmNj1aNHDxmGoYyMDG3fvl3BwcG69957az1/wYIF2rt3ryIiIvTxxx/r448/rjbm4osv1uTJkyVJDz74oLZt26Y333xTvXv31oABA9S1a1eVl5crMzNTW7ZsUUxMjHPFFgAtE4EbANzkr3/9q+Lj4/Xf//5XGRkZWrVqlQoKCuTv768hQ4bo6quv1u23367WrVtf8FwfffSRHn30Ub333nv6/PPPFRYWpnHjxunvf/+7/vSnP9V4TuULm7NmzdKaNWu0detWDR48WC+//LIyMzNrDNwdOnTQ2rVrNWfOHC1btkxLly6Vr6+vLr/8cv3pT3+qcSlDqWqbyC8Dd2Ufd1lZWb3bSZ5//nl98cUXSk1N1eeffy7p1FPt6dOna/r06dX6yX/JbrdLkrKysqo86f+lysBttVr1xhtvaPTo0Vq4cKHWrl2rtLQ0BQUFKTIyUvfdd5/Gjh1br58BQPNjMVz9mjoAAADQgtHDDQAAALgQgRsAAABwIQI3AAAA4EIEbgAAAMCFCNwAAACACxG4AQAAABcicAMAAAAuROAGAAAAXIjADQAAALgQgRsAAABwIQI3AAAA4EIEbgAAAMCFCNwAAACAC/1/Tt7HAfqwNEkAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(8, 5))\n", - "plt.xlabel(\"Shadow size\", fontsize=15)\n", - "plt.ylabel(r\"$|\\frac{E - E_{ground}}{E_{ground}}|$\", fontsize=20)\n", - "plt.plot(shadow_sizes, [-e / exact_energy for e in energies])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As could be expected, the estimation can be worse than what we got before : we added both randomness and sampling issues to the problem. Raising shadow size will allow more and more precise results. However, it can also be closer to the exact value for the same reasons." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Derandomized measurements" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we try out the derandomized measurements method. To implement this one, we need to decompose the Hamiltonian into individual Pauli strings, rather than group them when they share the same leading coefficient as we did before, as it reduced the number of estimations." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "coeff_non_fact = [\n", - " -0.81261,\n", - " 0.171201,\n", - " 0.171201,\n", - " -0.2227965,\n", - " -0.2227965,\n", - " 0.16862325,\n", - " 0.174349,\n", - " 0.12054625,\n", - " 0.12054625,\n", - " 0.165868,\n", - " 0.165868,\n", - " -0.04532175,\n", - " -0.04532175,\n", - " 0.04532175,\n", - " 0.04532175,\n", - "]\n", - "\n", - "paulis_str = [\n", - " \"1111\",\n", - " \"Z111\",\n", - " \"1Z11\",\n", - " \"11Z1\",\n", - " \"111Z\",\n", - " \"ZZ11\",\n", - " \"11ZZ\",\n", - " \"Z1Z1\",\n", - " \"1Z1Z\",\n", - " \"1ZZ1\",\n", - " \"Z11Z\",\n", - " \"YYXX\",\n", - " \"XXYY\",\n", - " \"XYYX\",\n", - " \"YXXY\",\n", - "]" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [], - "source": [ - "def exp_value_JW_non_fact(outcomes):\n", - " return sum(\n", - " [\n", - " c * exp_value(sigma, outcomes)\n", - " for c, sigma in zip(coeff_non_fact, paulis_str)\n", - " ]\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then, we ask the derandomization algorithm to return $60$ suitable Pauli measurements regarding our input Pauli observables. $60$ is arbitrary, but is small enough that the algorithm runs quickly and large enough that it gives good results." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ZZZZ measurements : 18, XXYY measurements : 11, YXXY measurements : 11, XYYX measurements : 10, YYXX measurements : 10 : total = 60 measurements\n" - ] - } - ], - "source": [ - "measurements = derandomization(60, paulis_str)\n", - "print(\n", - " f\"ZZZZ measurements : {measurements.count('ZZZZ')}, XXYY measurements : {measurements.count('XXYY')}, \"\n", - " + f\"YXXY measurements : {measurements.count('YXXY')}, XYYX measurements : {measurements.count('XYYX')}, \"\n", - " + f\"YYXX measurements : {measurements.count('YYXX')} : total = 60 measurements\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, since all Pauli observables appearing in the Jordan-Wigner Hamiltonian involving the $Z$-basis never involve another basis, we find that it is always worth it to measure Pauli string $ZZZZ$ rather than $ZZZX$, or $ZYZZ$, etc. This is a sign that our cost function is doing its job !" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [], - "source": [ - "def quantum_loop_derand(param, in_state, r=reg):\n", - " \"\"\"\n", - " Args:\n", - " param (np.array): time parameters for each mixing Hamiltonian. There are 2p time parameters in param.\n", - " in_state (qubit.Qobj): initial state.\n", - " \"\"\"\n", - " seq = Sequence(r, DigitalAnalogDevice)\n", - " seq.declare_channel(\"ch0\", \"rydberg_global\")\n", - " middle = len(param) // 2\n", - "\n", - " for tau, t in zip(param[middle:], param[:middle]):\n", - " pulse_1 = Pulse.ConstantPulse(tau, 1.0, 0, 0)\n", - " pulse_2 = Pulse.ConstantPulse(t, 1.0, 1.0, 0)\n", - " seq.add(pulse_1, \"ch0\")\n", - " seq.add(pulse_2, \"ch0\")\n", - "\n", - " seq.measure(\"ground-rydberg\")\n", - " simul = QutipEmulator.from_sequence(seq, sampling_rate=0.05)\n", - " simul.set_initial_state(in_state)\n", - "\n", - " # Classical shadow estimation\n", - " rho = simul.run().get_final_state().proj()\n", - " outcomes = classical_shadow_derand(rho, measurements)\n", - " return exp_value_JW_non_fact(outcomes)\n", - "\n", - "\n", - "def loop_JW_derand(param, in_state):\n", - " res = minimize(\n", - " quantum_loop_derand,\n", - " param,\n", - " method=\"Nelder-Mead\",\n", - " args=in_state,\n", - " options={\"return_all\": True, \"maxiter\": 150, \"adaptive\": True},\n", - " )\n", - " return res" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [], - "source": [ - "measurement_sizes = [20, 30, 40, 60, 80, 100]\n", - "energies_derand = []\n", - "for meas_size in measurement_sizes:\n", - " measurements = derandomization(meas_size, paulis_str)\n", - " energies_derand.append(\n", - " abs(loop_JW_derand(param, gggg).fun - exact_energy) / abs(exact_energy)\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(8, 5))\n", - "plt.xlabel(\"Measurement size\", fontsize=15)\n", - "plt.ylabel(r\"$|\\frac{E - E_{ground}}{E_{ground}}|$\", fontsize=20)\n", - "plt.plot(measurement_sizes, energies_derand)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We consistently obtain accurate results using this derandomized technique, and we obtain them far quicker than when dealing with randomized classical shadows. For roughly the same number of samples ($\\sim 60$ for each method, be it for shadow size or number of measurements), we experience much less computing time using the derandomized method. This was to be expected : by restricting the observables to Pauli strings, we allow for efficient estimation that can be easily computed in $O(M\\times n)$, as well as remove randomness problematic with higher-weight observables (such as $YYXX$ or $YXXY$).\n", - "\n", - "Note that we obtain $2\\%$ accuracy after about $50$ $Z-$ basis measurements (fluorescence) of the output state, rotated before each sampling in the bases returned by the derandomization algorithm." - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Efficient estimation techniques for Variational Quantum Simulation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$\\newcommand{\\ket}[1]{\\left|#1\\right>} \\newcommand{\\bra}[1]{\\left<#1\\right|}$\n", + "This notebook's purpose is to introduce the concept of classical shadow estimation, as well as its use in **VQS** (**V**ariational **Q**uantum **S**imulation). This technique, introduced in [this article by Huang, Kueng and Preskill](https://arxiv.org/abs/2002.08953), is used for efficiently estimating multiple observables, and is extremely powerful in that regard, asymptotically reaching theoretical lower bounds of quantum information theory regarding the number of required samples of a given state for estimation ([see here for details](https://arxiv.org/abs/2101.02464)). \n", + "\n", + "The primary goal of this notebook is to estimate the groundstate energy of the $H_2$ molecule, using a VQS. We will first implement the method of random classical shadows in Python. Then, we'll introduce its derandomized counterpart, which is particularly useful in our setting. We'll finally describe the VQS, and benchmark the estimation methods we introduced for computing the molecule's energy. This notebook draws some inspiration from [this PennyLane Jupyter notebook](https://pennylane.ai/qml/demos/tutorial_classical_shadows.html) on quantum machine learning and classical shadows." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Random classical shadows" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Main ideas and implementation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Classical shadow estimation relies on the fact that for a particular\n", + "choice of measurement, we can efficiently store snapshots of the state\n", + "that contain enough information to accurately predict linear functions\n", + "of observables.\n", + "\n", + "Let us consider an $n$-qubit quantum state $\\rho$ (prepared by a\n", + "pulse sequence) and apply a random unitary $U$ to the state:\n", + "\n", + "$$\\rho \\to U \\rho U^\\dagger.$$\n", + "\n", + "Next, we measure in the computational basis and obtain a bit string of\n", + "outcomes $|b\\rangle = |0011\\ldots10\\rangle$. If the unitaries $U$ are\n", + "chosen at random from a particular ensemble, then we can store the\n", + "reverse operation $U^\\dagger |b\\rangle\\langle b| U$ efficiently in\n", + "classical memory. We call this a *snapshot* of the state. Moreover, we\n", + "can view the average over these snapshots as a measurement channel:\n", + "\n", + "$$\\mathbb{E}\\left[U^\\dagger |b\\rangle\\langle b| U\\right] = \\mathcal{M}(\\rho).$$\n", + "\n", + "We restrict ourselves to unitary ensembles that define a tomographically complete set of\n", + "measurements (i.e $\\mathcal{M}$ is invertible), therefore :\n", + "\n", + "$$\\rho = \\mathbb{E}\\left[\\mathcal{M}^{-1}\\left(U^\\dagger |b\\rangle\\langle b| U \\right)\\right].$$\n", + "\n", + "If we apply the procedure outlined above $N$ times, then the collection\n", + "of inverted snapshots is what we call the *classical shadow*\n", + "\n", + "$$S(\\rho,N) = \\left\\{\\hat{\\rho}_1= \\mathcal{M}^{-1}\\left(U_1^\\dagger |b_1\\rangle\\langle b_1| U_1 \\right)\n", + ",\\ldots, \\hat{\\rho}_N= \\mathcal{M}^{-1}\\left(U_N^\\dagger |b_N\\rangle\\langle b_N| U_N \\right)\n", + "\\right\\}.$$\n", + "\n", + "Since the shadow approximates $\\rho$, we can now estimate **any**\n", + "observable with the empirical mean:\n", + "\n", + "$$\\langle O \\rangle = \\frac{1}{N}\\sum_i \\text{Tr}{\\hat{\\rho}_i O}.$$\n", + "\n", + "We will be using a median-of-means procedure in practice." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We start by defining several useful quantities, such as the unitary matrices associated with Pauli measurements : the Hadamard matrix, change of basis from $\\{\\ket{0}, \\ket{1}\\}$ to the eigenbasis of $\\sigma_X$, $\\{\\ket{+}, \\ket{-}\\}$, and its $\\sigma_Y, \\sigma_Z$ counterparts. We will then draw randomly from this tomographically complete set of $3$ unitaries.\n", + "\n", + "Note that we will need $4$ qubits for our VQS problem : we will explain the mapping from the molecule to qubits later." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import qutip\n", + "import matplotlib.pyplot as plt\n", + "from scipy.optimize import minimize\n", + "\n", + "from pulser import Register, Sequence, Pulse\n", + "from pulser.devices import DigitalAnalogDevice\n", + "from pulser_simulation import QutipEmulator" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "num_qubits = 4\n", + "zero_state = qutip.basis(2, 0).proj()\n", + "one_state = qutip.basis(2, 1).proj()\n", + "hadamard = 1 / np.sqrt(2) * qutip.Qobj([[1.0, 1.0], [1.0, -1.0]])\n", + "h_mul_phase = qutip.Qobj(np.array([[1.0, 1], [1.0j, -1.0j]])) / np.sqrt(2)\n", + "unitary_ensemble = [hadamard, h_mul_phase, qutip.qeye(2)]\n", + "\n", + "g = qutip.basis(2, 1)\n", + "r = qutip.basis(2, 0)\n", + "n = r * r.dag()\n", + "\n", + "sx = qutip.sigmax()\n", + "sy = qutip.sigmay()\n", + "sz = qutip.sigmaz()\n", + "\n", + "gggg = qutip.tensor([g, g, g, g])\n", + "ggrr = qutip.tensor([g, g, r, r])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first define a function that spits out a random bitstring sampled from a given density matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def measure_bitstring(state):\n", + " \"\"\"Auxiliary function that returns a bitstring according to the measure of a quantum state.\"\"\"\n", + " probs = np.real(state.diag())\n", + " probs /= np.sum(probs)\n", + " x = np.nonzero(np.random.multinomial(1, probs))[0][0]\n", + " bitstring = np.binary_repr(x, num_qubits)\n", + " return bitstring" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will need to compute the number of shadows needed given :\n", + "\n", + "* A list of observables $o_i$\n", + "* Desired precision on expectation values $\\epsilon$ : if $\\tilde{o}_i$ is the estimated expectation value for observable $o_i$, we wish for $|Tr(o_i \\rho) - \\tilde{o}_i| \\leq \\epsilon$\n", + "* Failure probability $\\delta$ : we wish for the above equation to be satisfied with probability $1-\\delta$\n", + "\n", + "Precise formulae are given in [Huang et al.](https://arxiv.org/abs/2002.08953)\n", + "The integer $K$ returned by the function will serve as the number of blocks in our median of means procedure afterwards." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def compute_shadow_size(delta, epsilon, observables):\n", + " \"\"\"Helper function.\n", + "\n", + " Computes both the number of shadows needed as well as the size of blocks needed\n", + " for the median_of_means method in order to approximate the expectation value of M\n", + " (linear) observables with additive error epsilon and fail probability delta.\n", + "\n", + " Args:\n", + " delta (float): Failure probability.\n", + " epsilon (float): Additive error on expectation values.\n", + " observables (list[qutip.Qobj]): Observables the expectation value of which is to be computed.\n", + " \"\"\"\n", + " M = len(observables)\n", + " K = 2 * np.log(2 * M / delta)\n", + " shadow_norm = (\n", + " lambda op: np.linalg.norm(\n", + " op - np.trace(op) / 2 ** int(np.log2(op.shape[0])), ord=np.inf\n", + " )\n", + " ** 2\n", + " )\n", + " # Theoretical number of shadows per cluster in the median of means procedure :\n", + " # N = 34 * max(shadow_norm(o) for o in observables) / epsilon ** 2\n", + " # We use N = 20 here to allow for quick simulation\n", + " N = 20\n", + " return int(np.ceil(N * K)), int(K)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we design a function that returns snapshots (bitstrings) of the rotated state as well as the sampled unitaries used to rotate the state $\\rho$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_classical_shadow(rho, shadow_size):\n", + " \"\"\"\n", + " Given a state rho, creates a collection of snapshots consisting of a bit string\n", + " and the index of a unitary operation.\n", + "\n", + " Returns:\n", + " Tuple of two numpy arrays. The first array contains measurement outcomes as bitstrings\n", + " while the second array contains the index for the sampled Pauli's (0,1,2=X,Y,Z).\n", + " \"\"\"\n", + " # sample random Pauli measurements uniformly\n", + " unitary_ids = np.random.randint(0, 3, size=(shadow_size, num_qubits))\n", + " outcomes = []\n", + " for ns in range(shadow_size):\n", + " unitmat = qutip.tensor(\n", + " [unitary_ensemble[unitary_ids[ns, i]] for i in range(num_qubits)]\n", + " )\n", + " outcomes.append(measure_bitstring(unitmat.dag() * rho * unitmat))\n", + "\n", + " # combine the computational basis outcomes and the sampled unitaries\n", + " return (outcomes, unitary_ids)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We then reconstruct an estimate of the quantum state from the sampled bitstrings, using the inverse quantum channel $\\mathcal{M}^{-1}$ defined above. In the particular case of Pauli measurements, we can actually compute the inverse channel : \n", + "\n", + "$$\\mathcal{M}^{-1} = \\otimes_{i=1}^n (3 U_i \\ket{b_i}\\bra{b_i} U^\\dagger_i - \\mathbb{1}_2)$$\n", + "\n", + "where $i$ runs over all qubits : $\\ket{b_i}$, $b_i \\in \\{0,1\\}$, is the single-bit snapshot of qubit $i$ and $U_i$ is the sampled unitary corresponding to the snapshot, acting on qubit $i$." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def snapshot_state(outcome_ns, unitary_ids_ns):\n", + " \"\"\"\n", + " Reconstructs an estimate of a state from a single snapshot in a shadow.\n", + "\n", + " Implements Eq. (S44) from https://arxiv.org/pdf/2002.08953.pdf\n", + "\n", + " Args:\n", + " outcome_ns: Bitstring at ns\n", + " unitary_ids_ns: Rotation applied at ns.\n", + "\n", + " Returns:\n", + " Reconstructed snapshot.\n", + " \"\"\"\n", + " state_list = []\n", + "\n", + " for k in range(num_qubits):\n", + " op = unitary_ensemble[unitary_ids_ns[k]]\n", + " b = zero_state if outcome_ns[k] == \"0\" else one_state\n", + " state_list.append(3 * op * b * op.dag() - qutip.qeye(2))\n", + "\n", + " return qutip.tensor(state_list)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We finally write a median of means procedure. We feed it an observable, the list of snapshots computed above and the number of blocks needed. It returns the median of the means of the observable acting on the snapshots in each block." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def _median_of_means(obs, snap_list, K):\n", + " if K > len(snap_list): # preventing the n_blocks > n_observations\n", + " K = int(np.ceil(len(snap_list) / 2))\n", + " # dividing seq in K random blocks\n", + " indic = np.array((list(range(K)) * int(len(snap_list) / K)))\n", + " np.random.shuffle(indic)\n", + " # computing and saving mean per block\n", + " means = []\n", + " for block in range(K):\n", + " states = [snap_list[i] for i in np.where(indic == block)[0]]\n", + " exp = qutip.expect(obs, states)\n", + " means.append(np.mean(exp))\n", + " return np.median(means)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Reconstructing a given quantum state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us try out the efficiency of this method. We will reconstruct a given density matrix from classical shadows estimation, and observe the evolution of the trace distance between the original state and its reconstruction according to the number of shadows used." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def state_reconstruction(snaps):\n", + " return sum(snaps) / len(snaps)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original density matrix :\n", + "[[0.5+0.j 0.5+0.j 0. +0.j 0. +0.j]\n", + " [0.5+0.j 0.5+0.j 0. +0.j 0. +0.j]\n", + " [0. +0.j 0. +0.j 0. +0.j 0. +0.j]\n", + " [0. +0.j 0. +0.j 0. +0.j 0. +0.j]]\n", + "Shadow reconstruction :\n", + "[[ 0.47+0.j 0.51+0.j 0. +0.j 0.01+0.01j]\n", + " [ 0.51-0.j 0.53+0.j 0. +0.01j 0.01+0.j ]\n", + " [ 0. -0.j 0. -0.01j 0. +0.j -0.01-0.01j]\n", + " [ 0.01-0.01j 0.01-0.j -0.01+0.01j -0.01+0.j ]]\n" + ] } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3", - "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.11.3" + ], + "source": [ + "num_qubits = 2\n", + "shadow_size = 10000\n", + "rho_1 = (\n", + " (\n", + " qutip.tensor([qutip.basis(2, 0), qutip.basis(2, 0)])\n", + " + qutip.tensor([qutip.basis(2, 0), qutip.basis(2, 1)])\n", + " )\n", + " .proj()\n", + " .unit()\n", + ")\n", + "print(\"Original density matrix :\")\n", + "print(rho_1.full())\n", + "outcomes, unitary_ids = calculate_classical_shadow(rho_1, shadow_size)\n", + "snapshots = [\n", + " snapshot_state(outcomes[ns], unitary_ids[ns]) for ns in range(shadow_size)\n", + "]\n", + "print(\"Shadow reconstruction :\")\n", + "print(np.around(state_reconstruction(snapshots).full(), 2))\n", + "\n", + "dist = np.zeros(5)\n", + "shadow_sizes = [100, 1000, 2000, 5000, 10000]\n", + "for i, shadow_size in enumerate(shadow_sizes):\n", + " outcomes, unitary_ids = calculate_classical_shadow(rho_1, shadow_size)\n", + " snapshots = [\n", + " snapshot_state(outcomes[ns], unitary_ids[ns])\n", + " for ns in range(shadow_size)\n", + " ]\n", + " dist[i] = qutip.tracedist(state_reconstruction(snapshots), rho_1)\n", + "num_qubits = 4" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } + ], + "source": [ + "plt.plot(shadow_sizes, dist)\n", + "plt.xlabel(\"Shadow size\")\n", + "plt.ylabel(r\"$||\\rho - \\hat{\\rho}||_1$\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can expect, the estimation gets better and better as shadow size gets larger, with about $2$% accuracy at $10000$ shadows. This mostly serves as a reality check, as we will be using classical shadows to estimate observables acting on quantum states, not to reconstruct those states." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Derandomized Paulis" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Derandomization Algorithm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Randomized classical shadows are useful when dealing with low-weight, general observables. However, suppose, as is the case when estimating the Hamiltonian of the $H_2$ molecule written as a sum of Pauli strings, that we're dealing with Pauli observables of varying weights. In this setting, choosing wisely each Pauli measurement instead of randomly drawing a basis is particularly useful : indeed, say one wants to measure observable $\\sigma_x^1 \\otimes \\sigma_x^2 \\otimes \\dots \\otimes \\sigma_x^n$. Using random rotations in each Pauli $X,Y$ or $Z$ basis and projection in the $Z$ (computational) basis, there is a probability $\\frac{1}{3^n}$ to get each measurement basis right (i.e. rotate the system using the Hadamard matrix). This is extremely unlikely and unefficient as the number of qubits goes up. [Huang et al](https://arxiv.org/abs/2103.07510) outline an interesting greedy algorithm used for choosing suitable measurement bases for the efficient estimation of $L$ $n-$qubit Pauli strings, $\\{O_i\\}$. \n", + "\n", + "Feeding these observables and chosen Pauli measurements {P_i} as input, the algorithm aims at optimizing a certain cost function. This function, labeled $Conf_\\epsilon(O_i, P_j)$ is such that, if $Conf_\\epsilon(O_i, P_j) \\leq \\frac{\\delta}{2}$, then the empirical averages $\\tilde{\\omega_l}$ of each Pauli observable $O_l$ will be $\\epsilon$-close to its true average $Tr(\\rho O_l)$ with probability $1-\\delta$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to implement this cost function, we first need to design two auxiliary functions. The first one decides if a given Pauli measurement $p$ is compatible with (\"hits\") a Pauli observable $o$. This means that each time $o$ acts non-trivially on a qubit $q_i$ with Pauli matrix $\\sigma \\in \\{\\sigma_X, \\sigma_Y, \\sigma_Z\\}, \\sigma \\neq \\mathbb{1}$, $p$ acts on $q_i$ with $\\sigma$. We denote it by $o \\triangleright p$." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def hits(p, o, end=-1):\n", + " \"\"\"Determines if measurement p hits observable o\n", + "\n", + " Args:\n", + " p (str): Pauli string in str format (ex \"XYZ\"), measurement\n", + " o (str): same as above, observable (ex \"11ZY\")\n", + " end (int): index before which to check if p hits o\n", + " \"\"\"\n", + " if end != -1:\n", + " o = o[:end]\n", + " for i, x in enumerate(o):\n", + " if not (x == p[i] or x == \"1\"):\n", + " return False\n", + " return True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The second function simply computes the number of qubits observable $o$ acts non-trivially upon." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def weight(o, start=0):\n", + " o_k = o[start:]\n", + " return len(o_k) - o_k.count(\"1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now implement the conditioned cost function using these auxiliary functions. We call it \"conditioned\", since we feed it only the first $m \\times n + k$ single-qubit Pauli measurements, and average over the others, not yet determined ones." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def cond_conf(o, P_sharp):\n", + " \"\"\"Returns the (modified) conditionned expectation value of the cost function depending\n", + " on already chosen Paulis in P_sharp.\n", + "\n", + " Args:\n", + " o (list[str]): list of Pauli strings to be measured\n", + " P_sharp (list[str]): list of already chosen Paulis\n", + " \"\"\"\n", + " # Hyperparameters : see Huang et al. for more details\n", + " eta = 0.9\n", + " nu = 1 - np.exp(-eta / 2)\n", + " L = len(o)\n", + " m = len(P_sharp) - 1 # index of last chosen Pauli string\n", + " k = (\n", + " len(P_sharp[-1]) - 1\n", + " ) # index of last chosen Pauli matrix in mth Pauli string\n", + " result = 0\n", + " for l in range(0, L):\n", + " v = 0\n", + " for m_prime in range(0, m):\n", + " v += (eta / 2) * int(hits(P_sharp[m_prime], o[l]))\n", + " v -= np.log(\n", + " 1\n", + " - (nu / 3 ** (weight(o[l], start=k + 1)))\n", + " * hits(P_sharp[m], o[l], end=k + 1)\n", + " )\n", + " result += np.exp(-v)\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we design a simple greedy algorithm which purpose is to minimize this conditioned cost function, choosing one single-qubit Pauli at a time." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "def derandomization(M, o):\n", + " \"\"\"Derandomization algorithm returning best Pauli indices according to a greedy algorithm\n", + " that aims at minimizing the cost function above.\n", + "\n", + " Args:\n", + " M (int): number of measurements\n", + " n (int): number of qubits (size of Pauli strings)\n", + " epsilon (float): desired accuracy on observable expectation values\n", + " o (list[str]): list of Pauli strings to be measured\n", + " \"\"\"\n", + " n = len(o[0])\n", + " P_sharp = []\n", + " for m in range(M):\n", + " P_sharp.append(\"\")\n", + " for k in range(n):\n", + " P_sharp_m = P_sharp[m]\n", + " P_sharp[m] += \"X\"\n", + " valmin = cond_conf(o, P_sharp)\n", + " argmin = \"X\"\n", + " for W in [\"Y\", \"Z\"]:\n", + " P_sharp[m] = P_sharp_m + W\n", + " val_W = cond_conf(o, P_sharp)\n", + " if val_W < valmin:\n", + " valmin = val_W\n", + " argmin = W\n", + " P_sharp[m] = P_sharp_m + argmin\n", + " return P_sharp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Estimating expectation values from Pauli measurements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have our Pauli measurements, we proceed differently from randomized classical shadows, where we gave an estimate of the actual quantum channels. Here, we're only interested in the Pauli averages $\\tilde{\\omega}_l$, that we can infer from Pauli measurements $p$ that **hit** observable $o_l$. Indeed, we have the following formula :\n", + "\n", + "$$\\tilde{\\omega}_{l}=\\frac{1}{h\\left(\\mathbf{o}_{l} ;\\left[\\mathbf{p}_{1}, \\ldots, \\mathbf{p}_{M}\\right]\\right)} \\sum_{m: \\mathbf{o}_{l} \\triangleright \\mathbf{p}_{m}} \\prod_{j: \\mathbf{o}_{l}[j] \\neq I} \\mathbf{q}_{m}[j]$$\n", + "\n", + "where $h\\left(\\mathbf{o}_{l} ;\\left[\\mathbf{p}_{1}, \\ldots, \\mathbf{p}_{M}\\right]\\right)$ is the number of times a Pauli measurement $p_i$ is such that $o \\triangleright p_i$, and $\\mathbf{q}_m$ is the output of the measurement of Pauli string $p_m$ ($\\mathbf{q}_m \\in \\{\\pm 1\\}^n$)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "def _pauli_index(letter):\n", + " if letter == \"X\":\n", + " return 0\n", + " elif letter == \"Y\":\n", + " return 1\n", + " else:\n", + " return 2" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "def pauli_string_value(x, sigma):\n", + " \"\"\"Returns the evaluation of a Pauli string sigma in a bitstring state $|x>$,\n", + " assuming the state is already rotated in the needed eigenbases of all single-qubit Paulis.\n", + "\n", + " NB : Faster than using qutip.measure due to not returning the eigenstates...\n", + "\n", + " Args:\n", + " x (str): input bitstring\n", + " sigma (str): input Pauli string to be measured on |x>\n", + " \"\"\"\n", + " outcomes = []\n", + " for i, q in enumerate(x):\n", + " if q == \"0\":\n", + " outcomes.append((sigma[i], 1))\n", + " else:\n", + " outcomes.append((sigma[i], -1))\n", + " return outcomes" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def classical_shadow_derand(rho, measurements):\n", + " \"\"\"Returns the n-strings of ±1 corresponding to measurements in the input list on state rho.\n", + "\n", + " Args:\n", + " rho (qutip.Qobj): input state as a density matrix\n", + " measurements (list[str]): derandomized measurement bases in which to measure state rho\n", + "\n", + " Returns:\n", + " Tuple of two numpy arrays. The first array contains measurement outcomes as bitstrings\n", + " while the second array contains the index for the derandomized Pauli's (0,1,2=X,Y,Z).\n", + " \"\"\"\n", + " # Fill the unitary ids with derandomized measurements ids\n", + " shadow_size = len(measurements)\n", + " outcomes = []\n", + " for ns in range(shadow_size):\n", + " # multi-qubit change of basis\n", + " unitmat = qutip.tensor(\n", + " [\n", + " unitary_ensemble[_pauli_index(measurements[ns][i])]\n", + " for i in range(num_qubits)\n", + " ]\n", + " )\n", + " x = measure_bitstring(unitmat.dag() * rho * unitmat)\n", + " outcomes.append(pauli_string_value(x, measurements[ns]))\n", + " # ±1 strings\n", + " return outcomes" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def exp_value(input_pauli, pm_strings):\n", + " \"\"\"Computes an estimation of the expectation value of a given Pauli string given multiple ±1 bitstring\n", + " outcomes.\n", + " \"\"\"\n", + " sum_product, cnt_match = 0, 0\n", + "\n", + " for single_measurement in pm_strings:\n", + " not_match = False\n", + " product = 1\n", + "\n", + " for i, pauli in enumerate(input_pauli):\n", + " if pauli != single_measurement[i][0] and pauli != \"1\":\n", + " not_match = True\n", + " break\n", + " if pauli != \"1\":\n", + " product *= single_measurement[i][1]\n", + " if not_match:\n", + " continue\n", + "\n", + " sum_product += product\n", + " cnt_match += 1\n", + " if cnt_match == 0:\n", + " return f\"No measurement given for {input_pauli}\"\n", + " return sum_product / cnt_match" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Variational Quantum Simulation for the $H_2$ molecule" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The main problem with usual variational classical algorithms, the classical counterparts of VQS, is computing the value of the $2^n \\times 2^n$ matrix on the output state vector $\\bra{\\psi}H\\ket{\\psi}$ after each loop of the algorithm, which grows exponentially in the size of the system. The purpose of VQS algorithms is to offer a solution which time complexity only grows polynomially, thanks to reading all the important properties on the quantum state. Therefore, we need accurate and efficient methods to estimate these properties, which we'll present afterwards.\n", + "\n", + "For now, let's focus on what makes a VQS algorithm, specifically for computing the groundstate energy of the $H_2$ molecule." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Jordan-Wigner Hamiltonian (cost function)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We need to write the Hamiltonian in a way that's compatible with the formalism of quantum computing. We first second-quantize the Hamiltonian, obtaining an expression in terms of fermionic operators $a, a^\\dagger$. Then, we use the Jordan-Wigner transformation, which maps the fermionic operators to Pauli matrices. We obtain the Hamiltonian below, acting on $4$ qubits, decomposed in terms of the coefficients in front of the Pauli matrices.\n", + "\n", + "[This article by Seeley et al.](https://math.berkeley.edu/~linlin/2018Spring_290/SRL12.pdf) gives us the value of \n", + "$H_{JW}$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$H_{J W}=-0.81261 \\mathbb{1}+0.171201 \\sigma_{0}^{z}+0.171201 \\sigma_{1}^{z}-0.2227965 \\sigma_{2}^{z} \\\\\n", + "-0.2227965 \\sigma_{3}^{z} +0.16862325 \\sigma_{1}^{z} \\sigma_{0}^{z}+0.12054625 \\sigma_{2}^{z} \\sigma_{0}^{z} \\\\\n", + "+0.165868 \\sigma_{2}^{z} \\sigma_{1}^{z}+0.165868 \\sigma_{3}^{z} \\sigma_{0}^{z} +0.12054625 \\sigma_{3}^{z}\\sigma_{1}^{z} \\\\\n", + "+0.17434925 \\sigma_{3}^{z} \\sigma_{2}^{z}-0.04532175 \\sigma_{3}^{x} \\sigma_{2}^{x} \\sigma_{1}^{y} \\sigma_{0}^{y}\\\\\n", + "+0.04532175 \\sigma_{3}^{x} \\sigma_{2}^{y} \\sigma_{1}^{y} \\sigma_{0}^{x}+0.04532175 \\sigma_{3}^{y} \\sigma_{2}^{x}\n", + "\\sigma_{1}^{x} \\sigma_{0}^{y} -0.04532175 \\sigma_{3}^{y} \\sigma_{2}^{y} \\sigma_{1}^{x} \\sigma_{0}^{x}$$" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "def pauli(positions=[], operators=[]):\n", + " op_list = [\n", + " operators[positions.index(j)] if j in positions else qutip.qeye(2)\n", + " for j in range(num_qubits)\n", + " ]\n", + " return qutip.tensor(op_list)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "coeff_fact = [\n", + " 0.81261,\n", + " 0.171201,\n", + " 0.2227965,\n", + " 0.16862325,\n", + " 0.174349,\n", + " 0.12054625,\n", + " 0.165868,\n", + " 0.04532175,\n", + "]\n", + "\n", + "paulis = [\n", + " pauli(),\n", + " pauli([0], [sz]) + pauli([1], [sz]),\n", + " pauli([2], [sz]) + pauli([3], [sz]),\n", + " pauli([1, 0], [sz, sz]),\n", + " pauli([3, 2], [sz, sz]),\n", + " pauli([2, 0], [sz, sz]) + pauli([3, 1], [sz, sz]),\n", + " pauli([2, 1], [sz, sz]) + pauli([3, 0], [sz, sz]),\n", + " pauli([3, 2, 1, 0], [sx, sx, sy, sy])\n", + " + pauli([3, 2, 1, 0], [sy, sy, sx, sx]),\n", + " pauli([3, 2, 1, 0], [sx, sy, sy, sx])\n", + " + pauli([3, 2, 1, 0], [sy, sx, sx, sy]),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# H2 Molecule : 4 qubits in Jordan-Wigner mapping of the Hamiltonian\n", + "a = 10\n", + "reg = Register.from_coordinates(\n", + " [\n", + " [0, 0],\n", + " [a, 0],\n", + " [0.5 * a, a * np.sqrt(3) / 2],\n", + " [0.5 * a, -a * np.sqrt(3) / 2],\n", + " ],\n", + " prefix=\"q\",\n", + ")\n", + "reg.draw()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us keep the exact ground-state energy of the molecule for future reference, by diagonalizing it exactly - this is possible for such a small system, however, this quickly becomes an intractable problem for large molecules." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-1.8510459284448646\n" + ] + } + ], + "source": [ + "def cost_hamiltonian_JW():\n", + " H = (\n", + " -coeff_fact[0] * paulis[0]\n", + " + coeff_fact[1] * paulis[1]\n", + " - coeff_fact[2] * paulis[2]\n", + " + coeff_fact[3] * paulis[3]\n", + " + coeff_fact[4] * paulis[4]\n", + " + coeff_fact[5] * paulis[5]\n", + " + coeff_fact[6] * paulis[6]\n", + " - coeff_fact[7] * paulis[7]\n", + " + coeff_fact[7] * paulis[8]\n", + " )\n", + " return H\n", + "\n", + "\n", + "global H\n", + "H = cost_hamiltonian_JW()\n", + "exact_energy, ground_state = cost_hamiltonian_JW().groundstate()\n", + "print(exact_energy)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Quantum Loop (VQS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Much like in the *Using QAOA to solve a QUBO problem* notebook, we will use a mixed classical-quantum approach for minimizing the energy. The quantum part will do the exploration in Hilbert space, according to a certain set of parameters $\\theta_i, \\tau_j$, and the classical part will find the optimal parameters given the value of the energy after each loop. For now, we will ignore sampling problems and simply compute the exact expectation value of $H_{JW}$. See [this article by Xiao Yuan et al.](https://arxiv.org/abs/1812.08767) for details about VQS algorithms." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Two mixing Hamiltonians are used for the exploration of the solution space :\n", + "$H_1 = \\hbar / 2 \\sum_i \\sigma_i^x + \\sum_{j" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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71955ZUaNGiQMjIytHnzZt15550qKyvTK6+84mC1vhszZowKCws1bNgwVVRUaMmSJfq3f/s3ffHFFzpw4ICioqJanRD69eunAwcOOFPwCXj11VdVWVmp2bNne98L9uP3Y57j0tbvoGfdgQMHlJaW1mJ9RESE+vbtG5TH9fjx47rzzjt1xRVXtHjo3a233qozzjhDffv21bp167Ro0SJVVFTokUcecbBa30yZMkWXXHKJhgwZoh07duiuu+7S1KlTtX79eoWHh4fcMSwqKlJ8fHyrIelgOYZtnR98+ft54MCBNn9XPev8gfDSw82dO1dffPFFi/kgklqMMY8cOVL9+/fXhAkTtGPHDmVnZ3d3mZZNnTrV+/Npp52mMWPGaNCgQXrppZfUq1cvByvzv4KCAk2dOlUZGRne94L9+PV0DQ0Nuuyyy2SM0dNPP91i3R133OH9+bTTTlNUVJRuuOEG5efnB/xt6H/5y196fx45cqROO+00ZWdna82aNZowYYKDldnjueee08yZMxUTE9Pi/WA5hu2dHwIBw0Y+SElJUXh4eKvZ1AcPHlR6erpDVZ24m2++WW+88YZWr16tAQMGdNh2zJgxkqTt27d3R2l+l5SUpKFDh2r79u1KT09XfX29KisrW7QJxuO5e/duvf/++7ruuus6bBfsx89zXDr6HUxPT281gb6xsVFHjx4NquPqCS67d+/WypUrW/S6tGXMmDFqbGzUrl27uqdAP8rKylJKSor332WoHENJ+vDDD1VWVtbp76YUmMewvfODL38/09PT2/xd9azzB8KLD6KiopSXl6dVq1Z532tubtaqVas0duxYByvrGmOMbr75Zq1YsUIffPCBhgwZ0uk2JSUlkqT+/fvbXJ09ampqtGPHDvXv3195eXmKjIxscTzLyspUXl4edMfz+eefV1pamqZNm9Zhu2A/fkOGDFF6enqLY+Z2u7VhwwbvMRs7dqwqKyu1adMmb5sPPvhAzc3N3vAW6DzBZdu2bXr//feVnJzc6TYlJSUKCwtrNdwSDPbu3asjR454/12GwjH0KCgoUF5enkaNGtVp20A6hp2dH3z5+zl27Fht2bKlRRD1BPHhw4f7rVD44K9//auJjo42hYWFZuvWreb66683SUlJLWZTB4s5c+aYxMREs2bNGlNRUeFdjh07ZowxZvv27ebee+81GzduNDt37jSvvfaaycrKMuecc47Dlftu/vz5Zs2aNWbnzp3mn//8p5k4caJJSUkxhw4dMsYYc+ONN5qBAweaDz74wGzcuNGMHTvWjB071uGqrWlqajIDBw40d955Z4v3g/X4VVdXm88++8x89tlnRpJ55JFHzGeffea90uaBBx4wSUlJ5rXXXjObN282F110kRkyZIj5/vvvvZ8xZcoUM3r0aLNhwwbz0UcfmZNPPtlcccUVTu1SKx3tY319vbnwwgvNgAEDTElJSYvfTc8VGuvWrTOPPvqoKSkpMTt27DAvvPCCSU1NNVdddZXDe/YvHe1fdXW1+c1vfmPWr19vdu7cad5//31zxhlnmJNPPtkcP37c+xnBfAw9qqqqTGxsrHn66adbbR/ox7Cz84Mxnf/9bGxsNCNGjDCTJk0yJSUl5p133jGpqalm0aJFfquT8GLBk08+aQYOHGiioqLMWWedZYqLi50uqUsktbk8//zzxhhjysvLzTnnnGP69u1roqOjTU5OjlmwYIGpqqpytnALLr/8ctO/f38TFRVlTjrpJHP55Zeb7du3e9d///335qabbjJ9+vQxsbGxZvr06aaiosLBiq179913jSRTVlbW4v1gPX6rV69u89/l1VdfbYz51+XSd999t+nXr5+Jjo42EyZMaLXvR44cMVdccYXp3bu3SUhIML/61a9MdXW1A3vTto72cefOne3+bq5evdoYY8ymTZvMmDFjTGJioomJiTGnnHKKuf/++1uc/J3U0f4dO3bMTJo0yaSmpprIyEgzaNAg8+tf/7rVfwAG8zH0+MMf/mB69eplKisrW20f6Mews/ODMb79/dy1a5eZOnWq6dWrl0lJSTHz5883DQ0NfqvT9f+LBQAACArMeQEAAEGF8AIAAIIK4QUAAAQVwgsAAAgqhBcAABBUCC8AACCoEF4AAEBQIbwAAICgQngBAABBhfACAACCCuEFAAAEFcILAAAIKv8PmGkBG7NBfFYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(\n", + " [quantum_loop(pars, gggg) for pars in loop_ising_results.allvecs], \"k\"\n", + ")\n", + "plt.axhline(exact_energy, color=\"red\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Seems like we can cut on calculation time by only allowing $100$ iterations, since we don't get much more accurate afterwards." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Estimating Jordan-Wigner $H_2$ Hamiltonian with classical shadows" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Randomized measurements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now consider the real-life problem where we don't have access to the exact value $\\bra{\\Psi(\\theta_i, \\tau_j)} H_{JW} \\ket{\\Psi(\\theta_i, \\tau_j)}$. It can be estimated with classical shadows.\n", + "We modify the quantum loop to add classical shadow estimation of the several Pauli strings making up the $H_{JW}$ Hamiltonian : this is the perfect setting to do so, because we have multiple Pauli strings and most of them have low weight." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "def exp_value_JW(exp_values):\n", + " return (\n", + " -coeff_fact[0] * exp_values[0]\n", + " + coeff_fact[1] * exp_values[1]\n", + " - coeff_fact[2] * exp_values[2]\n", + " + coeff_fact[3] * exp_values[3]\n", + " + coeff_fact[4] * exp_values[4]\n", + " + coeff_fact[5] * exp_values[5]\n", + " + coeff_fact[6] * exp_values[6]\n", + " - coeff_fact[7] * exp_values[7]\n", + " + coeff_fact[7] * exp_values[8]\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "def quantum_loop_shadows(param, in_state, shadow_size=20, r=reg):\n", + " \"\"\"\n", + " Args:\n", + " param (np.array): time parameters for each mixing Hamiltonian. There are 2p time parameters in param.\n", + " in_state (qubit.Qobj): initial state.\n", + " \"\"\"\n", + " seq = Sequence(r, DigitalAnalogDevice)\n", + " seq.declare_channel(\"ch0\", \"rydberg_global\")\n", + " middle = len(param) // 2\n", + "\n", + " for tau, t in zip(param[middle:], param[:middle]):\n", + " pulse_1 = Pulse.ConstantPulse(tau, 1.0, 0, 0)\n", + " pulse_2 = Pulse.ConstantPulse(t, 1.0, 1.0, 0)\n", + " seq.add(pulse_1, \"ch0\")\n", + " seq.add(pulse_2, \"ch0\")\n", + "\n", + " seq.measure(\"ground-rydberg\")\n", + " simul = QutipEmulator.from_sequence(seq, sampling_rate=0.01)\n", + " simul.set_initial_state(in_state)\n", + "\n", + " # Classical shadow estimation\n", + " # Theoretical shadow size and number of clusters :\n", + " # shadow_size, K = compute_shadow_size(0.1, 0.5, paulis)\n", + " # We use K=4 to allow for quick simulation\n", + " K = 4\n", + " rho = simul.run().get_final_state().proj()\n", + " outcomes, unitary_ids = calculate_classical_shadow(rho, shadow_size)\n", + " snapshots = [\n", + " snapshot_state(outcomes[ns], unitary_ids[ns])\n", + " for ns in range(shadow_size)\n", + " ]\n", + " meds = [_median_of_means(obs, snapshots, K) for obs in paulis]\n", + " return exp_value_JW(meds)\n", + "\n", + "\n", + "def loop_JW_shadows(param, in_state, shadow_size=20):\n", + " res = minimize(\n", + " quantum_loop_shadows,\n", + " param,\n", + " method=\"Nelder-Mead\",\n", + " args=(in_state, shadow_size),\n", + " options={\"return_all\": True, \"maxiter\": 100, \"adaptive\": True},\n", + " )\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "shadow_sizes = [10, 20, 40, 60, 80, 100]\n", + "energies = []\n", + "for shadow_size in shadow_sizes:\n", + " energies.append(\n", + " abs(\n", + " loop_JW_shadows(param, gggg, shadow_size=shadow_size).fun\n", + " - exact_energy\n", + " )\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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HzV48OAr/UqoBoM7NFGgr6fScgq14dBpDWgXYnZJAACgnrEY1fEI+Gf69eun1157TYMGDbqi4202W7VM55eZmanExERt375dkyZNuqJzxMTEqG/fvpo1a5YkyeFwKCIiQo8++qimTJlS4TF2u13XXnutfvvb32rt2rXKzMzUkiVLLvua2dnZCgwMVFZWlgICAq6obvxkyn+3aeGmY7qrX0vNvK2H2eUAAIA6oCp5zSU93H/+8581ZcoUpaenu+L0FQoODtbQoUP12GOP6Z133lF8fLx8fHw0ZMiQKw7bRUVFio+PV2xsrHPMarUqNjZWcXFxlR737LPPqmnTpnrggQcu6zqFhYXKzs4us6H6lLaVLN+RrGK7w+RqAABAfeOSwH3rrbdqw4YN6tSpkx5++OFq7aeuzAcffKBhw4YpNTVVr7zyiq655hr5+/urS5cuGj169BWdMz09XXa7XaGhoWXGQ0NDlZKSUuEx69at0zvvvKP58+df9nVmzpypwMBA58ZS9NXrmraNFdLQW5l5xVq3v+b+EggAACC5KHDv3r1bH330kR555BGdPHlSU6dOVbdu3dSwYUPFxMS44pIaPny4pk6dqoULFyopKUkZGRmaM2eOMjMz1bhxY5dc8+dycnJ03333af78+QoJufxe4alTpyorK8u5HTt2zIVV1j82q0XDe4RJkpZuZbYSAABQs1yy8E3Hjh3VsWNH3X777c6xrKwsJSYm1tgiNP7+/ho/frzatGmjzz777IrOERISIpvNptTU1DLjqampCgsLK7f/gQMHdPjwYY0YMcI55nCca2Hw8PDQnj171K5du3LHeXt7y9vb+4pqxOUZEdVM78Ud0de7UlVQbJePp83skgAAQD3hkifcBQUF5cYCAwN17bXXXnE/9ZW67rrrtHz58is61svLS3369NGqVaucYw6HQ6tWrVL//v3L7d+5c2dt375dW7dudW4333yzfvGLX2jr1q20ipiod8sgNW/kq7OFJVq955TZ5QAAgHrEJU+4GzZsqPbt2ysqKkqRkZGKjIxUVFSUWrZs6YrLSZKaNGminj17Oq8ZFRWlDh06aPPmzTpz5swVn3fy5MkaO3asoqOj1a9fP73xxhvKzc3VuHHjJEljxoxR8+bNNXPmTPn4+Kh79+5lji+dBvHn46hZVqtFv44M19vfH9TSxGTd0D3c7JIA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MRV9duH4iRMnyowPGTJEH3zwgX788UcNHTpUq1evVteuXdW0aVMNHTpUa9as0f79+9WuXTt9//338vf3V+/evSv9Hhd68cUXtX37di1dulRLly5VUFCQBg0apJtvvln33nuvfHx8Lus8pe6//34lJCTogQce0GOPPVbms8OHD0uSVq5cecmFidLT09WpU6cqXRtA3UHgBgCTNWrUSA8//LCaNWumW265Rd99953y8vLk5+dXZj+r9fL+UfLs2bO64447lJGRoenTp+vOO+9Uq1at5OvrK4vForvvvlsLFiwo96JldasshA4dOlQffPCBVq9ercjISG3fvl0TJkxwfvbMM89o9erVys/PV3p6um688cYyLypeTEREhDZv3qxvv/1Wy5Yt05o1a5zh++WXX1ZcXJwaN258Wed67rnn9PHHH2vQoEF66623yn1e+vS8ffv2Gjhw4EXPdbnXBFA3EbgBwE388pe/lCTZ7XZlZmaWC9yXa+3atTp9+rRGjRqlZ555ptznBw8eLDcWHh4uSTpy5EiF56xovFmzZhc9pvQJcPPmzcuMDxkyRNK5dpGoqCgZhuFsGbnmmmvk7e3tDNzS5beTlPLw8ND111/vbP84cuSIfvvb3+rbb7/VSy+9pJdffvmS5/jss880Y8YMtWrVSosXL5aXl1e5fUr/xaFz587697//XaUaAdQv9HADQA251BPl0unwvLy8FBIScsXXOXPmjKSKW1D279+vhISEcuODBw+WJH388cflPispKdF///vfcuODBg2SJK1YsaLCucA//PDDMucu1a5dO0VEROjHH3/UihUrZLFYnKHax8fH2cdd1f7tyrRq1UpPPvmkJGnHjh2X3D8xMVH33Xef/Pz89Pnnn1c6b3rfvn0VGBioNWvWKCMj46pqBFC3EbgBoIZMmzZNf/rTn8q8rFjqxIkT+t3vfidJuvnmmyt8onq5Sl+kXLx4cZke7szMTD3wwAMqLi4ud8ztt9+uxo0ba/Xq1Xrvvfec44ZhaMaMGTp69Gi5Y9q2bavhw4crJydHjz32WJnzxsXFac6cObLZbJo4cWK5Y0vn437//ffVtWvXMqF26NChOn78uL766qsq9W9L0uuvv66UlJRy41999ZUkVbiwzoXS0tJ0yy23KC8vT++//76ioqIq3dfb21tPPPGEcnJydNttt1X4LwcnTpzQBx98cNn1A6ibaCkBgBpy9uxZ/f3vf9crr7yijh07qmvXrvLx8dHx48e1YcMGFRcXq3379nrjjTeu6jrR0dH61a9+pZUrV6pjx47OJ8SrV69WSEiIbrnlFn3++edljvH399c777yj3/zmN7r//vs1Z84ctW3bVomJidq3b58efPBBzZ8/v9y13n77bQ0ePFjvv/++1qxZo/79+ystLU2rV6+W3W7Xq6++qp49e5Y7bsiQIfrwww9VUFBQ7gl2aR936WceHpf/R9UzzzyjP/7xj4qKilKHDh1kGIYSExO1d+9eBQcH649//ONFj587d66OHDmisLAwffHFF/riiy/K7TNo0CCNHz9ekjRlyhTt3r1bH3zwgbp06aJevXqpTZs2Kioq0p49e7Rr1y5FRkY6Z2wBUD8RuAGghvzlL39RdHS0/ve//ykxMVFr165VVlaWAgIC1K9fP91yyy165JFH1KBBg6u+1ueff64XXnhBH3/8sZYvX66mTZvqzjvv1PPPP6//+7//q/CY0hc2Z8yYoY0bNyopKUl9+/bVP//5T+3Zs6fCwN28eXNt2rRJM2fO1JIlS7R48WL5+fnpuuuu0//93/9VOJWhVLZN5OeBu7SPu7CwsMrtJG+++aZWrFih+Ph4LV++XNK5p9qTJ0/W5MmTy/WT/5zdbpckpaSklHnS/3Olgdtqter999/XqFGjNG/ePG3atEkJCQkKCgpSRESE/vSnP2n06NFV+g4A6h6L4erX1AEAAIB6jB5uAAAAwIUI3AAAAIALEbgBAAAAFyJwAwAAAC5E4AYAAABciMANAAAAuBCBGwAAAHAhAjcAAADgQgRuAAAAwIUI3AAAAIALEbgBAAAAFyJwAwAAAC5E4AYAAABc6P8BzdP3cotcEBIAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(8, 5))\n", + "plt.xlabel(\"Shadow size\", fontsize=15)\n", + "plt.ylabel(r\"$|\\frac{E - E_{ground}}{E_{ground}}|$\", fontsize=20)\n", + "plt.plot(shadow_sizes, [-e / exact_energy for e in energies])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As could be expected, the estimation can be worse than what we got before : we added both randomness and sampling issues to the problem. Raising shadow size will allow more and more precise results. However, it can also be closer to the exact value for the same reasons." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Derandomized measurements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we try out the derandomized measurements method. To implement this one, we need to decompose the Hamiltonian into individual Pauli strings, rather than group them when they share the same leading coefficient as we did before, as it reduced the number of estimations." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "coeff_non_fact = [\n", + " -0.81261,\n", + " 0.171201,\n", + " 0.171201,\n", + " -0.2227965,\n", + " -0.2227965,\n", + " 0.16862325,\n", + " 0.174349,\n", + " 0.12054625,\n", + " 0.12054625,\n", + " 0.165868,\n", + " 0.165868,\n", + " -0.04532175,\n", + " -0.04532175,\n", + " 0.04532175,\n", + " 0.04532175,\n", + "]\n", + "\n", + "paulis_str = [\n", + " \"1111\",\n", + " \"Z111\",\n", + " \"1Z11\",\n", + " \"11Z1\",\n", + " \"111Z\",\n", + " \"ZZ11\",\n", + " \"11ZZ\",\n", + " \"Z1Z1\",\n", + " \"1Z1Z\",\n", + " \"1ZZ1\",\n", + " \"Z11Z\",\n", + " \"YYXX\",\n", + " \"XXYY\",\n", + " \"XYYX\",\n", + " \"YXXY\",\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "def exp_value_JW_non_fact(outcomes):\n", + " return sum(\n", + " [\n", + " c * exp_value(sigma, outcomes)\n", + " for c, sigma in zip(coeff_non_fact, paulis_str)\n", + " ]\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we ask the derandomization algorithm to return $60$ suitable Pauli measurements regarding our input Pauli observables. $60$ is arbitrary, but is small enough that the algorithm runs quickly and large enough that it gives good results." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ZZZZ measurements : 18, XXYY measurements : 11, YXXY measurements : 11, XYYX measurements : 10, YYXX measurements : 10 : total = 60 measurements\n" + ] + } + ], + "source": [ + "measurements = derandomization(60, paulis_str)\n", + "print(\n", + " f\"ZZZZ measurements : {measurements.count('ZZZZ')}, XXYY measurements : {measurements.count('XXYY')}, \"\n", + " + f\"YXXY measurements : {measurements.count('YXXY')}, XYYX measurements : {measurements.count('XYYX')}, \"\n", + " + f\"YYXX measurements : {measurements.count('YYXX')} : total = 60 measurements\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, since all Pauli observables appearing in the Jordan-Wigner Hamiltonian involving the $Z$-basis never involve another basis, we find that it is always worth it to measure Pauli string $ZZZZ$ rather than $ZZZX$, or $ZYZZ$, etc. This is a sign that our cost function is doing its job !" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "def quantum_loop_derand(param, in_state, r=reg):\n", + " \"\"\"\n", + " Args:\n", + " param (np.array): time parameters for each mixing Hamiltonian. There are 2p time parameters in param.\n", + " in_state (qubit.Qobj): initial state.\n", + " \"\"\"\n", + " seq = Sequence(r, DigitalAnalogDevice)\n", + " seq.declare_channel(\"ch0\", \"rydberg_global\")\n", + " middle = len(param) // 2\n", + "\n", + " for tau, t in zip(param[middle:], param[:middle]):\n", + " pulse_1 = Pulse.ConstantPulse(tau, 1.0, 0, 0)\n", + " pulse_2 = Pulse.ConstantPulse(t, 1.0, 1.0, 0)\n", + " seq.add(pulse_1, \"ch0\")\n", + " seq.add(pulse_2, \"ch0\")\n", + "\n", + " seq.measure(\"ground-rydberg\")\n", + " simul = QutipEmulator.from_sequence(seq, sampling_rate=0.05)\n", + " simul.set_initial_state(in_state)\n", + "\n", + " # Classical shadow estimation\n", + " rho = simul.run().get_final_state().proj()\n", + " outcomes = classical_shadow_derand(rho, measurements)\n", + " return exp_value_JW_non_fact(outcomes)\n", + "\n", + "\n", + "def loop_JW_derand(param, in_state):\n", + " res = minimize(\n", + " quantum_loop_derand,\n", + " param,\n", + " method=\"Nelder-Mead\",\n", + " args=in_state,\n", + " options={\"return_all\": True, \"maxiter\": 150, \"adaptive\": True},\n", + " )\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "measurement_sizes = [20, 30, 40, 60, 80, 100]\n", + "energies_derand = []\n", + "for meas_size in measurement_sizes:\n", + " measurements = derandomization(meas_size, paulis_str)\n", + " energies_derand.append(\n", + " abs(loop_JW_derand(param, gggg).fun - exact_energy) / abs(exact_energy)\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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c3PDrVu3sGfPHkyaNInX5bamXIewsDAUFBTo/Wbhl19+gVarRWhoaNsU2iaPjlKHMHfuXGFnZycOHDigN/VWWVmZrs1zzz0nunfvLn755RcRGxsrwsLCRFhYmIRVt42FCxeKgwcPiitXroikpCSxcOFCIZPJxN69e4UQpntdDLlzlhUhTPfavPLKK+LAgQPiypUr4ujRo2Ls2LHCyclJ5OTkCCFM97qcPHlSmJmZiXfffVdcvHhRfPvtt8LKykp88803ujbvvfeesLe3F9u2bRNJSUli0qRJnXKqNkM0Go3o3r27eOONN+qtM9Xvmaefflp4eHjopj3cunWrcHJyEq+//rqujal+z+zevVvs2rVLpKamir1794qgoCARGhoqqqqqhBCmc12Ki4tFfHy8iI+PFwDEhx9+KOLj48W1a9eEEE27DhEREWLAgAHixIkT4siRI6J3796c9pCkAcDg58svv9S1KS8vF3/961+Fg4ODsLKyEo888ojIzMyUrug28swzz4gePXoIpVIpunbtKsaMGaML40KY7nUx5I+B3FSvzZQpU4Sbm5tQKpXCw8NDTJkyRW+ubVO9LkII8dNPP4mAgABhYWEh/Pz8xLp16/TWa7VasWTJEuHi4iIsLCzEmDFjxPnz5yWqtm3t2bNHADB4vqb6PVNUVCTmz58vunfvLlQqlfD29hZvvvmmqKys1LUx1e+ZzZs3C29vb6FUKoWrq6t4/vnnRUFBgW69qVyX/fv3G8wvTz/9tBCiadfh5s2bYurUqcLGxkao1Woxa9YsUVxc3GbnIBPijlddERERERFRm+IYciIiIiIiCTGQExERERFJiIGciIiIiEhCDORERERERBJiICciIiIikhADORERERGRhBjIiYiIiIgkxEBORNQAmUym+xw/frzBdt9//72unZeXV9sVSJ3azJkzIZPJcODAAalLISIjYyAnImqCb7/9tsF133zzTRtWQlL76quvIJPJsHz5cqlLIaJOgoGciKgRCoUC/fv3x+bNm1FTU1Nv/c2bN7F7924MHDhQguqoM1uxYgXOnTuHIUOGSF0KERkZAzkR0V1MmzYNeXl52LNnT711mzdvRnV1NaZPny5BZdSZubm5wc/PD1ZWVlKXQkRGxkBORHQXTz75JGQymcGhKd988w1sbGwwadKkRvdx7tw5zJw5E56enrCwsICLiwueeOIJnDlzpl7biooKfPHFF5g0aRK8vb1haWkJe3t73H///fjuu+8M7r+qqgqffvopBg8eDEdHR1hZWcHLywsPPfRQvW28vLwgk8kM7ufAgQOQyWSYOXOm3vI7xzPv2bMHo0ePhr29PWQyGQoKCnTtdu/ejQkTJqBr166wsLCAt7c3FixYgJs3b9Y71p373LdvH+6//37Y2trC2dkZf/nLX1BYWAgAyMnJwf/93//Bw8MDKpUKQ4YMaXRc9YkTJ/DYY4/Bzc0NSqUS3bp1w5w5c3D9+vV6bZcvXw6ZTIavvvoKycnJmDhxIhwcHGBtbY2RI0fi2LFjeu1HjRqFWbNmAQDefvttvecMvvrqqwZrqtOcr5OhMeR1yxr7/PHa5OfnY9GiRejXrx8sLS1hZ2eHBx54ADt27LhrvUTUNsykLoCIqL3z9PTE/fffj+3bt6OkpAQ2NjYAgNTUVBw/fhxPPfVUo72YUVFReOKJJ1BZWYng4GAMHToUaWlp+P777/HTTz9h165duP/++3Xtr169ijlz5sDd3R2+vr4YMmQIsrKycOzYMRw+fBgpKSn1xi9PmzYNP/zwA2xtbTFixAio1Wqkp6fjyJEjKCkpwRNPPNEq12Ljxo34/PPPERISgvHjx+Py5cu6cL9w4UKsXLkSSqUSgwcPhpubGxITE/HRRx9h+/btOHr0KFxcXOrt88cff8SaNWsQFhaGiIgI/Prrr/j8889x8eJF/PDDDwgLC4NGo8GIESNw9epVnDhxAhEREfjtt9/Qv39/vX19+umneOGFFwAAgwcPxogRI3D+/Hl88cUX2L59Ow4ePIi+ffvWqyE2NhbPP/88evXqhfDwcKSkpODQoUMYM2YMfvvtNwQEBAAAIiIiUFNTg6NHjyIoKAjBwcG6ffj4+Nz1+t3r1+m+++4zuFyj0WDTpk3QaDRQKBS65RcuXMDYsWORlpYGLy8vhIeHo7i4GL/++isefvhhvP/++3j11VfvWjcRGZkgIiKDAAiFQiGEEGL9+vUCgNiwYYNu/f/7f/9PABB79uwRmZmZAoDo0aOH3j6uXLkirK2thY2NjYiOjtZbt2vXLmFubi48PT1FZWWlbnleXp6Ijo4WWq1Wr31qaqrw8vIScrlcXLlyRW953bHz8vL0tikvLxfHjh3TW9ajRw/R0D//+/fvFwDE008/rbf86aefFgAEAPHdd9/V2+77778XAERAQIC4ePGibrlWqxVLly4VAMSUKVMM7lMul4sdO3bolhcVFYmAgAABQPTr109Mnz5dVFVV6da/9dZbAoCYMWOG3v6OHz8uFAqF8PDwELGxsXrrPv/8cwFAhIaG6i1ftmyZ7rw++eQTvXUvvfSSACCeeuopveVffvmlACCWLVtW7zo0prlfp7rrs3///rvu+8UXXxQAxEMPPSQ0Go0QQoiamhrRv39/AUD84x//0C0XQoiLFy+Knj17CoVCIZKTk5t1HkTU+hjIiYgacGcgv3XrlrCwsBDjxo3Trff19RVubm6ipqamwUA+f/58AUD861//MniMuiC1devWJtVU94PBP//5T92yEydOCAAiMjKySfu4l0A+YcIEg9sFBQUJAAbDnVarFcHBwUKhUIjc3Nx6+5w+fXq9bT755BMBQKjVapGfn6+3rqCgQMhksnrXetKkSQKA+OmnnwzWOHHiRAFAnDp1SresLpAPHz68Xvu8vDyDX9OWBvLmfp2aGsjrvif69esnioqKdMt//PFHAUA8+uijBrfbunWrACBefPHFJp8DERkHx5ATETWBvb09JkyYgJiYGGRlZeG3337D+fPn8cQTT+gNEfijvXv3AgAmT55scP2IESMAACdPnqy37siRI/jb3/6GuXPnYtasWZg5cya2bNkCALh48aKunZ+fH6ytrfHzzz/j/fffR0ZGRovP824mTpxYb1lOTg4SExPRu3dv3dCOO8lkMgwfPhwajQZxcXH11o8bN67eMm9vbwBASEgIHBwc9NbZ2dmhS5cuyMzM1C3TarWIiYmBlZUVwsPDDdbe2LU2VIOjo2O949wLY3ydDh8+jL/+9a9wdHTETz/9BFtbW926e/neI6K2xTHkRERNNH36dGzduhXfffcdrly5olvWmKtXrwIAPDw8Gm2Xl5en+3NhYSEmT56MX375pcH2xcXFuj+r1WqsX78ezz77LF5//XW8/vrr6NOnD0aPHo2nnnoKw4cPv9upNVn37t3rLas7x4sXLzb4sGidO8+zjqFrUzdOv6HrZmNjo/egaF5eHkpKSgAASqWy2TV069bNYFtbW1vk5+c3ur+mau2v07Vr1/Doo49CCIEtW7bofoipU/d1mTZtGqZNm9bgfgxdDyJqWwzkRERN9Kc//Qn29vb4+uuvkZGRgb59+951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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(8, 5))\n", + "plt.xlabel(\"Measurement size\", fontsize=15)\n", + "plt.ylabel(r\"$|\\frac{E - E_{ground}}{E_{ground}}|$\", fontsize=20)\n", + "plt.plot(measurement_sizes, energies_derand)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We consistently obtain accurate results using this derandomized technique, and we obtain them far quicker than when dealing with randomized classical shadows. For roughly the same number of samples ($\\sim 60$ for each method, be it for shadow size or number of measurements), we experience much less computing time using the derandomized method. This was to be expected : by restricting the observables to Pauli strings, we allow for efficient estimation that can be easily computed in $O(M\\times n)$, as well as remove randomness problematic with higher-weight observables (such as $YYXX$ or $YXXY$).\n", + "\n", + "Note that we obtain $2\\%$ accuracy after about $50$ $Z-$ basis measurements (fluorescence) of the output state, rotated before each sampling in the bases returned by the derandomization algorithm." + ] + } + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "pulserenv", + "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.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/tutorials/quantum_simulation/Spin chain of 3 atoms in XY mode.ipynb b/tutorials/quantum_simulation/Spin chain of 3 atoms in XY mode.ipynb index e87f3fb3b..515b412bc 100644 --- a/tutorials/quantum_simulation/Spin chain of 3 atoms in XY mode.ipynb +++ b/tutorials/quantum_simulation/Spin chain of 3 atoms in XY mode.ipynb @@ -69,7 +69,7 @@ "outputs": [], "source": [ "coords = np.array([[0, 0]])\n", - "qubits = dict(enumerate(coords))\n", + "qubits = {f\"q{i}\": coord for (i, coord) in enumerate(coords)}\n", "\n", "reg = Register(qubits)\n", "seq = Sequence(reg, MockDevice)\n", @@ -147,7 +147,7 @@ "outputs": [], "source": [ "coords = np.array([[-8.0, 0], [0, 0], [8.0, 0]])\n", - "qubits = dict(enumerate(coords))\n", + "qubits = {f\"q{i}\": coord for (i, coord) in enumerate(coords)}\n", "\n", "reg = Register(qubits)\n", "seq = Sequence(reg, MockDevice)\n", @@ -155,7 +155,7 @@ "reg.draw()\n", "\n", "# State preparation using SLM mask\n", - "masked_qubits = [1, 2]\n", + "masked_qubits = [\"q1\", \"q2\"]\n", "seq.config_slm_mask(masked_qubits)\n", "masked_pulse = Pulse.ConstantDetuning(BlackmanWaveform(200, np.pi), 0, 0)\n", "seq.add(masked_pulse, \"ch0\")\n", @@ -236,7 +236,7 @@ "outputs": [], "source": [ "coords = np.array([[-1.0, 0], [0, 0], [np.sqrt(2 / 3), np.sqrt(1 / 3)]]) * 8.0\n", - "qubits = dict(enumerate(coords))\n", + "qubits = {f\"q{i}\": coord for (i, coord) in enumerate(coords)}\n", "\n", "reg = Register(qubits)\n", "seq = Sequence(reg, MockDevice)\n", @@ -259,7 +259,7 @@ "outputs": [], "source": [ "# State preparation using SLM mask\n", - "masked_qubits = [1, 2]\n", + "masked_qubits = [\"q1\", \"q2\"]\n", "seq.config_slm_mask(masked_qubits)\n", "masked_pulse = Pulse.ConstantDetuning(BlackmanWaveform(200, np.pi), 0, 0)\n", "seq.add(masked_pulse, \"ch0\")\n", @@ -286,17 +286,17 @@ "plt.figure(figsize=[16, 18])\n", "plt.subplot(311)\n", "plt.plot(expectations[0])\n", - "plt.ylabel(\"Excitation of atom 0\", fontsize=\"x-large\")\n", + "plt.ylabel(\"Excitation of atom q0\", fontsize=\"x-large\")\n", "plt.xlabel(\"Time ($\\mu$s)\", fontsize=\"x-large\")\n", "plt.ylim([0, 1])\n", "plt.subplot(312)\n", "plt.plot(expectations[1])\n", - "plt.ylabel(\"Excitation of atom 1\", fontsize=\"x-large\")\n", + "plt.ylabel(\"Excitation of atom q1\", fontsize=\"x-large\")\n", "plt.xlabel(\"Time ($\\mu$s)\", fontsize=\"x-large\")\n", "plt.ylim([0, 1])\n", "plt.subplot(313)\n", "plt.plot(expectations[2])\n", - "plt.ylabel(\"Excitation of atom 2\", fontsize=\"x-large\")\n", + "plt.ylabel(\"Excitation of atom q2\", fontsize=\"x-large\")\n", "plt.xlabel(\"Time ($\\mu$s)\", fontsize=\"x-large\")\n", "plt.ylim([0, 1])\n", "plt.show()" From 796073760b9d43a69546dc259194139c47b02a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= <29920212+HGSilveri@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:10:35 +0100 Subject: [PATCH 10/15] Converting the Conventions page to markdown (#777) * Setting up Markdown support * Convert Conventions page to markdown * Attempt to use auto-generated labels --- docs/requirements.txt | 1 + docs/source/conf.py | 20 ++ docs/source/conventions.md | 260 ++++++++++++++++++ docs/source/conventions.rst | 246 ----------------- .../State Preparation with the SLM Mask.ipynb | 2 +- 5 files changed, 282 insertions(+), 247 deletions(-) create mode 100644 docs/source/conventions.md delete mode 100644 docs/source/conventions.rst diff --git a/docs/requirements.txt b/docs/requirements.txt index 9a81cc89c..5c9ef500f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,6 +5,7 @@ sphinx_autodoc_typehints == 1.21.3 nbsphinx nbsphinx-link ipython >= 8.10 # Avoids bug with code highlighting +myst-parser # Not on PyPI # pandoc diff --git a/docs/source/conf.py b/docs/source/conf.py index 507fb0657..e40503e2a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -34,6 +34,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "myst_parser", "nbsphinx", "nbsphinx_link", "sphinx.ext.autodoc", @@ -42,6 +43,25 @@ "sphinx_autodoc_typehints", ] +myst_enable_extensions = [ + "amsmath", + "attrs_inline", + "colon_fence", + "deflist", + "dollarmath", + # "fieldlist", + # "html_admonition", + "html_image", + # "linkify", + # "replacements", + # "smartquotes", + # "strikethrough", + # "substitution", + # "tasklist", +] + +myst_heading_anchors = 3 + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/source/conventions.md b/docs/source/conventions.md new file mode 100644 index 000000000..73c7e3b34 --- /dev/null +++ b/docs/source/conventions.md @@ -0,0 +1,260 @@ +# Conventions + +## States and Bases + +### Bases + +A basis refers to a set of two eigenstates. The transition between +these two states is said to be addressed by a channel that targets that basis. Namely: + +```{eval-rst} +.. list-table:: + :align: center + :widths: 50 35 35 + :header-rows: 1 + + * - Basis + - Eigenstates + - ``Channel`` type + * - ``ground-rydberg`` + - :math:`|g\rangle,~|r\rangle` + - ``Rydberg`` + * - ``digital`` + - :math:`|g\rangle,~|h\rangle` + - ``Raman`` + * - ``XY`` + - :math:`|0\rangle,~|1\rangle` + - ``Microwave`` + + +``` + +### Qutrit state + +The qutrit state combines the basis states of the `ground-rydberg` and `digital` bases, +which share the same ground state, $|g\rangle$. This qutrit state comes into play +in the digital approach, where the qubit state is encoded in $|g\rangle$ and +$|h\rangle$ but then the Rydberg state $|r\rangle$ is accessed in multi-qubit +gates. + +The qutrit state's basis vectors are defined as: + +$$ +|r\rangle = (1, 0, 0)^T,~~|g\rangle = (0, 1, 0)^T, ~~|h\rangle = (0, 0, 1)^T. +$$ + +### Qubit states + +:::{caution} +There is no implicit relationship between a state's vector representation and its +associated measurement value. To see the measurement value of a state for each +measurement basis, see {ref}`spam-table` . +::: + +When using only the `ground-rydberg` or `digital` basis, the qutrit state is not +needed and is thus reduced to a qubit state. This reduction is made simply by tracing-out +the extra basis state, so we obtain + +- `ground-rydberg`: $|r\rangle = (1, 0)^T,~~|g\rangle = (0, 1)^T$ +- `digital`: $|g\rangle = (1, 0)^T,~~|h\rangle = (0, 1)^T$ + +On the other hand, the `XY` basis uses an independent set of qubit states that are +labelled $|0\rangle$ and $|1\rangle$ and follow the standard convention: + +- `XY`: $|0\rangle = (1, 0)^T,~~|1\rangle = (0, 1)^T$ + +### Multi-partite states + +The combined quantum state of multiple atoms respects their order in the `Register`. +For a register with ordered atoms `(q0, q1, q2, ..., qn)`, the full quantum state will be + +$$ +|q_0, q_1, q_2, ...\rangle = |q_0\rangle \otimes |q_1\rangle \otimes |q_2\rangle \otimes ... \otimes |q_n\rangle +$$ + +:::{note} +The atoms may be labelled arbitrarily without any inherent order, it's only the +order with which they are stored in the `Register` (as returned by +`Register.qubit_ids`) that matters . +::: + + +## State Preparation and Measurement + +```{eval-rst} +.. list-table:: Initial State and Measurement Conventions + :name: spam-table + :align: center + :widths: 60 40 75 + :header-rows: 1 + + * - Basis + - Initial state + - Measurement + * - ``ground-rydberg`` + - :math:`|g\rangle` + - | + | :math:`|r\rangle \rightarrow 1` + | :math:`|g\rangle,|h\rangle \rightarrow 0` + * - ``digital`` + - :math:`|g\rangle` + - | + | :math:`|h\rangle \rightarrow 1` + | :math:`|g\rangle,|r\rangle \rightarrow 0` + * - ``XY`` + - :math:`|0\rangle` + - | + | :math:`|1\rangle \rightarrow 1` + | :math:`|0\rangle \rightarrow 0` +``` + +### Measurement samples order + +Measurement samples are returned as a sequence of 0s and 1s, in +the same order as the atoms in the `Register` and in the multi-partite state. + +For example, a four-qutrit state $|q_0, q_1, q_2, q_3\rangle$ that's +projected onto $|g, r, h, r\rangle$ when measured will record a count to +sample + +- `0101`, if measured in the `ground-rydberg` basis +- `0010`, if measured in the `digital` basis + +## Hamiltonians + +Independently of the mode of operation, the Hamiltonian describing the system +can be written as + +$$ +H(t) = \sum_i \left (H^D_i(t) + \sum_{j Date: Mon, 16 Dec 2024 09:58:14 -0500 Subject: [PATCH 11/15] Update displayed device specs (#763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add all missing specs * Add property specs * Move _specs to BaseDevice In this way, _specs and print_specs can also be called for virtual devices. * Fix syntax for compatibility with older Python * Fix style * Add missing docstring * Fix dosctring style * Improve specs method * Update to fix mypy errors * Fix mypy error * Add tests for BaseDevice.specs property * Fix import order * Various minor improvements One change is to use a string instead of joining elements of a list to get the final string. The reason is that lists were cumbersome to use when there were conditional statements. * Split _specs method in different methods Create one _specs method for each sections (register, layout, device, channels). The layout section is defined only in Device, such that it is not displayed for VirtualDevice. This commit also goes back to using lists for storing the lines. * Remove line * Fix typo in strings * Return list[str] instead of str for specs blocks Also move texts for layout to BaseDevice, since virtual devices can have some layouts properties. --------- Co-authored-by: Henrique Silvério <29920212+HGSilveri@users.noreply.github.com> --- pulser-core/pulser/devices/_device_datacls.py | 233 ++++++++++++------ tests/test_devices.py | 80 ++++++ 2 files changed, 239 insertions(+), 74 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 203cb6cf6..01b0181d0 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -19,7 +19,7 @@ from collections import Counter from collections.abc import Mapping from dataclasses import dataclass, field, fields -from typing import Any, Literal, cast, get_args +from typing import Any, Callable, Literal, cast, get_args import numpy as np from scipy.spatial.distance import squareform @@ -586,6 +586,154 @@ def to_abstract_repr(self) -> str: validate_abstract_repr(abstr_dev_str, "device") return abstr_dev_str + def print_specs(self) -> None: + """Prints the device specifications.""" + title = f"{self.name} Specifications" + header = ["-" * len(title), title, "-" * len(title)] + print("\n".join(header)) + print(self._specs()) + + @property + def specs(self) -> str: + """Text summarizing the specifications of the device.""" + return self._specs(for_docs=False) + + def _param_yes_no(self, param: Any) -> str: + return "Yes" if param is True else "No" + + def _param_check_none(self, param: Any) -> Callable[[str], str]: + def empty_str_if_none(line: str) -> str: + if param is None: + return "" + else: + return line.format(param) + + return empty_str_if_none + + def _register_lines(self) -> list[str]: + + register_lines = [ + "\nRegister parameters:", + f" - Dimensions: {self.dimensions}D", + f" - Rydberg level: {self.rydberg_level}", + self._param_check_none(self.max_atom_num)( + " - Maximum number of atoms: {}" + ), + self._param_check_none(self.max_radial_distance)( + " - Maximum distance from origin: {} µm" + ), + " - Minimum distance between neighbouring atoms: " + + f"{self.min_atom_distance} μm", + f" - SLM Mask: {self._param_yes_no(self.supports_slm_mask)}", + ] + + return [line for line in register_lines if line != ""] + + def _layout_lines(self) -> list[str]: + + layout_lines = [ + "\nLayout parameters:", + f" - Requires layout: {self._param_yes_no(self.requires_layout)}", + f" - Minimal number of traps: {self.min_layout_traps}", + self._param_check_none(self.max_layout_traps)( + " - Maximal number of traps: {}" + ), + f" - Maximum layout filling fraction: {self.max_layout_filling}", + ] + + return [line for line in layout_lines if line != ""] + + def _device_lines(self) -> list[str]: + + device_lines = [ + "\nDevice parameters:", + self._param_check_none(self.max_runs)( + " - Maximum number of runs: {}" + ), + self._param_check_none(self.max_sequence_duration)( + " - Maximum sequence duration: {} ns", + ), + " - Channels can be reused: " + + self._param_yes_no(self.reusable_channels), + f" - Supported bases: {', '.join(self.supported_bases)}", + f" - Supported states: {', '.join(self.supported_states)}", + self._param_check_none(self.interaction_coeff)( + " - Ising interaction coefficient: {}", + ), + self._param_check_none(self.interaction_coeff_xy)( + " - XY interaction coefficient: {}", + ), + self._param_check_none(self.default_noise_model)( + " - Default noise model: {}", + ), + ] + + return [line for line in device_lines if line != ""] + + def _channel_lines(self, for_docs: bool = False) -> list[str]: + + ch_lines = ["\nChannels:"] + for name, ch in {**self.channels, **self.dmm_channels}.items(): + if for_docs: + max_amp = "None" + if ch.max_abs_detuning is not None: + max_amp = f"{float(cast(float, ch.max_amp)):.4g} rad/µs" + + max_abs_detuning = "None" + if ch.max_abs_detuning is not None: + max_abs_detuning = ( + f"{float(ch.max_abs_detuning):.4g} rad/µs" + ) + + bottom_detuning = "None" + if isinstance(ch, DMM) and ch.bottom_detuning is not None: + bottom_detuning = f"{float(ch.bottom_detuning):.4g} rad/µs" + + ch_lines += [ + f" - ID: '{name}'", + f"\t- Type: {ch.name} (*{ch.basis}* basis)", + f"\t- Addressing: {ch.addressing}", + ("\t" + r"- Maximum :math:`\Omega`: " + max_amp), + ( + ( + "\t" + + r"- Maximum :math:`|\delta|`: " + + max_abs_detuning + ) + if not isinstance(ch, DMM) + else ( + "\t" + + r"- Bottom :math:`|\delta|`: " + + bottom_detuning + ) + ), + f"\t- Minimum average amplitude: {ch.min_avg_amp} rad/µs", + ] + if ch.addressing == "Local": + ch_lines += [ + "\t- Minimum time between retargets: " + f"{ch.min_retarget_interval} ns", + f"\t- Fixed retarget time: {ch.fixed_retarget_t} ns", + f"\t- Maximum simultaneous targets: {ch.max_targets}", + ] + ch_lines += [ + f"\t- Clock period: {ch.clock_period} ns", + f"\t- Minimum instruction duration: {ch.min_duration} ns", + ] + else: + ch_lines.append(f" - '{name}': {ch!r}") + + return [line for line in ch_lines if line != ""] + + def _specs(self, for_docs: bool = False) -> str: + + return "\n".join( + self._register_lines() + + self._layout_lines() + + self._device_lines() + + self._channel_lines(for_docs=for_docs) + ) + @dataclass(frozen=True, repr=False) class Device(BaseDevice): @@ -725,79 +873,6 @@ def to_virtual(self) -> VirtualDevice: del params[param] return VirtualDevice(**params) - def print_specs(self) -> None: - """Prints the device specifications.""" - title = f"{self.name} Specifications" - header = ["-" * len(title), title, "-" * len(title)] - print("\n".join(header)) - print(self._specs()) - - def _specs(self, for_docs: bool = False) -> str: - lines = [ - "\nRegister parameters:", - f" - Dimensions: {self.dimensions}D", - f" - Rydberg level: {self.rydberg_level}", - f" - Maximum number of atoms: {self.max_atom_num}", - f" - Maximum distance from origin: {self.max_radial_distance} μm", - ( - " - Minimum distance between neighbouring atoms: " - f"{self.min_atom_distance} μm" - ), - f" - Maximum layout filling fraction: {self.max_layout_filling}", - f" - SLM Mask: {'Yes' if self.supports_slm_mask else 'No'}", - ] - - if self.max_sequence_duration is not None: - lines.append( - " - Maximum sequence duration: " - f"{self.max_sequence_duration} ns" - ) - - ch_lines = ["\nChannels:"] - for name, ch in {**self.channels, **self.dmm_channels}.items(): - if for_docs: - ch_lines += [ - f" - ID: '{name}'", - f"\t- Type: {ch.name} (*{ch.basis}* basis)", - f"\t- Addressing: {ch.addressing}", - ( - "\t" - + r"- Maximum :math:`\Omega`:" - + f" {float(cast(float, ch.max_amp)):.4g} rad/µs" - ), - ( - ( - "\t" - + r"- Maximum :math:`|\delta|`:" - + f" {float(cast(float, ch.max_abs_detuning)):.4g}" - + " rad/µs" - ) - if not isinstance(ch, DMM) - else ( - "\t" - + r"- Bottom :math:`|\delta|`:" - + f" {float(cast(float, ch.bottom_detuning)):.4g}" - + " rad/µs" - ) - ), - f"\t- Minimum average amplitude: {ch.min_avg_amp} rad/µs", - ] - if ch.addressing == "Local": - ch_lines += [ - "\t- Minimum time between retargets: " - f"{ch.min_retarget_interval} ns", - f"\t- Fixed retarget time: {ch.fixed_retarget_t} ns", - f"\t- Maximum simultaneous targets: {ch.max_targets}", - ] - ch_lines += [ - f"\t- Clock period: {ch.clock_period} ns", - f"\t- Minimum instruction duration: {ch.min_duration} ns", - ] - else: - ch_lines.append(f" - '{name}': {ch!r}") - - return "\n".join(lines + ch_lines) - def _to_dict(self) -> dict[str, Any]: return obj_to_dict( self, _build=False, _module="pulser.devices", _name=self.name @@ -835,6 +910,16 @@ def from_abstract_repr(obj_str: str) -> Device: ) return device + def _layout_lines(self) -> list[str]: + layout_lines = super()._layout_lines() + layout_lines.insert( + 2, + " - Accepts new layout: " + + self._param_yes_no(self.accepts_new_layouts), + ) + + return layout_lines + @dataclass(frozen=True) class VirtualDevice(BaseDevice): diff --git a/tests/test_devices.py b/tests/test_devices.py index 5c4017bc8..df473a895 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -23,6 +23,7 @@ from pulser.channels import Microwave, Raman, Rydberg from pulser.channels.dmm import DMM from pulser.devices import ( + AnalogDevice, Device, DigitalAnalogDevice, MockDevice, @@ -257,6 +258,85 @@ def test_tuple_conversion(test_params): assert dev.channel_ids == ("custom_channel",) +@pytest.mark.parametrize( + "device", [MockDevice, AnalogDevice, DigitalAnalogDevice] +) +def test_device_specs(device): + def yes_no_fn(dev, attr, text): + if hasattr(dev, attr): + cond = getattr(dev, attr) + return f" - {text}: {'Yes' if cond else 'No'}\n" + + return "" + + def check_none_fn(dev, attr, text): + if hasattr(dev, attr): + var = getattr(dev, attr) + if var is not None: + return " - " + text.format(var) + "\n" + + return "" + + def specs(dev): + register_str = ( + "\nRegister parameters:\n" + + f" - Dimensions: {dev.dimensions}D\n" + + f" - Rydberg level: {dev.rydberg_level}\n" + + check_none_fn(dev, "max_atom_num", "Maximum number of atoms: {}") + + check_none_fn( + dev, + "max_radial_distance", + "Maximum distance from origin: {} µm", + ) + + " - Minimum distance between neighbouring atoms: " + + f"{dev.min_atom_distance} μm\n" + + yes_no_fn(dev, "supports_slm_mask", "SLM Mask") + ) + + layout_str = ( + "\nLayout parameters:\n" + + yes_no_fn(dev, "requires_layout", "Requires layout") + + ( + "" + if device is MockDevice + else yes_no_fn( + dev, "accepts_new_layouts", "Accepts new layout" + ) + ) + + f" - Minimal number of traps: {dev.min_layout_traps}\n" + + check_none_fn( + dev, "max_layout_traps", "Maximal number of traps: {}" + ) + + f" - Maximum layout filling fraction: {dev.max_layout_filling}\n" + ) + + device_str = ( + "\nDevice parameters:\n" + + check_none_fn(dev, "max_runs", "Maximum number of runs: {}") + + check_none_fn( + dev, + "max_sequence_duration", + "Maximum sequence duration: {} ns", + ) + + yes_no_fn(dev, "reusable_channels", "Channels can be reused") + + f" - Supported bases: {', '.join(dev.supported_bases)}\n" + + f" - Supported states: {', '.join(dev.supported_states)}\n" + + f" - Ising interaction coefficient: {dev.interaction_coeff}\n" + + check_none_fn( + dev, "interaction_coeff_xy", "XY interaction coefficient: {}" + ) + ) + + channel_str = "\nChannels:\n" + "\n".join( + f" - '{name}': {ch!r}" + for name, ch in {**dev.channels, **dev.dmm_channels}.items() + ) + + return register_str + layout_str + device_str + channel_str + + assert device.specs == specs(device) + + def test_valid_devices(): for dev in pulser.devices._valid_devices: assert dev.dimensions in (2, 3) From e2ad83747d91f4368a850a6b81386d8299f6b376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= <29920212+HGSilveri@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:21:41 +0100 Subject: [PATCH 12/15] Allow definition of a custom phase jump time + Suppress output modulation for phase (#779) * Adding Channel.custom_phase_jump_time * Restrict phase jump time for EOM mode * Eliminate modulation for phase * Unit tests * Improve docstring --- pulser-core/pulser/channels/base_channel.py | 28 +++++- .../abstract_repr/schemas/device-schema.json | 98 +++++++++++++++++++ pulser-core/pulser/sampler/samples.py | 13 ++- pulser-core/pulser/sequence/_schedule.py | 9 +- tests/test_abstract_repr.py | 1 + tests/test_channels.py | 2 + tests/test_sequence.py | 30 ++++-- tests/test_sequence_sampler.py | 31 ++++-- 8 files changed, 190 insertions(+), 22 deletions(-) diff --git a/pulser-core/pulser/channels/base_channel.py b/pulser-core/pulser/channels/base_channel.py index 54f27bfe0..f3fb11433 100644 --- a/pulser-core/pulser/channels/base_channel.py +++ b/pulser-core/pulser/channels/base_channel.py @@ -34,7 +34,11 @@ ChannelType = TypeVar("ChannelType", bound="Channel") -OPTIONAL_ABSTR_CH_FIELDS = ("min_avg_amp", "propagation_dir") +OPTIONAL_ABSTR_CH_FIELDS = ( + "min_avg_amp", + "custom_phase_jump_time", + "propagation_dir", +) # States ranked in decreasing order of their associated eigenenergy States = Literal["u", "d", "r", "g", "h", "x"] @@ -78,6 +82,9 @@ class Channel(ABC): min_avg_amp: The minimum average amplitude of a pulse (when not zero). mod_bandwidth: The modulation bandwidth at -3dB (50% reduction), in MHz. + custom_phase_jump_time: An optional custom value for the phase jump + time that overrides the default value estimated from the modulation + bandwidth. It is not enforced in EOM mode. propagation_dir: The propagation direction of the beam associated with the channel, given as a vector in 3D space. @@ -97,6 +104,7 @@ class Channel(ABC): max_duration: Optional[int] = int(1e8) # ns min_avg_amp: float = 0 mod_bandwidth: Optional[float] = None # MHz + custom_phase_jump_time: int | None = None eom_config: Optional[BaseEOM] = field(init=False, default=None) propagation_dir: tuple[float, float, float] | None = None @@ -172,6 +180,7 @@ def __post_init__(self) -> None: "max_duration", "mod_bandwidth", "min_avg_amp", + "custom_phase_jump_time", ] non_negative = [ "max_amp", @@ -179,6 +188,7 @@ def __post_init__(self) -> None: "min_retarget_interval", "fixed_retarget_t", "min_avg_amp", + "custom_phase_jump_time", ] local_only = [ "min_retarget_interval", @@ -191,6 +201,7 @@ def __post_init__(self) -> None: "max_duration", "mod_bandwidth", "max_targets", + "custom_phase_jump_time", ] if self.addressing == "Global": @@ -280,9 +291,14 @@ def rise_time(self) -> int: def phase_jump_time(self) -> int: """Time taken to change the phase between consecutive pulses (in ns). - Corresponds to two times the rise time. + Corresponds to two times the rise time when `custom_phase_jump_time` + is not defined. """ - return self.rise_time * 2 + return int( + self.rise_time * 2 + if self.custom_phase_jump_time is None + else self.custom_phase_jump_time + ) def is_virtual(self) -> bool: """Whether the channel is virtual (i.e. partially defined).""" @@ -336,6 +352,9 @@ def Local( bandwidth at -3dB (50% reduction), in MHz. min_avg_amp: The minimum average amplitude of a pulse (when not zero). + custom_phase_jump_time: An optional custom value for the phase jump + time that overrides the default value estimated from the + modulation bandwidth. It is not enforced in EOM mode. """ # Can't initialize a channel whose addressing is determined internally for cls_field in fields(cls): @@ -382,6 +401,9 @@ def Global( bandwidth at -3dB (50% reduction), in MHz. min_avg_amp: The minimum average amplitude of a pulse (when not zero). + custom_phase_jump_time: An optional custom value for the phase jump + time that overrides the default value estimated from the + modulation bandwidth. It is not enforced in EOM mode. propagation_dir: The propagation direction of the beam associated with the channel, given as a vector in 3D space. """ 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 7a97fbd34..d4c46a433 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -31,6 +31,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -417,6 +424,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "anyOf": [ { @@ -587,6 +601,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -693,6 +714,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -799,6 +827,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "anyOf": [ { @@ -972,6 +1007,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -1081,6 +1123,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -1195,6 +1244,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "anyOf": [ { @@ -1356,6 +1412,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -1453,6 +1516,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -1550,6 +1620,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "anyOf": [ { @@ -1711,6 +1788,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -1808,6 +1892,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" @@ -1911,6 +2002,13 @@ "description": "The duration of a clock cycle (in ns).", "type": "number" }, + "custom_phase_jump_time": { + "description": "An optional custom value for the phase jump time that overrides the default value estimated from the modulation bandwidth.", + "type": [ + "number", + "null" + ] + }, "eom_config": { "description": "Configuration of an associated EOM.", "type": "null" diff --git a/pulser-core/pulser/sampler/samples.py b/pulser-core/pulser/sampler/samples.py index 806d44a79..e10dfe91d 100644 --- a/pulser-core/pulser/sampler/samples.py +++ b/pulser-core/pulser/sampler/samples.py @@ -424,9 +424,16 @@ def masked( new_samples["amp"] = channel_obj.modulate(self.amp) new_samples["det"] = channel_obj.modulate(self.det, keep_ends=True) - new_samples["phase"] = channel_obj.modulate(self.phase, keep_ends=True) - new_samples["_centered_phase"] = channel_obj.modulate( - self.centered_phase, keep_ends=True + new_len_ = len(new_samples["amp"]) + new_samples["phase"] = pm.pad( + self.phase, + (0, new_len_ - len(self.phase)), + mode="edge", + ) + new_samples["_centered_phase"] = pm.pad( + self.centered_phase, + (0, new_len_ - len(self.centered_phase)), + mode="edge", ) for key in new_samples: new_samples[key] = new_samples[key].astype(float)[ diff --git a/pulser-core/pulser/sequence/_schedule.py b/pulser-core/pulser/sequence/_schedule.py index f5207d918..d48c39bed 100644 --- a/pulser-core/pulser/sequence/_schedule.py +++ b/pulser-core/pulser/sequence/_schedule.py @@ -434,11 +434,14 @@ def corrected_phase(tf: int) -> pm.AbstractArray: # last pulse from the phase_jump_time and adds the # fall_time to let the last pulse ramp down ch_obj = self[channel].channel_obj + in_eom_mode = self[channel].in_eom_mode() phase_jump_buffer = ( - ch_obj.phase_jump_time - + last_pulse.fall_time( - ch_obj, in_eom_mode=self[channel].in_eom_mode() + max( + ch_obj.phase_jump_time, + # In EOM mode, we must wait at least 2*rise_time + 2 * ch_obj.rise_time * in_eom_mode, ) + + last_pulse.fall_time(ch_obj, in_eom_mode=in_eom_mode) - (t0 - last_pulse_slot.tf) ) except RuntimeError: diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index f218fe77a..890226b34 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -513,6 +513,7 @@ def test_optional_device_fields(self, og_device, field, value): [ Rydberg.Global(None, None, min_avg_amp=1), Rydberg.Global(None, None, propagation_dir=(1, 0, 0)), + Rydberg.Global(None, None, custom_phase_jump_time=0), Rydberg.Global( None, None, diff --git a/tests/test_channels.py b/tests/test_channels.py index 5f2e2cfe0..a3e93e248 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -38,6 +38,7 @@ ("min_avg_amp", -1e-3), ("propagation_dir", (0, 0, 0)), ("propagation_dir", [1, 0]), + ("custom_phase_jump_time", -10), ], ) def test_bad_init_global_channel(bad_param, bad_value): @@ -66,6 +67,7 @@ def test_bad_init_global_channel(bad_param, bad_value): ("mod_bandwidth", MODBW_TO_TR * 1e3 + 1), ("min_avg_amp", -1e-3), ("propagation_dir", (1, 0, 0)), + ("custom_phase_jump_time", -0.5), ], ) def test_bad_init_local_channel(bad_param, bad_value): diff --git a/tests/test_sequence.py b/tests/test_sequence.py index d13d5bc3d..87b0f7456 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -1732,10 +1732,16 @@ def test_sequence(reg, device, patch_plt_show): assert str(seq) == str(seq_) +@pytest.mark.parametrize("custom_phase_jump_time", (None, 0)) @pytest.mark.parametrize("eom", [False, True]) -def test_estimate_added_delay(eom): +def test_estimate_added_delay(eom, custom_phase_jump_time): + ryd_ch_obj = dataclasses.replace( + AnalogDevice.channels["rydberg_global"], + custom_phase_jump_time=custom_phase_jump_time, + ) + device = dataclasses.replace(AnalogDevice, channel_objects=(ryd_ch_obj,)) reg = Register.square(2, 5) - seq = Sequence(reg, AnalogDevice) + seq = Sequence(reg, device) pulse_0 = Pulse.ConstantPulse(100, 1, 0, 0) pulse_pi_2 = Pulse.ConstantPulse(100, 1, 0, np.pi / 2) @@ -1771,7 +1777,14 @@ def test_estimate_added_delay(eom): seq._add(pulse_0, "ising", "min-delay") first_pulse = seq._last("ising") assert first_pulse.ti == 0 - delay = pulse_0.fall_time(ising_obj, eom) + ising_obj.phase_jump_time + phase_jump_time = ( + custom_phase_jump_time + if custom_phase_jump_time is not None and not eom + else 2 * ising_obj.rise_time + ) + if not eom: + assert ising_obj.phase_jump_time == phase_jump_time + delay = pulse_0.fall_time(ising_obj, eom) + phase_jump_time assert seq.estimate_added_delay(pulse_pi_2, "ising") == delay seq._add(pulse_pi_2, "ising", "min-delay") second_pulse = seq._last("ising") @@ -2004,7 +2017,7 @@ def test_draw_slm_mask_in_ising( seq1.draw( draw_qubit_det=True, draw_interp_pts=False, mode="output" ) # Drawing Sequence with only a DMM - assert len(record) == 9 + assert len(record) == 5 assert np.all( str(record[i].message).startswith( "No modulation bandwidth defined" @@ -2500,6 +2513,7 @@ def test_multiple_index_targets(reg): assert built_seq._last("ch0").targets == {"q2", "q3"} +@pytest.mark.parametrize("custom_phase_jump_time", (None, 0)) @pytest.mark.parametrize("check_wait_for_fall", (True, False)) @pytest.mark.parametrize("correct_phase_drift", (True, False)) @pytest.mark.parametrize("custom_buffer_time", (None, 400)) @@ -2509,6 +2523,7 @@ def test_eom_mode( custom_buffer_time, correct_phase_drift, check_wait_for_fall, + custom_phase_jump_time, patch_plt_show, ): # Setting custom_buffer_time @@ -2518,7 +2533,9 @@ def test_eom_mode( custom_buffer_time=custom_buffer_time, ) channels["rydberg_global"] = dataclasses.replace( - channels["rydberg_global"], eom_config=eom_config + channels["rydberg_global"], + eom_config=eom_config, + custom_phase_jump_time=custom_phase_jump_time, ) dev_ = dataclasses.replace( mod_device, channel_ids=None, channel_objects=tuple(channels.values()) @@ -2586,8 +2603,7 @@ def test_eom_mode( ) second_pulse_slot = seq._schedule["ch0"].last_pulse_slot() phase_buffer = ( - eom_pulse.fall_time(ch0_obj, in_eom_mode=True) - + seq.declared_channels["ch0"].phase_jump_time + eom_pulse.fall_time(ch0_obj, in_eom_mode=True) + 2 * ch0_obj.rise_time ) assert second_pulse_slot.ti == first_pulse_slot.tf + phase_buffer # Corrects the phase acquired during the phase buffer diff --git a/tests/test_sequence_sampler.py b/tests/test_sequence_sampler.py index 6b27d5be8..fb539ca63 100644 --- a/tests/test_sequence_sampler.py +++ b/tests/test_sequence_sampler.py @@ -438,7 +438,15 @@ def test_extend_duration(seq_rydberg, with_custom_centered_phase): assert extended_short.slots == short.slots -def test_phase_sampling(mod_device): +@pytest.mark.parametrize("custom_phase_jump_time", [None, 0, 100]) +def test_phase_sampling(mod_device, custom_phase_jump_time): + ryd_ch_obj = replace( + mod_device.channels["rydberg_global"], + custom_phase_jump_time=custom_phase_jump_time, + ) + mod_device = replace( + mod_device, channel_objects=(ryd_ch_obj,), channel_ids=None + ) reg = pulser.Register.from_coordinates(np.array([[0.0, 0.0]]), prefix="q") seq = pulser.Sequence(reg, mod_device) seq.declare_channel("ch0", "rydberg_global") @@ -463,7 +471,10 @@ def test_phase_sampling(mod_device): assert end_of_detuned_delay == full_duration - dt ph_jump_time = seq.declared_channels["ch0"].phase_jump_time - assert ph_jump_time > 0 + if custom_phase_jump_time is not None: + assert ph_jump_time == custom_phase_jump_time + else: + assert ph_jump_time > 0 expected_phase = np.zeros(full_duration) expected_phase[:dt] = 1.0 transition2_3 = pulse3_start - ph_jump_time @@ -474,12 +485,21 @@ def test_phase_sampling(mod_device): expected_phase[transition2_3:transition3_4] = 3.0 expected_phase[transition3_4:] = 4.0 - got_phase = (ch_samples_ := sample(seq).channel_samples["ch0"]).phase - np.testing.assert_array_equal(expected_phase, got_phase.as_array()) + ch_samples = sample(seq).channel_samples["ch0"] + ch_samples_mod = sample(seq, modulation=True).channel_samples["ch0"] + + np.testing.assert_array_equal(expected_phase, ch_samples.phase.as_array()) + # No difference when modulated, just longer + np.testing.assert_array_equal( + expected_phase, ch_samples_mod.phase.as_array()[:full_duration] + ) # Test centered phase expected_phase[expected_phase > np.pi] -= 2 * np.pi - np.testing.assert_array_equal(expected_phase, ch_samples_.centered_phase) + np.testing.assert_array_equal(expected_phase, ch_samples.centered_phase) + np.testing.assert_array_equal( + expected_phase, ch_samples_mod.centered_phase[:full_duration] + ) @pytest.mark.parametrize("with_diff", [False, True]) @@ -649,5 +669,4 @@ def mod_seq(mod_device: Device) -> pulser.Sequence: Pulse.ConstantDetuning(BlackmanWaveform(1000, np.pi / 2), 1.0, 1.0), "ch0", ) - seq.measure() return seq From 21c8d46a5a7c939bc69b9460a7435093f4ae8a8d Mon Sep 17 00:00:00 2001 From: Antoine Cornillot <61453516+a-corni@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:51:37 +0100 Subject: [PATCH 13/15] Upgrade pulser-simulation to qutip 5 (#783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Qutip 4 compatible changes * Qutip 5 breaking changes * Fix test, handling of exception in NoiseModel * Convert all Qobj to CSR * Fix typing * Fix typing * Fix lint * Delete print * Address nit * Convert initial state to CSR --------- Co-authored-by: HGSilveri Co-authored-by: Henrique Silvério <29920212+HGSilveri@users.noreply.github.com> --- pulser-core/pulser/noise_model.py | 14 +++-- .../pulser_simulation/hamiltonian.py | 11 ++-- .../pulser_simulation/qutip_backend.py | 2 +- .../pulser_simulation/qutip_result.py | 9 +++- .../pulser_simulation/simconfig.py | 14 +++-- .../pulser_simulation/simresults.py | 2 +- .../pulser_simulation/simulation.py | 15 +++--- pulser-simulation/requirements.txt | 4 +- tests/test_qutip_backend.py | 19 +++++++ tests/test_simresults.py | 18 ++----- tests/test_simulation.py | 53 ++++++++++--------- 11 files changed, 97 insertions(+), 64 deletions(-) diff --git a/pulser-core/pulser/noise_model.py b/pulser-core/pulser/noise_model.py index a79f25e00..0f9a48f7d 100644 --- a/pulser-core/pulser/noise_model.py +++ b/pulser-core/pulser/noise_model.py @@ -355,10 +355,16 @@ def _check_eff_noise( # type checking try: operator = np.array(op, dtype=complex) - except Exception: - raise TypeError( - f"Operator {op!r} is not castable to a Numpy array." - ) + except TypeError as e1: + try: + operator = np.array( + op.to("Dense").data_as("ndarray"), # type: ignore + dtype=complex, + ) + except AttributeError: + raise TypeError( + f"Operator {op!r} is not castable to a Numpy array." + ) from e1 if operator.ndim != 2: raise ValueError(f"Operator '{op!r}' is not a 2D array.") diff --git a/pulser-simulation/pulser_simulation/hamiltonian.py b/pulser-simulation/pulser_simulation/hamiltonian.py index c17decf96..738a8d283 100644 --- a/pulser-simulation/pulser_simulation/hamiltonian.py +++ b/pulser-simulation/pulser_simulation/hamiltonian.py @@ -366,10 +366,14 @@ def _build_operator( operator = self.op_matrix[operator] except KeyError: raise ValueError(f"{operator} is not a valid operator") + elif isinstance(operator, qutip.Qobj): + operator = operator.to("CSR") + else: + operator = qutip.Qobj(operator).to("CSR") for qubit in qubits: k = self._qid_index[qubit] op_list[k] = operator - return qutip.tensor(list(map(qutip.Qobj, op_list))) + return qutip.tensor(op_list) def build_operator(self, operations: Union[list, tuple]) -> qutip.Qobj: """Creates an operator with non-trivial actions on some qubits. @@ -443,8 +447,9 @@ def _get_basis_op_matrices( ) -> tuple[dict[States, qutip.Qobj], dict[str, qutip.Qobj]]: """Determine basis and projector operators.""" dim = len(eigenbasis) - basis = {b: qutip.basis(dim, i) for i, b in enumerate(eigenbasis)} - op_matrix = {"I": qutip.qeye(dim)} + with qutip.CoreOptions(default_dtype="CSR"): + basis = {b: qutip.basis(dim, i) for i, b in enumerate(eigenbasis)} + op_matrix = {"I": qutip.qeye(dim)} for proj0 in eigenbasis: for proj1 in eigenbasis: proj_name = "sigma_" + proj0 + proj1 diff --git a/pulser-simulation/pulser_simulation/qutip_backend.py b/pulser-simulation/pulser_simulation/qutip_backend.py index dd57c9862..8ea8581d9 100644 --- a/pulser-simulation/pulser_simulation/qutip_backend.py +++ b/pulser-simulation/pulser_simulation/qutip_backend.py @@ -72,7 +72,7 @@ def run( Args: progress_bar: If True, the progress bar of QuTiP's solver will be shown. If None or False, no text appears. - options: Used as arguments for qutip.Options(). If specified, will + options: Given directly to the Qutip solver. If specified, will override SimConfig solver_options. If no `max_step` value is provided, an automatic one is calculated from the `Sequence`'s schedule (half of the shortest duration among pulses and diff --git a/pulser-simulation/pulser_simulation/qutip_result.py b/pulser-simulation/pulser_simulation/qutip_result.py index b8c7da0f0..1ae584ae2 100644 --- a/pulser-simulation/pulser_simulation/qutip_result.py +++ b/pulser-simulation/pulser_simulation/qutip_result.py @@ -229,11 +229,16 @@ def get_state( ] ) ] - ex_probs = np.abs(state.extract_states(ex_inds).full()) ** 2 + state_arr = state.full() + ex_probs = np.abs(state_arr[ex_inds]) ** 2 if not np.all(np.isclose(ex_probs, 0, atol=tol)): raise TypeError( "Can't reduce to chosen basis because the population of a " "state to eliminate is above the allowed tolerance." ) - state = state.eliminate_states(ex_inds, normalize=normalize) + mask = np.ones_like(state_arr, dtype=bool) + mask[ex_inds] = False + state = qutip.Qobj(state_arr[mask]) + if normalize: + state.unit(inplace=True) return state.tidyup() diff --git a/pulser-simulation/pulser_simulation/simconfig.py b/pulser-simulation/pulser_simulation/simconfig.py index cf229f4b4..ba68fe688 100644 --- a/pulser-simulation/pulser_simulation/simconfig.py +++ b/pulser-simulation/pulser_simulation/simconfig.py @@ -17,7 +17,7 @@ import math from dataclasses import dataclass, field, fields -from typing import Any, Optional, Tuple, Type, TypeVar, Union, cast +from typing import Any, Tuple, Type, TypeVar, Union, cast import qutip @@ -123,7 +123,7 @@ class SimConfig: depolarizing_rate: float = _LEGACY_DEFAULTS["depolarizing_rate"] 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 + solver_options: dict[str, Any] | None = None @classmethod def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: @@ -144,6 +144,10 @@ def from_noise_model(cls: Type[T], noise_model: NoiseModel) -> T: if "amplitude" in noise_model.noise_types: kwargs.setdefault("laser_waist", float("inf")) kwargs.pop("with_leakage", None) + if "eff_noise_opers" in kwargs: + kwargs["eff_noise_opers"] = list( + map(qutip.Qobj, kwargs["eff_noise_opers"]) + ) return cls(**kwargs) def to_noise_model(self) -> NoiseModel: @@ -162,6 +166,10 @@ def to_noise_model(self) -> NoiseModel: kwargs[param] = getattr(self, _DIFF_NOISE_PARAMS.get(param, param)) if "temperature" in kwargs: kwargs["temperature"] *= 1e6 # Converts back to µK + if "eff_noise_opers" in kwargs: + kwargs["eff_noise_opers"] = [ + op.full() for op in kwargs["eff_noise_opers"] + ] return NoiseModel(**kwargs) def __post_init__(self) -> None: @@ -263,7 +271,7 @@ def _check_eff_noise(self) -> None: ) NoiseModel._check_eff_noise( self.eff_noise_rates, - self.eff_noise_opers, + [op.full() for op in self.eff_noise_opers], "eff_noise" in self.noise, self.with_leakage, ) diff --git a/pulser-simulation/pulser_simulation/simresults.py b/pulser-simulation/pulser_simulation/simresults.py index acfd65228..c4156fc61 100644 --- a/pulser-simulation/pulser_simulation/simresults.py +++ b/pulser-simulation/pulser_simulation/simresults.py @@ -26,7 +26,7 @@ import numpy as np import qutip from numpy.typing import ArrayLike -from qutip.piqs import isdiagonal +from qutip.piqs.piqs import isdiagonal from pulser.result import Results, ResultType, SampledResult from pulser_simulation.qutip_result import QutipResult diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index 3b3061890..653ef20d0 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -348,7 +348,7 @@ def set_initial_state( "Incompatible shape of initial state." + f"Expected {legal_shape}, got {shape}." ) - self._initial_state = qutip.Qobj(state, dims=legal_dims) + self._initial_state = qutip.Qobj(state, dims=legal_dims).to("CSR") @property def evaluation_times(self) -> np.ndarray: @@ -491,7 +491,7 @@ def run( Args: progress_bar: If True, the progress bar of QuTiP's solver will be shown. If None or False, no text appears. - options: Used as arguments for qutip.Options(). If specified, will + options: Given directly to the Qutip Solver. If specified, will override SimConfig solver_options. If no `max_step` value is provided, an automatic one is calculated from the `Sequence`'s schedule (half of the shortest duration among pulses and @@ -536,7 +536,6 @@ def get_min_variation(ch_sample: ChannelSamples) -> int: options["nsteps"] = max( 1000, self._tot_duration // options["max_step"] ) - solv_ops = qutip.Options(**options) meas_errors: Optional[Mapping[str, float]] = None if "SPAM" in self.config.noise: @@ -580,16 +579,18 @@ def _run_solver() -> CoherentResults: self.initial_state, self._eval_times_array, self._hamiltonian._collapse_ops, - progress_bar=p_bar, - options=solv_ops, + options=dict( + progress_bar=p_bar, normalize_output=False, **options + ), ) else: result = qutip.sesolve( self._hamiltonian._hamiltonian, self.initial_state, self._eval_times_array, - progress_bar=p_bar, - options=solv_ops, + options=dict( + progress_bar=p_bar, normalize_output=False, **options + ), ) results = [ QutipResult( diff --git a/pulser-simulation/requirements.txt b/pulser-simulation/requirements.txt index 81d0ae189..ab90d70a6 100644 --- a/pulser-simulation/requirements.txt +++ b/pulser-simulation/requirements.txt @@ -1,3 +1 @@ -qutip~=4.7.5 -# This is needed until qutip fixes the incompatibility with scipy 1.12 -scipy<1.13 +qutip >= 5, < 6 diff --git a/tests/test_qutip_backend.py b/tests/test_qutip_backend.py index 5e9dd48f0..6a88b35d9 100644 --- a/tests/test_qutip_backend.py +++ b/tests/test_qutip_backend.py @@ -87,3 +87,22 @@ def test_with_default_noise(sequence): new_results = backend.run() assert isinstance(new_results, NoisyResults) assert backend._sim_obj.config == SimConfig.from_noise_model(spam_noise) + + +proj = [[0, 0], [0, 1]] + + +@pytest.mark.parametrize( + "collapse_op", [qutip.sigmax(), qutip.Qobj(proj), np.array(proj), proj] +) +def test_collapse_op(sequence, collapse_op): + noise_model = pulser.NoiseModel( + eff_noise_opers=[collapse_op], eff_noise_rates=[0.1] + ) + backend = QutipBackend( + sequence, config=pulser.EmulatorConfig(noise_model=noise_model) + ) + assert [ + op.type == qutip.core.data.CSR + for op in backend._sim_obj._hamiltonian._collapse_ops + ] diff --git a/tests/test_simresults.py b/tests/test_simresults.py index 927a37567..cddd006f2 100644 --- a/tests/test_simresults.py +++ b/tests/test_simresults.py @@ -17,7 +17,7 @@ import numpy as np import pytest import qutip -from qutip.piqs import isdiagonal +from qutip.piqs.piqs import isdiagonal from pulser import AnalogDevice, Pulse, Register, Sequence from pulser.devices import DigitalAnalogDevice, MockDevice @@ -189,8 +189,8 @@ def test_get_final_state( results_.get_final_state(reduce_to_basis="digital") h_states = results_.get_final_state( reduce_to_basis="digital", tol=1, normalize=False - ).eliminate_states([0]) - assert h_states.norm() < 3e-6 + ).full()[1:] + assert np.linalg.norm(h_states) < 3e-6 assert np.all( np.isclose( @@ -236,18 +236,6 @@ def test_get_state_float_time(results): results.get_state(mean, t_tol=diff / 2) state = results.get_state(mean, t_tol=3 * diff / 2) assert state == results.get_state(results._sim_times[-2]) - np.testing.assert_allclose( - state.full(), - np.array( - [ - [0.76522907 + 0.0j], - [0.08339973 - 0.39374219j], - [0.08339973 - 0.39374219j], - [-0.27977172 - 0.11031832j], - ] - ), - atol=1e-5, - ) def test_expect(results, pi_pulse, reg): diff --git a/tests/test_simulation.py b/tests/test_simulation.py index cd4650e94..58866bc67 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -171,15 +171,7 @@ def test_initialization_and_construction_of_hamiltonian(seq, mod_device): ) assert isinstance(sim._hamiltonian._hamiltonian, qutip.QobjEvo) - # Checks adapt() method: - assert bool( - set(sim._hamiltonian._hamiltonian.tlist).intersection( - sim.sampling_times - ) - ) - for qobjevo in sim._hamiltonian._hamiltonian.ops: - for sh in qobjevo.qobj.shape: - assert sh == sim.dim**sim._hamiltonian._size + assert sim._hamiltonian._hamiltonian(0).dtype == qutip.core.data.CSR assert not seq.is_parametrized() with pytest.warns(UserWarning, match="returns a copy of itself"): @@ -295,7 +287,7 @@ def _config(dim): # Check building operator with one operator op_standard = sim.build_operator([("sigma_gg", ["target"])]) op_one = sim.build_operator(("sigma_gg", ["target"])) - assert np.linalg.norm(op_standard - op_one) < 1e-10 + assert (op_standard - op_one).norm() < 1e-10 # Global ground-rydberg seq2 = Sequence(reg, DigitalAnalogDevice) @@ -788,13 +780,13 @@ def test_noise_with_zero_epsilons(seq, matrices): @pytest.mark.parametrize( "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), - (("eff_noise", "leakage"), {"0": 595, "1": 405}, 1), + ("dephasing", {"0": 586, "1": 414}, 1), + ("relaxation", {"0": 586, "1": 414}, 1), + ("eff_noise", {"0": 586, "1": 414}, 1), + ("depolarizing", {"0": 581, "1": 419}, 3), + (("dephasing", "depolarizing", "relaxation"), {"0": 582, "1": 418}, 5), + (("eff_noise", "dephasing"), {"0": 587, "1": 413}, 2), + (("eff_noise", "leakage"), {"0": 586, "1": 414}, 1), ], ) def test_noises_rydberg(matrices, noise, result, n_collapse_ops): @@ -821,12 +813,15 @@ def test_noises_rydberg(matrices, noise, result, n_collapse_ops): eff_noise_rates=[0.1 if "leakage" in noise else 0.025], ), ) + assert [ + op.type == qutip.core.data.CSR for op in sim._hamiltonian._collapse_ops + ] res = sim.run() res_samples = res.sample_final_state() assert res_samples == Counter(result) assert len(sim._hamiltonian._collapse_ops) == n_collapse_ops - trace_2 = res.states[-1] ** 2 - assert np.trace(trace_2) < 1 and not np.isclose(np.trace(trace_2), 1) + trace_2 = np.trace((res.states[-1] ** 2).full()) + assert trace_2 < 1 and not np.isclose(trace_2, 1) if "leakage" in noise: state = res.get_final_state() assert np.all(np.isclose(state[2, :], np.zeros_like(state[2, :]))) @@ -841,6 +836,9 @@ def test_relaxation_noise(): sim = QutipEmulator.from_sequence(seq) sim.add_config(SimConfig(noise="relaxation", relaxation_rate=0.1)) + assert [ + op.type == qutip.core.data.CSR for op in sim._hamiltonian._collapse_ops + ] res = sim.run() start_samples = res.sample_state(1) ryd_pop = start_samples["1"] @@ -906,7 +904,9 @@ def test_noises_digital(matrices, noise, result, n_collapse_ops, seq_digital): eff_noise_rates=[0.1 if "leakage" in noise else 0.025], ), ) - + assert [ + op.type == qutip.core.data.CSR for op in sim._hamiltonian._collapse_ops + ] with pytest.raises( ValueError, match="'relaxation' noise requires addressing of the 'ground-rydberg'", @@ -919,8 +919,8 @@ def test_noises_digital(matrices, noise, result, n_collapse_ops, seq_digital): 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) + trace_2 = np.trace((res.states[-1] ** 2).full()) + assert trace_2 < 1 and not np.isclose(trace_2, 1) if "leakage" in noise: state = res.get_final_state() assert np.all(np.isclose(state[2, :], np.zeros_like(state[2, :]))) @@ -944,7 +944,7 @@ def test_noises_digital(matrices, noise, result, n_collapse_ops, seq_digital): ("eff_noise", {"111": 958, "110": 19, "011": 12, "101": 11}, 2), ( "relaxation", - {"000": 421, "010": 231, "001": 172, "100": 171, "101": 5}, + {"000": 420, "010": 231, "001": 173, "100": 171, "101": 5}, 1, ), (("dephasing", "relaxation"), res_deph_relax, 3), @@ -997,6 +997,9 @@ def test_noises_all(matrices, reg, noise, result, n_collapse_ops, seq): eff_noise_rates=[0.2, 0.2], ), ) + assert [ + op.type == qutip.core.data.CSR for op in sim._hamiltonian._collapse_ops + ] with pytest.raises( ValueError, match="Incompatible shape for effective noise operator n°0.", @@ -1023,8 +1026,8 @@ def test_noises_all(matrices, reg, noise, result, n_collapse_ops, seq): res = sim.run() res_samples = res.sample_final_state() assert res_samples == Counter(result) - trace_2 = res.states[-1] ** 2 - assert np.trace(trace_2) < 1 and not np.isclose(np.trace(trace_2), 1) + trace_2 = np.trace((res.states[-1] ** 2).full()) + assert trace_2 < 1 and not np.isclose(trace_2, 1) if "leakage" in noise: state = res.get_final_state() assert np.all(np.isclose(state[3, :], np.zeros_like(state[3, :]))) From 8db708e7346fd4ba92d47e0f0ec208e00bf9e4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= <29920212+HGSilveri@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:10:33 +0100 Subject: [PATCH 14/15] Add shortcut to find out if an AbstractArray is differentiable (#784) * Define AbstractArray.is_differentiable * Remove warnings.simplefilter that was overriding pytest filterwarnings * Rename is_differentiable -> requires_grad --- pulser-core/pulser/math/abstract_array.py | 10 ++++++---- pulser-core/pulser/register/base_register.py | 1 - tests/test_channels.py | 6 ++---- tests/test_eom.py | 6 ++---- tests/test_math.py | 10 ++++------ tests/test_parametrized.py | 7 ++----- tests/test_pulse.py | 6 +++--- tests/test_register.py | 4 ++-- tests/test_sequence.py | 12 ++++++------ tests/test_sequence_sampler.py | 10 +++++----- tests/test_waveforms.py | 14 ++++---------- 11 files changed, 36 insertions(+), 50 deletions(-) diff --git a/pulser-core/pulser/math/abstract_array.py b/pulser-core/pulser/math/abstract_array.py index c74805a69..eb38bcddd 100644 --- a/pulser-core/pulser/math/abstract_array.py +++ b/pulser-core/pulser/math/abstract_array.py @@ -71,6 +71,11 @@ def is_tensor(self) -> bool: """Whether the stored array is a tensor.""" return self.has_torch() and isinstance(self._array, torch.Tensor) + @property + def requires_grad(self) -> bool: + """Whether the stored array is a tensor that needs a gradient.""" + return self.is_tensor and cast(torch.Tensor, self._array).requires_grad + def astype(self, dtype: DTypeLike) -> AbstractArray: """Casts the data type of the array contents.""" if self.is_tensor: @@ -271,10 +276,7 @@ def __setitem__(self, indices: Any, values: AbstractArrayLike) -> None: self._process_indices(indices) ] = values # type: ignore[assignment] except RuntimeError as e: - if ( - self.is_tensor - and cast(torch.Tensor, self._array).requires_grad - ): + if self.requires_grad: raise RuntimeError( "Failed to modify a tensor that requires grad in place." ) from e diff --git a/pulser-core/pulser/register/base_register.py b/pulser-core/pulser/register/base_register.py index 9b30c33bd..ef73e43ac 100644 --- a/pulser-core/pulser/register/base_register.py +++ b/pulser-core/pulser/register/base_register.py @@ -79,7 +79,6 @@ def __init__( ) self._ids: tuple[QubitId, ...] = tuple(qubits.keys()) if any(not isinstance(id, str) for id in self._ids): - warnings.simplefilter("always") warnings.warn( "Usage of `int`s or any non-`str`types as `QubitId`s will be " "deprecated. Define your `QubitId`s as `str`s, prefer setting " diff --git a/tests/test_channels.py b/tests/test_channels.py index a3e93e248..582deb23b 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -292,8 +292,7 @@ def test_modulation(channel, tr, eom, side_buffer_len, requires_grad): tr, tr, ) - if requires_grad: - assert out_.as_tensor().requires_grad + assert out_.requires_grad == requires_grad wf2 = BlackmanWaveform(800, wf_vals[1]) out_ = channel.modulate(wf2.samples, eom=eom) @@ -302,8 +301,7 @@ def test_modulation(channel, tr, eom, side_buffer_len, requires_grad): side_buffer_len, side_buffer_len, ) - if requires_grad: - assert out_.as_tensor().requires_grad + assert out_.requires_grad == requires_grad @pytest.mark.parametrize( diff --git a/tests/test_eom.py b/tests/test_eom.py index ea63a4b2d..e10d508b1 100644 --- a/tests/test_eom.py +++ b/tests/test_eom.py @@ -190,8 +190,7 @@ def calc_offset(amp): ] ) assert calculated_det_off == min(det_off_options, key=abs) - if requires_grad: - assert calculated_det_off.as_tensor().requires_grad + assert calculated_det_off.requires_grad == requires_grad # Case where the EOM pulses are off-resonant detuning_on = detuning_on + 1.0 @@ -210,5 +209,4 @@ def calc_offset(amp): assert off_options[0] == eom_.calculate_detuning_off( amp, detuning_on, optimal_detuning_off=0.0 ) - if requires_grad: - assert off_options.as_tensor().requires_grad + assert off_options.requires_grad == requires_grad diff --git a/tests/test_math.py b/tests/test_math.py index 75aa0d50a..51b8abb38 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -39,8 +39,7 @@ def test_pad(cast_to, requires_grad): arr = torch.tensor(arr, requires_grad=requires_grad) def check_match(arr1: pm.AbstractArray, arr2): - if requires_grad: - assert arr1.as_tensor().requires_grad + assert arr1.requires_grad == requires_grad np.testing.assert_array_equal( arr1.as_array(detach=requires_grad), arr2 ) @@ -260,8 +259,7 @@ def test_items(self, use_tensor, requires_grad, indices): assert item == val[i] assert isinstance(item, pm.AbstractArray) assert item.is_tensor == use_tensor - if use_tensor: - assert item.as_tensor().requires_grad == requires_grad + assert item.requires_grad == requires_grad # setitem if not requires_grad: @@ -292,8 +290,8 @@ def test_items(self, use_tensor, requires_grad, indices): new_val[indices] = 0.0 assert np.all(arr_np == new_val) assert arr_np.is_tensor - # The resulting tensor requires grad if the assing one did - assert arr_np.as_tensor().requires_grad == requires_grad + # The resulting tensor requires grad if the assigned one did + assert arr_np.requires_grad == requires_grad @pytest.mark.parametrize("scalar", [False, True]) @pytest.mark.parametrize( diff --git a/tests/test_parametrized.py b/tests/test_parametrized.py index 7d0c4ccc8..87e555843 100644 --- a/tests/test_parametrized.py +++ b/tests/test_parametrized.py @@ -104,10 +104,7 @@ def test_var_diff(a, b, requires_grad): b._assign(torch.tensor([-1.0, 1.0], requires_grad=requires_grad)) for var in [a, b]: - assert ( - a.value is not None - and a.value.as_tensor().requires_grad == requires_grad - ) + assert a.value is not None and a.value.requires_grad == requires_grad def test_varitem(a, b, d): @@ -167,7 +164,7 @@ def test_paramobj(bwf, t, a, b): def test_opsupport(a, b, with_diff_tensor): def check_var_grad(var): if with_diff_tensor: - assert var.build().as_tensor().requires_grad + assert var.build().requires_grad a._assign(-2.0) if with_diff_tensor: diff --git a/tests/test_pulse.py b/tests/test_pulse.py index fe51866a6..e5a265660 100644 --- a/tests/test_pulse.py +++ b/tests/test_pulse.py @@ -234,9 +234,9 @@ def test_eq(): def _assert_pulse_requires_grad(pulse: Pulse, invert: bool = False) -> None: - assert pulse.amplitude.samples.as_tensor().requires_grad == (not invert) - assert pulse.detuning.samples.as_tensor().requires_grad == (not invert) - assert pulse.phase.as_tensor().requires_grad == (not invert) + assert pulse.amplitude.samples.requires_grad == (not invert) + assert pulse.detuning.samples.requires_grad == (not invert) + assert pulse.phase.requires_grad == (not invert) @pytest.mark.parametrize("requires_grad", [True, False]) diff --git a/tests/test_register.py b/tests/test_register.py index 5cbacf0ae..c7c387a7f 100644 --- a/tests/test_register.py +++ b/tests/test_register.py @@ -508,9 +508,9 @@ def _assert_reg_requires_grad( ) -> None: for coords in reg.qubits.values(): if invert: - assert not coords.as_tensor().requires_grad + assert not coords.requires_grad else: - assert coords.is_tensor and coords.as_tensor().requires_grad + assert coords.is_tensor and coords.requires_grad @pytest.mark.parametrize( diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 87b0f7456..a5daffd69 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -2886,12 +2886,12 @@ def test_sequence_diff(device, parametrized, with_modulation, with_eom): seq_samples = sample(seq, modulation=with_modulation) ryd_ch_samples = seq_samples.channel_samples["ryd_global"] - assert ryd_ch_samples.amp.as_tensor().requires_grad - assert ryd_ch_samples.det.as_tensor().requires_grad - assert ryd_ch_samples.phase.as_tensor().requires_grad + assert ryd_ch_samples.amp.requires_grad + assert ryd_ch_samples.det.requires_grad + assert ryd_ch_samples.phase.requires_grad if "dmm_0" in seq_samples.channel_samples: dmm_ch_samples = seq_samples.channel_samples["dmm_0"] # Only detuning is modulated - assert not dmm_ch_samples.amp.as_tensor().requires_grad - assert dmm_ch_samples.det.as_tensor().requires_grad - assert not dmm_ch_samples.phase.as_tensor().requires_grad + assert not dmm_ch_samples.amp.requires_grad + assert dmm_ch_samples.det.requires_grad + assert not dmm_ch_samples.phase.requires_grad diff --git a/tests/test_sequence_sampler.py b/tests/test_sequence_sampler.py index fb539ca63..cc7b67f07 100644 --- a/tests/test_sequence_sampler.py +++ b/tests/test_sequence_sampler.py @@ -523,11 +523,11 @@ def test_phase_modulation(off_center, with_diff): seq_samples = sample(seq).channel_samples["rydberg_global"] if with_diff: - assert full_phase.samples.as_tensor().requires_grad - assert not seq_samples.amp.as_tensor().requires_grad - assert seq_samples.det.as_tensor().requires_grad - assert seq_samples.phase.as_tensor().requires_grad - assert seq_samples.phase_modulation.as_tensor().requires_grad + assert full_phase.samples.requires_grad + assert not seq_samples.amp.requires_grad + assert seq_samples.det.requires_grad + assert seq_samples.phase.requires_grad + assert seq_samples.phase_modulation.requires_grad np.testing.assert_allclose( seq_samples.phase_modulation.as_array(detach=with_diff) diff --git a/tests/test_waveforms.py b/tests/test_waveforms.py index 59648cfbe..5d46de56b 100644 --- a/tests/test_waveforms.py +++ b/tests/test_waveforms.py @@ -490,10 +490,7 @@ def test_waveform_diff( samples_tensor = wf.samples.as_tensor() assert samples_tensor.requires_grad == requires_grad - assert ( - wf.modulated_samples(rydberg_global).as_tensor().requires_grad - == requires_grad - ) + assert wf.modulated_samples(rydberg_global).requires_grad == requires_grad wfx2_tensor = (-wf * 2).samples.as_tensor() assert torch.equal(wfx2_tensor, samples_tensor * -2.0) assert wfx2_tensor.requires_grad == requires_grad @@ -501,15 +498,12 @@ def test_waveform_diff( wfdiv2 = wf / torch.tensor(2.0, requires_grad=True) assert torch.equal(wfdiv2.samples.as_tensor(), samples_tensor / 2.0) # Should always be true because it was divided by diff tensor - assert wfdiv2.samples.as_tensor().requires_grad + assert wfdiv2.samples.requires_grad - assert wf[-1].as_tensor().requires_grad == requires_grad + assert wf[-1].requires_grad == requires_grad try: - assert ( - wf.change_duration(1000).samples.as_tensor().requires_grad - == requires_grad - ) + assert wf.change_duration(1000).samples.requires_grad == requires_grad except NotImplementedError: pass From 63c03097a4576cb5b2da5196bbca75b9556939f6 Mon Sep 17 00:00:00 2001 From: HGSilveri Date: Fri, 20 Dec 2024 12:18:29 +0100 Subject: [PATCH 15/15] Bump version to v1.2.0 --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 1ae500e1f..867e52437 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.2dev1 +1.2.0 \ No newline at end of file