From 091726ad9388a0f9707e87470d073ee7257a25b5 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Mon, 16 Dec 2024 09:58:14 -0500 Subject: [PATCH] Update displayed device specs (#763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 <29920212+HGSilveri@users.noreply.github.com> --- pulser-core/pulser/devices/_device_datacls.py | 233 ++++++++++++------ tests/test_devices.py | 80 ++++++ 2 files changed, 239 insertions(+), 74 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 203cb6cf6..01b0181d0 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -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 @@ -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): @@ -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 @@ -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): diff --git a/tests/test_devices.py b/tests/test_devices.py index 5c4017bc8..df473a895 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -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, @@ -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)