diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index f8516aff7..77650b6be 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -27,6 +27,7 @@ from pulser.devices.interaction_coefficients import c6_dict from pulser.json.utils import obj_to_dict from pulser.register.base_register import BaseRegister, QubitId +from pulser.register.mappable_reg import MappableRegister from pulser.register.register_layout import COORD_PRECISION, RegisterLayout if version_info[:2] >= (3, 8): # pragma: no cover @@ -63,6 +64,8 @@ class BaseDevice(ABC): different Rydberg states. Needed only if there is a Microwave channel in the device. If unsure, 3700.0 is a good default value. supports_slm_mask: Whether the device supports the SLM mask feature. + max_layout_filling: The largest fraction of a layout that can be filled + with atoms. """ name: str dimensions: DIMENSIONS @@ -73,6 +76,7 @@ class BaseDevice(ABC): max_radial_distance: Optional[int] interaction_coeff_xy: Optional[float] = None supports_slm_mask: bool = False + max_layout_filling: float = 0.5 reusable_channels: bool = field(default=False, init=False) def __post_init__(self) -> None: @@ -142,6 +146,13 @@ def type_check( type_check("supports_slm_mask", bool) type_check("reusable_channels", bool) + if not (0.0 < self.max_layout_filling <= 1.0): + raise ValueError( + "The maximum layout filling fraction must be " + "greater than 0. and less than or equal to 1., " + f"not {self.max_layout_filling}." + ) + def to_tuple(obj: tuple | list) -> tuple: if isinstance(obj, (tuple, list)): obj = tuple(to_tuple(el) for el in obj) @@ -222,6 +233,7 @@ def validate_register(self, register: BaseRegister) -> None: "The 'register' is associated with an incompatible " "register layout." ) + self.validate_layout_filling(register) def validate_layout(self, layout: RegisterLayout) -> None: """Checks if a register layout is compatible with this device. @@ -240,18 +252,41 @@ def validate_layout(self, layout: RegisterLayout) -> None: self._validate_coords(layout.traps_dict, kind="traps") - def _validate_atom_number( - self, coords: list[np.ndarray], kind: str + def validate_layout_filling( + self, register: BaseRegister | MappableRegister ) -> None: - max_number = cast(int, self.max_atom_num) * ( - 2 if kind == "traps" else 1 + """Checks if a register properly fills its layout. + + Args: + register: The register to validate. Must be created from a register + layout. + """ + if register.layout is None: + raise TypeError( + "'validate_layout_filling' can only be called for" + " registers with a register layout." + ) + n_qubits = len(register.qubit_ids) + max_qubits = int( + register.layout.number_of_traps * self.max_layout_filling ) - if len(coords) > max_number: + if n_qubits > max_qubits: + raise ValueError( + "Given the number of traps in the layout and the " + "device's maximum layout filling fraction, the given" + f" register has too many qubits ({n_qubits}). " + "On this device, this layout can hold at most " + f"{max_qubits} qubits." + ) + + def _validate_atom_number(self, coords: list[np.ndarray]) -> None: + max_atom_num = cast(int, self.max_atom_num) + if len(coords) > max_atom_num: raise ValueError( - f"The number of {kind} ({len(coords)})" + f"The number of atoms ({len(coords)})" " must be less than or equal to the maximum" - f" number of {kind} supported by this device" - f" ({max_number})." + f" number of atoms supported by this device" + f" ({max_atom_num})." ) def _validate_atom_distance( @@ -310,11 +345,11 @@ def _validate_coords( ) -> None: ids = list(coords_dict.keys()) coords = list(coords_dict.values()) - if not ( + if kind == "atoms" and not ( "max_atom_num" in self._optional_parameters and self.max_atom_num is None ): - self._validate_atom_number(coords, kind) + self._validate_atom_number(coords) self._validate_atom_distance(ids, coords, kind) if not ( "max_radial_distance" in self._optional_parameters @@ -349,6 +384,8 @@ class Device(BaseDevice): which sets the van der Waals interaction strength between atoms in different Rydberg states. supports_slm_mask: Whether the device supports the SLM mask feature. + max_layout_filling: The largest fraction of a layout that can be filled + with atoms. pre_calibrated_layouts: RegisterLayout instances that are already available on the Device. """ @@ -423,15 +460,17 @@ def print_specs(self) -> None: def _specs(self, for_docs: bool = False) -> str: lines = [ - "\nRegister requirements:", + "\nRegister parameters:", f" - Dimensions: {self.dimensions}D", - rf" - Rydberg level: {self.rydberg_level}", + f" - Rydberg level: {self.rydberg_level}", f" - Maximum number of atoms: {self.max_atom_num}", f" - Maximum distance from origin: {self.max_radial_distance} μm", ( " - Minimum distance between neighbouring atoms: " f"{self.min_atom_distance} μm" ), + f" - Maximum layout filling fraction: {self.max_layout_filling}", + f" - SLM Mask: {'Yes' if self.supports_slm_mask else 'No'}", "\nChannels:", ] @@ -499,6 +538,8 @@ class VirtualDevice(BaseDevice): which sets the van der Waals interaction strength between atoms in different Rydberg states. supports_slm_mask: Whether the device supports the SLM mask feature. + max_layout_filling: The largest fraction of a layout that can be filled + with atoms. reusable_channels: Whether each channel can be declared multiple times on the same pulse sequence. """ diff --git a/pulser-core/pulser/register/mappable_reg.py b/pulser-core/pulser/register/mappable_reg.py index 4a44b6b42..c3eb22697 100644 --- a/pulser-core/pulser/register/mappable_reg.py +++ b/pulser-core/pulser/register/mappable_reg.py @@ -38,11 +38,10 @@ class MappableRegister: def __init__(self, register_layout: RegisterLayout, *qubit_ids: QubitId): """Initializes the mappable register.""" self._layout = register_layout - if len(qubit_ids) > self._layout.max_atom_num: + if len(qubit_ids) > self._layout.number_of_traps: raise ValueError( - "The number of required traps is greater than the maximum " - "number of qubits allowed for this layout " - f"({self._layout.max_atom_num})." + "The number of required qubits is greater than the number of " + f"traps in this layout ({self._layout.number_of_traps})." ) self._qubit_ids = qubit_ids diff --git a/pulser-core/pulser/register/register_layout.py b/pulser-core/pulser/register/register_layout.py index d879be285..2f44b4dff 100644 --- a/pulser-core/pulser/register/register_layout.py +++ b/pulser-core/pulser/register/register_layout.py @@ -20,6 +20,7 @@ from hashlib import sha256 from sys import version_info from typing import Any, Optional, cast +from warnings import warn import matplotlib.pyplot as plt import numpy as np @@ -112,7 +113,15 @@ def number_of_traps(self) -> int: @property def max_atom_num(self) -> int: """Maximum number of atoms that can be trapped to form a Register.""" - return self.number_of_traps // 2 + warn( + "'RegisterLayout.max_atom_num' is deprecated and will be removed" + " in version 0.9.0.\n" + "It is now the same as 'RegisterLayout.number_of_traps' and " + "should be replaced accordingly.", + DeprecationWarning, + stacklevel=2, + ) + return self.number_of_traps @property def dimensionality(self) -> int: @@ -162,6 +171,7 @@ def define_register( raise ValueError("Every 'trap_id' must be a unique integer.") if not trap_ids_set.issubset(self.traps_dict): + # This check makes it redundant to check # qubits <= # traps raise ValueError( "All 'trap_ids' must correspond to the ID of a trap." ) @@ -177,12 +187,6 @@ def define_register( f"provided 'trap_ids' ({len(trap_ids)})." ) - if len(trap_ids) > self.max_atom_num: - raise ValueError( - "The number of required traps is greater than the maximum " - "number of qubits allowed for this layout " - f"({self.max_atom_num})." - ) ids = ( qubit_ids if qubit_ids else [f"q{i}" for i in range(len(trap_ids))] ) diff --git a/pulser-core/pulser/register/special_layouts.py b/pulser-core/pulser/register/special_layouts.py index b38479dc5..4fff347c9 100644 --- a/pulser-core/pulser/register/special_layouts.py +++ b/pulser-core/pulser/register/special_layouts.py @@ -78,14 +78,9 @@ def rectangular_register( Returns: The register instance created from this layout. """ - if rows * columns > self.max_atom_num: - raise ValueError( - f"A '{rows} x {columns}' array has more atoms than those " - f"available in this SquareLatticeLayout ({self.max_atom_num})." - ) if rows > self._rows or columns > self._columns: raise ValueError( - f"A '{rows} x {columns}' array doesn't fit a " + f"A '{rows}x{columns}' array doesn't fit a " f"{self._rows}x{self._columns} SquareLatticeLayout." ) points = patterns.square_rect(rows, columns) * self._spacing @@ -127,10 +122,11 @@ def hexagonal_register(self, n_atoms: int, prefix: str = "q") -> Register: Returns: The register instance created from this layout. """ - if n_atoms > self.max_atom_num: + if n_atoms > self.number_of_traps: raise ValueError( - f"This RegisterLayout can hold at most {self.max_atom_num} " - f"atoms, not '{n_atoms}'." + f"The desired register has more atoms ({n_atoms}) than there" + " are traps in this TriangularLatticeLayout" + f" ({self.number_of_traps})." ) points = patterns.triangular_hex(n_atoms) * self._spacing trap_ids = self.get_traps_from_coordinates(*points) @@ -154,11 +150,11 @@ def rectangular_register( Returns: The register instance created from this layout. """ - if rows * atoms_per_row > self.max_atom_num: + if rows * atoms_per_row > self.number_of_traps: raise ValueError( - f"A '{rows} x {atoms_per_row}' rectangular subset of a " - "triangular lattice has more atoms than those available in " - f"this TriangularLatticeLayout ({self.max_atom_num})." + f"A '{rows}x{atoms_per_row}' rectangular subset of a " + "triangular lattice has more atoms than there are traps in " + f"this TriangularLatticeLayout ({self.number_of_traps})." ) points = patterns.triangular_rect(rows, atoms_per_row) * self._spacing trap_ids = self.get_traps_from_coordinates(*points) diff --git a/pulser-core/pulser/sequence/sequence.py b/pulser-core/pulser/sequence/sequence.py index d4eee9d56..f7fcf8188 100644 --- a/pulser-core/pulser/sequence/sequence.py +++ b/pulser-core/pulser/sequence/sequence.py @@ -115,6 +115,7 @@ def __init__( # Checks if register is compatible with the device if isinstance(register, MappableRegister): device.validate_layout(register.layout) + device.validate_layout_filling(register) else: device.validate_register(register) diff --git a/tests/test_devices.py b/tests/test_devices.py index 3a4fff192..5c21d8895 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -92,6 +92,12 @@ def test_post_init_type_checks(test_params, param, value, msg): ), ("max_atom_num", 0, None), ("max_radial_distance", 0, None), + ( + "max_layout_filling", + 0.0, + "maximum layout filling fraction must be greater than 0. and" + " less than or equal to 1.", + ), ], ) def test_post_init_value_errors(test_params, param, value, msg): @@ -133,6 +139,7 @@ def test_valid_devices(): assert dev.max_radial_distance > 10 assert dev.min_atom_distance > 0 assert dev.interaction_coeff > 0 + assert 0 < dev.max_layout_filling <= 1 assert isinstance(dev.channels, dict) with pytest.raises(FrozenInstanceError): dev.name = "something else" @@ -197,16 +204,13 @@ def test_validate_register(): with pytest.raises( ValueError, match="associated with an incompatible register layout" ): - tri_layout = TriangularLatticeLayout(201, 5) + tri_layout = TriangularLatticeLayout(200, 20) Chadoq2.validate_register(tri_layout.hexagonal_register(10)) Chadoq2.validate_register(Register.rectangle(5, 10, spacing=5)) def test_validate_layout(): - with pytest.raises(ValueError, match="The number of traps"): - Chadoq2.validate_layout(RegisterLayout(Register.square(20)._coords)) - coords = [(100, 0), (-100, 0)] with pytest.raises(TypeError): Chadoq2.validate_layout(Register.from_coordinates(coords)) @@ -233,8 +237,38 @@ def test_validate_layout(): Chadoq2.validate_layout(valid_tri_layout) +@pytest.mark.parametrize( + "register", + [ + TriangularLatticeLayout(100, 5).hexagonal_register(80), + TriangularLatticeLayout(100, 5).make_mappable_register(51), + ], +) +def test_layout_filling(register): + assert Chadoq2.max_layout_filling == 0.5 + assert register.layout.number_of_traps == 100 + with pytest.raises( + ValueError, + match=re.escape( + "the given register has too many qubits " + f"({len(register.qubit_ids)}). " + "On this device, this layout can hold at most 50 qubits." + ), + ): + Chadoq2.validate_layout_filling(register) + + +def test_layout_filling_fail(): + with pytest.raises( + TypeError, + match="'validate_layout_filling' can only be called for" + " registers with a register layout.", + ): + Chadoq2.validate_layout_filling(Register.square(5)) + + def test_calibrated_layouts(): - with pytest.raises(ValueError, match="The number of traps"): + with pytest.raises(ValueError, match="The minimal distance between traps"): Device( name="TestDevice", dimensions=2, @@ -243,7 +277,7 @@ def test_calibrated_layouts(): max_radial_distance=50, min_atom_distance=4, _channels=(), - pre_calibrated_layouts=(TriangularLatticeLayout(201, 5),), + pre_calibrated_layouts=(TriangularLatticeLayout(201, 3),), ) TestDevice = Device( diff --git a/tests/test_register_layout.py b/tests/test_register_layout.py index fe9de46a2..dc7306369 100644 --- a/tests/test_register_layout.py +++ b/tests/test_register_layout.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re from hashlib import sha256 from unittest.mock import patch import numpy as np import pytest +import pulser from pulser.register import Register, Register3D from pulser.register.register_layout import RegisterLayout from pulser.register.special_layouts import ( @@ -50,11 +52,14 @@ def test_creation(layout, layout3d): layout3d.coords == [[0, 0, 0], [0, 1, 0], [1, 0, 1], [1, 1, 1]] ) assert layout.number_of_traps == 4 - assert layout.max_atom_num == 2 assert layout.dimensionality == 2 for i, coord in enumerate(layout.coords): assert np.all(layout.traps_dict[i] == coord) + with pytest.warns(DeprecationWarning): + assert pulser.__version__ < "0.9" + assert layout.max_atom_num == layout.number_of_traps + def test_slug(layout, layout3d): assert layout.slug == "2DLayout" @@ -76,11 +81,6 @@ def test_register_definition(layout, layout3d): with pytest.raises(ValueError, match="must have the same size"): layout.define_register(0, 1, qubit_ids=["a", "b", "c"]) - with pytest.raises( - ValueError, match="greater than the maximum number of qubits" - ): - layout.define_register(0, 1, 3) - assert layout.define_register(0, 1) == Register.from_coordinates( [[0, 0], [0, 1]], prefix="q", center=False ) @@ -162,15 +162,13 @@ def test_square_lattice_layout(): assert square.square_register(4) != Register.square( 4, spacing=5, prefix="q" ) - with pytest.raises( - ValueError, match="'6 x 6' array has more atoms than those available" - ): - square.square_register(6) + with pytest.raises(ValueError, match="'8x8' array doesn't fit"): + square.square_register(8) assert square.rectangular_register(3, 7, prefix="r") == Register.rectangle( 3, 7, spacing=5, prefix="r" ) - with pytest.raises(ValueError, match="'10 x 3' array doesn't fit"): + with pytest.raises(ValueError, match="'10x3' array doesn't fit"): square.rectangular_register(10, 3) @@ -181,28 +179,34 @@ def test_triangular_lattice_layout(): assert tri.hexagonal_register(19) == Register.hexagon( 2, spacing=5, prefix="q" ) - with pytest.raises(ValueError, match="hold at most 25 atoms, not '26'"): - tri.hexagonal_register(26) + with pytest.raises( + ValueError, + match=re.escape( + "The desired register has more atoms (51) than there" + " are traps in this TriangularLatticeLayout (50)" + ), + ): + tri.hexagonal_register(51) with pytest.raises( - ValueError, match="has more atoms than those available" + ValueError, match="has more atoms than there are traps" ): - tri.rectangular_register(7, 4) + tri.rectangular_register(7, 8) # Case where the register doesn't fit with pytest.raises(ValueError, match="not a part of the RegisterLayout"): tri.rectangular_register(8, 3) # But this fits fine, though off-centered with the Register default - tri.rectangular_register(5, 5) != Register.triangular_lattice( + assert tri.rectangular_register(5, 5) != Register.triangular_lattice( 5, 5, spacing=5, prefix="q" ) def test_mappable_register_creation(): tri = TriangularLatticeLayout(50, 5) - with pytest.raises(ValueError, match="greater than the maximum"): - tri.make_mappable_register(26) + with pytest.raises(ValueError, match="greater than the number of traps"): + tri.make_mappable_register(51) mapp_reg = tri.make_mappable_register(5) assert mapp_reg.qubit_ids == ("q0", "q1", "q2", "q3", "q4") diff --git a/tutorials/advanced_features/Register Layouts.ipynb b/tutorials/advanced_features/Register Layouts.ipynb index a63644e49..59ddcb643 100644 --- a/tutorials/advanced_features/Register Layouts.ipynb +++ b/tutorials/advanced_features/Register Layouts.ipynb @@ -110,22 +110,6 @@ "print(\"The unique ID layout:\", repr(layout))" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, `RegisterLayout.max_atom_num` fixes the maximum number of atoms it can hold (for now, this value is always equal to half the number of traps):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Maximum number of atoms supported:\", layout.max_atom_num)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -363,6 +347,7 @@ " rydberg_level=70,\n", " max_atom_num=100,\n", " max_radial_distance=50,\n", + " max_layout_filling=0.4,\n", " min_atom_distance=4,\n", " _channels=(\n", " (\"rydberg_global\", Rydberg.Global(2 * np.pi * 20, 2 * np.pi * 2.5)),\n", @@ -406,7 +391,7 @@ "layout = TestDevice.calibrated_register_layouts[\n", " \"SquareLatticeLayout(10x10, 4µm)\"\n", "]\n", - "reg = layout.square_register(7)\n", + "reg = layout.square_register(6)\n", "seq = Sequence(reg, TestDevice)" ] }, @@ -454,6 +439,40 @@ " print(e)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Maximum Layout Filling Fraction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Through the `Device.max_layout_filling`, a device also specifies how much a layout can be filled. Although the default value is 0.5, some devices might have slightly higher or lower values. \n", + "\n", + "In the case of our `TestDevice`, we specified the maximum layout filling fraction to be 0.4 . This means that we can use up to 40% of a `RegisterLayout` to form our register.\n", + "\n", + "Let us see what would happen if we were to go over this value (e.g. by making a register of 49 atoms from a layout with 100 atoms):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "layout = TestDevice.calibrated_register_layouts[\n", + " \"SquareLatticeLayout(10x10, 4µm)\"\n", + "]\n", + "too_big_reg = layout.square_register(7)\n", + "try:\n", + " seq = Sequence(too_big_reg, TestDevice)\n", + "except ValueError as e:\n", + " print(e)" + ] + }, { "cell_type": "markdown", "metadata": {},