Skip to content

Commit

Permalink
Update displayed device specs (#763)
Browse files Browse the repository at this point in the history
* Add all missing specs

* Add property specs

* Move _specs to BaseDevice

In this way, _specs and print_specs can also be called for virtual
devices.

* Fix syntax for compatibility with older Python

* Fix style

* Add missing docstring

* Fix dosctring style

* Improve specs method

* Update to fix mypy errors

* Fix mypy error

* Add tests for BaseDevice.specs property

* Fix import order

* Various minor improvements

One change is to use a string instead of joining elements of a list to
get the final string. The reason is that lists were cumbersome to use
when there were conditional statements.

* Split _specs method in different methods

Create one _specs method for each sections (register, layout, device,
channels). The layout section is defined only in Device, such that it is
not displayed for VirtualDevice.

This commit also goes back to using lists for storing the lines.

* Remove line

* Fix typo in strings

* Return list[str] instead of str for specs blocks

Also move texts for layout to BaseDevice, since virtual devices can have
some layouts properties.

---------

Co-authored-by: Henrique Silvério <[email protected]>
  • Loading branch information
HaroldErbin and HGSilveri authored Dec 16, 2024
1 parent 7960737 commit 091726a
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 74 deletions.
233 changes: 159 additions & 74 deletions pulser-core/pulser/devices/_device_datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from collections import Counter
from collections.abc import Mapping
from dataclasses import dataclass, field, fields
from typing import Any, Literal, cast, get_args
from typing import Any, Callable, Literal, cast, get_args

import numpy as np
from scipy.spatial.distance import squareform
Expand Down Expand Up @@ -586,6 +586,154 @@ def to_abstract_repr(self) -> str:
validate_abstract_repr(abstr_dev_str, "device")
return abstr_dev_str

def print_specs(self) -> None:
"""Prints the device specifications."""
title = f"{self.name} Specifications"
header = ["-" * len(title), title, "-" * len(title)]
print("\n".join(header))
print(self._specs())

@property
def specs(self) -> str:
"""Text summarizing the specifications of the device."""
return self._specs(for_docs=False)

def _param_yes_no(self, param: Any) -> str:
return "Yes" if param is True else "No"

def _param_check_none(self, param: Any) -> Callable[[str], str]:
def empty_str_if_none(line: str) -> str:
if param is None:
return ""
else:
return line.format(param)

return empty_str_if_none

def _register_lines(self) -> list[str]:

register_lines = [
"\nRegister parameters:",
f" - Dimensions: {self.dimensions}D",
f" - Rydberg level: {self.rydberg_level}",
self._param_check_none(self.max_atom_num)(
" - Maximum number of atoms: {}"
),
self._param_check_none(self.max_radial_distance)(
" - Maximum distance from origin: {} µm"
),
" - Minimum distance between neighbouring atoms: "
+ f"{self.min_atom_distance} μm",
f" - SLM Mask: {self._param_yes_no(self.supports_slm_mask)}",
]

return [line for line in register_lines if line != ""]

def _layout_lines(self) -> list[str]:

layout_lines = [
"\nLayout parameters:",
f" - Requires layout: {self._param_yes_no(self.requires_layout)}",
f" - Minimal number of traps: {self.min_layout_traps}",
self._param_check_none(self.max_layout_traps)(
" - Maximal number of traps: {}"
),
f" - Maximum layout filling fraction: {self.max_layout_filling}",
]

return [line for line in layout_lines if line != ""]

def _device_lines(self) -> list[str]:

device_lines = [
"\nDevice parameters:",
self._param_check_none(self.max_runs)(
" - Maximum number of runs: {}"
),
self._param_check_none(self.max_sequence_duration)(
" - Maximum sequence duration: {} ns",
),
" - Channels can be reused: "
+ self._param_yes_no(self.reusable_channels),
f" - Supported bases: {', '.join(self.supported_bases)}",
f" - Supported states: {', '.join(self.supported_states)}",
self._param_check_none(self.interaction_coeff)(
" - Ising interaction coefficient: {}",
),
self._param_check_none(self.interaction_coeff_xy)(
" - XY interaction coefficient: {}",
),
self._param_check_none(self.default_noise_model)(
" - Default noise model: {}",
),
]

return [line for line in device_lines if line != ""]

def _channel_lines(self, for_docs: bool = False) -> list[str]:

ch_lines = ["\nChannels:"]
for name, ch in {**self.channels, **self.dmm_channels}.items():
if for_docs:
max_amp = "None"
if ch.max_abs_detuning is not None:
max_amp = f"{float(cast(float, ch.max_amp)):.4g} rad/µs"

max_abs_detuning = "None"
if ch.max_abs_detuning is not None:
max_abs_detuning = (
f"{float(ch.max_abs_detuning):.4g} rad/µs"
)

bottom_detuning = "None"
if isinstance(ch, DMM) and ch.bottom_detuning is not None:
bottom_detuning = f"{float(ch.bottom_detuning):.4g} rad/µs"

ch_lines += [
f" - ID: '{name}'",
f"\t- Type: {ch.name} (*{ch.basis}* basis)",
f"\t- Addressing: {ch.addressing}",
("\t" + r"- Maximum :math:`\Omega`: " + max_amp),
(
(
"\t"
+ r"- Maximum :math:`|\delta|`: "
+ max_abs_detuning
)
if not isinstance(ch, DMM)
else (
"\t"
+ r"- Bottom :math:`|\delta|`: "
+ bottom_detuning
)
),
f"\t- Minimum average amplitude: {ch.min_avg_amp} rad/µs",
]
if ch.addressing == "Local":
ch_lines += [
"\t- Minimum time between retargets: "
f"{ch.min_retarget_interval} ns",
f"\t- Fixed retarget time: {ch.fixed_retarget_t} ns",
f"\t- Maximum simultaneous targets: {ch.max_targets}",
]
ch_lines += [
f"\t- Clock period: {ch.clock_period} ns",
f"\t- Minimum instruction duration: {ch.min_duration} ns",
]
else:
ch_lines.append(f" - '{name}': {ch!r}")

return [line for line in ch_lines if line != ""]

def _specs(self, for_docs: bool = False) -> str:

return "\n".join(
self._register_lines()
+ self._layout_lines()
+ self._device_lines()
+ self._channel_lines(for_docs=for_docs)
)


@dataclass(frozen=True, repr=False)
class Device(BaseDevice):
Expand Down Expand Up @@ -725,79 +873,6 @@ def to_virtual(self) -> VirtualDevice:
del params[param]
return VirtualDevice(**params)

def print_specs(self) -> None:
"""Prints the device specifications."""
title = f"{self.name} Specifications"
header = ["-" * len(title), title, "-" * len(title)]
print("\n".join(header))
print(self._specs())

def _specs(self, for_docs: bool = False) -> str:
lines = [
"\nRegister parameters:",
f" - Dimensions: {self.dimensions}D",
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'}",
]

if self.max_sequence_duration is not None:
lines.append(
" - Maximum sequence duration: "
f"{self.max_sequence_duration} ns"
)

ch_lines = ["\nChannels:"]
for name, ch in {**self.channels, **self.dmm_channels}.items():
if for_docs:
ch_lines += [
f" - ID: '{name}'",
f"\t- Type: {ch.name} (*{ch.basis}* basis)",
f"\t- Addressing: {ch.addressing}",
(
"\t"
+ r"- Maximum :math:`\Omega`:"
+ f" {float(cast(float, ch.max_amp)):.4g} rad/µs"
),
(
(
"\t"
+ r"- Maximum :math:`|\delta|`:"
+ f" {float(cast(float, ch.max_abs_detuning)):.4g}"
+ " rad/µs"
)
if not isinstance(ch, DMM)
else (
"\t"
+ r"- Bottom :math:`|\delta|`:"
+ f" {float(cast(float, ch.bottom_detuning)):.4g}"
+ " rad/µs"
)
),
f"\t- Minimum average amplitude: {ch.min_avg_amp} rad/µs",
]
if ch.addressing == "Local":
ch_lines += [
"\t- Minimum time between retargets: "
f"{ch.min_retarget_interval} ns",
f"\t- Fixed retarget time: {ch.fixed_retarget_t} ns",
f"\t- Maximum simultaneous targets: {ch.max_targets}",
]
ch_lines += [
f"\t- Clock period: {ch.clock_period} ns",
f"\t- Minimum instruction duration: {ch.min_duration} ns",
]
else:
ch_lines.append(f" - '{name}': {ch!r}")

return "\n".join(lines + ch_lines)

def _to_dict(self) -> dict[str, Any]:
return obj_to_dict(
self, _build=False, _module="pulser.devices", _name=self.name
Expand Down Expand Up @@ -835,6 +910,16 @@ def from_abstract_repr(obj_str: str) -> Device:
)
return device

def _layout_lines(self) -> list[str]:
layout_lines = super()._layout_lines()
layout_lines.insert(
2,
" - Accepts new layout: "
+ self._param_yes_no(self.accepts_new_layouts),
)

return layout_lines


@dataclass(frozen=True)
class VirtualDevice(BaseDevice):
Expand Down
80 changes: 80 additions & 0 deletions tests/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from pulser.channels import Microwave, Raman, Rydberg
from pulser.channels.dmm import DMM
from pulser.devices import (
AnalogDevice,
Device,
DigitalAnalogDevice,
MockDevice,
Expand Down Expand Up @@ -257,6 +258,85 @@ def test_tuple_conversion(test_params):
assert dev.channel_ids == ("custom_channel",)


@pytest.mark.parametrize(
"device", [MockDevice, AnalogDevice, DigitalAnalogDevice]
)
def test_device_specs(device):
def yes_no_fn(dev, attr, text):
if hasattr(dev, attr):
cond = getattr(dev, attr)
return f" - {text}: {'Yes' if cond else 'No'}\n"

return ""

def check_none_fn(dev, attr, text):
if hasattr(dev, attr):
var = getattr(dev, attr)
if var is not None:
return " - " + text.format(var) + "\n"

return ""

def specs(dev):
register_str = (
"\nRegister parameters:\n"
+ f" - Dimensions: {dev.dimensions}D\n"
+ f" - Rydberg level: {dev.rydberg_level}\n"
+ check_none_fn(dev, "max_atom_num", "Maximum number of atoms: {}")
+ check_none_fn(
dev,
"max_radial_distance",
"Maximum distance from origin: {} µm",
)
+ " - Minimum distance between neighbouring atoms: "
+ f"{dev.min_atom_distance} μm\n"
+ yes_no_fn(dev, "supports_slm_mask", "SLM Mask")
)

layout_str = (
"\nLayout parameters:\n"
+ yes_no_fn(dev, "requires_layout", "Requires layout")
+ (
""
if device is MockDevice
else yes_no_fn(
dev, "accepts_new_layouts", "Accepts new layout"
)
)
+ f" - Minimal number of traps: {dev.min_layout_traps}\n"
+ check_none_fn(
dev, "max_layout_traps", "Maximal number of traps: {}"
)
+ f" - Maximum layout filling fraction: {dev.max_layout_filling}\n"
)

device_str = (
"\nDevice parameters:\n"
+ check_none_fn(dev, "max_runs", "Maximum number of runs: {}")
+ check_none_fn(
dev,
"max_sequence_duration",
"Maximum sequence duration: {} ns",
)
+ yes_no_fn(dev, "reusable_channels", "Channels can be reused")
+ f" - Supported bases: {', '.join(dev.supported_bases)}\n"
+ f" - Supported states: {', '.join(dev.supported_states)}\n"
+ f" - Ising interaction coefficient: {dev.interaction_coeff}\n"
+ check_none_fn(
dev, "interaction_coeff_xy", "XY interaction coefficient: {}"
)
)

channel_str = "\nChannels:\n" + "\n".join(
f" - '{name}': {ch!r}"
for name, ch in {**dev.channels, **dev.dmm_channels}.items()
)

return register_str + layout_str + device_str + channel_str

assert device.specs == specs(device)


def test_valid_devices():
for dev in pulser.devices._valid_devices:
assert dev.dimensions in (2, 3)
Expand Down

0 comments on commit 091726a

Please sign in to comment.