From a8cd5f81afb01a641f31da3d570842a62520e9a1 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 11:53:40 +0100 Subject: [PATCH 01/16] Add all missing specs --- pulser-core/pulser/devices/_device_datacls.py | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 3a6872a3..ba62571a 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -746,6 +746,40 @@ def _specs(self, for_docs: bool = False) -> str: f"{self.max_sequence_duration} ns" ) + device_lines = [ + "\nDevice parameters:", + ] + if self.max_runs is not None: + device_lines.append( + f" - Maximum number of runs: {self.max_runs}" + ) + device_lines += [ + f" - Channels can be reused: {'Yes' if self.reusable_channels else 'No'}", + f" - Supported bases: {", ".join(self.supported_bases)}", + f" - Supported states: {", ".join(self.supported_states)}", + ] + if self.interaction_coeff is not None: + device_lines.append( + f" - Ising interaction coefficient: {self.interaction_coeff}" + ) + if self.interaction_coeff_xy is not None: + device_lines.append( + f" - XY interaction coefficient: {self.interaction_coeff_xy}" + ) + + if self.default_noise_model is not None: + device_lines.append( + f" - Default noise model: {self.default_noise_model}" + ) + + layout_lines = [ + "\nLayout parameters:", + f" - Requires layout: {'Yes' if self.requires_layout else 'No'}", + f" - Accepts new layout: {'Yes' if self.accepts_new_layouts else 'No'}", + f" - Minimal number of traps: {self.min_layout_traps}", + f" - Maximal number of traps: {self.max_layout_traps}" + ] + ch_lines = ["\nChannels:"] for name, ch in {**self.channels, **self.dmm_channels}.items(): if for_docs: @@ -789,7 +823,7 @@ def _specs(self, for_docs: bool = False) -> str: else: ch_lines.append(f" - '{name}': {ch!r}") - return "\n".join(lines + ch_lines) + return "\n".join(lines + device_lines + layout_lines + ch_lines) def _to_dict(self) -> dict[str, Any]: return obj_to_dict( From eaba9919a62832825c78ecfc60bc53ee2021f932 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 11:53:57 +0100 Subject: [PATCH 02/16] Add property specs --- pulser-core/pulser/devices/_device_datacls.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index ba62571a..e84916f2 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -725,6 +725,10 @@ def print_specs(self) -> None: print("\n".join(header)) print(self._specs()) + @property + def specs(self) -> str: + return self._specs(for_docs=True) + def _specs(self, for_docs: bool = False) -> str: lines = [ "\nRegister parameters:", From 10c3a672212b41c07f77990f72ffc2017f8f8857 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 12:18:15 +0100 Subject: [PATCH 03/16] Move _specs to BaseDevice In this way, _specs and print_specs can also be called for virtual devices. --- pulser-core/pulser/devices/_device_datacls.py | 235 +++++++++--------- 1 file changed, 124 insertions(+), 111 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index e84916f2..6221537a 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -579,6 +579,130 @@ 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: + return self._specs(for_docs=True) + + 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" + ) + + device_lines = [ + "\nDevice parameters:", + ] + if self.max_runs is not None: + device_lines.append(f" - Maximum number of runs: {self.max_runs}") + device_lines += [ + f" - Channels can be reused: {'Yes' if self.reusable_channels else 'No'}", + f" - Supported bases: {", ".join(self.supported_bases)}", + f" - Supported states: {", ".join(self.supported_states)}", + ] + if self.interaction_coeff is not None: + device_lines.append( + f" - Ising interaction coefficient: {self.interaction_coeff}" + ) + if self.interaction_coeff_xy is not None: + device_lines.append( + f" - XY interaction coefficient: {self.interaction_coeff_xy}" + ) + + if self.default_noise_model is not None: + device_lines.append( + f" - Default noise model: {self.default_noise_model}" + ) + + layout_lines = [ + "\nLayout parameters:", + f" - Requires layout: {'Yes' if self.requires_layout else 'No'}", + ] + try: + layout_lines.append( + f" - Accepts new layout: {'Yes' if self.accepts_new_layouts else 'No'}" + ) + except AttributeError: + pass + + layout_lines += [ + f" - Minimal number of traps: {self.min_layout_traps}", + f" - Maximal number of traps: {self.max_layout_traps}", + ] + + ch_lines = ["\nChannels:"] + for name, ch in {**self.channels, **self.dmm_channels}.items(): + if for_docs: + try: + max_amp = f"{float(cast(float, ch.max_amp)):.4g} rad/µs" + except (AttributeError, TypeError): + max_amp = "None" + try: + max_abs_detuning = f"{float(cast(float, ch.max_abs_detuning)):.4g} rad/µs" + except (AttributeError, TypeError): + max_abs_detuning = "None" + try: + bottom_detuning = f"{float(cast(float, ch.bottom_detuning)):.4g} rad/µs" + except (AttributeError, TypeError): + bottom_detuning = "None" + + 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 "\n".join(lines + device_lines + layout_lines + ch_lines) + @dataclass(frozen=True, repr=False) class Device(BaseDevice): @@ -718,117 +842,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()) - - @property - def specs(self) -> str: - return self._specs(for_docs=True) - - 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" - ) - - device_lines = [ - "\nDevice parameters:", - ] - if self.max_runs is not None: - device_lines.append( - f" - Maximum number of runs: {self.max_runs}" - ) - device_lines += [ - f" - Channels can be reused: {'Yes' if self.reusable_channels else 'No'}", - f" - Supported bases: {", ".join(self.supported_bases)}", - f" - Supported states: {", ".join(self.supported_states)}", - ] - if self.interaction_coeff is not None: - device_lines.append( - f" - Ising interaction coefficient: {self.interaction_coeff}" - ) - if self.interaction_coeff_xy is not None: - device_lines.append( - f" - XY interaction coefficient: {self.interaction_coeff_xy}" - ) - - if self.default_noise_model is not None: - device_lines.append( - f" - Default noise model: {self.default_noise_model}" - ) - - layout_lines = [ - "\nLayout parameters:", - f" - Requires layout: {'Yes' if self.requires_layout else 'No'}", - f" - Accepts new layout: {'Yes' if self.accepts_new_layouts else 'No'}", - f" - Minimal number of traps: {self.min_layout_traps}", - f" - Maximal number of traps: {self.max_layout_traps}" - ] - - 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 + device_lines + layout_lines + ch_lines) - def _to_dict(self) -> dict[str, Any]: return obj_to_dict( self, _build=False, _module="pulser.devices", _name=self.name From cf48c7adcc1a7061bf403d013074b8c759402229 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 12:37:29 +0100 Subject: [PATCH 04/16] Fix syntax for compatibility with older Python --- pulser-core/pulser/devices/_device_datacls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 6221537a..91b97b8a 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -618,8 +618,8 @@ def _specs(self, for_docs: bool = False) -> str: device_lines.append(f" - Maximum number of runs: {self.max_runs}") device_lines += [ f" - Channels can be reused: {'Yes' if self.reusable_channels else 'No'}", - f" - Supported bases: {", ".join(self.supported_bases)}", - f" - Supported states: {", ".join(self.supported_states)}", + f" - Supported bases: {', '.join(self.supported_bases)}", + f" - Supported states: {', '.join(self.supported_states)}", ] if self.interaction_coeff is not None: device_lines.append( From 93278c8c29b07d94b54e4632c9ac8d4e6e70ddcf Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 12:49:33 +0100 Subject: [PATCH 05/16] Fix style --- pulser-core/pulser/devices/_device_datacls.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 91b97b8a..09c12eea 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -617,7 +617,11 @@ def _specs(self, for_docs: bool = False) -> str: if self.max_runs is not None: device_lines.append(f" - Maximum number of runs: {self.max_runs}") device_lines += [ - f" - Channels can be reused: {'Yes' if self.reusable_channels else 'No'}", + ( + " - Channels can be reused: " "Yes" + if self.reusable_channels + else "No" + ), f" - Supported bases: {', '.join(self.supported_bases)}", f" - Supported states: {', '.join(self.supported_states)}", ] @@ -639,12 +643,12 @@ def _specs(self, for_docs: bool = False) -> str: "\nLayout parameters:", f" - Requires layout: {'Yes' if self.requires_layout else 'No'}", ] - try: + if hasattr(self, "accepts_new_layouts"): layout_lines.append( - f" - Accepts new layout: {'Yes' if self.accepts_new_layouts else 'No'}" + " - Accepts new layout: " "Yes" + if self.accepts_new_layouts + else "No" ) - except AttributeError: - pass layout_lines += [ f" - Minimal number of traps: {self.min_layout_traps}", @@ -659,11 +663,15 @@ def _specs(self, for_docs: bool = False) -> str: except (AttributeError, TypeError): max_amp = "None" try: - max_abs_detuning = f"{float(cast(float, ch.max_abs_detuning)):.4g} rad/µs" + max_abs_detuning = ( + f"{float(cast(float, ch.max_abs_detuning)):.4g} rad/µs" + ) except (AttributeError, TypeError): max_abs_detuning = "None" try: - bottom_detuning = f"{float(cast(float, ch.bottom_detuning)):.4g} rad/µs" + bottom_detuning = ( + f"{float(cast(float, ch.bottom_detuning)):.4g} rad/µs" + ) except (AttributeError, TypeError): bottom_detuning = "None" From e0691d5f699e1636253b0935e8b54cb0724e7c6c Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 12:59:20 +0100 Subject: [PATCH 06/16] Add missing docstring --- pulser-core/pulser/devices/_device_datacls.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 09c12eea..7cb328e9 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -588,7 +588,11 @@ def print_specs(self) -> None: @property def specs(self) -> str: - return self._specs(for_docs=True) + """ + Text summarizing the specifications of the device. + """ + + return self._specs(for_docs=False) def _specs(self, for_docs: bool = False) -> str: lines = [ From 3138ad144268e6ee10e5d3e935f1efd23813b62a Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 13:02:07 +0100 Subject: [PATCH 07/16] Fix dosctring style --- pulser-core/pulser/devices/_device_datacls.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 7cb328e9..64b8472b 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -588,10 +588,7 @@ def print_specs(self) -> None: @property def specs(self) -> str: - """ - Text summarizing the specifications of the device. - """ - + """ Text summarizing the specifications of the device. """ return self._specs(for_docs=False) def _specs(self, for_docs: bool = False) -> str: From 892908c6792ece84e8ee9012394298aa80f360ec Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 13:36:42 +0100 Subject: [PATCH 08/16] Improve specs method --- pulser-core/pulser/devices/_device_datacls.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 64b8472b..9cc98259 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -588,7 +588,7 @@ def print_specs(self) -> None: @property def specs(self) -> str: - """ Text summarizing the specifications of the device. """ + """Text summarizing the specifications of the device.""" return self._specs(for_docs=False) def _specs(self, for_docs: bool = False) -> str: @@ -659,22 +659,23 @@ def _specs(self, for_docs: bool = False) -> str: ch_lines = ["\nChannels:"] for name, ch in {**self.channels, **self.dmm_channels}.items(): if for_docs: - try: + max_amp = "None" + if ch.max_abs_detuning is not None: max_amp = f"{float(cast(float, ch.max_amp)):.4g} rad/µs" - except (AttributeError, TypeError): - max_amp = "None" - try: + + max_abs_detuning = "None" + if ch.max_abs_detuning is not None: max_abs_detuning = ( f"{float(cast(float, ch.max_abs_detuning)):.4g} rad/µs" ) - except (AttributeError, TypeError): - max_abs_detuning = "None" - try: - bottom_detuning = ( - f"{float(cast(float, ch.bottom_detuning)):.4g} rad/µs" - ) - except (AttributeError, TypeError): - bottom_detuning = "None" + + bottom_detuning = "None" + if hasattr(ch, "bottom_detuning"): + if ch.bottom_detuning is not None: + bottom_detuning = ( + f"{float(cast(float, ch.bottom_detuning)):.4g}" + " rad/µs" + ) ch_lines += [ f" - ID: '{name}'", From d6717534f943c26346ae46a1fda372317daee9ae Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 15:43:14 +0100 Subject: [PATCH 09/16] Update to fix mypy errors --- pulser-core/pulser/devices/_device_datacls.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 9cc98259..a0d49b97 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -666,16 +666,12 @@ def _specs(self, for_docs: bool = False) -> str: max_abs_detuning = "None" if ch.max_abs_detuning is not None: max_abs_detuning = ( - f"{float(cast(float, ch.max_abs_detuning)):.4g} rad/µs" + f"{float(ch.max_abs_detuning):.4g} rad/µs" ) bottom_detuning = "None" - if hasattr(ch, "bottom_detuning"): - if ch.bottom_detuning is not None: - bottom_detuning = ( - f"{float(cast(float, ch.bottom_detuning)):.4g}" - " rad/µs" - ) + 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}'", From 62e9a09c21119a519e6343e82426a2443bb6660d Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Tue, 19 Nov 2024 16:12:18 +0100 Subject: [PATCH 10/16] Fix mypy error --- pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index 5cb8de9c..ec732056 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -238,6 +238,7 @@ def _query_job_progress( get_batch_fn = backoff_decorator(self._sdk_connection.get_batch) batch = get_batch_fn(id=batch_id) + assert isinstance(batch.sequence_builder, str) seq_builder = Sequence.from_abstract_repr(batch.sequence_builder) reg = seq_builder.get_register(include_mappable=True) all_qubit_ids = reg.qubit_ids From a70e20de656f50dc641669cae35b01f2dea86697 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Wed, 20 Nov 2024 12:54:37 +0100 Subject: [PATCH 11/16] Add tests for BaseDevice.specs property --- pulser-core/pulser/devices/_device_datacls.py | 21 +++---- tests/test_devices.py | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index a0d49b97..791b68cd 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -592,6 +592,9 @@ def specs(self) -> str: return self._specs(for_docs=False) def _specs(self, for_docs: bool = False) -> str: + def yes_no_fn(cond: bool) -> str: + return "Yes" if cond else "No" + lines = [ "\nRegister parameters:", f" - Dimensions: {self.dimensions}D", @@ -603,7 +606,7 @@ def _specs(self, for_docs: bool = False) -> str: 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'}", + f" - SLM Mask: {yes_no_fn(self.supports_slm_mask)}", ] if self.max_sequence_duration is not None: @@ -615,21 +618,20 @@ def _specs(self, for_docs: bool = False) -> str: device_lines = [ "\nDevice parameters:", ] + if self.max_runs is not None: device_lines.append(f" - Maximum number of runs: {self.max_runs}") device_lines += [ - ( - " - Channels can be reused: " "Yes" - if self.reusable_channels - else "No" - ), + f" - Channels can be reused: {yes_no_fn(self.reusable_channels)}", f" - Supported bases: {', '.join(self.supported_bases)}", f" - Supported states: {', '.join(self.supported_states)}", ] + if self.interaction_coeff is not None: device_lines.append( f" - Ising interaction coefficient: {self.interaction_coeff}" ) + if self.interaction_coeff_xy is not None: device_lines.append( f" - XY interaction coefficient: {self.interaction_coeff_xy}" @@ -642,13 +644,12 @@ def _specs(self, for_docs: bool = False) -> str: layout_lines = [ "\nLayout parameters:", - f" - Requires layout: {'Yes' if self.requires_layout else 'No'}", + f" - Requires layout: {yes_no_fn(self.requires_layout)}", ] + if hasattr(self, "accepts_new_layouts"): layout_lines.append( - " - Accepts new layout: " "Yes" - if self.accepts_new_layouts - else "No" + f" - Accepts new layout: {yes_no_fn(self.accepts_new_layouts)}" ) layout_lines += [ diff --git a/tests/test_devices.py b/tests/test_devices.py index 5c4017bc..35b59107 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -24,6 +24,7 @@ from pulser.channels.dmm import DMM from pulser.devices import ( Device, + AnalogDevice, DigitalAnalogDevice, MockDevice, VirtualDevice, @@ -257,6 +258,65 @@ def test_tuple_conversion(test_params): assert dev.channel_ids == ("custom_channel",) +def test_device_specs(): + 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): + return ( + "\nRegister parameters:\n" + + f" - Dimensions: {dev.dimensions}D\n" + + f" - Rydberg level: {dev.rydberg_level}\n" + + f" - Maximum number of atoms: {dev.max_atom_num}\n" + + " - Maximum distance from origin: " + + f"{dev.max_radial_distance} μm\n" + + " - Minimum distance between neighbouring atoms: " + + f"{dev.min_atom_distance} μm\n" + + f" - Maximum layout filling fraction: {dev.max_layout_filling}\n" + + yes_no_fn(dev, "supports_slm_mask", "SLM Mask") + + check_none_fn( + dev, + "max_sequence_duration", + "Maximum sequence duration: {} ns", + ) + + "\nDevice parameters:\n" + + check_none_fn(dev, "max_runs", "Maximum number of runs: {}") + + 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: {}" + ) + + "\nLayout parameters:\n" + + yes_no_fn(dev, "requires_layout", "Requires layout") + + yes_no_fn(dev, "accepts_new_layouts", "Accepts new layout") + + f" - Minimal number of traps: {dev.min_layout_traps}\n" + + f" - Maximal number of traps: {dev.max_layout_traps}\n" + + "\nChannels:\n" + + "\n".join( + f" - '{name}': {ch!r}" + for name, ch in {**dev.channels, **dev.dmm_channels}.items() + ) + ) + + assert MockDevice.specs == specs(MockDevice) + assert AnalogDevice.specs == specs(AnalogDevice) + assert DigitalAnalogDevice.specs == specs(DigitalAnalogDevice) + + def test_valid_devices(): for dev in pulser.devices._valid_devices: assert dev.dimensions in (2, 3) From d61c6cd2965f576eca260e6b9c8caa9685ecf15c Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Wed, 20 Nov 2024 13:07:01 +0100 Subject: [PATCH 12/16] Fix import order --- tests/test_devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_devices.py b/tests/test_devices.py index 35b59107..5f46d25a 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -23,8 +23,8 @@ from pulser.channels import Microwave, Raman, Rydberg from pulser.channels.dmm import DMM from pulser.devices import ( - Device, AnalogDevice, + Device, DigitalAnalogDevice, MockDevice, VirtualDevice, From 9bddf28070ccc46b6044023b7571ac72b3ab627c Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Wed, 20 Nov 2024 20:52:00 +0100 Subject: [PATCH 13/16] 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. --- pulser-core/pulser/devices/_device_datacls.py | 111 +++++++++--------- tests/test_devices.py | 36 +++--- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 791b68cd..81b80380 100644 --- a/pulser-core/pulser/devices/_device_datacls.py +++ b/pulser-core/pulser/devices/_device_datacls.py @@ -595,67 +595,70 @@ def _specs(self, for_docs: bool = False) -> str: def yes_no_fn(cond: bool) -> str: return "Yes" if cond else "No" - 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_no_fn(self.supports_slm_mask)}", - ] - - if self.max_sequence_duration is not None: - lines.append( - " - Maximum sequence duration: " - f"{self.max_sequence_duration} ns" + def check_none(var: Any, line: str) -> str: + if var is None: + return "" + else: + return line.format(var) + + reg_lines = ( + "\nRegister parameters:\n" + + f" - Dimensions: {self.dimensions}D\n" + + f" - Rydberg level: {self.rydberg_level}\n" + + check_none(self.max_atom_num, " - Maximum number of atoms: {}\n") + + check_none( + self.max_radial_distance, + " - Maximum distance from origin: {} µm\n", ) + + " - Minimum distance between neighbouring atoms: " + + f"{self.min_atom_distance} μm\n" + + f" - SLM Mask: {yes_no_fn(self.supports_slm_mask)}\n" + ) - device_lines = [ - "\nDevice parameters:", - ] - - if self.max_runs is not None: - device_lines.append(f" - Maximum number of runs: {self.max_runs}") - device_lines += [ - f" - Channels can be reused: {yes_no_fn(self.reusable_channels)}", - f" - Supported bases: {', '.join(self.supported_bases)}", - f" - Supported states: {', '.join(self.supported_states)}", - ] - - if self.interaction_coeff is not None: - device_lines.append( - f" - Ising interaction coefficient: {self.interaction_coeff}" - ) + layout_lines = ( + "\nLayout parameters:\n" + + f" - Requires layout: {yes_no_fn(self.requires_layout)}\n" + ) - if self.interaction_coeff_xy is not None: - device_lines.append( - f" - XY interaction coefficient: {self.interaction_coeff_xy}" + if hasattr(self, "accepts_new_layouts"): + layout_lines += ( + " - Accepts new layout: " + + f"{yes_no_fn(self.accepts_new_layouts)}\n" ) - if self.default_noise_model is not None: - device_lines.append( - f" - Default noise model: {self.default_noise_model}" + layout_lines += ( + f" - Minimal number of traps: {self.min_layout_traps}\n" + + check_none( + self.max_layout_traps, " - Maximal number of traps: {}\n" ) + + " - Maximum layout filling fraction: " + + f"{self.max_layout_filling}\n" + ) - layout_lines = [ - "\nLayout parameters:", - f" - Requires layout: {yes_no_fn(self.requires_layout)}", - ] - - if hasattr(self, "accepts_new_layouts"): - layout_lines.append( - f" - Accepts new layout: {yes_no_fn(self.accepts_new_layouts)}" + device_lines = ( + "\nDevice parameters:\n" + + check_none(self.max_runs, " - Maximum number of runs: {}\n") + + check_none( + self.max_sequence_duration, + " - Maximum sequence duration: {} ns\n", ) - - layout_lines += [ - f" - Minimal number of traps: {self.min_layout_traps}", - f" - Maximal number of traps: {self.max_layout_traps}", - ] + + " - Channels can be reused: " + + f"{yes_no_fn(self.reusable_channels)}\n" + + f" - Supported bases: {', '.join(self.supported_bases)}\n" + + f" - Supported states: {', '.join(self.supported_states)}\n" + + check_none( + self.interaction_coeff, + " - Ising interaction coefficient: {}\n", + ) + + check_none( + self.interaction_coeff_xy, + " - XY interaction coefficient: {}\n", + ) + + check_none( + self.default_noise_model, + " - Default noise model: {}\n", + ) + ) ch_lines = ["\nChannels:"] for name, ch in {**self.channels, **self.dmm_channels}.items(): @@ -708,7 +711,7 @@ def yes_no_fn(cond: bool) -> str: else: ch_lines.append(f" - '{name}': {ch!r}") - return "\n".join(lines + device_lines + layout_lines + ch_lines) + return reg_lines + layout_lines + device_lines + "\n".join(ch_lines) @dataclass(frozen=True, repr=False) diff --git a/tests/test_devices.py b/tests/test_devices.py index 5f46d25a..33448a8c 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -258,7 +258,10 @@ def test_tuple_conversion(test_params): assert dev.channel_ids == ("custom_channel",) -def test_device_specs(): +@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) @@ -279,20 +282,30 @@ def specs(dev): "\nRegister parameters:\n" + f" - Dimensions: {dev.dimensions}D\n" + f" - Rydberg level: {dev.rydberg_level}\n" - + f" - Maximum number of atoms: {dev.max_atom_num}\n" - + " - Maximum distance from origin: " - + f"{dev.max_radial_distance} μm\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" - + f" - Maximum layout filling fraction: {dev.max_layout_filling}\n" + yes_no_fn(dev, "supports_slm_mask", "SLM Mask") + + "\nLayout parameters:\n" + + yes_no_fn(dev, "requires_layout", "Requires layout") + + 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" + + "\nDevice parameters:\n" + + check_none_fn(dev, "max_runs", "Maximum number of runs: {}") + check_none_fn( dev, "max_sequence_duration", "Maximum sequence duration: {} ns", ) - + "\nDevice parameters:\n" - + check_none_fn(dev, "max_runs", "Maximum number of runs: {}") + 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" @@ -300,11 +313,6 @@ def specs(dev): + check_none_fn( dev, "interaction_coeff_xy", "XY interaction coefficient: {}" ) - + "\nLayout parameters:\n" - + yes_no_fn(dev, "requires_layout", "Requires layout") - + yes_no_fn(dev, "accepts_new_layouts", "Accepts new layout") - + f" - Minimal number of traps: {dev.min_layout_traps}\n" - + f" - Maximal number of traps: {dev.max_layout_traps}\n" + "\nChannels:\n" + "\n".join( f" - '{name}': {ch!r}" @@ -312,9 +320,7 @@ def specs(dev): ) ) - assert MockDevice.specs == specs(MockDevice) - assert AnalogDevice.specs == specs(AnalogDevice) - assert DigitalAnalogDevice.specs == specs(DigitalAnalogDevice) + assert device.specs == specs(device) def test_valid_devices(): From e173206b9fe46073f4bafbd8ebaafe47f0240aed Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Thu, 21 Nov 2024 19:56:34 +0100 Subject: [PATCH 14/16] 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. --- pulser-core/pulser/devices/_device_datacls.py | 162 ++++++++++-------- tests/test_devices.py | 17 +- 2 files changed, 111 insertions(+), 68 deletions(-) diff --git a/pulser-core/pulser/devices/_device_datacls.py b/pulser-core/pulser/devices/_device_datacls.py index 81b80380..f114afd4 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 @@ -591,74 +591,65 @@ def specs(self) -> str: """Text summarizing the specifications of the device.""" return self._specs(for_docs=False) - def _specs(self, for_docs: bool = False) -> str: - def yes_no_fn(cond: bool) -> str: - return "Yes" if cond else "No" + def _param_yes_no(self, param: Any) -> str: + return "Yes" if param is True else "No" - def check_none(var: Any, line: str) -> str: - if var is None: + 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(var) - - reg_lines = ( - "\nRegister parameters:\n" - + f" - Dimensions: {self.dimensions}D\n" - + f" - Rydberg level: {self.rydberg_level}\n" - + check_none(self.max_atom_num, " - Maximum number of atoms: {}\n") - + check_none( - self.max_radial_distance, - " - Maximum distance from origin: {} µm\n", - ) - + " - Minimum distance between neighbouring atoms: " - + f"{self.min_atom_distance} μm\n" - + f" - SLM Mask: {yes_no_fn(self.supports_slm_mask)}\n" - ) - - layout_lines = ( - "\nLayout parameters:\n" - + f" - Requires layout: {yes_no_fn(self.requires_layout)}\n" - ) - - if hasattr(self, "accepts_new_layouts"): - layout_lines += ( - " - Accepts new layout: " - + f"{yes_no_fn(self.accepts_new_layouts)}\n" - ) - - layout_lines += ( - f" - Minimal number of traps: {self.min_layout_traps}\n" - + check_none( - self.max_layout_traps, " - Maximal number of traps: {}\n" - ) - + " - Maximum layout filling fraction: " - + f"{self.max_layout_filling}\n" - ) - - device_lines = ( - "\nDevice parameters:\n" - + check_none(self.max_runs, " - Maximum number of runs: {}\n") - + check_none( - self.max_sequence_duration, - " - Maximum sequence duration: {} ns\n", - ) - + " - Channels can be reused: " - + f"{yes_no_fn(self.reusable_channels)}\n" - + f" - Supported bases: {', '.join(self.supported_bases)}\n" - + f" - Supported states: {', '.join(self.supported_states)}\n" - + check_none( - self.interaction_coeff, - " - Ising interaction coefficient: {}\n", - ) - + check_none( - self.interaction_coeff_xy, - " - XY interaction coefficient: {}\n", - ) - + check_none( - self.default_noise_model, - " - Default noise model: {}\n", - ) - ) + return line.format(param) + + return empty_str_if_none + + def _register_lines(self) -> 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 "\n".join(line for line in register_lines if line != "") + + def _device_lines(self) -> 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 "\n".join(line for line in device_lines if line != "") + + def _channel_lines(self, for_docs: bool = False) -> str: ch_lines = ["\nChannels:"] for name, ch in {**self.channels, **self.dmm_channels}.items(): @@ -711,7 +702,17 @@ def check_none(var: Any, line: str) -> str: else: ch_lines.append(f" - '{name}': {ch!r}") - return reg_lines + layout_lines + device_lines + "\n".join(ch_lines) + return "\n".join(line for line in ch_lines if line != "") + + def _specs(self, for_docs: bool = False) -> str: + + return "\n".join( + [ + self._register_lines(), + self._device_lines(), + self._channel_lines(for_docs=for_docs), + ] + ) @dataclass(frozen=True, repr=False) @@ -889,6 +890,33 @@ def from_abstract_repr(obj_str: str) -> Device: ) return device + def _layout_lines(self) -> str: + + layout_lines = [ + "\nLayout parameters:", + f" - Requires layout: {self._param_yes_no(self.requires_layout)}", + " - Accepts new layout: " + + self._param_yes_no(self.accepts_new_layouts), + 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 "\n".join(line for line in layout_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) class VirtualDevice(BaseDevice): diff --git a/tests/test_devices.py b/tests/test_devices.py index 33448a8c..8626c455 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -278,7 +278,7 @@ def check_none_fn(dev, attr, text): return "" def specs(dev): - return ( + register_str = ( "\nRegister parameters:\n" + f" - Dimensions: {dev.dimensions}D\n" + f" - Rydberg level: {dev.rydberg_level}\n" @@ -291,6 +291,9 @@ def specs(dev): + " - 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") + yes_no_fn(dev, "accepts_new_layouts", "Accepts new layout") @@ -299,6 +302,9 @@ def specs(dev): 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( @@ -313,6 +319,9 @@ def specs(dev): + check_none_fn( dev, "interaction_coeff_xy", "XY interaction coefficient: {}" ) + ) + + channel_str = ( + "\nChannels:\n" + "\n".join( f" - '{name}': {ch!r}" @@ -320,6 +329,11 @@ def specs(dev): ) ) + if isinstance(device, MockDevice): + return register_str + device_str + channel_str + else: + return register_str + layout_str + device_str + channel_str + assert device.specs == specs(device) @@ -628,3 +642,4 @@ def test_dmm_channels(): channel_objects=(Rydberg.Global(None, None),), channel_ids=("dmm_0",), ) + From 6ce5a1519038cf6b286fb1b771edfe03ff435f39 Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Thu, 21 Nov 2024 20:03:29 +0100 Subject: [PATCH 15/16] Remove line --- tests/test_devices.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_devices.py b/tests/test_devices.py index 8626c455..aa1655d4 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -642,4 +642,3 @@ def test_dmm_channels(): channel_objects=(Rydberg.Global(None, None),), channel_ids=("dmm_0",), ) - From 846db9a0c168c91a35b7af1c5354aa4eb81c3dce Mon Sep 17 00:00:00 2001 From: Harold Erbin Date: Thu, 21 Nov 2024 20:10:10 +0100 Subject: [PATCH 16/16] Fix typo in strings --- tests/test_devices.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/test_devices.py b/tests/test_devices.py index 33448a8c..f6082af8 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -278,7 +278,7 @@ def check_none_fn(dev, attr, text): return "" def specs(dev): - return ( + register_str = ( "\nRegister parameters:\n" + f" - Dimensions: {dev.dimensions}D\n" + f" - Rydberg level: {dev.rydberg_level}\n" @@ -291,7 +291,10 @@ def specs(dev): + " - Minimum distance between neighbouring atoms: " + f"{dev.min_atom_distance} μm\n" + yes_no_fn(dev, "supports_slm_mask", "SLM Mask") - + "\nLayout parameters:\n" + ) + + layout_str = ( + "\nLayout parameters:\n" + yes_no_fn(dev, "requires_layout", "Requires layout") + yes_no_fn(dev, "accepts_new_layouts", "Accepts new layout") + f" - Minimal number of traps: {dev.min_layout_traps}\n" @@ -299,7 +302,10 @@ def specs(dev): dev, "max_layout_traps", "Maximal number of traps: {}" ) + f" - Maximum layout filling fraction: {dev.max_layout_filling}\n" - + "\nDevice parameters:\n" + ) + + device_str = ( + "\nDevice parameters:\n" + check_none_fn(dev, "max_runs", "Maximum number of runs: {}") + check_none_fn( dev, @@ -313,13 +319,18 @@ def specs(dev): + check_none_fn( dev, "interaction_coeff_xy", "XY interaction coefficient: {}" ) - + "\nChannels:\n" - + "\n".join( - f" - '{name}': {ch!r}" - for name, ch in {**dev.channels, **dev.dmm_channels}.items() - ) ) + channel_str = "\nChannels:\n" + "\n".join( + f" - '{name}': {ch!r}" + for name, ch in {**dev.channels, **dev.dmm_channels}.items() + ) + + if device is MockDevice: + return register_str + device_str + channel_str + else: + return register_str + layout_str + device_str + channel_str + assert device.specs == specs(device)