Skip to content

Commit bf28857

Browse files
weinbe58claude
andauthored
refactor(python): use cached_property and MappingProxyType for read-only collections (#284) (#286)
* refactor(python): use MappingProxyType and tuple for read-only property access Convert PlotParameters dict-returning properties in artist.py to @cached_property with MappingProxyType for immutable, cached access. Wrap DetectorResult.detectors and .observables in tuple() to prevent mutation of internal lists. Add comment to ArchSpec.paths explaining why it remains mutable (populated incrementally after construction). Closes #284 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(python): address PR review feedback on MappingProxyType changes (#284) - Add from __future__ import annotations to artist.py for Python 3.10 compatibility with MappingProxyType[str, Any] annotations - Make PlotParameters frozen=True to keep cached_property coherent - Deep-copy inner lists in DetectorResult: return tuple(tuple(shot)) instead of tuple(self._list) to prevent mutation of inner lists - Make ArchSpec.paths read-only with MappingProxyType — refactor impls.py to merge path dicts before construction instead of mutating after - Add caching identity check to test_plot_parameters_properties Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4ec31e2 commit bf28857

File tree

5 files changed

+96
-73
lines changed

5 files changed

+96
-73
lines changed

python/bloqade/lanes/arch/gemini/impls.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def generate_arch_hypercube(hypercube_dims: int = 4, word_size_y: int = 5) -> Ar
230230
num_words_x = 2**hypercube_dims
231231
base_arch = _generate_base_arch(num_words_x, word_size_y)
232232
word_buses = _hypercube_busses(hypercube_dims)
233-
base_arch.paths.update(_calc_hypercube_word_path_dict(base_arch.words))
233+
paths = {**base_arch.paths, **_calc_hypercube_word_path_dict(base_arch.words)}
234234
return ArchSpec.from_components(
235235
words=base_arch.words,
236236
zones=base_arch.zones,
@@ -240,14 +240,14 @@ def generate_arch_hypercube(hypercube_dims: int = 4, word_size_y: int = 5) -> Ar
240240
has_word_buses=base_arch.has_word_buses,
241241
site_buses=base_arch.site_buses,
242242
word_buses=word_buses,
243-
paths=base_arch.paths,
243+
paths=paths,
244244
)
245245

246246

247247
def generate_arch_linear(num_words: int = 16, word_size_y: int = 5) -> ArchSpec:
248248
base_arch = _generate_base_arch(num_words, word_size_y)
249249
word_buses = _generate_linear_busses(num_words)
250-
base_arch.paths.update(_calc_linear_word_path_dict(base_arch.words))
250+
paths = {**base_arch.paths, **_calc_linear_word_path_dict(base_arch.words)}
251251
return ArchSpec.from_components(
252252
words=base_arch.words,
253253
zones=base_arch.zones,
@@ -257,7 +257,7 @@ def generate_arch_linear(num_words: int = 16, word_size_y: int = 5) -> ArchSpec:
257257
has_word_buses=base_arch.has_word_buses,
258258
site_buses=base_arch.site_buses,
259259
word_buses=word_buses,
260-
paths=base_arch.paths,
260+
paths=paths,
261261
)
262262

263263

python/bloqade/lanes/device.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,24 +62,24 @@ def detector_error_model(self) -> DetectorErrorModel:
6262
return self._detector_error_model
6363

6464
@property
65-
def detectors(self) -> list[list[bool]]:
65+
def detectors(self) -> tuple[tuple[bool, ...], ...]:
6666
"""The detector outcomes from the simulation.
6767
6868
Returns:
69-
list[list[bool]]: The detector outcomes, one list per shot.
69+
tuple[tuple[bool, ...], ...]: The detector outcomes, one tuple per shot.
7070
7171
"""
72-
return self._detectors
72+
return tuple(tuple(shot) for shot in self._detectors)
7373

7474
@property
75-
def observables(self) -> list[list[bool]]:
75+
def observables(self) -> tuple[tuple[bool, ...], ...]:
7676
"""The observable outcomes from the simulation.
7777
7878
Returns:
79-
list[list[bool]]: The observable outcomes, one list per shot.
79+
tuple[tuple[bool, ...], ...]: The observable outcomes, one tuple per shot.
8080
8181
"""
82-
return self._observables
82+
return tuple(tuple(shot) for shot in self._observables)
8383

8484

8585
@dataclass(frozen=True)

python/bloqade/lanes/layout/arch.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from collections import defaultdict
44
from functools import cached_property
5+
from types import MappingProxyType
56
from typing import TYPE_CHECKING, Sequence
67

78
from bloqade.lanes.bytecode._native import (
@@ -34,7 +35,7 @@ class ArchSpec:
3435

3536
_inner: _RustArchSpec
3637
words: tuple[Word, ...]
37-
paths: dict[LaneAddress, tuple[tuple[float, float], ...]]
38+
paths: MappingProxyType[LaneAddress, tuple[tuple[float, float], ...]]
3839

3940
def __init__(
4041
self,
@@ -44,7 +45,7 @@ def __init__(
4445
):
4546
self._inner = inner
4647
self.words = words
47-
self.paths = paths if paths is not None else {}
48+
self.paths = MappingProxyType(paths if paths is not None else {})
4849

4950
self._inner.validate()
5051

python/bloqade/lanes/visualize/artist.py

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from __future__ import annotations
2+
13
from abc import ABC
24
from dataclasses import dataclass
35
from enum import Enum
4-
from typing import Callable, Sequence
6+
from functools import cached_property
7+
from types import MappingProxyType
8+
from typing import Any, Callable, Sequence
59

610
import numpy as np
711
from kirin import ir
@@ -22,7 +26,7 @@ class QuEraColorCode(str, Enum):
2226
AodLineColor = "#FFE8E9"
2327

2428

25-
@dataclass
29+
@dataclass(frozen=True)
2630
class PlotParameters:
2731
scale: float
2832

@@ -38,64 +42,76 @@ class PlotParameters:
3842
atom_color: str = QuEraColorCode.Purple
3943
aod_line_style: str = "dashed"
4044

41-
@property
42-
def atom_plot_args(self) -> dict:
43-
return {
44-
"color": self.atom_color,
45-
"marker": self.atom_marker,
46-
"linestyle": "",
47-
"s": self.scale * 65,
48-
}
45+
@cached_property
46+
def atom_plot_args(self) -> MappingProxyType[str, Any]:
47+
return MappingProxyType(
48+
{
49+
"color": self.atom_color,
50+
"marker": self.atom_marker,
51+
"linestyle": "",
52+
"s": self.scale * 65,
53+
}
54+
)
4955

50-
@property
51-
def gate_spot_args(self) -> dict:
52-
return {
53-
"marker": self.atom_marker,
54-
"s": self.scale * 160,
55-
"alpha": 0.3,
56-
}
56+
@cached_property
57+
def gate_spot_args(self) -> MappingProxyType[str, Any]:
58+
return MappingProxyType(
59+
{
60+
"marker": self.atom_marker,
61+
"s": self.scale * 160,
62+
"alpha": 0.3,
63+
}
64+
)
5765

58-
@property
59-
def slm_plot_args(self) -> dict:
60-
return {
61-
"facecolors": "none",
62-
"edgecolors": "k",
63-
"linestyle": "-",
64-
"s": self.scale * 80,
65-
"alpha": 1.0,
66-
"linewidth": 0.5 * np.sqrt(self.scale),
67-
"marker": self.atom_marker,
68-
}
66+
@cached_property
67+
def slm_plot_args(self) -> MappingProxyType[str, Any]:
68+
return MappingProxyType(
69+
{
70+
"facecolors": "none",
71+
"edgecolors": "k",
72+
"linestyle": "-",
73+
"s": self.scale * 80,
74+
"alpha": 1.0,
75+
"linewidth": 0.5 * np.sqrt(self.scale),
76+
"marker": self.atom_marker,
77+
}
78+
)
6979

70-
@property
71-
def aod_line_args(self) -> dict:
72-
return {
73-
"alpha": 1.0,
74-
"colors": self.aod_line_color,
75-
"linestyles": self.aod_line_style,
76-
"zorder": -101,
77-
}
80+
@cached_property
81+
def aod_line_args(self) -> MappingProxyType[str, Any]:
82+
return MappingProxyType(
83+
{
84+
"alpha": 1.0,
85+
"colors": self.aod_line_color,
86+
"linestyles": self.aod_line_style,
87+
"zorder": -101,
88+
}
89+
)
7890

79-
@property
80-
def aod_marker_args(self) -> dict:
81-
return {
82-
"color": QuEraColorCode.Red,
83-
"marker": self.aod_marker,
84-
"s": self.scale * 260,
85-
"linewidth": np.sqrt(self.scale),
86-
"alpha": 0.7,
87-
"zorder": -100,
88-
}
91+
@cached_property
92+
def aod_marker_args(self) -> MappingProxyType[str, Any]:
93+
return MappingProxyType(
94+
{
95+
"color": QuEraColorCode.Red,
96+
"marker": self.aod_marker,
97+
"s": self.scale * 260,
98+
"linewidth": np.sqrt(self.scale),
99+
"alpha": 0.7,
100+
"zorder": -100,
101+
}
102+
)
89103

90-
@property
91-
def atom_label_args(self) -> dict:
92-
return {
93-
"color": "white",
94-
"fontsize": max(4.0, self.scale * 6.0),
95-
"ha": "center",
96-
"va": "center",
97-
"zorder": 200,
98-
}
104+
@cached_property
105+
def atom_label_args(self) -> MappingProxyType[str, Any]:
106+
return MappingProxyType(
107+
{
108+
"color": "white",
109+
"fontsize": max(4.0, self.scale * 6.0),
110+
"ha": "center",
111+
"va": "center",
112+
"zorder": 200,
113+
}
114+
)
99115

100116

101117
def init_aod_x_lines(ax: Axes, aod_x: Sequence[float], plot_params: PlotParameters):

python/tests/visualize/test_artist.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from types import MappingProxyType
2+
13
from matplotlib import pyplot as plt
24
from scipy.interpolate import interp1d
35

@@ -14,12 +16,16 @@
1416

1517
def test_plot_parameters_properties():
1618
params = PlotParameters(scale=1.0)
17-
assert isinstance(params.atom_plot_args, dict)
18-
assert isinstance(params.atom_label_args, dict)
19-
assert isinstance(params.gate_spot_args, dict)
20-
assert isinstance(params.slm_plot_args, dict)
21-
assert isinstance(params.aod_line_args, dict)
22-
assert isinstance(params.aod_marker_args, dict)
19+
# All properties return read-only MappingProxyType
20+
assert isinstance(params.atom_plot_args, MappingProxyType)
21+
assert isinstance(params.atom_label_args, MappingProxyType)
22+
assert isinstance(params.gate_spot_args, MappingProxyType)
23+
assert isinstance(params.slm_plot_args, MappingProxyType)
24+
assert isinstance(params.aod_line_args, MappingProxyType)
25+
assert isinstance(params.aod_marker_args, MappingProxyType)
26+
# Cached: repeated access returns the same object
27+
assert params.atom_plot_args is params.atom_plot_args
28+
assert params.aod_line_args is params.aod_line_args
2329

2430

2531
def test_aod_x_lines():

0 commit comments

Comments
 (0)