Skip to content

Commit

Permalink
Maximum Layout Filling Fraction (#436)
Browse files Browse the repository at this point in the history
* Introduces the max_layout_filling parameter

* Update the UTs

* Update the Register Layouts tutorial

* Update Device documentation

* Correct typo
  • Loading branch information
HGSilveri authored Dec 15, 2022
1 parent 0f2bb26 commit 07e17e7
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 77 deletions.
65 changes: 53 additions & 12 deletions pulser-core/pulser/devices/_device_datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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:",
]

Expand Down Expand Up @@ -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.
"""
Expand Down
7 changes: 3 additions & 4 deletions pulser-core/pulser/register/mappable_reg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 11 additions & 7 deletions pulser-core/pulser/register/register_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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."
)
Expand All @@ -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))]
)
Expand Down
22 changes: 9 additions & 13 deletions pulser-core/pulser/register/special_layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pulser-core/pulser/sequence/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
46 changes: 40 additions & 6 deletions tests/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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))
Expand All @@ -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,
Expand All @@ -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(
Expand Down
Loading

0 comments on commit 07e17e7

Please sign in to comment.