Source code for qarp.blocks._composite_block
"""User-facing CompositeBlock with measurement / target_qubit handling.
Subclass of ``qarp.blocks.CompositeBlockBase`` (which is
``qarpx.CompositeBlock`` + Python state). Adds the high-level orchestration
that the C++ class doesn't carry: measurement-only-at-end validation and
qubit-count resolution from children. Children are wired as given —
controlisation is explicit through ``ControlledBlock`` (§13).
"""
from typing import List, Optional, Sequence
import qarpx as qx
from ._block import CompositeBlockBase
[docs]
class CompositeBlock(CompositeBlockBase):
"""Compose a sequence of pre-built sub-blocks into a single circuit.
Pattern B (composite) — populates ``self`` via ``self.add_child(...)`` in
``build_vanilla()``.
"""
def __init__(
self,
# Any qarp/qarpx block is a valid child (Simple, Composite, Controlled,
# ...) — the runtime check below is `isinstance(block, qx.Block)`.
blocks: Sequence[qx.Block],
n_qubits: Optional[int] = None,
target_qubits: Optional[List[int]] = None,
*,
name: str = "CompositeBlock",
) -> None:
# If n_qubits not given, infer from children's target_qubits / n_qubits.
if n_qubits is None:
n_qubits = self._infer_n_qubits(blocks)
super().__init__(n_qubits=n_qubits, target_qubits=target_qubits, name=name)
self.blocks = list(blocks)
# How many of ``self.blocks`` are already wired into the C++ children;
# a rebuild after ``add_child`` wires only the tail (P1.9).
self._n_wired = 0
self._validate_inputs()
# ── Validation ────────────────────────────────────────────────────
def _validate_inputs(self) -> None:
if not self.blocks and self.n_qubits is None:
raise ValueError("CompositeBlock must have at least one block or n_qubits specified.")
for block in self.blocks:
# Accept any qarp / qarpx block (Simple, Composite, Controlled,
# Measure, Reset, Conditional). The Python-level ``Block`` alias
# is ``SimpleBlock``, so we check the C++ base via ``qx.Block``
# which every qarp block inherits from.
if not isinstance(block, qx.Block):
raise TypeError(f"All elements must be Block instances, got {type(block)}")
@staticmethod
def _infer_n_qubits(blocks: Sequence[qx.Block]) -> int:
"""Look at each block's target_qubits / n_qubits to size the parent.
A ``ControlledBlock`` child already reports its full width (controls
included), so no child is re-interpreted here.
"""
required = 0
for b in blocks:
tq = getattr(b, "target_qubits", None)
if tq is not None:
required = max(required, max(tq) + 1 if tq else 0)
else:
required = max(required, getattr(b, "n_qubits", 0))
return max(required, 1)
# ── Build: walk children, populate self via add_child ─────────────
[docs]
def add_child(self, child: qx.Block) -> None:
"""Append a child and schedule it for wiring at the next ``build()``.
Idempotent under rebuild: a composite built once, then given another
child, wires only that child when built again. Wiring is deferred —
this method clears the Python built flag, so ``flatten()`` raises
until the next ``build()`` re-enters ``build_vanilla``.
"""
if not isinstance(child, qx.Block):
raise TypeError(f"All elements must be Block instances, got {type(child)}")
self.blocks.append(child)
self.symbols = None # re-published from the wider stream at build()
self._built = False
[docs]
def build_vanilla(self) -> None:
"""Wire the not-yet-wired children (all of them on the first build).
``add_wired_child`` auto-materialises any pending Python-level lazy
ops on the child, so a child created via ``set_symbols`` / ``dagger``
is folded into a concrete ``SimpleBlock`` at composition time.
"""
for block in self.blocks[self._n_wired :]:
self.add_wired_child(block)
self._n_wired = len(self.blocks)