Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions qualtran/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,6 @@

from ._infra.bloq_example import BloqExample, bloq_example, BloqDocSpec

from .bloqify_syntax import bloqify

# --------------------------------------------------------------------------------------------------
102 changes: 87 additions & 15 deletions qualtran/_infra/composite_bloq.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
if TYPE_CHECKING:
import cirq

import qualtran as qlt
import qualtran.dtype as qdt
from qualtran.bloqs.bookkeeping.auto_partition import Unused
from qualtran.cirq_interop._cirq_to_bloq import CirqQuregInT, CirqQuregT
from qualtran.drawing import WireSymbol
Expand Down Expand Up @@ -467,7 +469,7 @@ def final_soqs(self) -> Dict[str, _SoquetT]:

def copy(self) -> 'CompositeBloq':
"""Create a copy of this composite bloq by re-building it."""
bb, _ = BloqBuilder.from_signature(self.signature)
bb, _ = BloqBuilder.from_signature(self.signature, bloq_key=self.bloq_key)
soq_map = bb.initial_soq_map(self.signature.lefts())

for binst, in_soqs, old_out_soqs in self.iter_bloqsoqs():
Expand Down Expand Up @@ -641,6 +643,11 @@ def __str__(self):
return self.bloq_key
return f'CompositeBloq([{len(self.bloq_instances)} subbloqs...])'

def __repr__(self):
if self.bloq_key is not None:
return f'CompositeBloq(..., bloq_key={self.bloq_key!r})'
return f'CompositeBloq([{len(self.bloq_instances)} subbloqs...])'


def _create_binst_graph(
cxns: Iterable[Connection], nodes: Iterable[BloqInstance] = ()
Expand Down Expand Up @@ -1098,7 +1105,7 @@ def build_composite_bloq(self, bb: BloqBuilder, q0, q1):
by the framework or by the `BloqBuilder.from_signature(s)` factory method.
"""

def __init__(self, add_registers_allowed: bool = True):
def __init__(self, add_registers_allowed: bool = True, *, bloq_key: Optional[str] = None):
# To be appended to:
self._cxns: List[Connection] = []
self._regs: List[Register] = []
Expand All @@ -1113,6 +1120,8 @@ def __init__(self, add_registers_allowed: bool = True):
# Whether we can call `add_register` and do non-strict `finalize()`.
self.add_register_allowed = add_registers_allowed

self._bloq_key = bloq_key

def add_register_from_dtype(
self, reg: Union[str, Register], dtype: Optional[QCDType] = None
) -> Union[None, QVarT]:
Expand All @@ -1135,7 +1144,7 @@ def add_register_from_dtype(
from qualtran.symbolics import is_symbolic

if not self.add_register_allowed:
raise ValueError(
raise BloqError(
"This BloqBuilder was constructed from pre-specified registers. "
"Ad hoc addition of more registers is not allowed."
)
Expand Down Expand Up @@ -1208,15 +1217,19 @@ def add_register(

@classmethod
def from_signature(
cls, signature: Signature, add_registers_allowed: bool = False
cls,
signature: Signature,
add_registers_allowed: bool = False,
*,
bloq_key: Optional[str] = None,
) -> Tuple['BloqBuilder', Dict[str, QVarT]]:
"""Construct a BloqBuilder with a pre-specified signature.

This is safer if e.g. you're decomposing an existing Bloq and need the signatures
to match. This constructor is used by `Bloq.decompose_bloq()`.
"""
# Initial construction: allow register addition for the following loop.
bb = cls(add_registers_allowed=True)
bb = cls(add_registers_allowed=True, bloq_key=bloq_key)

initial_soqs: Dict[str, QVarT] = {}
for reg in signature:
Expand Down Expand Up @@ -1454,7 +1467,12 @@ def add(self, bloq: Bloq, **in_soqs: SoquetInT):
be unpacked with tuple unpacking. In this final case, the ordering is according
to `bloq.signature` and irrespective of the order of `**in_soqs`.
"""
outs = self.add_t(bloq, **in_soqs)
try:
outs = self.add_t(bloq, **in_soqs)
except BloqError as be:
# Error source shown as `bb.add(...)`
raise BloqError(*be.args) from None
Comment thread
mpharrigan marked this conversation as resolved.
Outdated

if len(outs) == 0:
return None
if len(outs) == 1:
Expand Down Expand Up @@ -1539,17 +1557,53 @@ def add_from(self, bloq: Bloq, **in_soqs: SoquetInT) -> Tuple['QVarT', ...]:
fsoqs = _map_soqs(cbloq.final_soqs(), soq_map)
return tuple(fsoqs[reg.name] for reg in cbloq.signature.rights())

def _change_THRU_to_LEFT(self, reg_name: str):
"""Used during loose `finalize` to force LEFT registers."""
for reg_i, reg in enumerate(self._regs):
if reg.name == reg_name:
break
else:
raise AssertionError(f"{reg_name} doesn't exist in the registers.")

if reg.side != Side.THRU:
raise ValueError(f"{reg} is supposed to be a THRU register.")

new_reg = attrs.evolve(reg, side=Side.LEFT)

# Replace in `self._available`
soqs_to_replace = []
for soq in self._available:
if soq.binst is LeftDangle and soq.reg == reg:
soqs_to_replace.append(soq)
for soq in soqs_to_replace:
self._available.remove(soq)
self._available.add(attrs.evolve(soq, reg=new_reg))

# Replace in `self._cxns`
for j in range(len(self._cxns)):
cxn = self._cxns[j]
if cxn.left.reg == reg:
new_cxn = attrs.evolve(cxn, left=attrs.evolve(cxn.left, reg=new_reg))
self._cxns[j] = new_cxn

# Replace in `self._regs`
self._regs[reg_i] = new_reg

def finalize(self, **final_soqs: SoquetInT) -> CompositeBloq:
"""Finish building a CompositeBloq and return the immutable CompositeBloq.

This method is similar to calling `add()` but instead of adding a new Bloq,
it configures the final "dangling" soquets that serve as the outputs for
the composite bloq as a whole.

If `self.add_registers_allowed` is set to `True`, additional register
names passed to this function will be added as RIGHT registers. Otherwise,
this method validates the provided `final_soqs` against our list of RIGHT
(and THRU) registers.
If `self.add_registers_allowed` is set to `False`, the kwqargs to this method must
exactly match the signature configured for this bloq builder.
Otherwise:
- surplus arguments (with register names not in our signature) will be interpreted as
output quantum variables and we'll add a corresponding RIGHT register.
- missing arguments (i.e. a register name was introduced somewhere but
no quantum variable was provided for it), we will set that register to be a
LEFT register.

Args:
**final_soqs: Keyword arguments mapping the composite bloq's register names to
Expand Down Expand Up @@ -1577,6 +1631,12 @@ def _infer_shaped_dtype(soq: SoquetT) -> Tuple['QCDType', Tuple[int, ...]]:
dtype, shape = _infer_shaped_dtype(np.asarray(soq))
self._regs.append(Register(name=name, dtype=dtype, shape=shape, side=Side.RIGHT))

# If a soquet is missing, we're charitable and consider it de-allocated, see docstring.
deletable_right_reg_names = [reg.name for reg in self._regs if reg.side == Side.THRU]
for name in deletable_right_reg_names:
if name not in final_soqs:
self._change_THRU_to_LEFT(reg_name=name)

return self._finalize_strict(**final_soqs)

def _finalize_strict(self, **final_soqs: SoquetInT) -> CompositeBloq:
Expand All @@ -1592,16 +1652,25 @@ def _fin(idxed_soq: _QVar, reg: Register, idx: Tuple[int, ...]):
# close over `RightDangle`
return self._add_cxn(RightDangle, idxed_soq, reg, idx)

if self._bloq_key:
debug_str = f'finalization of {self._bloq_key}'
else:
debug_str = 'finalization'
_process_soquets(
registers=signature.rights(), debug_str='Finalizing', in_soqs=final_soqs, func=_fin
registers=signature.rights(), debug_str=debug_str, in_soqs=final_soqs, func=_fin
)
if self._available:
raise BloqError(
f"During finalization, {self._available} Soquets were not used."
) from None
if self._bloq_key is None:
ctx = ''
else:
ctx = f' of {self._bloq_key}'
raise BloqError(f"During finalization{ctx}, {self._available} Soquets were not used.")

return CompositeBloq(
connections=self._cxns, signature=signature, bloq_instances=self._binsts
connections=self._cxns,
signature=signature,
bloq_instances=self._binsts,
bloq_key=self._bloq_key,
)

def allocate(
Expand Down Expand Up @@ -1653,3 +1722,6 @@ def join(self, soqs: SoquetInT, dtype: Optional[QDType] = None) -> 'Soquet':
dtype = QAny(n)

return self.add(Join(dtype=dtype), reg=soqs)

def in_register(self, name: str, dtype: QCDType, shape=()) -> Union[None, QVarT]:
return self.add_register_from_dtype(Register(name=name, dtype=dtype, shape=shape))
4 changes: 2 additions & 2 deletions qualtran/_infra/composite_bloq_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def test_finalize_missing_args():
x2, y2 = bb.add(TestTwoBitOp(), ctrl=x, target=y)

bb.add_register_allowed = False
with pytest.raises(BloqError, match=r"During Finalizing, we expected a value for 'x'\."):
with pytest.raises(BloqError, match=r"During finalization, we expected a value for 'x'\."):
bb.finalize(y=y2)


Expand All @@ -321,7 +321,7 @@ def test_finalize_strict_too_many_args():
x2, y2 = bb.add(TestTwoBitOp(), ctrl=x, target=y)

bb.add_register_allowed = False
with pytest.raises(BloqError, match=r'Finalizing does not accept Soquets.*z.*'):
with pytest.raises(BloqError, match=r'finalization does not accept Soquets.*z.*'):
bb.finalize(x=x2, y=y2, z=_Soquet(RightDangle, Register('asdf', QBit())))


Expand Down
16 changes: 16 additions & 0 deletions qualtran/bloqify_syntax/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from ._infra import bloqify
Loading
Loading