Skip to content

Commit 6d5ab48

Browse files
authored
Add Always metabloq to simplify decompositions with compute-uncompute pairs (#1605)
* Add `IgnoringCtrl` metabloq; fix ctrl ham-sim decomp * serialize * rename to `Always` * add tests * docs + bloq example * add small example * more docstring * render caution * simplify `AddK` using `Always` * cleanup `AddK` doc * fix notebook * more elaborate docstring in `AddK` * update caution * fix notebook
1 parent a05ba90 commit 6d5ab48

File tree

11 files changed

+312
-47
lines changed

11 files changed

+312
-47
lines changed

dev_tools/qualtran_dev_tools/notebook_specs.py

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
qualtran.bloqs.bookkeeping.partition._PARTITION_DOC,
252252
qualtran.bloqs.bookkeeping.auto_partition._AUTO_PARTITION_DOC,
253253
qualtran.bloqs.bookkeeping.cast._CAST_DOC,
254+
qualtran.bloqs.bookkeeping.always._ALWAYS_DOC,
254255
],
255256
),
256257
NotebookSpecV2(

qualtran/bloqs/arithmetic/addition.ipynb

+1-2
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,7 @@
325325
"\n",
326326
"#### Parameters\n",
327327
" - `dtype`: data type of the input register `x`\n",
328-
" - `k`: The classical integer value to be added to x.\n",
329-
" - `is_controlled`: if True, construct a singly-controlled bloq. \n",
328+
" - `k`: The classical integer value to be added to x. \n",
330329
"\n",
331330
"#### Registers\n",
332331
" - `x`: register of type `self.dtype` \n",

qualtran/bloqs/arithmetic/addition.py

+16-39
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from functools import cached_property
1616
from typing import Dict, Iterator, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union
1717

18-
import attrs
1918
import cirq
2019
import numpy as np
2120
import sympy
@@ -32,7 +31,6 @@
3231
CtrlSpec,
3332
DecomposeTypeError,
3433
GateWithRegisters,
35-
QAny,
3634
QInt,
3735
QMontgomeryUInt,
3836
QUInt,
@@ -43,6 +41,7 @@
4341
SoquetT,
4442
)
4543
from qualtran.bloqs.basic_gates import CNOT
44+
from qualtran.bloqs.bookkeeping import Always
4645
from qualtran.bloqs.mcmt.and_bloq import And
4746
from qualtran.bloqs.mcmt.specialized_ctrl import get_ctrl_system_1bit_cv
4847
from qualtran.cirq_interop import decompose_from_cirq_style_method
@@ -404,7 +403,6 @@ class AddK(Bloq):
404403
Args:
405404
dtype: data type of the input register `x`
406405
k: The classical integer value to be added to x.
407-
is_controlled: if True, construct a singly-controlled bloq.
408406
409407
Registers:
410408
x: register of type `self.dtype`
@@ -416,7 +414,6 @@ class AddK(Bloq):
416414

417415
dtype: Union[QInt, QUInt, QMontgomeryUInt]
418416
k: 'SymbolicInt'
419-
is_controlled: bool = False
420417

421418
def __attrs_post_init__(self):
422419
if not isinstance(self.dtype, (QInt, QUInt, QMontgomeryUInt)):
@@ -426,19 +423,18 @@ def __attrs_post_init__(self):
426423

427424
@cached_property
428425
def signature(self) -> 'Signature':
429-
return Signature.build_from_dtypes(ctrl=QAny(1 if self.is_controlled else 0), x=self.dtype)
426+
return Signature.build_from_dtypes(x=self.dtype)
430427

431428
def on_classical_vals(
432429
self, x: 'ClassicalValT', **vals: 'ClassicalValT'
433430
) -> Dict[str, 'ClassicalValT']:
434431
if is_symbolic(self.k) or is_symbolic(self.dtype):
435432
raise ValueError(f"Classical simulation isn't supported for symbolic block {self}")
436433

437-
if not self.is_controlled or vals['ctrl']:
438-
is_signed = isinstance(self.dtype, QInt)
439-
x = add_ints(int(x), int(self.k), num_bits=self.dtype.num_qubits, is_signed=is_signed)
434+
is_signed = isinstance(self.dtype, QInt)
435+
x = add_ints(int(x), int(self.k), num_bits=self.dtype.num_qubits, is_signed=is_signed)
440436

441-
return vals | {'x': x}
437+
return {'x': x}
442438

443439
@cached_property
444440
def _load_k_bloq(self) -> Bloq:
@@ -449,55 +445,36 @@ def _load_k_bloq(self) -> Bloq:
449445
# Since this is unsigned addition, adding `-v` is equivalent to adding `2**bitsize - v`
450446
k %= 2**self.dtype.bitsize
451447

452-
xork = XorK(self.dtype, k)
453-
return xork.controlled() if self.is_controlled else xork
448+
return XorK(self.dtype, k)
454449

455-
def build_composite_bloq(
456-
self, bb: 'BloqBuilder', x: Soquet, **soqs: Soquet
457-
) -> Dict[str, 'SoquetT']:
450+
def build_composite_bloq(self, bb: 'BloqBuilder', x: Soquet) -> Dict[str, 'SoquetT']:
458451
if is_symbolic(self.k) or is_symbolic(self.dtype):
459452
raise DecomposeTypeError(f"Cannot decompose symbolic {self}.")
460453

461-
# load `k` (conditional on ctrl if present)
454+
# load `k`
462455
k = bb.allocate(dtype=self.dtype)
463-
load_soqs = {'x': k}
464-
if self.is_controlled:
465-
load_soqs |= {'ctrl': soqs.pop('ctrl')}
466-
load_soqs = bb.add_d(self._load_k_bloq, **load_soqs)
467-
k = load_soqs.pop('x')
456+
k = bb.add(self._load_k_bloq, x=k)
468457

469-
# quantum-quantum addition
470-
k, x = bb.add(Add(self.dtype, self.dtype), a=k, b=x)
458+
# perform the quantum-quantum addition
459+
# we always perform this addition (even when controlled), so we wrap in `Always`
460+
# controlling the data loading is sufficient to control this bloq.
461+
k, x = bb.add(Always(Add(self.dtype, self.dtype)), a=k, b=x)
471462

472463
# unload `k`
473-
load_soqs['x'] = k
474-
load_soqs = bb.add_d(self._load_k_bloq.adjoint(), **load_soqs)
475-
k = load_soqs.pop('x')
476-
assert isinstance(k, Soquet)
464+
k = bb.add(self._load_k_bloq.adjoint(), x=k)
477465
bb.free(k)
478466

479-
return {'x': x} | load_soqs
467+
return {'x': x}
480468

481469
def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT':
482470
counts = Counter[Bloq]()
483471

484472
counts[self._load_k_bloq] += 1
485-
counts[Add(self.dtype, self.dtype)] += 1
473+
counts[Always(Add(self.dtype, self.dtype))] += 1
486474
counts[self._load_k_bloq.adjoint()] += 1
487475

488476
return counts
489477

490-
def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> Tuple['Bloq', 'AddControlledT']:
491-
from qualtran.bloqs.mcmt.specialized_ctrl import get_ctrl_system_1bit_cv_from_bloqs
492-
493-
return get_ctrl_system_1bit_cv_from_bloqs(
494-
self,
495-
ctrl_spec,
496-
current_ctrl_bit=1 if self.is_controlled else None,
497-
bloq_with_ctrl=attrs.evolve(self, is_controlled=True),
498-
ctrl_reg_name='ctrl',
499-
)
500-
501478

502479
@bloq_example(generalizer=ignore_split_join)
503480
def _add_k() -> AddK:

qualtran/bloqs/arithmetic/addition_test.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,6 @@ def test_out_of_place_adder():
317317
def test_controlled_add_k():
318318
n, k = sympy.symbols('n k')
319319
addk = AddK(QUInt(n), k)
320-
assert addk.controlled() == AddK(QUInt(n), k, is_controlled=True)
321320
_, sigma = addk.controlled(CtrlSpec(cvs=0)).call_graph(max_depth=1)
322321
assert sigma == {addk.controlled(): 1, XGate(): 2}
323322

@@ -346,7 +345,7 @@ def test_add_k_decomp_signed(bitsize, k, cvs):
346345
'bitsize,k,x,cvs,ctrls,result',
347346
[
348347
(5, 1, 2, (), (), 3),
349-
(5, 3, 2, (1,), 1, 5),
348+
(5, 3, 2, (1,), (1,), 5),
350349
(5, 2, 0, (1, 0), (1, 0), 2),
351350
(5, 1, 2, (1, 0, 1), (0, 0, 0), 2),
352351
],

qualtran/bloqs/bookkeeping/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Bloqs for virtual operations and register reshaping."""
1515

1616
from qualtran.bloqs.bookkeeping.allocate import Allocate
17+
from qualtran.bloqs.bookkeeping.always import Always
1718
from qualtran.bloqs.bookkeeping.arbitrary_clifford import ArbitraryClifford
1819
from qualtran.bloqs.bookkeeping.auto_partition import AutoPartition
1920
from qualtran.bloqs.bookkeeping.cast import Cast

qualtran/bloqs/bookkeeping/always.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
from typing import Iterable, Optional, Sequence
15+
16+
import attrs
17+
18+
from qualtran import (
19+
AddControlledT,
20+
Bloq,
21+
bloq_example,
22+
BloqBuilder,
23+
BloqDocSpec,
24+
CtrlSpec,
25+
Signature,
26+
SoquetT,
27+
)
28+
29+
30+
@attrs.frozen
31+
class Always(Bloq):
32+
"""Always execute the wrapped bloq, even when a controlled version is requested
33+
34+
A controlled version of a composite bloq in turn controls each subbloq in the decomposition.
35+
Wrapping a particular subbloq with `Always` lets it bypass the controls,
36+
i.e. it is "always" executed, irrespective of what the controls are.
37+
38+
This is useful when writing decompositions for two known patterns:
39+
40+
1. Compute-uncompute pairs: If a decomposition contains a compute-uncompute pair,
41+
then for a controlled version, we only need to control the rest of the bloqs.
42+
Wrapping both the compute and uncompute bloqs in `Always` lets them bypass the controls.
43+
44+
2. Controlled data-loading: For example, in the `AddK` bloq which adds a constant `k` to the
45+
register, we (controlled) load the value `k` into a quantum register, and "always" perform an
46+
quantum-quantum addition using `Add`, and unload `k`. Here wrapping the middle `Add` with
47+
`Always` lets it bypass controls, e.g. when using `AddK.controlled()`.
48+
49+
This simplifies the decompositions by avoiding the need to explicitly define the decomposition
50+
for the controlled version of bloq.
51+
52+
**Caution:** This wrapper should be used with care. It is up to the bloq author to ensure that
53+
the controlled version of a decomposition containing `Always` bloqs still respects the
54+
controlled protocol. That is, ignoring controls on these subbloqs wrapped in `Always` should not
55+
change the action of the overall bloq with respect to the reference controlled implementation.
56+
57+
Args:
58+
subbloq: The bloq to always apply, irrespective of any controls.
59+
"""
60+
61+
subbloq: Bloq
62+
63+
@property
64+
def signature(self) -> 'Signature':
65+
return self.subbloq.signature
66+
67+
def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']:
68+
return bb.add_d(self.subbloq, **soqs)
69+
70+
def get_ctrl_system(
71+
self, ctrl_spec: Optional['CtrlSpec'] = None
72+
) -> tuple['Bloq', 'AddControlledT']:
73+
"""Pass-through the control registers as-is"""
74+
75+
def add_controlled(
76+
bb: 'BloqBuilder', ctrl_soqs: Sequence['SoquetT'], in_soqs: dict[str, 'SoquetT']
77+
) -> tuple[Iterable['SoquetT'], Iterable['SoquetT']]:
78+
out_soqs = bb.add_t(self, **in_soqs)
79+
return ctrl_soqs, out_soqs
80+
81+
return self, add_controlled
82+
83+
def adjoint(self) -> 'Always':
84+
return Always(self.subbloq.adjoint())
85+
86+
def __str__(self) -> str:
87+
return str(self.subbloq)
88+
89+
90+
@bloq_example
91+
def _always_and() -> Always:
92+
from qualtran.bloqs.mcmt.and_bloq import And
93+
94+
always_and = Always(And())
95+
96+
return always_and
97+
98+
99+
_ALWAYS_DOC = BloqDocSpec(bloq_cls=Always, examples=[_always_and])
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
from qualtran.bloqs.bookkeeping.always import _always_and, Always
15+
from qualtran.bloqs.for_testing import TestAtom
16+
17+
18+
def test_example(bloq_autotester):
19+
bloq_autotester(_always_and)
20+
21+
22+
def test_always():
23+
bloq = Always(TestAtom())
24+
assert bloq.controlled() == bloq

0 commit comments

Comments
 (0)