diff --git a/pulser-core/pulser/sequence/_schedule.py b/pulser-core/pulser/sequence/_schedule.py index 0107886e8..2387f172b 100644 --- a/pulser-core/pulser/sequence/_schedule.py +++ b/pulser-core/pulser/sequence/_schedule.py @@ -258,7 +258,9 @@ def __getitem__( self, key: Union[int, slice] ) -> Union[_TimeSlot, list[_TimeSlot]]: if key == -1 and not self.slots: - raise ValueError("The chosen channel has no target.") + raise ValueError( + f"The chosen channel ({self.channel_id!r}) has no target." + ) return self.slots[key] def __iter__(self) -> Iterator[_TimeSlot]: @@ -290,7 +292,7 @@ def get_samples( if qubits is None: raise ValueError( "'qubits' must be defined when extracting the samples of a" - " DMM channel." + f" DMM channel; got None for channel {self.channel_id!r}." ) return DMMSamples( **init_fields, detuning_map=self.detuning_map, qubits=qubits @@ -714,7 +716,7 @@ def _check_duration( if self.max_duration is not None and t > self.max_duration: msg = ( "The sequence's duration exceeded the maximum duration allowed" - f" by the device ({self.max_duration} ns)." + f" by the device ({self.max_duration} ns); got {t} ns." ) if block_over_max_duration: raise RuntimeError(msg) diff --git a/pulser-core/pulser/sequence/_seq_drawer.py b/pulser-core/pulser/sequence/_seq_drawer.py index 6e80daca3..d1ad901c0 100644 --- a/pulser-core/pulser/sequence/_seq_drawer.py +++ b/pulser-core/pulser/sequence/_seq_drawer.py @@ -1018,8 +1018,14 @@ def _draw_qubit_content( for ch, ch_obj in sampled_seq._ch_objs.items() ] ): + wrong_basis = { + ch: ch_obj.basis + for ch, ch_obj in sampled_seq._ch_objs.items() + if ch_obj.basis != "ground-rydberg" + } raise NotImplementedError( - "Can only draw qubit contents for channels in rydberg basis." + "Can only draw qubit contents for channels in the " + f"'ground-rydberg' basis; got {wrong_basis}." ) # Gather data per targeted qubits total_duration = data["total_duration"] diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index a820dabbc..51b430589 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -19,6 +19,7 @@ import json import os import warnings +from collections import Counter from collections.abc import Collection, Mapping from typing import ( Any, @@ -120,7 +121,8 @@ def __init__( """Initializes a new pulse sequence.""" if not isinstance(device, BaseDevice): raise TypeError( - f"'device' must be of type 'BaseDevice', not {type(device)}." + "'device' must be an instance of 'BaseDevice', not " + f"{type(device)}." ) # Checks if register is compatible with the device @@ -181,7 +183,9 @@ def _in_ising(self) -> bool: @_in_ising.setter def _in_ising(self, value: bool) -> None: if not isinstance(value, bool): - raise TypeError("_in_ising must be a bool.") + raise TypeError( + f"_in_ising must be a bool; got {type(value)}: {value!r}." + ) if self._in_ising == value: # If the value doesn't change, do nothing return @@ -351,7 +355,8 @@ def magnetic_field(self) -> np.ndarray: if not self._in_xy: raise AttributeError( "The magnetic field is only defined when the " - "sequence is in 'XY Mode'." + "sequence is in 'XY Mode'; this sequence addresses " + f"{self.get_addressed_bases()}." ) return np.array(self._mag_field) @@ -475,12 +480,14 @@ def current_phase_ref( if qubit not in self._qids: raise ValueError( "'qubit' must be the id of a qubit declared in " - "this sequence's register." + f"this sequence's register; got {qubit!r}, declared: " + f"{list(self._register.qubit_ids)}." ) if basis not in self._basis_ref: raise ValueError( - f"No declared channel targets the given 'basis' ('{basis}')." + f"No declared channel targets the given 'basis' ('{basis}'); " + f"declared bases are {list(self._basis_ref)}." ) return float(self._basis_ref[basis][qubit].phase.last_phase) @@ -506,21 +513,32 @@ def set_magnetic_field( """ if not self._in_xy: if self._schedule: + declared_ids = { + n: cs.channel_id for n, cs in self._schedule.items() + } raise ValueError( - "The magnetic field can only be set in 'XY Mode'." + "The magnetic field can only be set in 'XY Mode'; " + f"declared channels are {declared_ids}." ) # No channels declared yet self._in_xy = True elif not self._empty_sequence: # Not all channels are empty + with_contents = { + n: cs.channel_id + for n, cs in self._schedule.items() + if cs.slots + } raise ValueError( - "The magnetic field can only be set on an empty sequence." + "The magnetic field can only be set on an empty sequence; " + f"channels with contents are {with_contents}." ) mag_vector = (bx, by, bz) if np.linalg.norm(mag_vector) == 0.0: raise ValueError( - "The magnetic field must have a magnitude greater than 0." + "The magnetic field must have a magnitude greater than 0; " + f"got {mag_vector}." ) self._mag_field = mag_vector @@ -596,17 +614,32 @@ def config_slm_mask( try: targets = set(qubits) except TypeError: - raise TypeError("The SLM targets must be castable to set.") + raise TypeError( + "The SLM targets must be castable to set; got " + f"{type(qubits)}: {qubits!r}." + ) if not targets.issubset(self._qids): - raise ValueError("SLM mask targets must exist in the register.") + raise ValueError( + "SLM mask targets must exist in the register; " + f"{[q for q in qubits if q not in self._qids]} not in " + f"{list(self._register.qubit_ids)}." + ) # If sequence is parametrized slm is configured at build if self.is_parametrized(): return if self._slm_mask_targets: - raise ValueError("SLM mask can be configured only once.") + configured = [ + q + for q in self._register.qubit_ids + if q in self._slm_mask_targets + ] + raise ValueError( + "SLM mask can be configured only once; already configured " + f"with targets {configured}." + ) if self._in_xy or (not self._in_xy and not self._in_ising): if dmm_id not in self.device.dmm_channels: @@ -677,7 +710,15 @@ def _config_detuning_map( "with the declared 'Microwave' channel." ) if dmm_id not in self.available_channels: - raise ValueError(f"DMM {dmm_id} is not available.") + still_available = [ + ch_id + for ch_id, ch_obj in self.available_channels.items() + if isinstance(ch_obj, DMM) + ] + raise ValueError( + f"DMM {dmm_id} is not available; still available DMM " + f"channels are {still_available}." + ) # Configures the DMM implementing an SLM mask if configured before self._in_ising = True @@ -865,28 +906,47 @@ def declare_channel( """ if name.startswith("dmm_"): raise ValueError( - "Name starting by 'dmm_' are reserved for DMM channels." + "Name starting by 'dmm_' are reserved for DMM channels; " + f"got {name!r}." ) if name in self._schedule: - raise ValueError("The given name is already in use.") + raise ValueError( + f"The given name is already in use; got {name!r}, already " + f"declared: {list(self._schedule)}." + ) if channel_id not in self.device.channels: - raise ValueError(f"No channel {channel_id} in the device.") + raise ValueError( + f"No channel {channel_id!r} in the device; the device's " + f"channels are {list(self.device.channels)}." + ) ch = self.device.channels[channel_id] if channel_id not in self.available_channels: if self._in_xy and ch.basis != "XY": + declared_ids = { + n: cs.channel_id for n, cs in self._schedule.items() + } raise ValueError( - f"Channel '{ch}' cannot work simultaneously " - "with the declared 'Microwave' channel." + "Channel cannot work simultaneously with the declared " + f"'Microwave' channel; got {name!r} ({channel_id!r}) " + f"with the declared {declared_ids}." ) elif not self._in_xy and ch.basis == "XY": + declared_ids = { + n: cs.channel_id for n, cs in self._schedule.items() + } raise ValueError( "Channel of type 'Microwave' cannot work " - "simultaneously with the declared channels." + "simultaneously with the declared channels; got " + f"{name!r} ({channel_id!r}) with the declared " + f"{declared_ids}." ) else: - raise ValueError(f"Channel {channel_id} is not available.") + raise ValueError( + f"Channel {channel_id!r} is not available; still " + f"available are {list(self.available_channels)}." + ) if initial_target is not None: try: @@ -897,7 +957,10 @@ def declare_channel( except TypeError: cond = isinstance(initial_target, Parametrized) if cond: - raise TypeError("The initial_target cannot be parametrized") + raise TypeError( + "The initial_target cannot be parametrized; got " + f"{initial_target!r}." + ) if ch.basis == "XY": if not self._in_xy: @@ -984,14 +1047,19 @@ def declare_variable( To avoid confusion, it is recommended to store the returned Variable instance in a Python variable with the same name. """ - if name in ("qubits", "seq_name", "json_dumps_options"): + protected_names = ("qubits", "seq_name", "json_dumps_options") + if name in protected_names: raise ValueError( f"'{name}' is a protected name. Please choose a different name" - " for the variable." + " for the variable; protected names are " + f"{list(protected_names)}." ) if name in self._variables: - raise ValueError("Name for variable is already being used.") + raise ValueError( + f"Name for variable is already being used; got {name!r}, " + f"already declared: {list(self._variables)}." + ) if size is None: var = self.declare_variable(name, size=1, dtype=dtype) @@ -1316,7 +1384,10 @@ def add_eom_pulse( raise TypeError float(pm.AbstractArray(arg, dtype=float)) except TypeError: - raise TypeError("Phase values must be a numeric value.") + raise TypeError( + "Phase values must be a numeric value; got " + f"{type(arg)}: {arg!r}." + ) return eom_settings = self._schedule[channel].eom_blocks[-1] @@ -1378,7 +1449,8 @@ def add( if isinstance(self.declared_channels[channel], DMM): raise ValueError( "`Sequence.add()` can't be used on a DMM channel. " - "Use `Sequence.add_dmm_detuning()` instead." + "Use `Sequence.add_dmm_detuning()` instead; got channel " + f"{channel!r}." ) self._add(pulse, channel, protocol) @@ -1523,9 +1595,17 @@ def estimate_added_delay( ) self._validate_add_protocol(protocol) if self.is_parametrized() or isinstance(pulse, Parametrized): + parametrized = [ + what + for what, cond in ( + ("sequence", self.is_parametrized()), + ("pulse", isinstance(pulse, Parametrized)), + ) + if cond + ] raise ValueError( - "Can't compute the delay to add before a pulse if sequence or" - "pulse is parametrized." + "Can't compute the delay to add before a pulse if sequence " + f"or pulse is parametrized; parametrized: {parametrized}." ) if self.is_in_eom_mode(channel): eom_settings = self._schedule[channel].eom_blocks[-1] @@ -1553,9 +1633,15 @@ def estimate_added_delay( if isinstance(channel_obj, DMM): phase_ref = None elif len(ph_refs) != 1: + refs = { + q: float(self._basis_ref[basis][q].phase.last_phase) + for q in self._register.qubit_ids + if q in last.targets + } raise ValueError( "Cannot do a multiple-target pulse on qubits with different " - "phase references for the same basis." + f"phase references for the same basis; got {refs} in basis " + f"{basis!r}." ) else: phase_ref = ph_refs.pop() @@ -1730,13 +1816,23 @@ def align(self, *channels: str, at_rest: bool = True) -> None: # channels have to be a subset of the declared channels if not ch_set <= set(self._schedule): raise ValueError( - "All channel names must correspond to declared channels." + "All channel names must correspond to declared channels; " + f"{[c for c in channels if c not in self._schedule]} not in " + f"{list(self._schedule)}." ) if len(channels) != len(ch_set): - raise ValueError("The same channel was provided more than once.") + raise ValueError( + "The same channel was provided more than once; found " + "repeated names " + f"{[c for c, n in Counter(channels).items() if n > 1]} in " + f"{list(channels)}." + ) if len(channels) < 2: - raise ValueError("Needs at least two channels for alignment.") + raise ValueError( + "Needs at least two channels for alignment; got " + f"{len(channels)}: {list(channels)}." + ) if self.is_parametrized(): return @@ -1788,7 +1884,8 @@ def build( if qubits is None: raise ValueError( "'qubits' must be specified when the sequence is created " - "with a MappableRegister." + "with a MappableRegister; the register declares " + f"{list(self._register.qubit_ids)}." ) elif qubits is not None: @@ -2151,9 +2248,15 @@ def _add( if isinstance(channel_obj, DMM): phase_ref = None elif len(ph_refs) != 1: + refs = { + q: float(self._basis_ref[basis][q].phase.last_phase) + for q in self._register.qubit_ids + if q in last.targets + } raise ValueError( "Cannot do a multiple-target pulse on qubits with different " - "phase references for the same basis." + f"phase references for the same basis; got {refs} in basis " + f"{basis!r}." ) else: phase_ref = ph_refs.pop() @@ -2227,14 +2330,27 @@ def _target( ) if channel_obj.addressing != "Local": - raise ValueError("Can only choose target of 'Local' channels.") + raise ValueError( + "Can only choose target of 'Local' channels; channel " + f"{channel!r} has addressing {channel_obj.addressing!r}." + ) elif ( channel_obj.max_targets is not None and len(qubits_set) > channel_obj.max_targets ): + given_targets = ( + [qubits] + if isinstance(qubits, str) + or not isinstance(qubits, Collection) + else list(qubits) + ) + limit = channel_obj.max_targets raise ValueError( - f"This channel can target at most {channel_obj.max_targets} " - "qubits at a time." + "This channel can target at most " + f"{limit} {'qubit' if limit == 1 else 'qubits'} " + f"at a time; got {len(qubits_set)} " + f"{'target' if len(qubits_set) == 1 else 'targets'}: " + f"{given_targets}." ) qubit_ids_set = self._check_qubits_give_ids(*qubits_set, _index=_index) @@ -2245,9 +2361,15 @@ def _target( for q in qubit_ids_set } if len(phase_refs) != 1: + refs = { + q: float(self._basis_ref[basis][q].phase.last_phase) + for q in self._register.qubit_ids + if q in qubit_ids_set + } raise ValueError( "Cannot target multiple qubits with different " - "phase references for the same basis." + f"phase references for the same basis; got {refs} in " + f"basis {basis!r}." ) self._schedule.add_target(qubit_ids_set, channel) @@ -2277,12 +2399,18 @@ def _check_qubits_give_ids( for index in qubits } except IndexError: - raise IndexError("Indices must exist for the register.") + raise IndexError( + "Indices must exist for the register; got " + f"{list(qubits)} for a register of " + f"{len(self._register.qubit_ids)} qubits." + ) ids = set(cast(Tuple[QubitId, ...], qubits)) if not ids <= self._qids: raise ValueError( "All given ids have to be qubit ids declared" - " in this sequence's register." + f" in this sequence's register; " + f"{[q for q in qubits if q not in self._qids]} not in " + f"{list(self._register.qubit_ids)}." ) return ids @@ -2311,7 +2439,8 @@ def _phase_shift( ) -> None: if basis not in self._basis_ref: raise ValueError( - f"No declared channel targets the given 'basis' ('{basis}')." + f"No declared channel targets the given 'basis' ('{basis}'); " + f"declared bases are {list(self._basis_ref)}." ) if not specific_targets: @@ -2387,12 +2516,17 @@ def _validate_channel( if isinstance(channel, Parametrized): raise NotImplementedError( "Using parametrized objects or variables to refer to channels " - "is not supported." + f"is not supported; got {channel!r}." ) if channel not in self.declared_channels: - raise ValueError("Use the name of a declared channel.") + raise ValueError( + f"Use the name of a declared channel; got {channel!r}, " + f"declared: {list(self.declared_channels)}." + ) if block_eom_mode and self.is_in_eom_mode(channel): - raise RuntimeError("The chosen channel is in EOM mode.") + raise RuntimeError( + f"The chosen channel is in EOM mode; got {channel!r}." + ) if ( block_if_slm and channel == self._slm_mask_dmm @@ -2400,9 +2534,17 @@ def _validate_channel( _DMMSchedule, self._schedule[self._slm_mask_dmm] )._waiting_for_first_pulse ): + global_channels = [ + n + for n, ch_obj in self.declared_channels.items() + if ch_obj.addressing == "Global" + and not isinstance(ch_obj, DMM) + ] raise ValueError( "You should add a Pulse to a Global Channel prior to" - " modulating the DMM used for the SLM Mask." + " modulating the DMM used for the SLM Mask; before adding a " + f"pulse to {channel!r}, make sure at least one of " + f"{global_channels} already has a pulse." ) def _validate_and_adjust_pulse( diff --git a/tests/test_paramseq.py b/tests/test_paramseq.py index 7048a20a6..26992b164 100644 --- a/tests/test_paramseq.py +++ b/tests/test_paramseq.py @@ -13,6 +13,7 @@ # limitations under the License. import copy +import re from collections.abc import Iterable import numpy as np @@ -36,12 +37,24 @@ def test_var_declarations(): assert isinstance(var, Variable) assert var.dtype == float assert var.size == 1 - with pytest.raises(ValueError, match="already being used"): + with pytest.raises( + ValueError, + match=re.escape( + "already being used; got 'var', already declared: ['var']." + ), + ): sb.declare_variable("var", dtype=int, size=10) var3 = sb.declare_variable("var3") assert sb.declared_variables["var3"] == var3.var assert isinstance(var3, VariableItem) - with pytest.raises(ValueError, match="'qubits' is a protected name"): + with pytest.raises( + ValueError, + match=re.escape( + "'qubits' is a protected name. Please choose a different name " + "for the variable; protected names are ['qubits', 'seq_name', " + "'json_dumps_options']." + ), + ): sb.declare_variable("qubits", size=10, dtype=int) @@ -62,7 +75,10 @@ def test_stored_calls(): var = sb.declare_variable("var") assert sb._to_build_calls == [] with pytest.raises( - TypeError, match="initial_target cannot be parametrized" + TypeError, + match=re.escape( + "initial_target cannot be parametrized; got VariableItem(" + ), ): sb.declare_channel("ch1", "rydberg_local", initial_target=var) sb.declare_channel("ch1", "rydberg_local") @@ -70,7 +86,13 @@ def test_stored_calls(): assert sb._calls[-1].name == "declare_channel" assert sb._to_build_calls[-1].name == "target_index" assert sb._to_build_calls[-1].args == (var, "ch1") - with pytest.raises(ValueError, match="name of a declared channel"): + with pytest.raises( + ValueError, + match=re.escape( + "name of a declared channel; got 'rydberg_local', declared: " + "['ch1']." + ), + ): sb.delay(1000, "rydberg_local") x = Variable("x", int) var_ = copy.deepcopy(var) @@ -79,12 +101,21 @@ def test_stored_calls(): with pytest.raises(ValueError, match="come from this Sequence"): sb.target(var_, "ch1") - with pytest.raises(ValueError, match="ids have to be qubit ids"): + with pytest.raises( + ValueError, + match=re.escape( + "ids have to be qubit ids declared in this sequence's register; " + "['q20'] not in " + ), + ): sb.target("q20", "ch1") with pytest.raises( NotImplementedError, - match="Using parametrized objects or variables to refer to channels", + match=re.escape( + "Using parametrized objects or variables to refer to channels " + "is not supported; got VariableItem(" + ), ): sb.target("q0", var) sb.delay(var, "ch1") @@ -121,7 +152,13 @@ def test_stored_calls(): assert sb._calls[-1].name == "declare_channel" with pytest.raises(ValueError, match="'Local' channels"): sb.target(0, "ch2") - with pytest.raises(ValueError, match="target at most 1 qubits"): + with pytest.raises( + ValueError, + match=re.escape( + "This channel can target at most 1 qubit at a time; got 5 " + "targets: [Variable(name='q_var', dtype=, size=5)]." + ), + ): sb.target_index(q_var, "ch1") sb2 = Sequence(reg, MockDevice) @@ -159,11 +196,25 @@ def test_stored_calls(): ): sb.target_index("q1", channel="ch1") - with pytest.raises(ValueError, match="correspond to declared channels"): + with pytest.raises( + ValueError, + match=re.escape("correspond to declared channels; [VariableItem("), + ): sb.align("ch1", var) - with pytest.raises(ValueError, match="more than once"): + with pytest.raises( + ValueError, + match=re.escape( + "more than once; found repeated names ['ch2'] in " + "['ch1', 'ch2', 'ch2']." + ), + ): sb.align("ch1", "ch2", "ch2") - with pytest.raises(ValueError, match="at least two channels"): + with pytest.raises( + ValueError, + match=re.escape( + "at least two channels for alignment; got 1: ['ch1']." + ), + ): sb.align("ch1") with pytest.raises(ValueError, match="not supported"): @@ -260,7 +311,8 @@ def test_parametrized_in_eom_mode(mod_device): seq.enable_eom_mode("ch0", amp_on=2.0, detuning_on=0.0) with pytest.raises( - RuntimeError, match="The chosen channel is in EOM mode" + RuntimeError, + match=re.escape("The chosen channel is in EOM mode; got 'ch0'."), ): seq.target_index(1, "ch0") @@ -314,7 +366,10 @@ def test_parametrized_before_eom_mode(mod_device): seq.add_eom_pulse("ch0", 1000, 0.0, protocol="smallest") with pytest.raises( - TypeError, match="Phase values must be a numeric value." + TypeError, + match=re.escape( + "Phase values must be a numeric value; got : '0.'." + ), ): seq.add_eom_pulse("ch0", 200, "0.") diff --git a/tests/test_sequence.py b/tests/test_sequence.py index aa1428503..d1ab9e8b3 100644 --- a/tests/test_sequence.py +++ b/tests/test_sequence.py @@ -72,7 +72,7 @@ def device(): def test_init(reg, device): - with pytest.raises(TypeError, match="must be of type 'BaseDevice'"): + with pytest.raises(TypeError, match="must be an instance of 'BaseDevice'"): Sequence(reg, Device) seq = Sequence(reg, device) @@ -89,7 +89,13 @@ def test_channel_declaration(reg, device): available_channels = set(seq.available_channels) assert seq.get_addressed_bases() == () assert seq.get_addressed_states() == [] - with pytest.raises(ValueError, match="Name starting by 'dmm_'"): + with pytest.raises( + ValueError, + match=re.escape( + "Name starting by 'dmm_' are reserved for DMM channels; got " + "'dmm_1_2'." + ), + ): seq.declare_channel("dmm_1_2", "raman") seq.declare_channel("ch0", "rydberg_global") assert seq.get_addressed_bases() == ("ground-rydberg",) @@ -97,11 +103,28 @@ def test_channel_declaration(reg, device): seq.declare_channel("ch1", "raman_local") assert seq.get_addressed_bases() == ("ground-rydberg", "digital") assert seq.get_addressed_states() == ["r", "g", "h"] - with pytest.raises(ValueError, match="No channel"): + with pytest.raises( + ValueError, + match=re.escape( + "No channel 'raman' in the device; the device's channels are " + "['rydberg_global', 'rydberg_local', 'raman_local']." + ), + ): seq.declare_channel("ch2", "raman") - with pytest.raises(ValueError, match="not available"): + with pytest.raises( + ValueError, + match=re.escape( + "Channel 'rydberg_global' is not available; still available are " + "['rydberg_local', 'dmm_0', 'dmm_1']." + ), + ): seq.declare_channel("ch2", "rydberg_global") - with pytest.raises(ValueError, match="name is already in use"): + with pytest.raises( + ValueError, + match=re.escape( + "name is already in use; got 'ch0', already declared: " + ), + ): seq.declare_channel("ch0", "raman_local") chs = {"rydberg_global", "raman_local"} @@ -124,7 +147,13 @@ def test_channel_declaration(reg, device): seq2._schedule[channel].channel_id for channel in seq2.declared_channels ) == set(channel_map.values()) - with pytest.raises(ValueError, match="type 'Microwave' cannot work "): + with pytest.raises( + ValueError, + match=re.escape( + "type 'Microwave' cannot work simultaneously with the declared " + "channels; got 'ch3' ('mw_global') with the declared " + ), + ): seq2.declare_channel("ch3", "mw_global") seq2 = Sequence(reg, MockDevice) @@ -132,7 +161,11 @@ def test_channel_declaration(reg, device): assert set(seq2.available_channels) == {"mw_global", "dmm_0"} with pytest.raises( ValueError, - match="cannot work simultaneously with the declared 'Microwave'", + match=re.escape( + "cannot work simultaneously with the declared 'Microwave' " + "channel; got 'ch3' ('rydberg_global') with the declared " + "{'ch0': 'mw_global'}." + ), ): seq2.declare_channel("ch3", "rydberg_global") assert seq2.get_addressed_bases() == ("XY",) @@ -169,7 +202,12 @@ def test_dmm_declaration(reg, device, det_map, first_dmm_id): r"available: \['dmm_0', 'dmm_1'\]\.", ): seq.config_detuning_map(det_map, "dmm_2") - with pytest.raises(ValueError, match="DMM dmm_0 is not available"): + with pytest.raises( + ValueError, + match=re.escape( + "DMM dmm_0 is not available; still available DMM channels are []." + ), + ): seq.config_detuning_map(det_map, "dmm_0") with pytest.raises(ValueError, match="No DMM channel is still available"): seq.config_detuning_map(det_map) @@ -227,7 +265,11 @@ def test_slm_declaration(reg, device, det_map): seq.config_slm_mask(["q0", "q1", "q3", "q4"]) assert seq.get_addressed_bases() == tuple() with pytest.raises( - ValueError, match="SLM mask can be configured only once." + ValueError, + match=re.escape( + "SLM mask can be configured only once; already configured with " + "targets " + ), ): seq.config_slm_mask(["q0", "q1", "q3", "q4"], "dmm_1") # no channel has been declared @@ -293,7 +335,10 @@ def test_magnetic_field(reg): seq = Sequence(reg, MockDevice) with pytest.raises( AttributeError, - match="only defined when the sequence " "is in 'XY Mode'.", + match=re.escape( + "only defined when the sequence is in 'XY Mode'; this " + "sequence addresses ()." + ), ): seq.magnetic_field seq.declare_channel("ch0", "mw_global") # seq in XY mode @@ -301,24 +346,45 @@ def test_magnetic_field(reg): assert np.all(seq.magnetic_field == np.array((0.0, 0.0, 30.0))) seq.set_magnetic_field(bx=1.0, by=-1.0, bz=0.5) assert np.all(seq.magnetic_field == np.array((1.0, -1.0, 0.5))) - with pytest.raises(ValueError, match="magnitude greater than 0"): + with pytest.raises( + ValueError, + match=re.escape("magnitude greater than 0; got (0.0, 0.0, 0.0)."), + ): seq.set_magnetic_field(bz=0.0) assert seq._empty_sequence seq.add(Pulse.ConstantPulse(100, 1, 1, 0), "ch0") assert not seq._empty_sequence - with pytest.raises(ValueError, match="can only be set on an empty seq"): + with pytest.raises( + ValueError, + match=re.escape( + "can only be set on an empty sequence; channels with contents " + "are {'ch0': 'mw_global'}." + ), + ): seq.set_magnetic_field(1.0, 0.0, 0.0) # Raises an error if a Global channel is declared (not in xy) seq2 = Sequence(reg, MockDevice) seq2.declare_channel("ch0", "rydberg_global") - with pytest.raises(ValueError, match="can only be set in 'XY Mode'."): + with pytest.raises( + ValueError, + match=re.escape( + "can only be set in 'XY Mode'; declared channels are " + "{'ch0': 'rydberg_global'}." + ), + ): seq2.set_magnetic_field(1.0, 0.0, 0.0) # Same if a dmm channel was configured seq2 = Sequence(reg, MockDevice) seq2.config_detuning_map(det_map, "dmm_0") # not in XY mode - with pytest.raises(ValueError, match="can only be set in 'XY Mode'."): + with pytest.raises( + ValueError, + match=re.escape( + "can only be set in 'XY Mode'; declared channels are " + "{'dmm_0': 'dmm_0'}." + ), + ): seq2.set_magnetic_field(1.0, 0.0, 0.0) # Works if a slm mask was configured @@ -342,7 +408,13 @@ def test_magnetic_field(reg): # Sequence is marked as non-empty when parametrized too seq3.add(Pulse.ConstantPulse(100, var, 1, 0), "ch0") assert seq3.is_parametrized() - with pytest.raises(ValueError, match="can only be set on an empty seq"): + with pytest.raises( + ValueError, + match=re.escape( + "can only be set on an empty sequence; channels with contents " + "are {'ch0': 'mw_global'}." + ), + ): seq3.set_magnetic_field() seq3_str = seq3._serialize() @@ -560,7 +632,10 @@ def test_ising_mode( assert not seq._in_ising and not seq._in_xy seq.declare_channel("ch0", "rydberg_global") assert seq._in_ising and not seq._in_xy - with pytest.raises(TypeError, match="_in_ising must be a bool."): + with pytest.raises( + TypeError, + match=re.escape("_in_ising must be a bool; got : 1."), + ): seq._in_ising = 1 with pytest.raises(ValueError, match="Cannot quit ising."): seq._in_ising = False @@ -1571,15 +1646,32 @@ def test_target(reg, device): seq.declare_channel("ch0", "raman_local", initial_target="q1") seq.declare_channel("ch1", "rydberg_global") - with pytest.raises(ValueError, match="name of a declared channel"): + with pytest.raises( + ValueError, + match=re.escape( + "name of a declared channel; got 'ch2', declared: ['ch0', 'ch1']." + ), + ): seq.target("q0", "ch2") with pytest.raises(ValueError, match="ids have to be qubit ids"): seq.target(0, "ch0") with pytest.raises(ValueError, match="ids have to be qubit ids"): seq.target("0", "ch0") - with pytest.raises(ValueError, match="Can only choose target of 'Local'"): + with pytest.raises( + ValueError, + match=re.escape( + "Can only choose target of 'Local' channels; channel 'ch1' has " + "addressing 'Global'." + ), + ): seq.target("q3", "ch1") - with pytest.raises(ValueError, match="can target at most 1 qubits"): + with pytest.raises( + ValueError, + match=re.escape( + "This channel can target at most 1 qubit at a time; got 2 " + "targets: ['q1', 'q5']." + ), + ): seq.target(["q1", "q5"], "ch0") with pytest.raises(ValueError, match="Need at least one qubit to target"): seq.target([], "ch0") @@ -1629,9 +1721,18 @@ def test_target(reg, device): def test_delay(reg, device, at_rest): seq = Sequence(reg, device) seq.declare_channel("ch0", "raman_local") - with pytest.raises(ValueError, match="Use the name of a declared channel"): + with pytest.raises( + ValueError, + match=re.escape( + "Use the name of a declared channel; got 'ch01', declared: " + "['ch0']." + ), + ): seq.delay(1e3, "ch01") - with pytest.raises(ValueError, match="channel has no target"): + with pytest.raises( + ValueError, + match=re.escape("The chosen channel ('raman_local') has no target."), + ): seq.delay(100, "ch0") seq.target("q19", "ch0") seq.add(Pulse.ConstantPulse(100, 1, 0, 0), "ch0") @@ -1723,11 +1824,29 @@ def test_phase(reg, device, det_map, catch_phase_shift_warning): seq = Sequence(reg, device) seq.declare_channel("ch0", "raman_local", initial_target="q0") seq.phase_shift(-1, "q0", "q1") - with pytest.raises(ValueError, match="id of a qubit declared"): + with pytest.raises( + ValueError, + match=re.escape( + "id of a qubit declared in this sequence's register; got 0, " + "declared: " + ), + ): seq.current_phase_ref(0, "digital") - with pytest.raises(ValueError, match="targets the given 'basis'"): + with pytest.raises( + ValueError, + match=re.escape( + "targets the given 'basis' ('ground-rydberg'); declared bases " + "are ['digital']." + ), + ): seq.current_phase_ref("q1", "ground-rydberg") - with pytest.raises(ValueError, match="No declared channel targets"): + with pytest.raises( + ValueError, + match=re.escape( + "No declared channel targets the given 'basis' ('hyperfine'); " + "declared bases are ['digital']." + ), + ): seq.phase_shift(1, "q3", basis="hyperfine") assert seq.current_phase_ref("q0", "digital") == 2 * np.pi - 1 @@ -1761,8 +1880,10 @@ def test_phase(reg, device, det_map, catch_phase_shift_warning): seq.phase_shift(1.0, "q0", basis="ground-rydberg") with pytest.raises( ValueError, - match="Cannot do a multiple-target pulse on qubits with different " - "phase references for the same basis.", + match=re.escape( + "Cannot do a multiple-target pulse on qubits with different " + "phase references for the same basis; got {'q0': 2.0, " + ), ): seq.add(Pulse.ConstantPulse(100, 1, 0, 0), "ch1") # But it works on the DMM @@ -1785,12 +1906,33 @@ def test_align(reg, device): seq = Sequence(reg, device) seq.declare_channel("ch0", "raman_local", initial_target="q0") seq.declare_channel("ch1", "rydberg_global") - with pytest.raises(ValueError, match="names must correspond to declared"): + with pytest.raises( + ValueError, + match=re.escape( + "names must correspond to declared channels; ['ch2'] not in " + "['ch0', 'ch1']." + ), + ): seq.align("ch0", "ch1", "ch2") - with pytest.raises(ValueError, match="more than once"): + with pytest.raises( + ValueError, + match=re.escape( + "more than once; found repeated names ['ch0'] in " + "['ch0', 'ch1', 'ch0']." + ), + ): seq.align("ch0", "ch1", "ch0") - with pytest.raises(ValueError, match="at least two channels"): + with pytest.raises( + ValueError, + match=re.escape("at least two channels for alignment; got 0: []."), + ): seq.align() + with pytest.raises( + ValueError, + match=re.escape( + "at least two channels for alignment; got 1: ['ch1']." + ), + ): seq.align("ch1") @@ -2090,14 +2232,21 @@ def test_estimate_added_delay(eom, custom_phase_jump_time): assert seq.estimate_added_delay(pulse_0, "ising") == delay - 100 with pytest.warns( UserWarning, - match="The sequence's duration exceeded the maximum duration", + match=re.escape( + "The sequence's duration exceeded the maximum duration allowed by" + " the device (6000 ns); got " + ), ): seq.estimate_added_delay( pulser.Pulse.ConstantPulse(6000, 1, 0, np.pi), "ising" ) var = seq.declare_variable("var", dtype=int) with pytest.raises( - ValueError, match="Can't compute the delay to add before a pulse" + ValueError, + match=re.escape( + "Can't compute the delay to add before a pulse if sequence or " + "pulse is parametrized; parametrized: ['pulse']." + ), ): seq.estimate_added_delay(Pulse.ConstantPulse(var, 1, 0, 0), "ising") # We shift the phase of just one qubit, which blocks addition @@ -2105,7 +2254,10 @@ def test_estimate_added_delay(eom, custom_phase_jump_time): seq.phase_shift_index(1.0, 0, basis="ground-rydberg") with pytest.raises( ValueError, - match="Cannot do a multiple-target pulse on qubits with different", + match=re.escape( + "Cannot do a multiple-target pulse on qubits with different " + "phase references for the same basis; got {" + ), ): seq.estimate_added_delay(pulse_0, "ising") @@ -2149,11 +2301,21 @@ def test_config_slm_mask(qubit_ids, device, det_map): seq_ = Sequence(reg, AnalogDevice) seq_.config_slm_mask(["q0" if is_str_qubit_id else 0]) - with pytest.raises(TypeError, match="must be castable to set"): + with pytest.raises( + TypeError, + match=re.escape("must be castable to set; got : 0."), + ): seq.config_slm_mask(0) - with pytest.raises(TypeError, match="must be castable to set"): + with pytest.raises( + TypeError, + match=re.escape("must be castable to set; got : 0."), + ): seq.config_slm_mask((0)) - with pytest.raises(ValueError, match="exist in the register"): + # a bare string is iterated character by character + with pytest.raises( + ValueError, + match=re.escape("exist in the register; ['q', '0'] not in "), + ): seq.config_slm_mask("q0") with pytest.raises(ValueError, match="exist in the register"): seq.config_slm_mask(["q3" if is_str_qubit_id else 3]) @@ -2175,14 +2337,25 @@ def test_config_slm_mask(qubit_ids, device, det_map): else: assert seq._slm_mask_targets == {0, 2} assert not seq._schedule - with pytest.raises(ValueError, match="DMM dmm_0 is not available."): + with pytest.raises( + ValueError, + match=re.escape( + "DMM dmm_0 is not available; still available DMM channels are " + "['dmm_1']." + ), + ): 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] == 1.0 assert seq._schedule["dmm_0"].detuning_map.weights[2] == 1.0 - with pytest.raises(ValueError, match="configured only once"): + with pytest.raises( + ValueError, + match=re.escape( + "configured only once; already configured with targets " + ), + ): seq.config_slm_mask(targets) mapp_reg = MappableRegister( RegisterLayout(trap_ids + [(0, 10), (0, 20), (0, -10)]), *qubit_ids @@ -2201,6 +2374,16 @@ def test_slm_mask_in_xy(reg, patch_plt_show): pulse1 = Pulse.ConstantPulse(100, 10, 0, 0) pulse2 = Pulse.ConstantPulse(200, 10, 0, 0) + # Targets must be qubits of the register + with pytest.raises( + ValueError, + match=re.escape( + "SLM mask targets must exist in the register; ['zz'] not in " + "['q0', 'q1', 'q2']." + ), + ): + Sequence(reg, MockDevice).config_slm_mask(["q0", "zz"]) + # Set mask when an XY pulse is already in the schedule seq_xy1 = Sequence(reg, MockDevice) seq_xy1.declare_channel("ch_xy", "mw_global") @@ -2360,7 +2543,10 @@ def test_draw_slm_mask_in_ising( if draw_qubit_det or draw_qubit_amp: with pytest.raises( NotImplementedError, - match="Can only draw qubit contents for channels in rydberg basis", + match=re.escape( + "Can only draw qubit contents for channels in the " + "'ground-rydberg' basis; got {'raman_glob': 'digital'}." + ), ): seq1.draw( mode, @@ -2392,6 +2578,18 @@ def test_slm_mask_in_ising(patch_plt_show, bottom_detunings): ), ) seq2.config_slm_mask(targets) + # The SLM's DMM can't be modulated before a global pulse exists + seq2.declare_channel("ryd", "rydberg_global") + with pytest.raises( + ValueError, + match=re.escape( + "You should add a Pulse to a Global Channel prior to modulating" + " the DMM used for the SLM Mask; before adding a pulse to" + " 'dmm_0', make sure at least one of ['ryd'] already has a" + " pulse." + ), + ): + seq2.add_dmm_detuning(ConstantWaveform(100, -10), "dmm_0") seq2.declare_channel("ryd_glob", "rydberg_global") seq2.config_detuning_map(det_map, "dmm_0") # configured as dmm_0_1 with pytest.raises( @@ -2648,7 +2846,14 @@ def test_mappable_register(det_map, patch_plt_show, with_dmm): seq.draw() else: seq.draw() - with pytest.raises(ValueError, match="'qubits' must be specified"): + with pytest.raises( + ValueError, + match=re.escape( + "'qubits' must be specified when the sequence is created with a " + "MappableRegister; the register declares ['q0', 'q1', 'q2', " + "'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9']." + ), + ): seq.build() with pytest.raises( @@ -2683,7 +2888,14 @@ def test_mappable_register(det_map, patch_plt_show, with_dmm): seq_.build(qubits={"q2": 20, "q0": 10, "q1": 0}) # Also possible to build the default register - with pytest.raises(ValueError, match="'qubits' must be specified"): + with pytest.raises( + ValueError, + match=re.escape( + "'qubits' must be specified when the sequence is created with a " + "MappableRegister; the register declares ['q0', 'q1', 'q2', " + "'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9']." + ), + ): seq.build() @@ -2739,7 +2951,10 @@ def test_parametrized_index_functions( assert built_seq.current_phase_ref(expected_target, "digital") == phi with pytest.raises( - IndexError, match="Indices must exist for the register" + IndexError, + match=re.escape( + "Indices must exist for the register; got [20] for a register of " + ), ): seq.build(**build_params, index=20) @@ -2779,11 +2994,17 @@ def test_non_parametrized_non_mappable_register_index_functions( seq.declare_channel("ch1", "raman_local") phi = np.pi / 4 with pytest.raises( - IndexError, match="Indices must exist for the register" + IndexError, + match=re.escape( + "Indices must exist for the register; got [20] for a register of " + ), ): seq.target_index(20, channel="ch0") with pytest.raises( - IndexError, match="Indices must exist for the register" + IndexError, + match=re.escape( + "Indices must exist for the register; got [20] for a register of " + ), ): seq.phase_shift_index(phi, 20) seq.target_index(index, channel="ch0") @@ -3144,12 +3365,21 @@ def test_max_duration(reg, mod_device): seq = Sequence(reg, dev_) seq.declare_channel("ch0", "rydberg_global") seq.delay(100, "ch0") - catch_statement = pytest.raises( - RuntimeError, match="duration exceeded the maximum duration allowed" - ) - with catch_statement: + with pytest.raises( + RuntimeError, + match=re.escape( + "duration exceeded the maximum duration allowed by the device " + "(100 ns); got 116 ns." + ), + ): seq.delay(16, "ch0") - with catch_statement: + with pytest.raises( + RuntimeError, + match=re.escape( + "duration exceeded the maximum duration allowed by the device " + "(100 ns); got 200 ns." + ), + ): seq.add(Pulse.ConstantPulse(100, 1, 0, 0), "ch0") @@ -3157,7 +3387,13 @@ def test_add_to_dmm_fails(reg, device, det_map): seq = Sequence(reg, device) seq.config_detuning_map(det_map, "dmm_0") pulse = Pulse.ConstantPulse(100, 0, -1, 0) - with pytest.raises(ValueError, match="can't be used on a DMM"): + with pytest.raises( + ValueError, + match=re.escape( + "can't be used on a DMM channel. Use `Sequence.add_dmm_detuning()`" + " instead; got channel 'dmm_0'." + ), + ): seq.add(pulse, "dmm_0") seq.declare_channel("ryd", "rydberg_global") diff --git a/tests/test_sequence_sampler.py b/tests/test_sequence_sampler.py index 074e05866..5fed26e56 100644 --- a/tests/test_sequence_sampler.py +++ b/tests/test_sequence_sampler.py @@ -375,6 +375,24 @@ def test_seq_with_DMM_and_map_reg(): sample(seq) +def test_dmm_samples_need_qubits(): + reg = pulser.Register.square(2, 6, prefix="q") + seq = pulser.Sequence(reg, MockDevice) + seq.declare_channel("ryd", "rydberg_global") + seq.config_detuning_map( + reg.define_detuning_map({f"q{i}": 1.0 for i in range(4)}), "dmm_0" + ) + seq.add(pulser.Pulse.ConstantPulse(100, 1, 0, 0), "ryd") + with pytest.raises( + ValueError, + match=re.escape( + "'qubits' must be defined when extracting the samples of a DMM" + " channel; got None for channel 'dmm_0'." + ), + ): + seq._schedule["dmm_0"].get_samples() + + def seq_with_SLM( ch_name: Literal["mw_global", "rydberg_global"], ) -> pulser.Sequence: