From 5cfc5aaea78907afaa44b0fdec482049e8d2bdc5 Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 10 Nov 2023 17:19:49 +0100 Subject: [PATCH 01/19] Delete summing constraint DetuningMap --- pulser-core/pulser/register/_reg_drawer.py | 5 +---- pulser-core/pulser/register/base_register.py | 3 +-- pulser-core/pulser/register/mappable_reg.py | 3 +-- .../pulser/register/register_layout.py | 3 +-- pulser-core/pulser/register/weight_maps.py | 19 +++++++++---------- pulser-core/pulser/sequence/sequence.py | 9 ++++----- tests/test_abstract_repr.py | 10 +++++----- tests/test_dmm.py | 13 ++++--------- tests/test_sequence.py | 12 ++++++------ tests/test_simulation.py | 4 +--- .../Local addressability with DMM.ipynb | 8 ++++---- .../State Preparation with the SLM Mask.ipynb | 4 ++-- 12 files changed, 39 insertions(+), 54 deletions(-) diff --git a/pulser-core/pulser/register/_reg_drawer.py b/pulser-core/pulser/register/_reg_drawer.py index 638a1b3c..298e9886 100644 --- a/pulser-core/pulser/register/_reg_drawer.py +++ b/pulser-core/pulser/register/_reg_drawer.py @@ -88,10 +88,7 @@ def _draw_2D( ): raise ValueError("masked qubits and dmm qubits must be the same.") elif masked_qubits: - dmm_qubits = { - masked_qubit: 1.0 / len(masked_qubits) - for masked_qubit in masked_qubits - } + dmm_qubits = {masked_qubit: 1.0 for masked_qubit in masked_qubits} if dmm_qubits: dmm_pos = [] diff --git a/pulser-core/pulser/register/base_register.py b/pulser-core/pulser/register/base_register.py index 8552349e..5327fc45 100644 --- a/pulser-core/pulser/register/base_register.py +++ b/pulser-core/pulser/register/base_register.py @@ -214,8 +214,7 @@ def define_detuning_map( Args: detuning_weights: A mapping between the IDs of the targeted qubits - and detuning weights (between 0 and 1, their sum must be equal - to 1). + and detuning weights (between 0 and 1). slug: An optional identifier for the detuning map. Returns: diff --git a/pulser-core/pulser/register/mappable_reg.py b/pulser-core/pulser/register/mappable_reg.py index 4513493e..47c6ace1 100644 --- a/pulser-core/pulser/register/mappable_reg.py +++ b/pulser-core/pulser/register/mappable_reg.py @@ -132,8 +132,7 @@ def define_detuning_map( Args: detuning_weights: A mapping between the IDs of the targeted traps - and detuning weights (between 0 and 1, their sum must be equal - to 1). + and detuning weights (between 0 and 1). slug: An optional identifier for the detuning map. Returns: diff --git a/pulser-core/pulser/register/register_layout.py b/pulser-core/pulser/register/register_layout.py index 5da293ae..8df147d0 100644 --- a/pulser-core/pulser/register/register_layout.py +++ b/pulser-core/pulser/register/register_layout.py @@ -108,8 +108,7 @@ def define_detuning_map( Args: detuning_weights: A mapping between the IDs of the targeted traps - and detuning weights (between 0 and 1, their sum must be equal - to 1). + and detuning weights (between 0 and 1). slug: An optional identifier for the detuning map. Returns: diff --git a/pulser-core/pulser/register/weight_maps.py b/pulser-core/pulser/register/weight_maps.py index 5c93744d..6ec30961 100644 --- a/pulser-core/pulser/register/weight_maps.py +++ b/pulser-core/pulser/register/weight_maps.py @@ -37,11 +37,9 @@ class WeightMap(Traps, RegDrawer): """Defines a generic map of weights on traps. - The sum of the provided weights must be equal to 1. - Args: trap_coordinates: An array containing the coordinates of the traps. - weights: A list weights to associate to the traps. + weights: A list of weights (between 0 and 1) to associate to the traps. """ weights: tuple[float, ...] @@ -56,10 +54,10 @@ def __init__( super().__init__(trap_coordinates, slug) if len(cast(list, trap_coordinates)) != len(weights): raise ValueError("Number of traps and weights don't match.") - if not np.all(np.array(weights) >= 0): - raise ValueError("All weights must be non-negative.") - if not np.isclose(sum(weights), 1.0, atol=1e-16): - raise ValueError("The sum of the weights should be 1.") + if not ( + np.all(np.array(weights) >= 0) and np.all(np.array(weights) <= 1) + ): + raise ValueError("All weights must be between 0 and 1.") object.__setattr__(self, "weights", tuple(weights)) @property @@ -173,10 +171,11 @@ def _to_abstract_repr(self) -> dict[str, Any]: class DetuningMap(WeightMap): """Defines a DetuningMap. - A DetuningMap associates a detuning weight to the coordinates of a trap. - The sum of the provided weights must be equal to 1. + A DetuningMap associates a detuning weight (a value between 0 and 1) + to the coordinates of a trap. Args: trap_coordinates: An array containing the coordinates of the traps. - weights: A list of detuning weights to associate to the traps. + weights: A list of detuning weights (between 0 and 1) to associate + to the traps. """ diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 57b22c5f..1fc0a9f1 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -496,10 +496,9 @@ def set_magnetic_field( self._calls.append(_Call("set_magnetic_field", mag_vector, {})) def _set_slm_mask_dmm(self, dmm_id: str, targets: set[QubitId]) -> None: - ntargets = len(targets) detuning_map = self.register.define_detuning_map( { - qubit: (1 / ntargets if qubit in targets else 0) + qubit: (1.0 if qubit in targets else 0) for qubit in self.register.qubit_ids } ) @@ -538,7 +537,7 @@ def config_slm_mask( channel starting the earliest in the schedule. If the sequence is in Ising, the SLM Mask is a DetuningMap where - the detuning of each masked qubit is the same. DMM "dmm_id" is + the detuning of each masked qubit is 1.0. DMM "dmm_id" is configured using this Detuning Map, and modulated by a pulse having a large negative detuning and either a duration defined from pulses already present in the sequence (same as in XY mode) or by the first @@ -598,8 +597,8 @@ def config_detuning_map( ``MockDevice`` DMM can be repeatedly declared if needed. Args: - detuning_map: A DetuningMap defining atoms to act on and bottom - detuning to modulate. + detuning_map: A DetuningMap defining the amont of detuning each + atom receives. dmm_id: How the channel is identified in the device. See in ``Sequence.available_channels`` which DMM IDs are still available (start by "dmm" ) and the associated description. diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index d85d85dd..037e77a7 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -859,13 +859,13 @@ def test_parametrized_fails_validation(self): @pytest.mark.parametrize("is_empty", [True, False]) def test_dmm_slm_mask(self, triangular_lattice, is_empty): mask = {"q0", "q2", "q4", "q5"} - dmm = {"q0": 0.2, "q1": 0.3, "q2": 0.4, "q3": 0.1} + det_map = {"q0": 1.0, "q1": 0.5, "q2": 0.5, "q3": 0.0} reg = triangular_lattice.rectangular_register(3, 4) seq = Sequence(reg, MockDevice) seq.config_slm_mask(mask, "dmm_0") if not is_empty: seq.config_detuning_map( - reg.define_detuning_map(dmm, "det_map"), "dmm_0" + reg.define_detuning_map(det_map, "det_map"), "dmm_0" ) seq.add_dmm_detuning(ConstantWaveform(100, -10), "dmm_0_1") seq.declare_channel("rydberg_global", "rydberg_global") @@ -900,7 +900,7 @@ def test_dmm_slm_mask(self, triangular_lattice, is_empty): "x": reg._coords[i][0], "y": reg._coords[i][1], } - for i, weight in enumerate(list(dmm.values())) + for i, weight in enumerate(list(det_map.values())) ] assert ( abstract["operations"][1]["detuning_map"]["slug"] == "det_map" @@ -1121,8 +1121,8 @@ def test_deserialize_seq_with_slm_mask_xy(self): def test_deserialize_seq_with_slm_dmm(self): traps = [ - {"weight": 0.5, "x": -2.0, "y": 9.0}, - {"weight": 0.5, "x": 0.0, "y": 2.0}, + {"weight": 1.0, "x": -2.0, "y": 9.0}, + {"weight": 1.0, "x": 0.0, "y": 2.0}, {"weight": 0, "x": 12.0, "y": 0.0}, ] op = [ diff --git a/tests/test_dmm.py b/tests/test_dmm.py index fe3673ba..95d0cb00 100644 --- a/tests/test_dmm.py +++ b/tests/test_dmm.py @@ -43,7 +43,7 @@ def map_reg(self, layout: RegisterLayout) -> MappableRegister: @pytest.fixture def det_dict(self) -> dict[int, float]: - return {0: 0.7, 1: 0.3, 2: 0} + return {0: 1.0, 1: 0.3, 2: 0} @pytest.fixture def det_map( @@ -53,7 +53,7 @@ def det_map( @pytest.fixture def slm_dict(self) -> dict[int, float]: - return {0: 1 / 3, 1: 1 / 3, 2: 1 / 3} + return {0: 1.0, 1: 1.0, 2: 1.0} @pytest.fixture def slm_map( @@ -89,7 +89,7 @@ def test_define_detuning_map( def test_qubit_weight_map(self, register): # Purposefully unsorted - qid_weight_map = {1: 0.5, 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 @@ -158,16 +158,11 @@ def test_detuning_map_bad_init( DetuningMap([(0, 0), (1, 0)], [0]) bad_weights = {0: -1.0, 1: 1.0, 2: 1.0} - bad_sum = {0: 0.1, 2: 0.9, 3: 0.1} for reg in (layout, map_reg, register): with pytest.raises( - ValueError, match="All weights must be non-negative." + ValueError, match="All weights must be between 0 and 1." ): reg.define_detuning_map(bad_weights) # type: ignore - with pytest.raises( - ValueError, match="The sum of the weights should be 1." - ): - reg.define_detuning_map(bad_sum) # type: ignore def test_init( self, diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 507bf0d7..6b31e084 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -52,7 +52,7 @@ def reg(): @pytest.fixture def det_map(reg: Register): return reg.define_detuning_map( - {"q" + str(i): (1 / 4 if i in [0, 1, 3, 4] else 0) for i in range(10)} + {"q" + str(i): (1.0 if i in [0, 1, 3, 4] else 0) for i in range(10)} ) @@ -496,7 +496,7 @@ def init_seq( if config_det_map: det_map = reg.define_detuning_map( { - "q" + str(i): (1 / 4 if i in [0, 1, 3, 4] else 0) + "q" + str(i): (1.0 if i in [0, 1, 3, 4] else 0) for i in range(10) } ) @@ -865,7 +865,7 @@ def test_switch_device_up( mod_trap_ids = [20, 32, 54, 66] assert np.all( nested_s_loc[:100] - == (-2.5 if trap_id in mod_trap_ids else 0) + == (-10.0 if trap_id in mod_trap_ids else 0) ) else: # first pulse is covered by SLM Mask @@ -1339,8 +1339,8 @@ def test_config_slm_mask(qubit_ids, device, det_map): seq.config_detuning_map(det_map, "dmm_0") seq.declare_channel("rydberg_global", "rydberg_global") assert set(seq._schedule.keys()) == {"dmm_0", "rydberg_global"} - assert seq._schedule["dmm_0"].detuning_map.weights[0] == 0.5 - assert seq._schedule["dmm_0"].detuning_map.weights[2] == 0.5 + assert seq._schedule["dmm_0"].detuning_map.weights[0] == 1.0 + assert seq._schedule["dmm_0"].detuning_map.weights[2] == 1.0 with pytest.raises(ValueError, match="configured only once"): seq.config_slm_mask(targets) @@ -1572,7 +1572,7 @@ def test_draw_register_det_maps(reg, ch_name, patch_plt_show): [(0, 0), (10, 10), (-10, -10), (20, 20), (30, 30), (40, 40)] ) det_map = reg_layout.define_detuning_map( - {0: 0, 1: 0, 2: 0, 3: 0.5, 4: 0.5} + {0: 0, 1: 0, 2: 0, 3: 1.0, 4: 1.0} ) reg = reg_layout.define_register(0, 1, 2, qubit_ids=["q0", "q1", "q2"]) targets = ["q0", "q2"] diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 27c7c9e9..3f1107a1 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -1122,9 +1122,7 @@ def test_mask_local_channel(): if q in masked_qubits: assert np.array_equal( sim.samples["Local"]["ground-rydberg"][q]["det"], - np.concatenate( - (-10 / len(masked_qubits) * pulse.amplitude.samples, [0]) - ), + np.concatenate((-10 * pulse.amplitude.samples, [0])), ) else: assert np.all( diff --git a/tutorials/advanced_features/Local addressability with DMM.ipynb b/tutorials/advanced_features/Local addressability with DMM.ipynb index daa16505..f19c1bc0 100644 --- a/tutorials/advanced_features/Local addressability with DMM.ipynb +++ b/tutorials/advanced_features/Local addressability with DMM.ipynb @@ -40,7 +40,7 @@ "source": [ "Even when working with **global** addressing channels, the **detuning** of individual qubits can be addressed **locally** by using a specific channel named the **Detuning Map Modulator** or `DMM`.\n", "\n", - "This `Channel` applies a `Global` pulse of **zero amplitude** and **negative detuning** on a `DetuningMap`. The `DetuningMap` consists of a set of weights on specific sites that dictate how the detuning applied by the `DMM` is distributed.\n", + "This `Channel` applies a `Global` pulse of **zero amplitude** and **negative detuning** on a `DetuningMap`. The `DetuningMap` consists of a set of weights on specific sites that dictate the amount of detuning applied by the `DMM` each site receives.\n", "\n", "This modulation of the `DetuningMap` by the `DMM` Channel is equivalent to adding a term $-\\frac{\\hbar}{2}\\sum_{i}\\epsilon_{i}\\Delta(t)\\sigma^{z}_{i}$ to the Ising Hamiltonian. Here, $\\Delta(t)$ is the detuning applied on the `DMM`, and $(\\epsilon_i)_{i}$ are the weights defined in the `DetuningMap` for each atom." ] @@ -56,7 +56,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A `DetuningMap` associates a set of locations with a set of weights. The weights $(\\epsilon_i)_i$ have to be positive, between 0 and 1, and their sum has to be equal to 1. The locations are the trap coordinates to address." + "A `DetuningMap` associates a set of locations with a set of weights. The locations are the trap coordinates to address and the weights $(\\epsilon_i)_i$ have to be between 0 and 1." ] }, { @@ -66,8 +66,8 @@ "outputs": [], "source": [ "trap_coordinates = [(0.0, 0.0), (0.0, 5.0), (5.0, 0.0), (5.0, 5.0)]\n", - "weights_1 = [0.5, 0.25, 0.25, 0] # between 0 and 1, sum equal to 1\n", - "weights_2 = [1 / 3, 1 / 3, 1 / 3, 0] # between 0 and 1, sum equal to 1" + "weights_1 = [1.0, 0.5, 0.5, 0] # between 0 and 1\n", + "weights_2 = [1.0, 1.0, 1.0, 0] # between 0 and 1" ] }, { diff --git a/tutorials/advanced_features/State Preparation with the SLM Mask.ipynb b/tutorials/advanced_features/State Preparation with the SLM Mask.ipynb index 3683fdb0..807cd2c9 100644 --- a/tutorials/advanced_features/State Preparation with the SLM Mask.ipynb +++ b/tutorials/advanced_features/State Preparation with the SLM Mask.ipynb @@ -269,9 +269,9 @@ "id": "09b56b15", "metadata": {}, "source": [ - "In Ising mode, configuring an SLM Mask with a `DMM` labeled `dmm_id` in the device internally configures a detuning map using `config_detuning_map` (see notebook [\"Local Addressability with DMM\"](dmm.nblink) for an introduction) with `dmm_id` and a `DetuningMap` **distributing the applied detuning equally over all the masked qubits**.\n", + "In Ising mode, configuring an SLM Mask with a `DMM` labeled `dmm_id` in the device internally configures a detuning map using `config_detuning_map` (see notebook [\"Local Addressability with DMM\"](dmm.nblink) for an introduction) with `dmm_id` and a `DetuningMap` **distributing all the applied detuning to the masked qubits**.\n", "\n", - "For instance in the last example qubits \"q1\" and \"q2\" are masked, so we expect a `DetuningMap` associating to the trap location of \"q0\" the weight 0, and to the trap locations of \"q1\" and \"q2\" the weight $1/2 = 0.5$:" + "For instance in the last example qubits \"q1\" and \"q2\" are masked, so we expect a `DetuningMap` associating to the trap location of \"q0\" the weight $0$, and to the trap locations of \"q1\" and \"q2\" the weight $1$:" ] }, { From 98e9ba427e10d692ffe8ed40c729ca9bec8c0d8a Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 10 Nov 2023 17:38:31 +0100 Subject: [PATCH 02/19] Bump version to 0.15.3 --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index c1c30390..1985d914 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.16dev2 +0.15.3 From 24c0b916b46de49d1e5c65532c2f1d0f2c9a51b7 Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 10 Nov 2023 18:10:51 +0100 Subject: [PATCH 03/19] Reverting version to v0.16.2 --- VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.txt b/VERSION.txt index 1985d914..c1c30390 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.15.3 +0.16dev2 From 674923d07a5c7754f6b7d79e61fe693b0e738525 Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 14 Nov 2023 11:39:39 +0100 Subject: [PATCH 04/19] Add global_bottom_detuning --- pulser-core/pulser/channels/dmm.py | 56 ++++++++++++++++---- pulser-core/pulser/devices/_devices.py | 3 +- pulser-core/pulser/sequence/sequence.py | 68 +++++++++++++++++++++---- tests/conftest.py | 9 +++- tests/test_devices.py | 1 + tests/test_dmm.py | 41 +++++++++++++-- tests/test_sequence.py | 14 +++-- 7 files changed, 160 insertions(+), 32 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 8d88cf39..e8edd7b2 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -21,6 +21,7 @@ from pulser.channels.base_channel import Channel from pulser.pulse import Pulse +from pulser.register.weight_maps import DetuningMap @dataclass(init=True, repr=False, frozen=True) @@ -31,16 +32,20 @@ class DMM(Channel): (of zero amplitude and phase). These Pulses are locally modulated by the weights of a `DetuningMap`, thus providing a local control over the detuning. The detuning of the pulses added to a DMM has to be negative, - between 0 and `bottom_detuning`. Channel targeting the transition between - the ground and rydberg states, thus encoding the 'ground-rydberg' basis. + between 0 and `bottom_detuning`, and the sum of the weights multiplied by + that detuning has to be blow `global_bottom_detuning`. Channel targeting + the transition between the ground and rydberg states, thus encoding the + 'ground-rydberg' basis. Note: The protocol to add pulses to the DMM Channel is by default "no-delay". Args: - bottom_detuning: Minimum possible detuning (in rad/µs), must be below - zero. + bottom_detuning: Minimum possible detuning for each atom (in rad/µs), + must be below zero. + global_bottom_detuning: Minimum possible detuning distributed on all + atoms (in rad/µs), must be below zero. clock_period: The duration of a clock cycle (in ns). The duration of a pulse or delay instruction is enforced to be a multiple of the clock cycle. @@ -52,6 +57,7 @@ class DMM(Channel): """ bottom_detuning: Optional[float] = field(default=None, init=True) + global_bottom_detuning: Optional[float] = field(default=None, init=True) addressing: Literal["Global"] = field(default="Global", init=False) max_abs_detuning: Optional[float] = field(default=None, init=False) max_amp: float = field(default=0, init=False) @@ -63,6 +69,17 @@ def __post_init__(self) -> None: super().__post_init__() if self.bottom_detuning and self.bottom_detuning > 0: raise ValueError("bottom_detuning must be negative.") + if self.global_bottom_detuning: + if self.global_bottom_detuning > 0: + raise ValueError("global_bottom_detuning must be negative.") + if ( + self.bottom_detuning + and self.bottom_detuning < self.global_bottom_detuning + ): + raise ValueError( + "global_bottom_detuning must be lower than" + " bottom_detuning." + ) @property def basis(self) -> Literal["ground-rydberg"]: @@ -72,26 +89,47 @@ def basis(self) -> Literal["ground-rydberg"]: def _undefined_fields(self) -> list[str]: optional = [ "bottom_detuning", + "global_bottom_detuning", "max_duration", ] return [field for field in optional if getattr(self, field) is None] - def validate_pulse(self, pulse: Pulse) -> None: - """Checks if a pulse can be executed in this DMM. + def validate_pulse( + self, + pulse: Pulse, + detuning_map: DetuningMap = DetuningMap( + trap_coordinates=[(0, 0)], weights=[1.0] + ), + ) -> None: + """Checks if a pulse can be executed via this DMM on a DetuningMap. Args: pulse: The pulse to validate. + detuning_map: The detuning map on which the pulse is applied + (defaults to a detuning map with weight 1.0). """ super().validate_pulse(pulse) round_detuning = np.round(pulse.detuning.samples, decimals=6) + # Check that detuning is negative if np.any(round_detuning > 0): raise ValueError("The detuning in a DMM must not be positive.") + # Check that detuning on each atom is above bottom_detuning if self.bottom_detuning is not None and np.any( - round_detuning < self.bottom_detuning + np.max(detuning_map.weights) * round_detuning + < self.bottom_detuning ): raise ValueError( - "The detuning goes below the bottom detuning " - f"of the DMM ({self.bottom_detuning} rad/µs)." + "The detunings on some atoms go below the local bottom " + f"detuning of the DMM ({self.bottom_detuning} rad/µs)." + ) + # Check that distributed detuning is above global_bottom_detuning + if self.global_bottom_detuning is not None and np.any( + np.sum(detuning_map.weights) * round_detuning + < self.global_bottom_detuning + ): + raise ValueError( + "The applied detuning goes below the global bottom detuning " + f"of the DMM ({self.global_bottom_detuning} rad/µs)." ) diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index 18a14e04..fdda6f04 100644 --- a/pulser-core/pulser/devices/_devices.py +++ b/pulser-core/pulser/devices/_devices.py @@ -61,7 +61,8 @@ clock_period=4, min_duration=16, max_duration=2**26, - bottom_detuning=-20, + bottom_detuning=-2 * np.pi * 20, + global_bottom_detuning=-2 * np.pi * 2000, ), ), ) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 1fc0a9f1..57c9ff11 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -234,6 +234,25 @@ def get_register( """The atom register on which to apply the pulses.""" return self._register if include_mappable else self.register + def _get_dmm_id_detuning_map(self, call: _Call) -> tuple[str, DetuningMap]: + dmm_id: str + det_map: DetuningMap + # Get DMM name + if "dmm_id" in call.kwargs: + dmm_id = call.kwargs["dmm_id"] + elif len(call.args) > 1: + dmm_id = call.args[1] + else: + dmm_id = "dmm_0" + # Get DetuningMap + if "detuning_map" in call.kwargs: + det_map = call.kwargs["detuning_map"] + elif isinstance(call.args[0], DetuningMap): + det_map = call.args[0] + else: # SLM case: + det_map = self._slm_detuning_map(set(call.args[0])) + return (dmm_id, det_map) + @property def declared_channels(self) -> dict[str, Channel]: """Channels declared in this Sequence.""" @@ -246,13 +265,7 @@ def declared_channels(self) -> dict[str, Channel]: call.name == "config_slm_mask" or call.name == "config_detuning_map" ): - dmm_id: str - if "dmm_id" in call.kwargs: - dmm_id = call.kwargs["dmm_id"] - elif len(call.args) > 1: - dmm_id = call.args[1] - else: - dmm_id = "dmm_0" + (dmm_id, _) = self._get_dmm_id_detuning_map(call) dmm_name = _get_dmm_name( dmm_id, list(all_declared_channels.keys()) ) @@ -495,13 +508,16 @@ def set_magnetic_field( # No parametrization -> Always stored as a regular call self._calls.append(_Call("set_magnetic_field", mag_vector, {})) - def _set_slm_mask_dmm(self, dmm_id: str, targets: set[QubitId]) -> None: - detuning_map = self.register.define_detuning_map( + def _slm_detuning_map(self, targets: set[QubitId]) -> DetuningMap: + return self.register.define_detuning_map( { qubit: (1.0 if qubit in targets else 0) for qubit in self.register.qubit_ids } ) + + def _set_slm_mask_dmm(self, dmm_id: str, targets: set[QubitId]) -> None: + detuning_map = self._slm_detuning_map(targets) self._config_detuning_map(detuning_map, dmm_id) # Find the name of the dmm in the declared channels. for key in reversed(self.declared_channels.keys()): @@ -2171,12 +2187,42 @@ def _validate_and_adjust_pulse( self, pulse: Pulse, channel: str, phase_ref: Optional[float] = None ) -> Pulse: channel_obj: Channel + detuning_map: DetuningMap | None = None if channel in self._schedule: channel_obj = self._schedule[channel].channel_obj + if isinstance(channel_obj, DMM): + detuning_map = cast( + _DMMSchedule, self._schedule[channel] + ).detuning_map else: # Sequence is parametrized and channel is a dmm_name - channel_obj = self.device.dmm_channels[_dmm_id_from_name(channel)] - channel_obj.validate_pulse(pulse) + dmm_id = _dmm_id_from_name(channel) + channel_obj = self.device.dmm_channels[dmm_id] + dmm_idx = -1 + for call in self._calls[1:] + self._to_build_calls: + if ( + call.name == "config_detuning_map" + or call.name == "config_slm_mask" + ): + # Check whether dmm_name matches with channel + ( + current_dmm, + current_det_map, + ) = self._get_dmm_id_detuning_map(call) + if current_dmm == dmm_id: + dmm_idx += 1 + current_dmm_name = ( + current_dmm + if dmm_idx == 0 + else current_dmm + f"_{dmm_idx}" + ) + if current_dmm_name == channel: + detuning_map = current_det_map + break + if detuning_map is None: + channel_obj.validate_pulse(pulse) + else: + cast(DMM, channel_obj).validate_pulse(pulse, detuning_map) _duration = channel_obj.validate_duration(pulse.duration) new_phase = pulse.phase + (phase_ref if phase_ref else 0) if _duration != pulse.duration: diff --git a/tests/conftest.py b/tests/conftest.py index 556b9ca7..1270975f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,8 +74,13 @@ def mod_device() -> Device: ), ), dmm_objects=( - DMM(bottom_detuning=-100), - DMM(clock_period=4, mod_bandwidth=4.0, bottom_detuning=-50), + DMM(bottom_detuning=-100, global_bottom_detuning=-10000), + DMM( + clock_period=4, + mod_bandwidth=4.0, + bottom_detuning=-50, + global_bottom_detuning=-5000, + ), ), ) diff --git a/tests/test_devices.py b/tests/test_devices.py index 54d8aa69..2cfc6f1c 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -436,6 +436,7 @@ def test_dmm_channels(): replace(Chadoq2, dmm_objects=(DMM(),)) dmm = DMM( bottom_detuning=-1, + global_bottom_detuning=-100, clock_period=1, min_duration=1, max_duration=1e6, diff --git a/tests/test_dmm.py b/tests/test_dmm.py index 95d0cb00..f17ecee6 100644 --- a/tests/test_dmm.py +++ b/tests/test_dmm.py @@ -25,6 +25,7 @@ from pulser.register.base_register import BaseRegister from pulser.register.mappable_reg import MappableRegister from pulser.register.register_layout import RegisterLayout +from pulser.register.special_layouts import TriangularLatticeLayout from pulser.register.weight_maps import DetuningMap, WeightMap @@ -228,6 +229,7 @@ class TestDMM: def physical_dmm(self): return DMM( bottom_detuning=-1, + global_bottom_detuning=-10, clock_period=1, min_duration=1, max_duration=1e6, @@ -236,12 +238,12 @@ def physical_dmm(self): def test_init(self, physical_dmm): assert DMM().is_virtual() - dmm = physical_dmm assert not dmm.is_virtual() assert dmm.basis == "ground-rydberg" assert dmm.addressing == "Global" assert dmm.bottom_detuning == -1 + assert dmm.global_bottom_detuning == -10 assert dmm.max_amp == 0 for value in ( dmm.max_abs_detuning, @@ -254,6 +256,14 @@ def test_init(self, physical_dmm): ValueError, match="bottom_detuning must be negative." ): DMM(bottom_detuning=1) + with pytest.raises( + ValueError, match="global_bottom_detuning must be negative." + ): + DMM(global_bottom_detuning=10) + with pytest.raises( + ValueError, match="global_bottom_detuning must be lower" + ): + DMM(global_bottom_detuning=-1, bottom_detuning=-10) with pytest.raises( NotImplementedError, match=f"{DMM} cannot be initialized from `Global` method.", @@ -278,13 +288,34 @@ def test_validate_pulse(self, physical_dmm): with pytest.raises( ValueError, match=re.escape( - "The detuning goes below the bottom detuning " - f"of the DMM ({physical_dmm.bottom_detuning} rad/µs)" + "The detunings on some atoms go below the local " + "bottom detuning of the DMM " + f"({physical_dmm.bottom_detuning} rad/µs)" ), ): + # tested with detuning map with weight 1 physical_dmm.validate_pulse(too_low_pulse) - # Should be valid in a virtual DMM - virtual_dmm = DMM() + # Should be valid in a virtual DMM without local bottom detuning + virtual_dmm = DMM(global_bottom_detuning=-10) assert virtual_dmm.is_virtual() virtual_dmm.validate_pulse(too_low_pulse) + + # Not too low if weights of detuning map are lower than 1 + det_map = TriangularLatticeLayout(100, 10).define_detuning_map( + {i: 0.5 if i < 20 else 0.0 for i in range(100)} + ) + with pytest.raises( + ValueError, + match=re.escape( + "The applied detuning goes below the global bottom detuning " + f"of the DMM ({physical_dmm.global_bottom_detuning} rad/µs)" + ), + ): + # local detunings match bottom_detuning, global don't + physical_dmm.validate_pulse(too_low_pulse, det_map) + + # Should be valid in a virtual DMM without global bottom detuning + virtual_dmm = DMM(bottom_detuning=-1) + assert virtual_dmm.is_virtual() + virtual_dmm.validate_pulse(too_low_pulse, det_map) diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 6b31e084..ef08bd33 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -60,7 +60,10 @@ def det_map(reg: Register): def device(): return dataclasses.replace( Chadoq2, - dmm_objects=(DMM(bottom_detuning=-70), DMM(bottom_detuning=-100)), + dmm_objects=( + DMM(bottom_detuning=-70, global_bottom_detuning=-700), + DMM(bottom_detuning=-100, global_bottom_detuning=-10000), + ), ) @@ -348,7 +351,8 @@ def devices(): clock_period=4, min_duration=16, max_duration=2**26, - bottom_detuning=-20, + bottom_detuning=-2 * np.pi * 20, + global_bottom_detuning=-2 * np.pi * 2000, ), ), ) @@ -384,7 +388,8 @@ def devices(): clock_period=4, min_duration=16, max_duration=2**26, - bottom_detuning=-20, + bottom_detuning=-2 * np.pi * 20, + global_bottom_detuning=-2 * np.pi * 2000, ), ), ) @@ -439,7 +444,8 @@ def devices(): clock_period=4, min_duration=16, max_duration=2**26, - bottom_detuning=-20, + bottom_detuning=-2 * np.pi * 20, + global_bottom_detuning=-2 * np.pi * 2000, ), ), ) From f1575660a828db362cd752b206cac4b40e451c56 Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 14 Nov 2023 11:44:19 +0100 Subject: [PATCH 05/19] Update schema --- .../abstract_repr/schemas/device-schema.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 80a63c3b..42e81863 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -21,7 +21,7 @@ "type": "string" }, "bottom_detuning": { - "description": "Minimum possible detuning (in rad/µs), must be below zero.", + "description": "Minimum possible detuning per trap (in rad/µs), must be below zero.", "type": [ "number", "null" @@ -39,6 +39,13 @@ "description": "Time taken to change the target (in ns).", "type": "null" }, + "global_bottom_detuning": { + "description": "Minimum possible detuning of the whole channel (in rad/µs), must be below zero.", + "type": [ + "number", + "null" + ] + }, "id": { "$ref": "#/definitions/ChannelId", "description": "The identifier of the channel within its device." @@ -90,6 +97,7 @@ "clock_period", "eom_config", "fixed_retarget_t", + "global_bottom_detuning", "id", "max_abs_detuning", "max_amp", @@ -1435,7 +1443,7 @@ "type": "string" }, "bottom_detuning": { - "description": "Minimum possible detuning (in rad/µs), must be below zero.", + "description": "Minimum possible detuning per trap (in rad/µs), must be below zero.", "type": "number" }, "clock_period": { @@ -1450,6 +1458,10 @@ "description": "Time taken to change the target (in ns).", "type": "null" }, + "global_bottom_detuning": { + "description": "Minimum possible detuning of the whole channel (in rad/µs), must be below zero.", + "type": "number" + }, "id": { "$ref": "#/definitions/ChannelId", "description": "The identifier of the channel within its device." @@ -1498,6 +1510,7 @@ "clock_period", "eom_config", "fixed_retarget_t", + "global_bottom_detuning", "id", "max_abs_detuning", "max_amp", From f8fcc7957817aa6fc2b5104b3126b7cabc378565 Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 14 Nov 2023 12:07:51 +0100 Subject: [PATCH 06/19] Updating documentation --- .../Local addressability with DMM.ipynb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tutorials/advanced_features/Local addressability with DMM.ipynb b/tutorials/advanced_features/Local addressability with DMM.ipynb index f19c1bc0..e025a650 100644 --- a/tutorials/advanced_features/Local addressability with DMM.ipynb +++ b/tutorials/advanced_features/Local addressability with DMM.ipynb @@ -13,8 +13,12 @@ "metadata": {}, "outputs": [], "source": [ + "import numpy as np\n", + "\n", "from dataclasses import replace\n", + "\n", "from matplotlib import pyplot as plt\n", + "\n", "from pulser.channels.dmm import DMM\n", "from pulser.devices import AnalogDevice\n", "from pulser.register import Register\n", @@ -40,7 +44,7 @@ "source": [ "Even when working with **global** addressing channels, the **detuning** of individual qubits can be addressed **locally** by using a specific channel named the **Detuning Map Modulator** or `DMM`.\n", "\n", - "This `Channel` applies a `Global` pulse of **zero amplitude** and **negative detuning** on a `DetuningMap`. The `DetuningMap` consists of a set of weights on specific sites that dictate the amount of detuning applied by the `DMM` each site receives.\n", + "This `Channel` applies a `Global` pulse of **zero amplitude** and **negative detuning** on a `DetuningMap`. The `DetuningMap` consists of a set of weights on specific sites that dictate the proportion of detuning applied by the `DMM` each site receives.\n", "\n", "This modulation of the `DetuningMap` by the `DMM` Channel is equivalent to adding a term $-\\frac{\\hbar}{2}\\sum_{i}\\epsilon_{i}\\Delta(t)\\sigma^{z}_{i}$ to the Ising Hamiltonian. Here, $\\Delta(t)$ is the detuning applied on the `DMM`, and $(\\epsilon_i)_{i}$ are the weights defined in the `DetuningMap` for each atom." ] @@ -137,7 +141,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A `DMM` Channel is a `Channel` that accepts pulses of zero amplitude and detuning below 0 and above `bottom_detuning`:" + "A `DMM` Channel is a `Channel` that accepts pulses of zero amplitude and detuning below 0 and above:\n", + "- `bottom_detuning` for each site.\n", + "- `global_bottom_detuning` for the total detuning distributed among the atoms:" ] }, { @@ -151,7 +157,8 @@ " min_duration=16,\n", " max_duration=2**26,\n", " mod_bandwidth=8,\n", - " bottom_detuning=-20, # detuning between 0 and -20 rad/µs\n", + " bottom_detuning=-2 * np.pi * 20, # detuning between 0 and -20 MHz\n", + " global_bottom_detuning=-2 * np.pi * 2000 # total detuning\n", ")" ] }, From 790967996ff41f320f1316cc90b0a799f5348b45 Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 14 Nov 2023 15:11:01 +0100 Subject: [PATCH 07/19] Fix typing, tests --- tests/test_sequence.py | 2 +- tutorials/advanced_features/Local addressability with DMM.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_sequence.py b/tests/test_sequence.py index ef08bd33..594b7bf2 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -507,7 +507,7 @@ def init_seq( } ) if mappable_reg: - seq.config_detuning_map(det_map, "dmm_0") + seq.config_detuning_map(detuning_map=det_map, dmm_id="dmm_0") else: seq.config_slm_mask(["q0"], "dmm_0") return seq diff --git a/tutorials/advanced_features/Local addressability with DMM.ipynb b/tutorials/advanced_features/Local addressability with DMM.ipynb index e025a650..de370fca 100644 --- a/tutorials/advanced_features/Local addressability with DMM.ipynb +++ b/tutorials/advanced_features/Local addressability with DMM.ipynb @@ -158,7 +158,7 @@ " max_duration=2**26,\n", " mod_bandwidth=8,\n", " bottom_detuning=-2 * np.pi * 20, # detuning between 0 and -20 MHz\n", - " global_bottom_detuning=-2 * np.pi * 2000 # total detuning\n", + " global_bottom_detuning=-2 * np.pi * 2000, # total detuning\n", ")" ] }, From d0906b0776d9ee2f94f33480d6719c503d1d160d Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 14 Nov 2023 16:49:06 +0100 Subject: [PATCH 08/19] Add global_bottom_det on slm, switch device --- pulser-core/pulser/sequence/sequence.py | 19 ++++++-- tests/test_sequence.py | 64 ++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 57c9ff11..d20e4aeb 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -793,6 +793,7 @@ def check_retarget(ch_obj: Channel) -> bool: ] if isinstance(old_ch_obj, DMM): params_to_check.append("bottom_detuning") + params_to_check.append("global_bottom_detuning") if check_retarget(old_ch_obj) or check_retarget(new_ch_obj): params_to_check.append("min_retarget_interval") for param_ in params_to_check: @@ -1922,12 +1923,20 @@ def _modulate_slm_mask_dmm(self, duration: int, max_amp: float) -> None: bottom_detuning = cast( DMM, self.declared_channels[self._slm_mask_dmm] ).bottom_detuning + global_bottom_detuning = cast( + DMM, self.declared_channels[self._slm_mask_dmm] + ).global_bottom_detuning min_det = -10 * max_amp - min_det = ( - bottom_detuning - if (bottom_detuning and min_det < bottom_detuning) - else min_det - ) + if bottom_detuning and min_det < bottom_detuning: + min_det = bottom_detuning + if ( + global_bottom_detuning + and min_det * len(set(self._slm_mask_targets)) + < global_bottom_detuning + ): + min_det = global_bottom_detuning / len( + set(self._slm_mask_targets) + ) cast( _DMMSchedule, self._schedule[self._slm_mask_dmm] )._waiting_for_first_pulse = False diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 594b7bf2..3714dba0 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -62,7 +62,7 @@ def device(): Chadoq2, dmm_objects=( DMM(bottom_detuning=-70, global_bottom_detuning=-700), - DMM(bottom_detuning=-100, global_bottom_detuning=-10000), + DMM(bottom_detuning=-100, global_bottom_detuning=-1000), ), ) @@ -650,6 +650,25 @@ def test_switch_device_down( ), strict=True, ) + with pytest.raises( + ValueError, + match="No match for channel dmm_0_1 with the same " + "global_bottom_detuning.", + ): + # Can't find a match for the 1st dmm_0 + seq.switch_device( + dataclasses.replace( + Chadoq2, + dmm_objects=( + Chadoq2.dmm_channels["dmm_0"], + dataclasses.replace( + Chadoq2.dmm_channels["dmm_0"], + global_bottom_detuning=-500, + ), + ), + ), + strict=True, + ) seq_ising = init_seq( reg, MockDevice, @@ -1431,8 +1450,7 @@ def test_slm_mask_in_xy(reg, patch_plt_show): @pytest.mark.parametrize("draw_register", [True, False]) @pytest.mark.parametrize("mode", ["input", "input+output"]) @pytest.mark.parametrize("mod_bandwidth", [0, 10]) -def test_slm_mask_in_ising( - reg, +def test_draw_slm_mask_in_ising( patch_plt_show, dims3D, mode, @@ -1536,8 +1554,30 @@ def test_slm_mask_in_ising( draw_qubit_det=draw_qubit_det, draw_qubit_amp=draw_qubit_amp, ) + + +@pytest.mark.parametrize( + "bottom_detunings", [(None, None), (-20, None), (None, -20), (-20, -20)] +) +def test_slm_mask_in_ising(patch_plt_show, bottom_detunings): + reg = Register({"q0": (0, 0), "q1": (10, 10), "q2": (-10, -10)}) + det_map = reg.define_detuning_map({"q0": 0.2, "q1": 0.8, "q2": 0.0}) + targets = ["q0", "q2"] + amp = 10 + pulse = Pulse.ConstantPulse(200, amp, 0, 0) # Set mask and then add ising pulses to the schedule - seq2 = Sequence(reg, MockDevice) + seq2 = Sequence( + reg, + dataclasses.replace( + MockDevice, + dmm_objects=( + DMM( + bottom_detuning=bottom_detunings[0], + global_bottom_detuning=bottom_detunings[1], + ), + ), + ), + ) seq2.config_slm_mask(targets) seq2.declare_channel("ryd_glob", "rydberg_global") seq2.config_detuning_map(det_map, "dmm_0") # configured as dmm_0_1 @@ -1550,14 +1590,24 @@ def test_slm_mask_in_ising( ): seq2.add(Pulse.ConstantPulse(300, 0, -10, 0), "dmm_0") seq2.add_dmm_detuning(RampWaveform(300, -10, 0), "dmm_0_1") # not slm - seq2.add(pulse2, "ryd_glob") # slm pulse between 0 and 500 + seq2.add(pulse, "ryd_glob") # slm pulse between 0 and 500 assert seq2._slm_mask_time == [0, 500] + slm_det: float + if bottom_detunings == (None, None): + slm_det = -10 * amp + elif bottom_detunings[0] is None: + slm_det = max(-10 * amp, bottom_detunings[1] / len(targets)) + elif bottom_detunings[1] is None: + slm_det = max(-10 * amp, bottom_detunings[0]) + else: + assert bottom_detunings[1] / len(targets) > bottom_detunings[0] + slm_det = max(-10 * amp, bottom_detunings[1] / len(targets)) assert seq2._schedule["dmm_0"].slots[1].type == Pulse.ConstantPulse( - 500, 0, -100, 0 + 500, 0, slm_det, 0 ) # Check that adding extra pulses does not change SLM mask time - seq2.add(pulse2, "ryd_glob") + seq2.add(pulse, "ryd_glob") assert seq2._slm_mask_time == [0, 500] seq5 = Sequence(reg, MockDevice) From 67d14b0b1c113b20d2af58252e24b5b5f517f98a Mon Sep 17 00:00:00 2001 From: a_corni Date: Thu, 16 Nov 2023 17:09:13 +0100 Subject: [PATCH 09/19] Update descr bottom_det, dmm, --- pulser-core/pulser/channels/dmm.py | 7 ++++--- .../pulser/json/abstract_repr/schemas/device-schema.json | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index e8edd7b2..53a529b7 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -42,7 +42,7 @@ class DMM(Channel): "no-delay". Args: - bottom_detuning: Minimum possible detuning for each atom (in rad/µs), + bottom_detuning: Minimum possible detuning per atom (in rad/µs), must be below zero. global_bottom_detuning: Minimum possible detuning distributed on all atoms (in rad/µs), must be below zero. @@ -114,8 +114,9 @@ def validate_pulse( if np.any(round_detuning > 0): raise ValueError("The detuning in a DMM must not be positive.") # Check that detuning on each atom is above bottom_detuning - if self.bottom_detuning is not None and np.any( - np.max(detuning_map.weights) * round_detuning + if ( + self.bottom_detuning is not None + and np.max(detuning_map.weights) * round_detuning < self.bottom_detuning ): raise ValueError( 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 42e81863..1a6d0b70 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -40,7 +40,7 @@ "type": "null" }, "global_bottom_detuning": { - "description": "Minimum possible detuning of the whole channel (in rad/µs), must be below zero.", + "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", "type": [ "number", "null" @@ -1459,7 +1459,7 @@ "type": "null" }, "global_bottom_detuning": { - "description": "Minimum possible detuning of the whole channel (in rad/µs), must be below zero.", + "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", "type": "number" }, "id": { From 8a64c783fc0903a2494a5877f8d6dc4eba5da358 Mon Sep 17 00:00:00 2001 From: a_corni Date: Thu, 16 Nov 2023 17:17:11 +0100 Subject: [PATCH 10/19] Fixing test of array --- pulser-core/pulser/channels/dmm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 53a529b7..854c85d3 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -114,9 +114,8 @@ def validate_pulse( if np.any(round_detuning > 0): raise ValueError("The detuning in a DMM must not be positive.") # Check that detuning on each atom is above bottom_detuning - if ( - self.bottom_detuning is not None - and np.max(detuning_map.weights) * round_detuning + if self.bottom_detuning is not None and np.any( + np.max(detuning_map.weights) * round_detuning < self.bottom_detuning ): raise ValueError( @@ -124,8 +123,9 @@ def validate_pulse( f"detuning of the DMM ({self.bottom_detuning} rad/µs)." ) # Check that distributed detuning is above global_bottom_detuning - if self.global_bottom_detuning is not None and np.any( - np.sum(detuning_map.weights) * round_detuning + if ( + self.global_bottom_detuning is not None + and np.sum(detuning_map.weights) * round_detuning < self.global_bottom_detuning ): raise ValueError( From 32aa5ed943f46520bf73d233374d9ed5907dd340 Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 17 Nov 2023 13:59:08 +0100 Subject: [PATCH 11/19] replace array comparison by min comparisons --- pulser-core/pulser/channels/dmm.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 854c85d3..4ecee9d4 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -114,8 +114,10 @@ def validate_pulse( if np.any(round_detuning > 0): raise ValueError("The detuning in a DMM must not be positive.") # Check that detuning on each atom is above bottom_detuning - if self.bottom_detuning is not None and np.any( - np.max(detuning_map.weights) * round_detuning + min_round_detuning = np.min(round_detuning) + if ( + self.bottom_detuning is not None + and np.max(detuning_map.weights) * min_round_detuning < self.bottom_detuning ): raise ValueError( @@ -125,7 +127,7 @@ def validate_pulse( # Check that distributed detuning is above global_bottom_detuning if ( self.global_bottom_detuning is not None - and np.sum(detuning_map.weights) * round_detuning + and np.sum(detuning_map.weights) * min_round_detuning < self.global_bottom_detuning ): raise ValueError( From 852aa6ead24ab107ef8cd7e045bb532464410f5a Mon Sep 17 00:00:00 2001 From: a_corni Date: Thu, 23 Nov 2023 14:52:12 +0100 Subject: [PATCH 12/19] global_bottom_detuning to total_bottom_detuning --- pulser-core/pulser/channels/dmm.py | 26 +++++++++---------- pulser-core/pulser/devices/_devices.py | 2 +- .../abstract_repr/schemas/device-schema.json | 8 +++--- pulser-core/pulser/sequence/sequence.py | 12 ++++----- tests/conftest.py | 4 +-- tests/test_devices.py | 2 +- tests/test_dmm.py | 16 ++++++------ tests/test_sequence.py | 16 ++++++------ .../Local addressability with DMM.ipynb | 2 +- 9 files changed, 44 insertions(+), 44 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 4ecee9d4..273ef260 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -33,7 +33,7 @@ class DMM(Channel): weights of a `DetuningMap`, thus providing a local control over the detuning. The detuning of the pulses added to a DMM has to be negative, between 0 and `bottom_detuning`, and the sum of the weights multiplied by - that detuning has to be blow `global_bottom_detuning`. Channel targeting + that detuning has to be blow `total_bottom_detuning`. Channel targeting the transition between the ground and rydberg states, thus encoding the 'ground-rydberg' basis. @@ -44,7 +44,7 @@ class DMM(Channel): Args: bottom_detuning: Minimum possible detuning per atom (in rad/µs), must be below zero. - global_bottom_detuning: Minimum possible detuning distributed on all + total_bottom_detuning: Minimum possible detuning distributed on all atoms (in rad/µs), must be below zero. clock_period: The duration of a clock cycle (in ns). The duration of a pulse or delay instruction is enforced to be a multiple of the @@ -57,7 +57,7 @@ class DMM(Channel): """ bottom_detuning: Optional[float] = field(default=None, init=True) - global_bottom_detuning: Optional[float] = field(default=None, init=True) + total_bottom_detuning: Optional[float] = field(default=None, init=True) addressing: Literal["Global"] = field(default="Global", init=False) max_abs_detuning: Optional[float] = field(default=None, init=False) max_amp: float = field(default=0, init=False) @@ -69,15 +69,15 @@ def __post_init__(self) -> None: super().__post_init__() if self.bottom_detuning and self.bottom_detuning > 0: raise ValueError("bottom_detuning must be negative.") - if self.global_bottom_detuning: - if self.global_bottom_detuning > 0: - raise ValueError("global_bottom_detuning must be negative.") + if self.total_bottom_detuning: + if self.total_bottom_detuning > 0: + raise ValueError("total_bottom_detuning must be negative.") if ( self.bottom_detuning - and self.bottom_detuning < self.global_bottom_detuning + and self.bottom_detuning < self.total_bottom_detuning ): raise ValueError( - "global_bottom_detuning must be lower than" + "total_bottom_detuning must be lower than" " bottom_detuning." ) @@ -89,7 +89,7 @@ def basis(self) -> Literal["ground-rydberg"]: def _undefined_fields(self) -> list[str]: optional = [ "bottom_detuning", - "global_bottom_detuning", + "total_bottom_detuning", "max_duration", ] return [field for field in optional if getattr(self, field) is None] @@ -124,15 +124,15 @@ def validate_pulse( "The detunings on some atoms go below the local bottom " f"detuning of the DMM ({self.bottom_detuning} rad/µs)." ) - # Check that distributed detuning is above global_bottom_detuning + # Check that distributed detuning is above total_bottom_detuning if ( - self.global_bottom_detuning is not None + self.total_bottom_detuning is not None and np.sum(detuning_map.weights) * min_round_detuning - < self.global_bottom_detuning + < self.total_bottom_detuning ): raise ValueError( "The applied detuning goes below the global bottom detuning " - f"of the DMM ({self.global_bottom_detuning} rad/µs)." + f"of the DMM ({self.total_bottom_detuning} rad/µs)." ) diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index fdda6f04..a93958b1 100644 --- a/pulser-core/pulser/devices/_devices.py +++ b/pulser-core/pulser/devices/_devices.py @@ -62,7 +62,7 @@ min_duration=16, max_duration=2**26, bottom_detuning=-2 * np.pi * 20, - global_bottom_detuning=-2 * np.pi * 2000, + total_bottom_detuning=-2 * np.pi * 2000, ), ), ) 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 1a6d0b70..a10997cf 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -39,7 +39,7 @@ "description": "Time taken to change the target (in ns).", "type": "null" }, - "global_bottom_detuning": { + "total_bottom_detuning": { "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", "type": [ "number", @@ -97,7 +97,7 @@ "clock_period", "eom_config", "fixed_retarget_t", - "global_bottom_detuning", + "total_bottom_detuning", "id", "max_abs_detuning", "max_amp", @@ -1458,7 +1458,7 @@ "description": "Time taken to change the target (in ns).", "type": "null" }, - "global_bottom_detuning": { + "total_bottom_detuning": { "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", "type": "number" }, @@ -1510,7 +1510,7 @@ "clock_period", "eom_config", "fixed_retarget_t", - "global_bottom_detuning", + "total_bottom_detuning", "id", "max_abs_detuning", "max_amp", diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index d20e4aeb..1452740e 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -793,7 +793,7 @@ def check_retarget(ch_obj: Channel) -> bool: ] if isinstance(old_ch_obj, DMM): params_to_check.append("bottom_detuning") - params_to_check.append("global_bottom_detuning") + params_to_check.append("total_bottom_detuning") if check_retarget(old_ch_obj) or check_retarget(new_ch_obj): params_to_check.append("min_retarget_interval") for param_ in params_to_check: @@ -1923,18 +1923,18 @@ def _modulate_slm_mask_dmm(self, duration: int, max_amp: float) -> None: bottom_detuning = cast( DMM, self.declared_channels[self._slm_mask_dmm] ).bottom_detuning - global_bottom_detuning = cast( + total_bottom_detuning = cast( DMM, self.declared_channels[self._slm_mask_dmm] - ).global_bottom_detuning + ).total_bottom_detuning min_det = -10 * max_amp if bottom_detuning and min_det < bottom_detuning: min_det = bottom_detuning if ( - global_bottom_detuning + total_bottom_detuning and min_det * len(set(self._slm_mask_targets)) - < global_bottom_detuning + < total_bottom_detuning ): - min_det = global_bottom_detuning / len( + min_det = total_bottom_detuning / len( set(self._slm_mask_targets) ) cast( diff --git a/tests/conftest.py b/tests/conftest.py index 1270975f..14941bda 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,12 +74,12 @@ def mod_device() -> Device: ), ), dmm_objects=( - DMM(bottom_detuning=-100, global_bottom_detuning=-10000), + DMM(bottom_detuning=-100, total_bottom_detuning=-10000), DMM( clock_period=4, mod_bandwidth=4.0, bottom_detuning=-50, - global_bottom_detuning=-5000, + total_bottom_detuning=-5000, ), ), ) diff --git a/tests/test_devices.py b/tests/test_devices.py index 2cfc6f1c..c121fd17 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -436,7 +436,7 @@ def test_dmm_channels(): replace(Chadoq2, dmm_objects=(DMM(),)) dmm = DMM( bottom_detuning=-1, - global_bottom_detuning=-100, + total_bottom_detuning=-100, clock_period=1, min_duration=1, max_duration=1e6, diff --git a/tests/test_dmm.py b/tests/test_dmm.py index f17ecee6..381223a6 100644 --- a/tests/test_dmm.py +++ b/tests/test_dmm.py @@ -229,7 +229,7 @@ class TestDMM: def physical_dmm(self): return DMM( bottom_detuning=-1, - global_bottom_detuning=-10, + total_bottom_detuning=-10, clock_period=1, min_duration=1, max_duration=1e6, @@ -243,7 +243,7 @@ def test_init(self, physical_dmm): assert dmm.basis == "ground-rydberg" assert dmm.addressing == "Global" assert dmm.bottom_detuning == -1 - assert dmm.global_bottom_detuning == -10 + assert dmm.total_bottom_detuning == -10 assert dmm.max_amp == 0 for value in ( dmm.max_abs_detuning, @@ -257,13 +257,13 @@ def test_init(self, physical_dmm): ): DMM(bottom_detuning=1) with pytest.raises( - ValueError, match="global_bottom_detuning must be negative." + ValueError, match="total_bottom_detuning must be negative." ): - DMM(global_bottom_detuning=10) + DMM(total_bottom_detuning=10) with pytest.raises( - ValueError, match="global_bottom_detuning must be lower" + ValueError, match="total_bottom_detuning must be lower" ): - DMM(global_bottom_detuning=-1, bottom_detuning=-10) + DMM(total_bottom_detuning=-1, bottom_detuning=-10) with pytest.raises( NotImplementedError, match=f"{DMM} cannot be initialized from `Global` method.", @@ -297,7 +297,7 @@ def test_validate_pulse(self, physical_dmm): physical_dmm.validate_pulse(too_low_pulse) # Should be valid in a virtual DMM without local bottom detuning - virtual_dmm = DMM(global_bottom_detuning=-10) + virtual_dmm = DMM(total_bottom_detuning=-10) assert virtual_dmm.is_virtual() virtual_dmm.validate_pulse(too_low_pulse) @@ -309,7 +309,7 @@ def test_validate_pulse(self, physical_dmm): ValueError, match=re.escape( "The applied detuning goes below the global bottom detuning " - f"of the DMM ({physical_dmm.global_bottom_detuning} rad/µs)" + f"of the DMM ({physical_dmm.total_bottom_detuning} rad/µs)" ), ): # local detunings match bottom_detuning, global don't diff --git a/tests/test_sequence.py b/tests/test_sequence.py index 3714dba0..e5d929ff 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -61,8 +61,8 @@ def device(): return dataclasses.replace( Chadoq2, dmm_objects=( - DMM(bottom_detuning=-70, global_bottom_detuning=-700), - DMM(bottom_detuning=-100, global_bottom_detuning=-1000), + DMM(bottom_detuning=-70, total_bottom_detuning=-700), + DMM(bottom_detuning=-100, total_bottom_detuning=-1000), ), ) @@ -352,7 +352,7 @@ def devices(): min_duration=16, max_duration=2**26, bottom_detuning=-2 * np.pi * 20, - global_bottom_detuning=-2 * np.pi * 2000, + total_bottom_detuning=-2 * np.pi * 2000, ), ), ) @@ -389,7 +389,7 @@ def devices(): min_duration=16, max_duration=2**26, bottom_detuning=-2 * np.pi * 20, - global_bottom_detuning=-2 * np.pi * 2000, + total_bottom_detuning=-2 * np.pi * 2000, ), ), ) @@ -445,7 +445,7 @@ def devices(): min_duration=16, max_duration=2**26, bottom_detuning=-2 * np.pi * 20, - global_bottom_detuning=-2 * np.pi * 2000, + total_bottom_detuning=-2 * np.pi * 2000, ), ), ) @@ -653,7 +653,7 @@ def test_switch_device_down( with pytest.raises( ValueError, match="No match for channel dmm_0_1 with the same " - "global_bottom_detuning.", + "total_bottom_detuning.", ): # Can't find a match for the 1st dmm_0 seq.switch_device( @@ -663,7 +663,7 @@ def test_switch_device_down( Chadoq2.dmm_channels["dmm_0"], dataclasses.replace( Chadoq2.dmm_channels["dmm_0"], - global_bottom_detuning=-500, + total_bottom_detuning=-500, ), ), ), @@ -1573,7 +1573,7 @@ def test_slm_mask_in_ising(patch_plt_show, bottom_detunings): dmm_objects=( DMM( bottom_detuning=bottom_detunings[0], - global_bottom_detuning=bottom_detunings[1], + total_bottom_detuning=bottom_detunings[1], ), ), ), diff --git a/tutorials/advanced_features/Local addressability with DMM.ipynb b/tutorials/advanced_features/Local addressability with DMM.ipynb index de370fca..2fc5a1fc 100644 --- a/tutorials/advanced_features/Local addressability with DMM.ipynb +++ b/tutorials/advanced_features/Local addressability with DMM.ipynb @@ -158,7 +158,7 @@ " max_duration=2**26,\n", " mod_bandwidth=8,\n", " bottom_detuning=-2 * np.pi * 20, # detuning between 0 and -20 MHz\n", - " global_bottom_detuning=-2 * np.pi * 2000, # total detuning\n", + " total_bottom_detuning=-2 * np.pi * 2000, # total detuning\n", ")" ] }, From 67d9242e937fa787f83375bee11869de8a6d8045 Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 24 Nov 2023 12:10:14 +0100 Subject: [PATCH 13/19] Taking into account review comments --- pulser-core/pulser/channels/dmm.py | 23 +++++++--- pulser-core/pulser/devices/_devices.py | 2 +- pulser-core/pulser/sequence/sequence.py | 42 ++++++++++--------- .../Local addressability with DMM.ipynb | 2 +- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 273ef260..7c9cddbb 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -13,7 +13,7 @@ # limitations under the License. """Defines the detuning map modulator.""" from __future__ import annotations - +import warnings from dataclasses import dataclass, field from typing import Literal, Optional @@ -33,7 +33,7 @@ class DMM(Channel): weights of a `DetuningMap`, thus providing a local control over the detuning. The detuning of the pulses added to a DMM has to be negative, between 0 and `bottom_detuning`, and the sum of the weights multiplied by - that detuning has to be blow `total_bottom_detuning`. Channel targeting + that detuning has to be below `total_bottom_detuning`. Channel targeting the transition between the ground and rydberg states, thus encoding the 'ground-rydberg' basis. @@ -56,8 +56,8 @@ class DMM(Channel): MHz. """ - bottom_detuning: Optional[float] = field(default=None, init=True) - total_bottom_detuning: Optional[float] = field(default=None, init=True) + bottom_detuning: float | None = None + total_bottom_detuning: float | None = None addressing: Literal["Global"] = field(default="Global", init=False) max_abs_detuning: Optional[float] = field(default=None, init=False) max_amp: float = field(default=0, init=False) @@ -89,11 +89,22 @@ def basis(self) -> Literal["ground-rydberg"]: def _undefined_fields(self) -> list[str]: optional = [ "bottom_detuning", - "total_bottom_detuning", "max_duration", + # TODO: "total_bottom_detuning" ] return [field for field in optional if getattr(self, field) is None] + def is_virtual(self) -> bool: + """Whether the channel is virtual (i.e. partially defined).""" + virtual_dmm = bool(self._undefined_fields()) + if not virtual_dmm and self.total_bottom_detuning is None: + warnings.warn( + "`total_bottom_detuning` should be defined to define a" + " physical DMM.", + DeprecationWarning, + ) + return virtual_dmm + def validate_pulse( self, pulse: Pulse, @@ -131,7 +142,7 @@ def validate_pulse( < self.total_bottom_detuning ): raise ValueError( - "The applied detuning goes below the global bottom detuning " + "The applied detuning goes below the total bottom detuning " f"of the DMM ({self.total_bottom_detuning} rad/µs)." ) diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index a93958b1..d39acec1 100644 --- a/pulser-core/pulser/devices/_devices.py +++ b/pulser-core/pulser/devices/_devices.py @@ -62,7 +62,7 @@ min_duration=16, max_duration=2**26, bottom_detuning=-2 * np.pi * 20, - total_bottom_detuning=-2 * np.pi * 2000, + total_bottom_detuning=None, # -2 * np.pi * 2000 ), ), ) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index 1452740e..fcff74d8 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -613,7 +613,7 @@ def config_detuning_map( ``MockDevice`` DMM can be repeatedly declared if needed. Args: - detuning_map: A DetuningMap defining the amont of detuning each + detuning_map: A DetuningMap defining the amount of detuning each atom receives. dmm_id: How the channel is identified in the device. See in ``Sequence.available_channels`` which DMM IDs are still @@ -647,8 +647,8 @@ def _config_detuning_map( dmm_name = dmm_id if dmm_id in self.declared_channels: assert self._device.reusable_channels - dmm_name += ( - f"_{''.join(self.declared_channels.keys()).count(dmm_id)}" + dmm_name = _get_dmm_name( + dmm_id, list(self.declared_channels.keys()) ) self._schedule[dmm_name] = _DMMSchedule( @@ -2195,42 +2195,46 @@ def _validate_channel( def _validate_and_adjust_pulse( self, pulse: Pulse, channel: str, phase_ref: Optional[float] = None ) -> Pulse: + # Get the channel object and its detuning map if the channel is a DMM channel_obj: Channel + # Detuning map is None if channel is not DMM detuning_map: DetuningMap | None = None if channel in self._schedule: + # channel name can refer to a Channel or a DMM object + # Get channel object channel_obj = self._schedule[channel].channel_obj + # Get its associated detuning map if channel is a DMM if isinstance(channel_obj, DMM): + # stored in _DMMSchedule with channel object detuning_map = cast( _DMMSchedule, self._schedule[channel] ).detuning_map else: + # If channel name can't be found among _schedule keys, the # Sequence is parametrized and channel is a dmm_name dmm_id = _dmm_id_from_name(channel) + # Get channel object channel_obj = self.device.dmm_channels[dmm_id] - dmm_idx = -1 + # Go over the calls to find the associated detuning map + declared_dmms: list[str] = [] for call in self._calls[1:] + self._to_build_calls: if ( call.name == "config_detuning_map" or call.name == "config_slm_mask" ): - # Check whether dmm_name matches with channel - ( - current_dmm, - current_det_map, - ) = self._get_dmm_id_detuning_map(call) - if current_dmm == dmm_id: - dmm_idx += 1 - current_dmm_name = ( - current_dmm - if dmm_idx == 0 - else current_dmm + f"_{dmm_idx}" - ) - if current_dmm_name == channel: - detuning_map = current_det_map - break + # Extract dmm_id, detuning map of call + call_id, call_det_map = self._get_dmm_id_detuning_map(call) + # Quit if dmm_name of call matches with channel + call_name = _get_dmm_name(call_id, declared_dmms) + declared_dmms.append(call_name) + if call_name == channel: + detuning_map = call_det_map + break if detuning_map is None: + # channel points to a Channel object channel_obj.validate_pulse(pulse) else: + # channel points to a DMM object cast(DMM, channel_obj).validate_pulse(pulse, detuning_map) _duration = channel_obj.validate_duration(pulse.duration) new_phase = pulse.phase + (phase_ref if phase_ref else 0) diff --git a/tutorials/advanced_features/Local addressability with DMM.ipynb b/tutorials/advanced_features/Local addressability with DMM.ipynb index 2fc5a1fc..21305ec7 100644 --- a/tutorials/advanced_features/Local addressability with DMM.ipynb +++ b/tutorials/advanced_features/Local addressability with DMM.ipynb @@ -143,7 +143,7 @@ "source": [ "A `DMM` Channel is a `Channel` that accepts pulses of zero amplitude and detuning below 0 and above:\n", "- `bottom_detuning` for each site.\n", - "- `global_bottom_detuning` for the total detuning distributed among the atoms:" + "- `total_bottom_detuning` for the total detuning distributed among the atoms:" ] }, { From d3109567aed61735194a4d437a9b93603ec626bb Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 24 Nov 2023 15:23:53 +0100 Subject: [PATCH 14/19] Modify schema, add deprecate warning --- pulser-core/pulser/devices/_devices.py | 2 +- .../abstract_repr/schemas/device-schema.json | 27 ++++++++++--------- pyproject.toml | 3 ++- tests/test_dmm.py | 10 +++---- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index d39acec1..2df54fcf 100644 --- a/pulser-core/pulser/devices/_devices.py +++ b/pulser-core/pulser/devices/_devices.py @@ -62,7 +62,7 @@ min_duration=16, max_duration=2**26, bottom_detuning=-2 * np.pi * 20, - total_bottom_detuning=None, # -2 * np.pi * 2000 + # TODO: total_bottom_detuning=-2 * np.pi * 2000 ), ), ) 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 a10997cf..ccff36f5 100644 --- a/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json +++ b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json @@ -39,13 +39,6 @@ "description": "Time taken to change the target (in ns).", "type": "null" }, - "total_bottom_detuning": { - "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", - "type": [ - "number", - "null" - ] - }, "id": { "$ref": "#/definitions/ChannelId", "description": "The identifier of the channel within its device." @@ -88,6 +81,13 @@ "number", "null" ] + }, + "total_bottom_detuning": { + "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", + "type": [ + "number", + "null" + ] } }, "required": [ @@ -97,7 +97,6 @@ "clock_period", "eom_config", "fixed_retarget_t", - "total_bottom_detuning", "id", "max_abs_detuning", "max_amp", @@ -1458,10 +1457,6 @@ "description": "Time taken to change the target (in ns).", "type": "null" }, - "total_bottom_detuning": { - "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", - "type": "number" - }, "id": { "$ref": "#/definitions/ChannelId", "description": "The identifier of the channel within its device." @@ -1501,6 +1496,13 @@ "number", "null" ] + }, + "total_bottom_detuning": { + "description": "Minimum possible detuning of the whole DMM channel (in rad/µs), must be below zero.", + "type": [ + "number", + "null" + ] } }, "required": [ @@ -1510,7 +1512,6 @@ "clock_period", "eom_config", "fixed_retarget_t", - "total_bottom_detuning", "id", "max_abs_detuning", "max_amp", diff --git a/pyproject.toml b/pyproject.toml index 4b3ce146..a8044e8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ src_paths = ["pulser-core", "pulser-simulation", "pulser-pasqal"] filterwarnings = [ # All warnings are turned into errors "error", - # Except this particular warnings, which is ignored + # Except these particular warnings, which are ignored 'ignore:A duration of \d+ ns is not a multiple of:UserWarning', + "ignore:`total_bottom_detuning` should be defined to:DeprecationWarning", ] \ No newline at end of file diff --git a/tests/test_dmm.py b/tests/test_dmm.py index 381223a6..0e59be69 100644 --- a/tests/test_dmm.py +++ b/tests/test_dmm.py @@ -308,14 +308,14 @@ def test_validate_pulse(self, physical_dmm): with pytest.raises( ValueError, match=re.escape( - "The applied detuning goes below the global bottom detuning " + "The applied detuning goes below the total bottom detuning " f"of the DMM ({physical_dmm.total_bottom_detuning} rad/µs)" ), ): # local detunings match bottom_detuning, global don't physical_dmm.validate_pulse(too_low_pulse, det_map) - # Should be valid in a virtual DMM without global bottom detuning - virtual_dmm = DMM(bottom_detuning=-1) - assert virtual_dmm.is_virtual() - virtual_dmm.validate_pulse(too_low_pulse, det_map) + # Should be valid in a physical DMM without global bottom detuning + physical_dmm = DMM(bottom_detuning=-1) + assert not physical_dmm.is_virtual() + physical_dmm.validate_pulse(too_low_pulse, det_map) From 1fe5581f963817bd4fb9d611d0652c47ab10a871 Mon Sep 17 00:00:00 2001 From: a_corni Date: Fri, 24 Nov 2023 18:46:32 +0100 Subject: [PATCH 15/19] Fix imports --- pulser-core/pulser/channels/dmm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 7c9cddbb..12cfc1f2 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -13,6 +13,7 @@ # limitations under the License. """Defines the detuning map modulator.""" from __future__ import annotations + import warnings from dataclasses import dataclass, field from typing import Literal, Optional From 1e84c391a65c0c3f106713648cfb64932692f689 Mon Sep 17 00:00:00 2001 From: a_corni Date: Mon, 27 Nov 2023 16:45:20 +0100 Subject: [PATCH 16/19] Make total_bottom_detuning optional in abstr_repr --- pulser-core/pulser/channels/dmm.py | 20 ++++- pulser-core/pulser/devices/_devices.py | 94 +++++++++---------- pyproject.toml | 1 - tests/test_abstract_repr.py | 119 ++++++++++++++++++------- tests/test_backend.py | 4 +- tests/test_dmm.py | 3 +- tests/test_pasqal.py | 9 +- tests/test_sequence.py | 71 ++++++++------- 8 files changed, 202 insertions(+), 119 deletions(-) diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 12cfc1f2..f5f6009e 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -15,15 +15,18 @@ from __future__ import annotations import warnings -from dataclasses import dataclass, field -from typing import Literal, Optional +from dataclasses import dataclass, field, fields +from typing import Any, Literal, Optional import numpy as np from pulser.channels.base_channel import Channel +from pulser.json.utils import get_dataclass_defaults from pulser.pulse import Pulse from pulser.register.weight_maps import DetuningMap +OPTIONAL_ABSTR_DMM_FIELDS = ["total_bottom_detuning"] + @dataclass(init=True, repr=False, frozen=True) class DMM(Channel): @@ -100,8 +103,8 @@ def is_virtual(self) -> bool: virtual_dmm = bool(self._undefined_fields()) if not virtual_dmm and self.total_bottom_detuning is None: warnings.warn( - "`total_bottom_detuning` should be defined to define a" - " physical DMM.", + "From v0.17 and onwards, `total_bottom_detuning` must be" + " defined to define a physical DMM.", DeprecationWarning, ) return virtual_dmm @@ -147,6 +150,15 @@ def validate_pulse( f"of the DMM ({self.total_bottom_detuning} rad/µs)." ) + def _to_abstract_repr(self, id: str) -> dict[str, Any]: + all_fields = fields(self) + defaults = get_dataclass_defaults(all_fields) + params = super()._to_abstract_repr(id) + for p in OPTIONAL_ABSTR_DMM_FIELDS: + if params[p] == defaults[p]: + params.pop(p, None) + return params + def _dmm_id_from_name(dmm_name: str) -> str: """Converts a dmm_name into a dmm_id. diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index 2df54fcf..caf4a94b 100644 --- a/pulser-core/pulser/devices/_devices.py +++ b/pulser-core/pulser/devices/_devices.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Definitions of real devices.""" +import warnings + import numpy as np from pulser.channels import DMM, Raman, Rydberg @@ -19,53 +21,55 @@ from pulser.devices._device_datacls import Device from pulser.register.special_layouts import TriangularLatticeLayout -Chadoq2 = Device( - name="Chadoq2", - dimensions=2, - rydberg_level=70, - max_atom_num=100, - max_radial_distance=50, - min_atom_distance=4, - supports_slm_mask=True, - channel_objects=( - Rydberg.Global( - max_abs_detuning=2 * np.pi * 20, - max_amp=2 * np.pi * 2.5, - clock_period=4, - min_duration=16, - max_duration=2**26, - ), - Rydberg.Local( - max_abs_detuning=2 * np.pi * 20, - max_amp=2 * np.pi * 10, - min_retarget_interval=220, - fixed_retarget_t=0, - max_targets=1, - clock_period=4, - min_duration=16, - max_duration=2**26, - ), - Raman.Local( - max_abs_detuning=2 * np.pi * 20, - max_amp=2 * np.pi * 10, - min_retarget_interval=220, - fixed_retarget_t=0, - max_targets=1, - clock_period=4, - min_duration=16, - max_duration=2**26, +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + Chadoq2 = Device( + name="Chadoq2", + dimensions=2, + rydberg_level=70, + max_atom_num=100, + max_radial_distance=50, + min_atom_distance=4, + supports_slm_mask=True, + channel_objects=( + Rydberg.Global( + max_abs_detuning=2 * np.pi * 20, + max_amp=2 * np.pi * 2.5, + clock_period=4, + min_duration=16, + max_duration=2**26, + ), + Rydberg.Local( + max_abs_detuning=2 * np.pi * 20, + max_amp=2 * np.pi * 10, + min_retarget_interval=220, + fixed_retarget_t=0, + max_targets=1, + clock_period=4, + min_duration=16, + max_duration=2**26, + ), + Raman.Local( + max_abs_detuning=2 * np.pi * 20, + max_amp=2 * np.pi * 10, + min_retarget_interval=220, + fixed_retarget_t=0, + max_targets=1, + clock_period=4, + min_duration=16, + max_duration=2**26, + ), ), - ), - dmm_objects=( - DMM( - clock_period=4, - min_duration=16, - max_duration=2**26, - bottom_detuning=-2 * np.pi * 20, - # TODO: total_bottom_detuning=-2 * np.pi * 2000 + dmm_objects=( + DMM( + clock_period=4, + min_duration=16, + max_duration=2**26, + bottom_detuning=-2 * np.pi * 20, + # TODO: total_bottom_detuning=-2 * np.pi * 2000 + ), ), - ), -) + ) IroiseMVP = Device( name="IroiseMVP", diff --git a/pyproject.toml b/pyproject.toml index a8044e8e..3d26dc2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,5 +12,4 @@ filterwarnings = [ "error", # Except these particular warnings, which are ignored 'ignore:A duration of \d+ ns is not a multiple of:UserWarning', - "ignore:`total_bottom_detuning` should be defined to:DeprecationWarning", ] \ No newline at end of file diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 037e77a7..47d4c8b6 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -61,9 +61,19 @@ "blackman_max": (BlackmanWaveform.from_max_val, ("max_val", "area")), } +phys_Chadoq2 = replace( + Chadoq2, + name="phys_Chadoq2", + dmm_objects=( + replace(Chadoq2.dmm_objects[0], total_bottom_detuning=-2000), + ), +) + class TestDevice: - @pytest.fixture(params=[Chadoq2, IroiseMVP, MockDevice, AnalogDevice]) + @pytest.fixture( + params=[Chadoq2, phys_Chadoq2, IroiseMVP, MockDevice, AnalogDevice] + ) def abstract_device(self, request): device = request.param return json.loads(device.to_abstract_repr()) @@ -82,8 +92,17 @@ def test_device_schema(self, abstract_device, device_schema): jsonschema.validate(instance=abstract_device, schema=device_schema) def test_roundtrip(self, abstract_device): - device = deserialize_device(json.dumps(abstract_device)) - assert json.loads(device.to_abstract_repr()) == abstract_device + def _roundtrip(abstract_device): + device = deserialize_device(json.dumps(abstract_device)) + assert json.loads(device.to_abstract_repr()) == abstract_device + + if abstract_device["name"] == "Chadoq2": + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards" + ): + _roundtrip(abstract_device) + else: + _roundtrip(abstract_device) def test_exceptions(self, abstract_device, device_schema): def check_error_raised( @@ -97,7 +116,13 @@ def check_error_raised( assert re.search(re.escape(err_msg), str(cause)) is not None return cause - good_device = deserialize_device(json.dumps(abstract_device)) + if abstract_device["name"] == "Chadoq2": + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards" + ): + good_device = deserialize_device(json.dumps(abstract_device)) + else: + good_device = deserialize_device(json.dumps(abstract_device)) check_error_raised( abstract_device, TypeError, "'obj_str' must be a string" @@ -1032,11 +1057,13 @@ def _get_expression(op: dict) -> Any: class TestDeserialization: def test_deserialize_device_and_channels(self) -> None: s = _get_serialized_seq() - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) + deserialized_device = deserialize_device(json.dumps(s["device"])) # Check device - assert seq._device == deserialize_device(json.dumps(s["device"])) + assert seq._device == deserialized_device # Check channels assert len(seq.declared_channels) == len(s["channels"]) @@ -1055,8 +1082,9 @@ def test_deserialize_register(self, layout_coords): ) else: s = _get_serialized_seq() - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # Check register assert len(seq.register.qubits) == len(s["register"]) @@ -1085,8 +1113,9 @@ def test_deserialize_mappable_register(self): "slug": "test_layout", }, ) - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) assert seq.is_register_mappable() qids = [q["qid"] for q in s["register"]] @@ -1208,10 +1237,14 @@ def test_deserialize_variables(self, without_default): UserWarning, match="Building a non-parametrized sequence" ): _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + seq = Sequence.from_abstract_repr(json.dumps(s)) if without_default: # Serialize and deserialize again, without the defaults - seq = Sequence.from_abstract_repr(seq.to_abstract_repr()) + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards," + ): + seq = Sequence.from_abstract_repr(seq.to_abstract_repr()) # Check variables assert len(seq.declared_variables) == len(s["variables"]) @@ -1340,8 +1373,9 @@ def test_deserialize_non_parametrized_waveform(self, wf_obj): } ] ) - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare channels + 1 operation offset = 1 + len(s["channels"]) @@ -1407,10 +1441,12 @@ def test_deserialize_non_parametrized_waveform(self, wf_obj): def test_deserialize_measurement(self): s = _get_serialized_seq() - _check_roundtrip(s) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) s["measurement"] = "ground-rydberg" - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + seq = Sequence.from_abstract_repr(json.dumps(s)) assert seq._measurement == s["measurement"] @@ -1477,8 +1513,9 @@ def test_deserialize_parametrized_op(self, op): "var2": {"type": "int", "value": [42]}, }, ) - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare channels + 1 operation offset = 1 + len(s["channels"]) @@ -1600,8 +1637,9 @@ def test_deserialize_parametrized_pulse(self, op, pulse_cls): "var2": {"type": "int", "value": [42]}, }, ) - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare channels + 1 operation offset = 1 + len(s["channels"]) @@ -1814,8 +1852,9 @@ def test_deserialize_parametrized_waveform(self, wf_obj): "var_times": {"type": "float", "value": [0, 0.4, 0.8, 0.9]}, }, ) - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) seq_var1 = seq._variables["var1"] seq_var2 = seq._variables["var2"] @@ -1931,13 +1970,13 @@ def test_deserialize_param(self, json_param): # where a single value is expected. Still, all we want to # see is whether the parametrization of the operations # works as expected - if ( - json_param["lhs"] != {"variable": "var1"} - and json_param["expression"] != "index" - ): - _check_roundtrip(s) - - seq = Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + if ( + json_param["lhs"] != {"variable": "var1"} + and json_param["expression"] != "index" + ): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) seq_var1 = seq._variables["var1"] # init + declare channels + 1 operation @@ -2030,12 +2069,18 @@ def test_param_exceptions(self, param, msg, patch_jsonschema): std_error = jsonschema.exceptions.ValidationError with patch("jsonschema.validate"): with pytest.raises(AbstractReprError, match=msg): - Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards," + ): + Sequence.from_abstract_repr(json.dumps(s)) else: std_error = AbstractReprError extra_params["match"] = msg with pytest.raises(std_error, **extra_params): - Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards," + ): + Sequence.from_abstract_repr(json.dumps(s)) def test_unknow_waveform(self): s = _get_serialized_seq( @@ -2060,14 +2105,20 @@ def test_unknow_waveform(self): ] ) with pytest.raises(jsonschema.exceptions.ValidationError): - Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards," + ): + Sequence.from_abstract_repr(json.dumps(s)) with pytest.raises( AbstractReprError, match="The object does not encode a known waveform.", ): with patch("jsonschema.validate"): - Sequence.from_abstract_repr(json.dumps(s)) + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards," + ): + Sequence.from_abstract_repr(json.dumps(s)) @pytest.mark.parametrize("device", [Chadoq2, IroiseMVP, MockDevice]) def test_legacy_device(self, device): diff --git a/tests/test_backend.py b/tests/test_backend.py index a2e96db2..551b39c8 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -245,8 +245,8 @@ def test_qpu_backend(sequence): TypeError, match="must be a real device, instance of 'Device'" ): QPUBackend(sequence, connection) - - seq = sequence.switch_device(replace(Chadoq2, max_runs=10)) + with pytest.warns(DeprecationWarning, match="From v0.17"): + seq = sequence.switch_device(replace(Chadoq2, max_runs=10)) qpu_backend = QPUBackend(seq, connection) with pytest.raises(ValueError, match="'job_params' must be specified"): qpu_backend.run() diff --git a/tests/test_dmm.py b/tests/test_dmm.py index 0e59be69..fe0f8fba 100644 --- a/tests/test_dmm.py +++ b/tests/test_dmm.py @@ -317,5 +317,6 @@ def test_validate_pulse(self, physical_dmm): # Should be valid in a physical DMM without global bottom detuning physical_dmm = DMM(bottom_detuning=-1) - assert not physical_dmm.is_virtual() + with pytest.warns(DeprecationWarning, match="From v0.17 and onwards"): + assert not physical_dmm.is_virtual() physical_dmm.validate_pulse(too_low_pulse, det_map) diff --git a/tests/test_pasqal.py b/tests/test_pasqal.py index 84d57f5e..f86e9370 100644 --- a/tests/test_pasqal.py +++ b/tests/test_pasqal.py @@ -51,7 +51,14 @@ class CloudFixture: mock_cloud_sdk: Any -test_device = Chadoq2 +test_device = dataclasses.replace( + Chadoq2, + dmm_objects=( + dataclasses.replace( + Chadoq2.dmm_objects[0], total_bottom_detuning=-1000 + ), + ), +) virtual_device = test_device.to_virtual() diff --git a/tests/test_sequence.py b/tests/test_sequence.py index e5d929ff..c3dfdfe3 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -538,10 +538,18 @@ def test_ising_mode( def test_switch_device_down( reg, det_map, devices, pulses, mappable_reg, parametrized ): + phys_Chadoq2 = dataclasses.replace( + Chadoq2, + dmm_objects=( + dataclasses.replace( + Chadoq2.dmm_objects[0], total_bottom_detuning=-2000 + ), + ), + ) # Device checkout seq = init_seq( reg, - Chadoq2, + phys_Chadoq2, "ising", "rydberg_global", None, @@ -553,12 +561,12 @@ def test_switch_device_down( match="Switching a sequence to the same device" + " returns the sequence unchanged.", ): - seq.switch_device(Chadoq2) + seq.switch_device(phys_Chadoq2) # From sequence reusing channels to Device without reusable channels seq = init_seq( reg, - dataclasses.replace(Chadoq2.to_virtual(), reusable_channels=True), + dataclasses.replace(phys_Chadoq2.to_virtual(), reusable_channels=True), "global", "rydberg_global", None, @@ -573,7 +581,7 @@ def test_switch_device_down( " right type, basis and addressing.", ): # Can't find a match for the 2nd raman_local - seq.switch_device(Chadoq2) + seq.switch_device(phys_Chadoq2) with pytest.raises( TypeError, @@ -581,36 +589,37 @@ def test_switch_device_down( " right type, basis and addressing.", ): # Can't find a match for the 2nd raman_local - seq.switch_device(Chadoq2, strict=True) + seq.switch_device(phys_Chadoq2, strict=True) with pytest.raises( ValueError, match="No match for channel raman_1 with the" " same clock_period.", ): - # Can't find a match for the 2nd rydberg_local - seq.switch_device( - dataclasses.replace( - Chadoq2, - channel_objects=( - Chadoq2.channels["rydberg_global"], - dataclasses.replace( - Chadoq2.channels["raman_local"], clock_period=10 + with pytest.warns(DeprecationWarning, match="From v0.17"): + # Can't find a match for the 2nd rydberg_local + seq.switch_device( + dataclasses.replace( + phys_Chadoq2, + channel_objects=( + Chadoq2.channels["rydberg_global"], + dataclasses.replace( + Chadoq2.channels["raman_local"], clock_period=10 + ), + Chadoq2.channels["raman_local"], + ), + channel_ids=( + "rydberg_global", + "rydberg_local", + "rydberg_local1", ), - Chadoq2.channels["raman_local"], - ), - channel_ids=( - "rydberg_global", - "rydberg_local", - "rydberg_local1", ), - ), - strict=True, - ) + strict=True, + ) # From sequence reusing DMMs to Device without reusable channels seq = init_seq( reg, - dataclasses.replace(Chadoq2.to_virtual(), reusable_channels=True), + dataclasses.replace(phys_Chadoq2.to_virtual(), reusable_channels=True), "global", "rydberg_global", None, @@ -631,20 +640,20 @@ def test_switch_device_down( " right type, basis and addressing.", ): # Can't find a match for the 2nd dmm_0 - seq.switch_device(Chadoq2) + seq.switch_device(phys_Chadoq2) # Strict switch imposes to have same bottom detuning for DMMs with pytest.raises( ValueError, - match="No match for channel dmm_0_1 with the" " same bottom_detuning.", + match="No match for channel dmm_0_1 with the same bottom_detuning.", ): # Can't find a match for the 1st dmm_0 seq.switch_device( dataclasses.replace( - Chadoq2, + phys_Chadoq2, dmm_objects=( - Chadoq2.dmm_channels["dmm_0"], + phys_Chadoq2.dmm_channels["dmm_0"], dataclasses.replace( - Chadoq2.dmm_channels["dmm_0"], bottom_detuning=-10 + phys_Chadoq2.dmm_channels["dmm_0"], bottom_detuning=-10 ), ), ), @@ -658,11 +667,11 @@ def test_switch_device_down( # Can't find a match for the 1st dmm_0 seq.switch_device( dataclasses.replace( - Chadoq2, + phys_Chadoq2, dmm_objects=( - Chadoq2.dmm_channels["dmm_0"], + phys_Chadoq2.dmm_channels["dmm_0"], dataclasses.replace( - Chadoq2.dmm_channels["dmm_0"], + phys_Chadoq2.dmm_channels["dmm_0"], total_bottom_detuning=-500, ), ), From 5749976a64ceba475b1c0222443195ee053e30ab Mon Sep 17 00:00:00 2001 From: a_corni Date: Mon, 27 Nov 2023 17:03:34 +0100 Subject: [PATCH 17/19] Test de- & serializ. of dev with total_bottom_det --- tests/test_abstract_repr.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 47d4c8b6..4ce3431d 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -1055,13 +1055,25 @@ def _get_expression(op: dict) -> Any: class TestDeserialization: - def test_deserialize_device_and_channels(self) -> None: - s = _get_serialized_seq() - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): + @pytest.mark.parametrize("is_phys_Chadoq2", [True, False]) + def test_deserialize_device_and_channels(self, is_phys_Chadoq2) -> None: + kwargs = {} + if is_phys_Chadoq2: + kwargs["device"] = json.loads(phys_Chadoq2.to_abstract_repr()) + s = _get_serialized_seq(**kwargs) + if not is_phys_Chadoq2: + with pytest.warns( + DeprecationWarning, match="From v0.17 and onwards," + ): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) + deserialized_device = deserialize_device( + json.dumps(s["device"]) + ) + else: _check_roundtrip(s) seq = Sequence.from_abstract_repr(json.dumps(s)) deserialized_device = deserialize_device(json.dumps(s["device"])) - # Check device assert seq._device == deserialized_device From cc306ae6d9683eb4c0b5fc69c0d7b6cc17ec3e89 Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 28 Nov 2023 10:57:56 +0100 Subject: [PATCH 18/19] Modify warning handling --- pulser-core/pulser/devices/_devices.py | 2 +- tests/test_abstract_repr.py | 124 +++++++++++++------------ 2 files changed, 66 insertions(+), 60 deletions(-) diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index caf4a94b..5e32dcdf 100644 --- a/pulser-core/pulser/devices/_devices.py +++ b/pulser-core/pulser/devices/_devices.py @@ -22,7 +22,7 @@ from pulser.register.special_layouts import TriangularLatticeLayout with warnings.catch_warnings(): - warnings.simplefilter("ignore") + warnings.simplefilter("ignore", category=DeprecationWarning) Chadoq2 = Device( name="Chadoq2", dimensions=2, diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 326b78b5..a0c3c467 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -1076,20 +1076,18 @@ def _get_expression(op: dict) -> Any: class TestDeserialization: @pytest.mark.parametrize("is_phys_Chadoq2", [True, False]) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_device_and_channels(self, is_phys_Chadoq2) -> None: kwargs = {} if is_phys_Chadoq2: kwargs["device"] = json.loads(phys_Chadoq2.to_abstract_repr()) s = _get_serialized_seq(**kwargs) if not is_phys_Chadoq2: - with pytest.warns( - DeprecationWarning, match="From v0.17 and onwards," - ): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) - deserialized_device = deserialize_device( - json.dumps(s["device"]) - ) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) + deserialized_device = deserialize_device(json.dumps(s["device"])) else: _check_roundtrip(s) seq = Sequence.from_abstract_repr(json.dumps(s)) @@ -1106,6 +1104,9 @@ def test_deserialize_device_and_channels(self, is_phys_Chadoq2) -> None: _coords = np.concatenate((_coords, -_coords)) @pytest.mark.parametrize("layout_coords", [None, _coords]) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_register(self, layout_coords): if layout_coords is not None: reg_layout = RegisterLayout(layout_coords) @@ -1114,9 +1115,8 @@ def test_deserialize_register(self, layout_coords): ) else: s = _get_serialized_seq() - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # Check register assert len(seq.register.qubits) == len(s["register"]) @@ -1136,6 +1136,9 @@ def test_deserialize_register(self, layout_coords): assert "layout" not in s assert seq.register.layout is None + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_mappable_register(self): layout_coords = (5 * np.arange(8)).reshape((4, 2)) s = _get_serialized_seq( @@ -1145,9 +1148,8 @@ def test_deserialize_mappable_register(self): "slug": "test_layout", }, ) - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) assert seq.is_register_mappable() qids = [q["qid"] for q in s["register"]] @@ -1258,6 +1260,9 @@ def test_deserialize_seq_with_mag_field(self): assert np.all(seq.magnetic_field == mag_field) @pytest.mark.parametrize("without_default", [True, False]) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_variables(self, without_default): s = _get_serialized_seq( variables={ @@ -1269,14 +1274,10 @@ def test_deserialize_variables(self, without_default): UserWarning, match="Building a non-parametrized sequence" ): _check_roundtrip(s) - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - seq = Sequence.from_abstract_repr(json.dumps(s)) + seq = Sequence.from_abstract_repr(json.dumps(s)) if without_default: # Serialize and deserialize again, without the defaults - with pytest.warns( - DeprecationWarning, match="From v0.17 and onwards," - ): - seq = Sequence.from_abstract_repr(seq.to_abstract_repr()) + seq = Sequence.from_abstract_repr(seq.to_abstract_repr()) # Check variables assert len(seq.declared_variables) == len(s["variables"]) @@ -1391,6 +1392,9 @@ def test_deserialize_non_parametrized_op(self, op): ], ids=_get_kind, ) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_non_parametrized_waveform(self, wf_obj): s = _get_serialized_seq( operations=[ @@ -1405,9 +1409,8 @@ def test_deserialize_non_parametrized_waveform(self, wf_obj): } ] ) - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare channels + 1 operation offset = 1 + len(s["channels"]) @@ -1471,14 +1474,15 @@ def test_deserialize_non_parametrized_waveform(self, wf_obj): assert isinstance(wf, CustomWaveform) assert np.array_equal(wf._samples, wf_obj["samples"]) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_measurement(self): s = _get_serialized_seq() - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) + _check_roundtrip(s) s["measurement"] = "ground-rydberg" - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - seq = Sequence.from_abstract_repr(json.dumps(s)) + seq = Sequence.from_abstract_repr(json.dumps(s)) assert seq._measurement == s["measurement"] @@ -1537,6 +1541,9 @@ def test_deserialize_measurement(self): ], ids=_get_op, ) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_parametrized_op(self, op): s = _get_serialized_seq( operations=[op], @@ -1545,9 +1552,8 @@ def test_deserialize_parametrized_op(self, op): "var2": {"type": "int", "value": [42]}, }, ) - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare channels + 1 operation offset = 1 + len(s["channels"]) @@ -1661,6 +1667,9 @@ def test_deserialize_parametrized_op(self, op): ), ], ) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_parametrized_pulse(self, op, pulse_cls): s = _get_serialized_seq( operations=[op], @@ -1669,9 +1678,8 @@ def test_deserialize_parametrized_pulse(self, op, pulse_cls): "var2": {"type": "int", "value": [42]}, }, ) - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) # init + declare channels + 1 operation offset = 1 + len(s["channels"]) @@ -1861,6 +1869,9 @@ def test_deserialize_eom_ops(self, correct_phase_drift, var_detuning_on): ], ids=_get_kind, ) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_parametrized_waveform(self, wf_obj): # var1,2 = duration 1000, 2000 # var2,4 = value - 2, 5 @@ -1884,9 +1895,8 @@ def test_deserialize_parametrized_waveform(self, wf_obj): "var_times": {"type": "float", "value": [0, 0.4, 0.8, 0.9]}, }, ) - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) seq_var1 = seq._variables["var1"] seq_var2 = seq._variables["var2"] @@ -1972,6 +1982,9 @@ def test_deserialize_parametrized_waveform(self, wf_obj): ], ids=_get_expression, ) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_deserialize_param(self, json_param): s = _get_serialized_seq( operations=[ @@ -2002,13 +2015,12 @@ def test_deserialize_param(self, json_param): # where a single value is expected. Still, all we want to # see is whether the parametrization of the operations # works as expected - with pytest.warns(DeprecationWarning, match="From v0.17 and onwards,"): - if ( - json_param["lhs"] != {"variable": "var1"} - and json_param["expression"] != "index" - ): - _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) + if ( + json_param["lhs"] != {"variable": "var1"} + and json_param["expression"] != "index" + ): + _check_roundtrip(s) + seq = Sequence.from_abstract_repr(json.dumps(s)) seq_var1 = seq._variables["var1"] # init + declare channels + 1 operation @@ -2086,6 +2098,9 @@ def test_deserialize_param(self, json_param): ], ids=["bad_var", "bad_param", "bad_exp"], ) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_param_exceptions(self, param, msg, patch_jsonschema): s = _get_serialized_seq( [ @@ -2101,19 +2116,16 @@ def test_param_exceptions(self, param, msg, patch_jsonschema): std_error = jsonschema.exceptions.ValidationError with patch("jsonschema.validate"): with pytest.raises(AbstractReprError, match=msg): - with pytest.warns( - DeprecationWarning, match="From v0.17 and onwards," - ): - Sequence.from_abstract_repr(json.dumps(s)) + Sequence.from_abstract_repr(json.dumps(s)) else: std_error = AbstractReprError extra_params["match"] = msg with pytest.raises(std_error, **extra_params): - with pytest.warns( - DeprecationWarning, match="From v0.17 and onwards," - ): - Sequence.from_abstract_repr(json.dumps(s)) + Sequence.from_abstract_repr(json.dumps(s)) + @pytest.mark.filterwarnings( + "ignore:From v0.17 and onwards,.*:DeprecationWarning" + ) def test_unknow_waveform(self): s = _get_serialized_seq( [ @@ -2137,20 +2149,14 @@ def test_unknow_waveform(self): ] ) with pytest.raises(jsonschema.exceptions.ValidationError): - with pytest.warns( - DeprecationWarning, match="From v0.17 and onwards," - ): - Sequence.from_abstract_repr(json.dumps(s)) + Sequence.from_abstract_repr(json.dumps(s)) with pytest.raises( AbstractReprError, match="The object does not encode a known waveform.", ): with patch("jsonschema.validate"): - with pytest.warns( - DeprecationWarning, match="From v0.17 and onwards," - ): - Sequence.from_abstract_repr(json.dumps(s)) + Sequence.from_abstract_repr(json.dumps(s)) @pytest.mark.parametrize("device", [Chadoq2, IroiseMVP, MockDevice]) def test_legacy_device(self, device): From cc3c8579ae30e7f62872eec9cd0d4215ca1f1ce7 Mon Sep 17 00:00:00 2001 From: a_corni Date: Tue, 28 Nov 2023 11:13:44 +0100 Subject: [PATCH 19/19] Adding assertion in _validate_and_adjust_pulse --- pulser-core/pulser/sequence/sequence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index fcff74d8..7b4f778a 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -2230,6 +2230,7 @@ def _validate_and_adjust_pulse( if call_name == channel: detuning_map = call_det_map break + assert detuning_map is not None if detuning_map is None: # channel points to a Channel object channel_obj.validate_pulse(pulse)