diff --git a/pulser-core/pulser/_hamiltonian_data/hamiltonian_data.py b/pulser-core/pulser/_hamiltonian_data/hamiltonian_data.py index 63d231711..88f88f422 100644 --- a/pulser-core/pulser/_hamiltonian_data/hamiltonian_data.py +++ b/pulser-core/pulser/_hamiltonian_data/hamiltonian_data.py @@ -217,14 +217,17 @@ def __init__( # Initializing the samples obj if not isinstance(samples, SequenceSamples): raise TypeError( - "The provided sequence has to be a valid " - "SequenceSamples instance." + "The provided samples must be an instance of " + f"'SequenceSamples', not {type(samples)}." ) if samples.max_duration == 0: raise ValueError("SequenceSamples is empty.") # Check compatibility of register and device if not isinstance(device, BaseDevice): - raise TypeError("The device must be a Device or BaseDevice.") + raise TypeError( + "'device' must be an instance of 'BaseDevice', not " + f"{type(device)}." + ) self._device = device self.device.validate_register(register) self._register = register @@ -234,14 +237,32 @@ def __init__( "Samples use SLM mask but device does not have one." ) if not samples.used_bases <= self.device.supported_bases: + missing_bases = samples.used_bases - self.device.supported_bases + unsupported = [ + b + for b in dict.fromkeys( + ch.basis for ch in samples._ch_objs.values() + ) + if b in missing_bases + ] + supported = list( + dict.fromkeys(ch.basis for ch in self.device.channel_objects) + ) raise ValueError( - "Bases used in samples should be supported by device." + "Bases used in samples must be supported by the device; " + f"{unsupported} not in {supported}." ) # Check compatibility of masked samples and register if not samples._slm_mask.targets <= set(self.register.qubits.keys()): + # The mask targets are a set, so there is no caller order to keep + missing_targets = sorted( + samples._slm_mask.targets - set(self.register.qubits.keys()), + key=str, + ) raise ValueError( - "The ids of qubits targeted in SLM mask" - " should be defined in register." + "The ids of qubits targeted in the SLM mask must be defined " + f"in the register; {missing_targets} not in " + f"{list(self.register.qubit_ids)}." ) self._samples = self._delocalize_samples(samples) @@ -281,12 +302,19 @@ def _delocalize_samples(self, samples: SequenceSamples) -> SequenceSamples: if samples._ch_objs[ch].addressing == "Local": # Check that targets of Local Channels are defined # in register - if not set().union( + targets = set().union( *(slot.targets for slot in ch_samples.slots) - ) <= set(self.register.qubits.keys()): + ) + if not targets <= set(self.register.qubits.keys()): + # Slot targets are sets: there is no caller order to keep + missing_targets = sorted( + targets - set(self.register.qubits.keys()), key=str + ) raise ValueError( - "The ids of qubits targeted in Local channels" - " should be defined in register." + "The ids of qubits targeted by Local channel " + f"{ch!r} must be defined in the register; " + f"{missing_targets} " + f"not in {list(self.register.qubit_ids)}." ) samples_list.append(ch_samples) else: @@ -359,8 +387,8 @@ def from_sequence( """ if not isinstance(sequence, Sequence): raise TypeError( - "The provided sequence has to be a valid " - "pulser.Sequence instance." + "'sequence' must be an instance of 'Sequence', not " + f"{type(sequence)}." ) if sequence.is_parametrized() or sequence.is_register_mappable(): raise ValueError( diff --git a/pulser-simulation/pulser_simulation/simulation.py b/pulser-simulation/pulser_simulation/simulation.py index e4fe3f20a..cb74786fa 100644 --- a/pulser-simulation/pulser_simulation/simulation.py +++ b/pulser-simulation/pulser_simulation/simulation.py @@ -143,8 +143,8 @@ def __init__( # Initializing the samples obj if not isinstance(sampled_seq, SequenceSamples): raise TypeError( - "The provided sequence has to be a valid " - "SequenceSamples instance." + "The provided samples must be an instance of " + f"'SequenceSamples', not {type(sampled_seq)}." ) if sampled_seq.max_duration == 0: raise ValueError("SequenceSamples is empty.") @@ -159,14 +159,32 @@ def __init__( "Samples use SLM mask but device does not have one." ) if not sampled_seq.used_bases <= device.supported_bases: + missing_bases = sampled_seq.used_bases - device.supported_bases + unsupported = [ + b + for b in dict.fromkeys( + ch.basis for ch in sampled_seq._ch_objs.values() + ) + if b in missing_bases + ] + supported = list( + dict.fromkeys(ch.basis for ch in device.channel_objects) + ) raise ValueError( - "Bases used in samples should be supported by device." + "Bases used in samples must be supported by the device; " + f"{unsupported} not in {supported}." ) # Check compatibility of masked samples and register if not sampled_seq._slm_mask.targets <= set(register.qubit_ids): + # The mask targets are a set, so there is no caller order to keep + missing_targets = sorted( + sampled_seq._slm_mask.targets - set(register.qubit_ids), + key=str, + ) raise ValueError( - "The ids of qubits targeted in SLM mask" - " should be defined in register." + "The ids of qubits targeted in the SLM mask must be defined " + f"in the register; {missing_targets} not in " + f"{list(register.qubit_ids)}." ) self._tot_duration = sampled_seq.max_duration @@ -180,9 +198,11 @@ def __init__( f"{sampling_rate}) must be greater than 0 and " "less than or equal to 1." ) - if int(self._tot_duration * sampling_rate) < 4: + if (n_points := int(self._tot_duration * sampling_rate)) < 4: raise ValueError( - "`sampling_rate` is too small, less than 4 data points." + f"'sampling_rate' is too small; {sampling_rate} on a " + f"{self._tot_duration} ns sequence gives {n_points} data " + "points, at least 4 are needed." ) if noise_model is not None and config is not None: @@ -557,14 +577,15 @@ def set_evaluation_times( eval_times = np.array([]) else: raise ValueError( - "Wrong evaluation time label. It should " - "be `Full`, `Minimal`, an array of times or" - + " a float between 0 and 1." + f"Wrong evaluation time label; got {value!r}. It should " + "be `Full`, `Minimal`, an array of times or a float " + "between 0 and 1." ) elif isinstance(value, float): if value > 1 or value <= 0: raise ValueError( - "evaluation_times float must be between 0 and 1." + "'evaluation_times' float must be between 0 and 1; got " + f"{value}." ) indices = np.linspace( 0, @@ -577,20 +598,22 @@ def set_evaluation_times( elif isinstance(value, (list, tuple, np.ndarray)): if np.max(value, initial=0) > self._tot_duration * 1e-3: raise ValueError( - "Provided evaluation-time list extends " - "further than sequence duration." + "Provided evaluation-time list extends further than the " + f"sequence duration; got a maximum of {np.max(value)} µs " + f"for a {self._tot_duration / 1000} µs sequence." ) if np.min(value, initial=0) < 0: + arr = np.asarray(value) raise ValueError( - "Provided evaluation-time list contains " - "negative values." + "Provided evaluation-time list contains negative values; " + f"got {arr[arr < 0].tolist()}." ) eval_times = np.array(value) else: raise ValueError( - "Wrong evaluation time label. It should " - "be `Full`, `Minimal`, an array of times or a " - + "float between 0 and 1." + f"Wrong evaluation time label; got {value!r}. It should " + "be `Full`, `Minimal`, an array of times or a float " + "between 0 and 1." ) # Ensure 0 and final time are included: self._eval_times_array = np.union1d( @@ -700,7 +723,9 @@ def _run_solver( elif (progress_bar is False) or (progress_bar is None): options["progress_bar"] = "" else: - raise ValueError("`progress_bar` must be a bool.") + raise ValueError( + f"'progress_bar' must be a bool, not {progress_bar!r}." + ) solver_fn: Callable[..., Any] = qutip.sesolve @@ -1010,8 +1035,8 @@ def from_sequence( """ if not isinstance(sequence, Sequence): raise TypeError( - "The provided sequence has to be a valid " - "pulser.Sequence instance." + "'sequence' must be an instance of 'Sequence', not " + f"{type(sequence)}." ) if sequence.is_parametrized() or sequence.is_register_mappable(): raise ValueError( diff --git a/tests/pulser_simulation/test_simulation.py b/tests/pulser_simulation/test_simulation.py index 0ce41a291..1cfa1c179 100644 --- a/tests/pulser_simulation/test_simulation.py +++ b/tests/pulser_simulation/test_simulation.py @@ -110,16 +110,31 @@ def matrices(): def test_initialization_and_construction_of_hamiltonian(seq, mod_device): fake_sequence = {"pulse1": "fake", "pulse2": "fake"} - with pytest.raises(TypeError, match="sequence has to be a valid"): + with pytest.raises( + TypeError, + match=re.escape( + "'sequence' must be an instance of 'Sequence', not ." + ), + ): QutipEmulator.from_sequence(fake_sequence) - with pytest.raises(TypeError, match="sequence has to be a valid"): + with pytest.raises( + TypeError, + match=re.escape( + "The provided samples must be an instance of 'SequenceSamples', " + "not ." + ), + ): QutipEmulator( fake_sequence, Register.square(2, prefix="q"), mod_device ) # Simulation cannot be run on a register not defining "control1" with pytest.raises( ValueError, - match="The ids of qubits targeted in Local channels", + match=re.escape( + "The ids of qubits targeted by Local channel 'raman' must be " + "defined in the register; ['control1'] not in " + "['target', 'control2']." + ), ): QutipEmulator( sampler.sample(seq), @@ -188,7 +203,13 @@ def test_initialization_and_construction_of_hamiltonian(seq, mod_device): "control2": 2, } - with pytest.raises(ValueError, match="too small, less than"): + with pytest.raises( + ValueError, + match=re.escape( + "'sampling_rate' is too small; 0.0001 on a 9000 ns sequence gives " + "0 data points, at least 4 are needed." + ), + ): QutipEmulator.from_sequence(seq, sampling_rate=0.0001) with pytest.raises(ValueError, match="`sampling_rate`"): QutipEmulator.from_sequence(seq, sampling_rate=5) @@ -400,7 +421,10 @@ def _noise_model(dim): # seq2 cannot be run on DigitalAnalogDevice because it does not support mw with pytest.raises( ValueError, - match="Bases used in samples should be supported by device.", + match=re.escape( + "Bases used in samples must be supported by the device; ['XY'] " + "not in ['ground-rydberg', 'digital']." + ), ): QutipEmulator(sampler.sample(seq2), seq2.register, DigitalAnalogDevice) sim2 = QutipEmulator.from_sequence( @@ -695,7 +719,7 @@ def test_run(seq, patch_plt_show): sim.run(progress_bar=None) with pytest.raises( ValueError, - match="`progress_bar` must be a bool.", + match=re.escape("'progress_bar' must be a bool, not 1."), ): sim.run(progress_bar=1) @@ -720,28 +744,48 @@ def test_run(seq, patch_plt_show): def test_eval_times(seq): with pytest.raises( - ValueError, match="evaluation_times float must be between 0 " "and 1." + ValueError, + match=re.escape( + "'evaluation_times' float must be between 0 and 1; got 3.0." + ), ): sim = QutipEmulator.from_sequence(seq, sampling_rate=1.0) sim.set_evaluation_times(3.0) - with pytest.raises(ValueError, match="Wrong evaluation time label."): + with pytest.raises( + ValueError, + match=re.escape( + "Wrong evaluation time label; got 123. It should be `Full`, " + "`Minimal`, an array of times or a float between 0 and 1." + ), + ): sim = QutipEmulator.from_sequence(seq, sampling_rate=1.0) sim.set_evaluation_times(123) - with pytest.raises(ValueError, match="Wrong evaluation time label."): + with pytest.raises( + ValueError, + match=re.escape( + "Wrong evaluation time label; got 'Best'. It should be `Full`, " + "`Minimal`, an array of times or a float between 0 and 1." + ), + ): sim = QutipEmulator.from_sequence(seq, sampling_rate=1.0) sim.set_evaluation_times("Best") with pytest.raises( ValueError, - match="Provided evaluation-time list contains " "negative values.", + match=re.escape( + "Provided evaluation-time list contains negative values; got " + "[-1.0]." + ), ): sim = QutipEmulator.from_sequence(seq, sampling_rate=1.0) sim.set_evaluation_times([-1, 0, sim.sampling_times[-2]]) with pytest.raises( ValueError, - match="Provided evaluation-time list extends " - "further than sequence duration.", + match=re.escape( + "Provided evaluation-time list extends further than the sequence " + "duration; got a maximum of 19.0 µs for a 9.0 µs sequence." + ), ): sim = QutipEmulator.from_sequence(seq, sampling_rate=1.0) sim.set_evaluation_times([0, sim.sampling_times[-1] + 10]) @@ -1772,7 +1816,10 @@ def test_mask_equals_remove_xy(): # Simulation cannot be run on a register not defining "q2" with pytest.raises( ValueError, - match="The ids of qubits targeted in SLM mask", + match=re.escape( + "The ids of qubits targeted in the SLM mask must be defined in " + "the register; ['q2'] not in ['q0', 'q1']." + ), ): QutipEmulator(sampler.sample(seq_masked), reg_two, MockDevice) # Simulation on reduced register diff --git a/tests/test_hamiltonian_data.py b/tests/test_hamiltonian_data.py index 3a6b22d94..d7c1ee8da 100644 --- a/tests/test_hamiltonian_data.py +++ b/tests/test_hamiltonian_data.py @@ -1,3 +1,4 @@ +import re import unittest from dataclasses import replace from types import SimpleNamespace @@ -187,15 +188,19 @@ def test_init_errors(): register = pulser.Register.square(3, spacing=6, prefix="") with pytest.raises( TypeError, - match=( - "The provided sequence has to be a " - "valid SequenceSamples instance." + match=re.escape( + "The provided samples must be an instance of " + "'SequenceSamples', not ." ), ): HamiltonianData(None, None, None, None, None) with pytest.raises( - TypeError, match="The device must be a Device or BaseDevice." + TypeError, + match=re.escape( + "'device' must be an instance of 'BaseDevice', not " + "." + ), ): HamiltonianData(seq_samples, None, None, None, None) @@ -208,9 +213,10 @@ def test_init_errors(): with pytest.raises( ValueError, - match=( - "The ids of qubits targeted in SLM " - "mask should be defined in register." + match=re.escape( + "The ids of qubits targeted in the SLM mask must be defined in " + "the register; ['batman'] not in " + "['0', '1', '2', '3', '4', '5', '6', '7', '8']." ), ): HamiltonianData( @@ -219,9 +225,10 @@ def test_init_errors(): with pytest.raises( ValueError, - match=( - "The ids of qubits targeted in Local " - "channels should be defined in register." + match=re.escape( + "The ids of qubits targeted by Local channel 'ch1' must be " + "defined in the register; ['q0', 'q1'] not in " + "['0', '1', '2', '3', '4', '5', '6', '7', '8']." ), ): HamiltonianData( @@ -249,7 +256,10 @@ def test_init_errors(): seq_samples = sample(seq) with pytest.raises( ValueError, - match="Bases used in samples should be supported by device.", + match=re.escape( + "Bases used in samples must be supported by the device; ['XY'] " + "not in ['ground-rydberg', 'digital']." + ), ): HamiltonianData( seq_samples, seq.register, pulser.DigitalAnalogDevice, None, None @@ -278,9 +288,9 @@ def test_from_sequence(): with pytest.raises( TypeError, - match=( - "The provided sequence has to be " - "a valid pulser.Sequence instance." + match=re.escape( + "'sequence' must be an instance of 'Sequence', not " + "." ), ): HamiltonianData.from_sequence(None)