Skip to content

Commit 99fe289

Browse files
authored
Qualtran-L1: Objectstrings (#1823)
Qualtran defines bloq classes, but we need to provide compile-time classical parameters to instantiate those into bloq objects. In principle, arbitrary Python values can be used as classical parameters, as long as they are immutable and hashable. For a human readable intermediate representation, `qualtran.l1` implements a limited serialization syntax called **objectstrings** that roughly mimics standard Python object instantiation. <img width="858" height="649" alt="image" src="https://github.com/user-attachments/assets/e11a1c70-d249-4e96-9f22-d12520511a1d" />
1 parent bc0263c commit 99fe289

23 files changed

Lines changed: 2882 additions & 2 deletions

dev_tools/dump-bloq-manifest.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Script to dump a manifest of all bloq classes and bloq examples.
17+
18+
This script finds all Bloq classes and examples in the library, serializes the examples
19+
to their objectstring representation, and writes the list of class names and
20+
example objectstrings to `qualtran/bloqs/manifest.py`.
21+
22+
See `qualtran.l1.load_objectstring()` to load bloq objects from these strings.
23+
"""
24+
25+
from functools import cached_property
26+
from typing import List, Tuple
27+
28+
from attrs import frozen
29+
from qualtran_dev_tools.bloq_finder import get_bloq_classes, get_bloq_examples
30+
from qualtran_dev_tools.git_tools import get_git_root
31+
32+
from qualtran import Bloq
33+
from qualtran.l1 import eval_cvalue_node, parse_objectstring, to_cobject_node
34+
from qualtran.l1.nodes import CArgNode, CObjectNode, LiteralNode
35+
36+
MAXLEN = 300
37+
"""If the objectstring is too long, we make the executive decision to truncate it."""
38+
39+
COPYRIGHT_NOTICE = """# Copyright 2026 Google LLC
40+
#
41+
# Licensed under the Apache License, Version 2.0 (the "License");
42+
# you may not use this file except in compliance with the License.
43+
# You may obtain a copy of the License at
44+
#
45+
# https://www.apache.org/licenses/LICENSE-2.0
46+
#
47+
# Unless required by applicable law or agreed to in writing, software
48+
# distributed under the License is distributed on an "AS IS" BASIS,
49+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
50+
# See the License for the specific language governing permissions and
51+
# limitations under the License."""
52+
53+
54+
@frozen
55+
class BloqExampleListItem:
56+
"""Formatting a bloq example and its serialized form.
57+
58+
Attributes:
59+
bloq: The bloq instance from the example.
60+
bloq_example_name: The name of the bloq example.
61+
cobject_node: The CObjectNode representing the serialized bloq.
62+
"""
63+
64+
bloq: Bloq
65+
bloq_example_name: str
66+
cobject_node: CObjectNode
67+
68+
@cached_property
69+
def objectstring(self) -> str:
70+
"""Returns the canonical string representation of the CObjectNode."""
71+
return self.cobject_node.canonical_str()
72+
73+
def maybe_commented_out(self, be_column_width: int = 30) -> Tuple[str, str, str]:
74+
"""Generates a string representation for the manifest, potentially commented out.
75+
76+
This method checks if the object string is too long, unparsable, unloadable, or
77+
if the re-loaded bloq is unequal to the original. If any of these conditions are met,
78+
it returns a commented-out string with a reason. Otherwise, it returns the
79+
executable string representation.
80+
81+
Args:
82+
be_column_width: The width of the column for the bloq example name for nicer formatting.
83+
84+
Returns:
85+
manifest_entry_str: The string to be written to the manifest (possibly commented out).
86+
reason: A reason string if it was commented out (empty otherwise).
87+
reason_details: Detailed error message if applicable (empty otherwise).
88+
"""
89+
quoted_be = f"'{self.bloq_example_name}',"
90+
be = f'{quoted_be:{be_column_width}}'
91+
be2 = f'{quoted_be:{be_column_width+2}}'
92+
93+
if len(self.objectstring) > MAXLEN:
94+
trunc = repr(self.objectstring)[:120] + '...'
95+
return f'# ({be}{trunc}', 'too long', ''
96+
97+
try:
98+
node = parse_objectstring(self.objectstring)
99+
except Exception as e: # pylint: disable=broad-except
100+
return f'# ({be}{self.objectstring!r}),', 'unparsable', str(e)
101+
102+
try:
103+
bloq = eval_cvalue_node(node, safe=False)
104+
except Exception as e: # pylint: disable=broad-except
105+
return f'# ({be}{self.objectstring!r}),', 'unloadable', str(e)
106+
107+
if bloq != self.bloq:
108+
return f'# ({be}{self.objectstring!r}),', 'unequal', ''
109+
110+
return f'({be2}{self.objectstring!r}),', '', ''
111+
112+
113+
def main():
114+
"""Main entry point for the script.
115+
116+
Finds all bloq classes and examples, processes them, and writes the
117+
`BLOQ_CLASS_NAMES` and `BLOQ_EXAMPLE_OBJECTSTRINGS` lists to
118+
`qualtran/bloqs/manifest.py`.
119+
"""
120+
bcs = get_bloq_classes()
121+
names = sorted(bc._class_name_in_pkg_() for bc in bcs)
122+
123+
bes = get_bloq_examples()
124+
items: List[BloqExampleListItem] = []
125+
for be in bes:
126+
bloq = be.make()
127+
try:
128+
cobject_node = to_cobject_node(bloq)
129+
assert isinstance(cobject_node, CObjectNode)
130+
except Exception as e: # pylint: disable=broad-except
131+
cobject_node = CObjectNode(
132+
name=bloq._class_name_in_pkg_(), cargs=[CArgNode(None, LiteralNode(str(e)))]
133+
)
134+
items.append(
135+
BloqExampleListItem(bloq=bloq, bloq_example_name=be.name, cobject_node=cobject_node)
136+
)
137+
138+
items = sorted(items, key=lambda x: x.objectstring)
139+
include_commented_out = True
140+
be_objectstrings = []
141+
for item in items:
142+
serstr, reason, details = item.maybe_commented_out()
143+
144+
if (not reason) or include_commented_out:
145+
be_objectstrings.append(serstr)
146+
147+
if reason:
148+
reason = f'({reason})'
149+
print(f"Skipping {reason:20s} {serstr}")
150+
151+
if details:
152+
print(f' {"":20s} ->', details)
153+
154+
reporoot = get_git_root()
155+
with (reporoot / 'qualtran/bloqs/manifest.py').open('w') as f:
156+
f.write(COPYRIGHT_NOTICE)
157+
f.write('\n\n')
158+
f.write('# This file is autogenerated\n')
159+
f.write('# See dev_tools/dump-bloq-manifest.py\n')
160+
f.write('# fmt: off\n\n')
161+
f.write('BLOQ_CLASS_NAMES = [\n')
162+
f.write('\n'.join([f' "{name}",' for name in names]))
163+
f.write('\n]\n')
164+
165+
f.write('\n')
166+
167+
f.write('BLOQ_EXAMPLE_OBJECTSTRINGS = [\n')
168+
f.write('\n'.join([f' {objstr}' for objstr in be_objectstrings]))
169+
f.write('\n]\n')
170+
171+
172+
if __name__ == '__main__':
173+
main()

docs/bloq_infra.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ types (``Register``), and algorithms (``CompositeBloq``).
5555
drawing/musical_score.ipynb
5656
drawing/drawing_call_graph.ipynb
5757
simulation/xcheck_classical_quimb.ipynb
58+
l1/L1-Objectstring.ipynb
5859
Autodoc.ipynb

qualtran/_infra/adjoint.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ def __str__(self) -> str:
174174
"""Delegate to subbloq's `__str__` method."""
175175
return f'{str(self.subbloq)}†'
176176

177+
@classmethod
178+
def _pkg_(cls) -> str:
179+
return 'qualtran'
180+
177181
def wire_symbol(
178182
self, reg: Optional['Register'], idx: Tuple[int, ...] = tuple()
179183
) -> 'WireSymbol':

qualtran/_infra/bloq.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,10 @@ def wire_symbol(
695695
def __str__(self):
696696
return self.__class__.__name__
697697

698+
@classmethod
699+
def _pkg_(cls) -> str:
700+
return '.'.join(cls.__module__.split('.')[:-1])
701+
698702
@classmethod
699703
def _class_name_in_pkg_(cls) -> str:
700704
"""The bloq class's name with its package.
@@ -703,5 +707,4 @@ def _class_name_in_pkg_(cls) -> str:
703707
`qualtran.bloqs.*`. Each bloq class is defined in a module (i.e. the
704708
"*.py" file) and re-exported one level up.
705709
"""
706-
pkg = '.'.join(cls.__module__.split('.')[:-1])
707-
return f'{pkg}.{cls.__name__}'
710+
return f'{cls._pkg_()}.{cls.__name__}'

qualtran/_infra/controlled.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ def get_single_ctrl_val(self) -> ControlBit:
311311

312312
return int(control_bit)
313313

314+
@classmethod
315+
def _pkg_(cls):
316+
return 'qualtran'
317+
314318

315319
class AddControlledT(Protocol):
316320
"""The signature for the `add_controlled` callback part of `ctrl_system`.
@@ -674,6 +678,10 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT':
674678
def adjoint(self) -> 'Bloq':
675679
return self.subbloq.adjoint().controlled(ctrl_spec=self.ctrl_spec)
676680

681+
@classmethod
682+
def _pkg_(cls) -> str:
683+
return 'qualtran'
684+
677685

678686
def make_ctrl_system_with_correct_metabloq(
679687
bloq: 'Bloq', ctrl_spec: 'CtrlSpec'

qualtran/_infra/data_types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ def iteration_length_or_zero(self) -> SymbolicInt:
245245
# TODO: remove https://github.com/quantumlib/Qualtran/issues/1716
246246
return getattr(self, 'iteration_length', 0)
247247

248+
@classmethod
249+
def _pkg_(cls):
250+
return 'qualtran'
251+
248252
def __str__(self):
249253
return f'{self.__class__.__name__}({self.num_bits})'
250254

qualtran/_infra/registers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class Side(enum.Flag):
4949
THRU = LEFT | RIGHT
5050
"""The register is input/output."""
5151

52+
def __repr__(self):
53+
return f'{self.__class__.__name__}.{self._name_}'
54+
5255

5356
@frozen
5457
class Register:
@@ -75,6 +78,13 @@ class Register:
7578
)
7679
side: Side = Side.THRU
7780

81+
@classmethod
82+
def _pkg_(cls):
83+
return 'qualtran'
84+
85+
def __repr__(self):
86+
return f'Register({self.name!r}, dtype={self.dtype!r}, shape={self._shape!r}, side={self.side!r})'
87+
7888
def __attrs_post_init__(self):
7989
if not isinstance(self.dtype, QCDType):
8090
raise ValueError(f'dtype must be a QCDType: found {type(self.dtype)}')

qualtran/bloqs/arithmetic/permutation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ def build_call_graph(
135135

136136
return super().build_call_graph(ssa)
137137

138+
@classmethod
139+
def _pkg_(cls) -> str:
140+
# This isn't re-exported
141+
return '.'.join(cls.__module__.split('.')[:])
142+
138143

139144
@bloq_example
140145
def _permutation_cycle() -> PermutationCycle:
@@ -281,6 +286,11 @@ def build_call_graph(
281286

282287
return super().build_call_graph(ssa)
283288

289+
@classmethod
290+
def _pkg_(cls) -> str:
291+
# This isn't re-exported
292+
return '.'.join(cls.__module__.split('.')[:])
293+
284294

285295
@bloq_example
286296
def _permutation() -> Permutation:

qualtran/bloqs/arithmetic/sorting.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ def build_composite_bloq(
282282
def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
283283
return {Comparator(self.bitsize): self.num_comparisons}
284284

285+
@classmethod
286+
def _pkg_(cls) -> str:
287+
# Local import
288+
return '.'.join(cls.__module__.split('.')[:])
289+
285290

286291
@bloq_example
287292
def _bitonic_merge() -> BitonicMerge:
@@ -383,6 +388,11 @@ def build_composite_bloq(self, bb: 'BloqBuilder', xs: 'SoquetT') -> dict[str, 'S
383388
def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
384389
return {Comparator(self.bitsize): self.num_comparisons}
385390

391+
@classmethod
392+
def _pkg_(cls) -> str:
393+
# This isn't re-exported
394+
return '.'.join(cls.__module__.split('.')[:])
395+
386396

387397
@bloq_example
388398
def _bitonic_sort() -> BitonicSort:

0 commit comments

Comments
 (0)