diff --git a/pulser-core/pulser/channels/dmm.py b/pulser-core/pulser/channels/dmm.py index 8d88cf390..f5f6009e0 100644 --- a/pulser-core/pulser/channels/dmm.py +++ b/pulser-core/pulser/channels/dmm.py @@ -14,13 +14,18 @@ """Defines the detuning map modulator.""" from __future__ import annotations -from dataclasses import dataclass, field -from typing import Literal, Optional +import warnings +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) @@ -31,16 +36,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 below `total_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 per atom (in rad/µs), + must be below zero. + 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 clock cycle. @@ -51,7 +60,8 @@ class DMM(Channel): MHz. """ - 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) @@ -63,6 +73,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.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.total_bottom_detuning + ): + raise ValueError( + "total_bottom_detuning must be lower than" + " bottom_detuning." + ) @property def basis(self) -> Literal["ground-rydberg"]: @@ -73,26 +94,70 @@ def _undefined_fields(self) -> list[str]: optional = [ "bottom_detuning", "max_duration", + # TODO: "total_bottom_detuning" ] 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 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( + "From v0.17 and onwards, `total_bottom_detuning` must be" + " defined to define a physical DMM.", + DeprecationWarning, + ) + return virtual_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.") - if self.bottom_detuning is not None and np.any( - round_detuning < self.bottom_detuning + # Check that detuning on each atom is above bottom_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( - "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 total_bottom_detuning + if ( + self.total_bottom_detuning is not None + and np.sum(detuning_map.weights) * min_round_detuning + < self.total_bottom_detuning + ): + raise ValueError( + "The applied detuning goes below the total bottom detuning " + 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: diff --git a/pulser-core/pulser/devices/_devices.py b/pulser-core/pulser/devices/_devices.py index 18a14e040..5e32dcdf1 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,52 +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", category=DeprecationWarning) + 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=-20, + 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/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json b/pulser-core/pulser/json/abstract_repr/schemas/device-schema.json index 80a63c3b7..ccff36f58 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" @@ -81,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": [ @@ -1435,7 +1442,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": { @@ -1489,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": [ diff --git a/pulser-core/pulser/register/_reg_drawer.py b/pulser-core/pulser/register/_reg_drawer.py index 638a1b3c0..298e9886d 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 8552349eb..5327fc45f 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 4513493e7..47c6ace10 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 5da293aeb..8df147d02 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 5c93744d3..6ec309616 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 57b22c5f5..7b4f778ac 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,14 +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: - ntargets = len(targets) - detuning_map = self.register.define_detuning_map( + def _slm_detuning_map(self, targets: set[QubitId]) -> DetuningMap: + return 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 } ) + + 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()): @@ -538,7 +553,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 +613,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 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 available (start by "dmm" ) and the associated description. @@ -632,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( @@ -778,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("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: @@ -1907,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 + total_bottom_detuning = cast( + DMM, self.declared_channels[self._slm_mask_dmm] + ).total_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 ( + total_bottom_detuning + and min_det * len(set(self._slm_mask_targets)) + < total_bottom_detuning + ): + min_det = total_bottom_detuning / len( + set(self._slm_mask_targets) + ) cast( _DMMSchedule, self._schedule[self._slm_mask_dmm] )._waiting_for_first_pulse = False @@ -2171,13 +2195,48 @@ 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 - channel_obj = self.device.dmm_channels[_dmm_id_from_name(channel)] - channel_obj.validate_pulse(pulse) + dmm_id = _dmm_id_from_name(channel) + # Get channel object + channel_obj = self.device.dmm_channels[dmm_id] + # 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" + ): + # 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 + assert detuning_map is not None + 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) if _duration != pulse.duration: diff --git a/pyproject.toml b/pyproject.toml index 4b3ce1468..3d26dc2a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,6 @@ 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', ] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 556b9ca78..14941bdab 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, total_bottom_detuning=-10000), + DMM( + clock_period=4, + mod_bandwidth=4.0, + bottom_detuning=-50, + total_bottom_detuning=-5000, + ), ), ) diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index 9dab80644..a0c3c4678 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" @@ -859,13 +884,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 +925,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" @@ -1050,13 +1075,25 @@ 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)) - + @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: + _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 == deserialize_device(json.dumps(s["device"])) + assert seq._device == deserialized_device # Check channels assert len(seq.declared_channels) == len(s["channels"]) @@ -1067,6 +1104,9 @@ def test_deserialize_device_and_channels(self) -> 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) @@ -1096,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( @@ -1141,8 +1184,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 = [ @@ -1217,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={ @@ -1346,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=[ @@ -1425,6 +1474,9 @@ 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() _check_roundtrip(s) @@ -1489,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], @@ -1612,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], @@ -1811,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 @@ -1921,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=[ @@ -1956,7 +2020,6 @@ def test_deserialize_param(self, json_param): and json_param["expression"] != "index" ): _check_roundtrip(s) - seq = Sequence.from_abstract_repr(json.dumps(s)) seq_var1 = seq._variables["var1"] @@ -2035,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( [ @@ -2057,6 +2123,9 @@ def test_param_exceptions(self, param, msg, patch_jsonschema): with pytest.raises(std_error, **extra_params): 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( [ diff --git a/tests/test_backend.py b/tests/test_backend.py index a2e96db23..551b39c8d 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_devices.py b/tests/test_devices.py index 54d8aa696..c121fd171 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, + 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 fe3673ba8..fe0f8fba8 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 @@ -43,7 +44,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 +54,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 +90,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 +159,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, @@ -233,6 +229,7 @@ class TestDMM: def physical_dmm(self): return DMM( bottom_detuning=-1, + total_bottom_detuning=-10, clock_period=1, min_duration=1, max_duration=1e6, @@ -241,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.total_bottom_detuning == -10 assert dmm.max_amp == 0 for value in ( dmm.max_abs_detuning, @@ -259,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="total_bottom_detuning must be negative." + ): + DMM(total_bottom_detuning=10) + with pytest.raises( + ValueError, match="total_bottom_detuning must be lower" + ): + DMM(total_bottom_detuning=-1, bottom_detuning=-10) with pytest.raises( NotImplementedError, match=f"{DMM} cannot be initialized from `Global` method.", @@ -283,13 +288,35 @@ 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(total_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 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 physical DMM without global bottom detuning + physical_dmm = DMM(bottom_detuning=-1) + 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 84d57f5e9..f86e93700 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 507bf0d7a..c3dfdfe34 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)} ) @@ -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, total_bottom_detuning=-700), + DMM(bottom_detuning=-100, total_bottom_detuning=-1000), + ), ) @@ -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, + total_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, + total_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, + total_bottom_detuning=-2 * np.pi * 2000, ), ), ) @@ -496,12 +502,12 @@ 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) } ) 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 @@ -532,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, @@ -547,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, @@ -567,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, @@ -575,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, @@ -625,20 +640,39 @@ 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 + ), + ), + ), + strict=True, + ) + with pytest.raises( + ValueError, + match="No match for channel dmm_0_1 with the same " + "total_bottom_detuning.", + ): + # Can't find a match for the 1st dmm_0 + seq.switch_device( + dataclasses.replace( + phys_Chadoq2, + dmm_objects=( + phys_Chadoq2.dmm_channels["dmm_0"], + dataclasses.replace( + phys_Chadoq2.dmm_channels["dmm_0"], + total_bottom_detuning=-500, ), ), ), @@ -865,7 +899,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 +1373,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) @@ -1425,8 +1459,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, @@ -1530,8 +1563,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], + total_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 @@ -1544,14 +1599,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) @@ -1572,7 +1637,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 27c7c9e99..3f1107a10 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 daa165050..21305ec73 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 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 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." ] @@ -56,7 +60,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 +70,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" ] }, { @@ -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", + "- `total_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", + " total_bottom_detuning=-2 * np.pi * 2000, # total detuning\n", ")" ] }, 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 3683fdb0a..807cd2c9f 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$:" ] }, {