diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 0bd99e041..8244948c9 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -37,11 +37,15 @@ DIMENSIONS = Literal[2, 3] -ALWAYS_OPTIONAL_PARAMS = ( - "max_sequence_duration", - "max_runs", - "dmm_objects", - "default_noise_model", +ALWAYS_OPTIONAL_PARAMS = ("max_sequence_duration", "max_runs") +OPTIONAL_IN_ABSTR_REPR = tuple( + list(ALWAYS_OPTIONAL_PARAMS) + + [ + "dmm_objects", + "default_noise_model", + "requires_layout", + "accepts_new_layouts", + ] ) PARAMS_WITH_ABSTR_REPR = ("channel_objects", "channel_ids", "dmm_objects") @@ -61,7 +65,7 @@ class BaseDevice(ABC): dmm_objects: The DMM subclass instances specifying each channel in the device. They are referenced by their order in the list, with the ID "dmm_[index in dmm_objects]". - rybderg_level: The value of the principal quantum number :math:`n` + rydberg_level: The value of the principal quantum number :math:`n` when the Rydberg level used is of the form :math:`|nS_{1/2}, m_j = +1/2\rangle`. max_atom_num: Maximum number of atoms supported in an array. @@ -83,6 +87,8 @@ class BaseDevice(ABC): default_noise_model: An optional noise model characterizing the default noise of the device. Can be used by emulator backends that support noise. + requires_layout: Whether the register used in the sequence must be + created from a register layout. Only enforced in QPU execution. """ name: str @@ -96,6 +102,7 @@ class BaseDevice(ABC): max_layout_filling: float = 0.5 max_sequence_duration: int | None = None max_runs: int | None = None + requires_layout: bool = False reusable_channels: bool = field(default=False, init=False) channel_ids: tuple[str, ...] | None = None channel_objects: tuple[Channel, ...] = field(default_factory=tuple) @@ -464,8 +471,8 @@ def _to_dict(self) -> dict[str, Any]: def _to_abstract_repr(self) -> dict[str, Any]: defaults = get_dataclass_defaults(fields(self)) params = self._params() - for p in ALWAYS_OPTIONAL_PARAMS: - if params[p] == defaults[p]: + for p in OPTIONAL_IN_ABSTR_REPR: + if p in params and params[p] == defaults[p]: params.pop(p, None) # Delete parameters of PARAMS_WITH_ABSTR_REPR in params for p in PARAMS_WITH_ABSTR_REPR: @@ -500,7 +507,15 @@ class Device(BaseDevice): Attributes: name: The name of the device. dimensions: Whether it supports 2D or 3D arrays. - rybderg_level : The value of the principal quantum number :math:`n` + channel_objects: The Channel subclass instances specifying each + channel in the device. + channel_ids: Custom IDs for each channel object. When defined, + an ID must be given for each channel. If not defined, the IDs are + generated internally based on the channels' names and addressing. + dmm_objects: The DMM subclass instances specifying each channel in the + device. They are referenced by their order in the list, with the ID + "dmm_[index in dmm_objects]". + rydberg_level: The value of the principal quantum number :math:`n` when the Rydberg level used is of the form :math:`|nS_{1/2}, m_j = +1/2\rangle`. max_atom_num: Maximum number of atoms supported in an array. @@ -522,15 +537,22 @@ class Device(BaseDevice): default_noise_model: An optional noise model characterizing the default noise of the device. Can be used by emulator backends that support noise. + requires_layout: Whether the register used in the sequence must be + created from a register layout. Only enforced in QPU execution. pre_calibrated_layouts: RegisterLayout instances that are already available on the Device. + accepts_new_layouts: Whether registers built from register layouts + that are not already calibrated are accepted. Only enforced in + QPU execution. """ max_atom_num: int max_radial_distance: int + requires_layout: bool = True pre_calibrated_layouts: tuple[RegisterLayout, ...] = field( default_factory=tuple ) + accepts_new_layouts: bool = True def __post_init__(self) -> None: super().__post_init__() @@ -701,7 +723,15 @@ class VirtualDevice(BaseDevice): Attributes: name: The name of the device. dimensions: Whether it supports 2D or 3D arrays. - rybderg_level : The value of the principal quantum number :math:`n` + channel_objects: The Channel subclass instances specifying each + channel in the device. + channel_ids: Custom IDs for each channel object. When defined, + an ID must be given for each channel. If not defined, the IDs are + generated internally based on the channels' names and addressing. + dmm_objects: The DMM subclass instances specifying each channel in the + device. They are referenced by their order in the list, with the ID + "dmm_[index in dmm_objects]". + rydberg_level: The value of the principal quantum number :math:`n` when the Rydberg level used is of the form :math:`|nS_{1/2}, m_j = +1/2\rangle`. max_atom_num: Maximum number of atoms supported in an array. @@ -723,6 +753,8 @@ class VirtualDevice(BaseDevice): default_noise_model: An optional noise model characterizing the default noise of the device. Can be used by emulator backends that support noise. + requires_layout: Whether the register used in the sequence must be + created from a register layout. Only enforced in QPU execution. reusable_channels: Whether each channel can be declared multiple times on the same pulse sequence. """ diff --git a/tests/test_abstract_repr.py b/tests/test_abstract_repr.py index a9cb5b698..64075c8fb 100644 --- a/tests/test_abstract_repr.py +++ b/tests/test_abstract_repr.py @@ -307,9 +307,18 @@ def check_error_raised( ) assert isinstance(prev_err.__cause__, ValueError) - @pytest.mark.parametrize("field", ["max_sequence_duration", "max_runs"]) - def test_optional_device_fields(self, field): - device = replace(MockDevice, **{field: 1000}) + @pytest.mark.parametrize( + "og_device, field, value", + [ + (MockDevice, "max_sequence_duration", 1000), + (MockDevice, "max_runs", 100), + (MockDevice, "requires_layout", True), + (AnalogDevice, "requires_layout", False), + (AnalogDevice, "accepts_new_layouts", False), + ], + ) + def test_optional_device_fields(self, og_device, field, value): + device = replace(og_device, **{field: value}) dev_str = device.to_abstract_repr() assert device == deserialize_device(dev_str) diff --git a/tests/test_devices.py b/tests/test_devices.py index f47a31edc..5264d0b65 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -419,6 +419,7 @@ def test_convert_to_virtual(): ).to_virtual() == VirtualDevice( supports_slm_mask=False, reusable_channels=False, + requires_layout=True, dmm_objects=(), **params, ) @@ -434,7 +435,8 @@ def test_device_params(): init_virtual_params = virtual_DigitalAnalogDevice._params(init_only=True) assert all_virtual_params == init_virtual_params assert set(all_params) - set(all_virtual_params) == { - "pre_calibrated_layouts" + "pre_calibrated_layouts", + "accepts_new_layouts", }