Source code for uniqc.circuit_builder.named_circuit

"""
Named circuit definitions for reusable quantum subroutines.

This module provides:
- @circuit_def: Decorator to create named circuit definitions
- NamedCircuit: Reusable circuit definition with signature

Named circuits can be applied to parent circuits with qubit mapping
and parameter binding, similar to QASM3 gate definitions.

Example usage::

    @circuit_def(name="bell_pair", qregs={"q": 2})
    def bell_pair(circ, q):
        circ.h(q[0])
        circ.cnot(q[0], q[1])
        return circ

    # Apply to parent circuit
    c = Circuit(qregs={"data": 4})
    bell_pair(c, qreg_mapping={"q": [c.get_qreg("data")[0], c.get_qreg("data")[1]]})
"""

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING

from .qcircuit import Circuit

if TYPE_CHECKING:
    from .qubit import QReg, QRegSlice, Qubit

__all__ = ["circuit_def", "NamedCircuit"]


[docs] class NamedCircuit: """Reusable circuit definition with named qregs and parameters. A NamedCircuit is a template for a quantum subroutine that can be instantiated multiple times with different qubit mappings and parameter values. Attributes: name: Circuit definition name qregs: Dictionary mapping qreg names to sizes params: List of parameter names builder: Function that builds the circuit body Example: >>> @circuit_def(name="u3", qregs={"q": 1}, params=["theta", "phi", "lam"]) ... def u3_gate(circ, q, theta, phi, lam): ... circ.rz(q[0], phi) ... circ.ry(q[0], theta) ... circ.rz(q[0], lam) ... return circ ... >>> u3_gate.num_qubits # 1 >>> u3_gate.num_parameters # 3 """ def __init__( self, name: str, qregs: dict[str, int] | list[str] | None = None, params: list[str] | None = None, builder: Callable | None = None, ) -> None: """Initialize a NamedCircuit. Args: name: Circuit definition name qregs: Qubit register specification (dict or list of names with size 1) params: Parameter names builder: Function that builds the circuit body """ self._name = name # Normalize qregs to dict if qregs is None: self._qregs: dict[str, int] = {} elif isinstance(qregs, dict): self._qregs = qregs elif isinstance(qregs, list): # Each name gets size 1 self._qregs = dict.fromkeys(qregs, 1) else: self._qregs = {} self._params = params or [] self._builder = builder @property def name(self) -> str: return self._name @property def qregs(self) -> dict[str, int]: return self._qregs @property def params(self) -> list[str]: return self._params @property def num_qubits(self) -> int: """Total number of qubits used by this circuit.""" return sum(self._qregs.values()) @property def num_parameters(self) -> int: """Number of parameters.""" return len(self._params) def __call__( self, circuit: Circuit, qreg_mapping: dict[str, QReg | QRegSlice | Qubit | list[int] | list[Qubit]] | None = None, param_values: dict[str, float] | list[float] | None = None, ) -> Circuit: """Apply this circuit definition to a parent circuit. Args: circuit: Parent circuit to add gates to qreg_mapping: Map this circuit's qreg names to parent qubits - QReg: Use entire register - QRegSlice: Use slice of register - Qubit: Use single qubit - List[int]: Explicit qubit indices - List[Qubit]: List of Qubit objects param_values: Concrete values for parameters - dict: Mapping parameter names to values - list: Values in order of params list Returns: The modified parent circuit """ if self._builder is None: raise ValueError(f"NamedCircuit '{self._name}' has no builder function") # Normalize qreg_mapping if qreg_mapping is None: qreg_mapping = {} # Resolve qreg mappings to concrete qubit references resolved_mapping: dict[str, list[int]] = {} for qreg_name, target in qreg_mapping.items(): if isinstance(target, int): resolved_mapping[qreg_name] = [target] elif hasattr(target, "__iter__") and not isinstance(target, str): # QReg, QRegSlice, or list resolved = circuit._resolve_qubit(target) if isinstance(resolved, list): resolved_mapping[qreg_name] = resolved else: resolved_mapping[qreg_name] = [resolved] else: # Single Qubit resolved = circuit._resolve_qubit(target) resolved_mapping[qreg_name] = [resolved] # Normalize param_values if param_values is None: param_dict: dict[str, float] = {} elif isinstance(param_values, dict): param_dict = param_values else: # List - map to params in order if len(param_values) != len(self._params): raise ValueError(f"Expected {len(self._params)} parameters, got {len(param_values)}") param_dict = dict(zip(self._params, param_values, strict=True)) # Create qreg proxies for the builder function from .qubit import QReg as QRegClass qreg_proxies: dict[str, QRegClass] = {} for qreg_name, indices in resolved_mapping.items(): # Create a temporary QReg that maps to the resolved indices size = self._qregs.get(qreg_name, len(indices)) # Create proxy with base_index=0, then manually set qubit resolution proxy = QRegClass(name=qreg_name, size=size, base_index=indices[0] if indices else 0) # Override the __getitem__ behavior by storing indices proxy._resolved_indices = indices # type: ignore[attr-defined] qreg_proxies[qreg_name] = proxy # Create a wrapper circuit that maps qubits class QregProxyWrapper: """Wrapper that provides qreg access with resolved indices.""" def __init__(self, indices: list[int]): self._indices = indices def __getitem__(self, key: int) -> int: return self._indices[key] def __len__(self) -> int: return len(self._indices) # Build argument list for builder # First arg is circuit, then qregs in order of qregs keys, then params builder_args = [circuit] # Add qreg proxies for qreg_name in self._qregs: if qreg_name in resolved_mapping: indices = resolved_mapping[qreg_name] proxy = QregProxyWrapper(indices) builder_args.append(proxy) else: raise ValueError(f"Missing qreg mapping for '{qreg_name}'") # Add parameter values for param_name in self._params: if param_name in param_dict: builder_args.append(param_dict[param_name]) else: # Pass None for unbound parameters (may cause issues downstream) builder_args.append(None) # Call the builder self._builder(*builder_args) return circuit
[docs] def build_standalone( self, param_values: dict[str, float] | list[float] | None = None, ) -> Circuit: """Build a standalone Circuit with this definition. Creates a new Circuit with qregs matching this definition's signature. Args: param_values: Optional parameter values Returns: A new Circuit instance """ c = Circuit(qregs=self._qregs) # Apply this definition to the new circuit with default mapping # Use integer indices instead of QReg objects to avoid resolution issues qreg_mapping = {} offset = 0 for name, size in self._qregs.items(): qreg_mapping[name] = list(range(offset, offset + size)) offset += size return self(c, qreg_mapping=qreg_mapping, param_values=param_values)
[docs] def to_originir_def(self) -> str: """Export as OriginIR DEF block. Note: This is a placeholder. Full DEF export requires symbolic parameter support in OriginIR format. """ lines = [] # Header: DEF name(qreg_list) (param_list) qreg_str = ", ".join(f"q[{i}]" for i in range(self.num_qubits)) if self._params: param_str = ", ".join(self._params) lines.append(f"DEF {self._name}({qreg_str}) ({param_str})") else: lines.append(f"DEF {self._name}({qreg_str})") # Build body circuit - use qreg_mapping with integer indices if self._builder: qreg_mapping = {} offset = 0 for name, size in self._qregs.items(): qreg_mapping[name] = list(range(offset, offset + size)) offset += size c = Circuit(qregs=self._qregs) self(c, qreg_mapping=qreg_mapping) for line in c.originir.split("\n"): if line.startswith("QINIT") or line.startswith("CREG") or line.startswith("MEASURE"): continue if line.strip(): lines.append(line) lines.append("ENDDEF") return "\n".join(lines)
def __repr__(self) -> str: qregs_str = ", ".join(f"{k}:{v}" for k, v in self._qregs.items()) params_str = ", ".join(self._params) return f"NamedCircuit({self._name!r}, qregs={{{qregs_str}}}, params=[{params_str}])"
[docs] def circuit_def( name: str, qregs: dict[str, int] | list[str] | None = None, params: list[str] | None = None, ) -> Callable: """Decorator to create a NamedCircuit definition. Args: name: Circuit definition name qregs: Qubit register specification - dict: {name: size} pairs - list: Names (each with size 1) params: Parameter names Returns: Decorator that wraps a function into a NamedCircuit Example: >>> @circuit_def(name="bell_pair", qregs={"q": 2}) ... def bell_pair(circ, q): ... circ.h(q[0]) ... circ.cnot(q[0], q[1]) ... return circ ... >>> c = Circuit(2) >>> bell_pair(c, qreg_mapping={"q": [0, 1]}) """ def decorator(func: Callable) -> NamedCircuit: return NamedCircuit( name=name, qregs=qregs, params=params, builder=func, ) return decorator