Source code for uniqc.circuit_builder.qcircuit

"""Quantum circuit builder with OriginIR and OpenQASM 2.0 output.

This module provides a Circuit class for building quantum circuits programmatically.
It supports various quantum gates, controlled operations, dagger (adjoint) blocks,
and measurement operations. The circuit can be exported to OriginIR or OpenQASM format.

Key exports:
    Circuit: Main quantum circuit builder class.
    OpcodeType: Type alias for opcode tuples.
"""

from __future__ import annotations

from copy import deepcopy
from typing import TYPE_CHECKING, Union

from uniqc.exceptions import CircuitTranslationError

from .opcode import (
    make_header_originir,
    make_header_qasm,
    make_measure_originir,
    make_measure_qasm,
    opcode_to_line_originir,
    opcode_to_line_originir_official,
    opcode_to_line_qasm,
)

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

    try:
        import qiskit

        _QiskitCircuit = qiskit.QuantumCircuit
    except ImportError:
        _QiskitCircuit = None  # type: ignore[assignment,misc]
    try:
        from pyqpanda3.intermediate_compiler import (
            QProg as _PyQProg,
        )

        _PyQCircuit = _PyQProg
    except ImportError:
        _PyQCircuit = None  # type: ignore[assignment,misc]
else:
    _QiskitCircuit = None
    _PyQCircuit = None

# Opcode: (op_name, qubits, cbits, params, dagger, control_qubits)
QubitSpec = int | list[int]
CbitSpec = int | list[int] | None
ParamSpec = float | list[float] | tuple[float, ...] | None
OpCode = tuple[str, QubitSpec, CbitSpec, ParamSpec, bool, QubitSpec]

# Extended types for Qubit/QRegSlice support
QubitInput = Union[int, "Qubit", "QRegSlice", list]

# The universal circuit input type accepted by :func:`~uniqc.compile.compile`,
# :class:`~uniqc.simulator.Simulator`, and :func:`~uniqc.submit_task`.
#
# At runtime this is ``Union[Circuit, str]``.  ``qiskit.QuantumCircuit`` and
# ``pyqpanda3.QProg`` are also accepted but are resolved at type-check time
# only (via ``TYPE_CHECKING``) to avoid hard import requirements.
#
# See :class:`Circuit` class docstring for full details.
AnyQuantumCircuit = Union["Circuit", str]

__all__ = ["Circuit", "OpcodeType", "AnyQuantumCircuit"]

# Backward-compatible type alias
OpcodeType = OpCode


[docs] class CircuitControlContext: """Context manager for controlled gate blocks.""" c: Circuit control_list: tuple[int, ...] def __init__(self, c: Circuit, control_list: tuple[int, ...]) -> None: self.c = c self.control_list = control_list def _qubit_list(self) -> str: ret = "" for q in self.control_list: ret += f"q[{q}], " return ret[:-2] def __enter__(self) -> None: # Keep circuit_str for backward-compat with tests that inspect it directly. ret = "CONTROL " + self._qubit_list() + "\n" self.c.circuit_str += ret # Push controls onto active-control stack so add_gate can merge them. self.c._control_stack.append(tuple(self.control_list)) self.c._active_controls = self.c._active_controls + list(self.control_list) def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def] self.c.circuit_str += "ENDCONTROL\n" # Pop the controls this context pushed. if self.c._control_stack: popped = self.c._control_stack.pop() self.c._active_controls = self.c._active_controls[: len(self.c._active_controls) - len(popped)]
[docs] class CircuitDagContext: """Context manager for dagger (adjoint) gate blocks.""" c: Circuit def __init__(self, c: Circuit) -> None: self.c = c def __enter__(self) -> None: self.c.circuit_str += "DAGGER\n" self.c._active_dagger = not self.c._active_dagger def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def] self.c.circuit_str += "ENDDAGGER\n" self.c._active_dagger = not self.c._active_dagger
[docs] class Circuit: """Quantum circuit builder that generates OriginIR and OpenQASM output. Attributes ---------- used_qubit_list : list[int] Qubits referenced in the circuit. circuit_str : str Raw string builder used by context managers. max_qubit : int Highest qubit index used. qubit_num : int Total number of qubits. cbit_num : int Total number of classical bits. measure_list : list[int] Qubits scheduled for measurement. opcode_list : list[OpCode] Internal list of gate opcodes. _qregs : dict[str, QReg] Named quantum registers (if created with qregs parameter). .. rubric:: AnyQuantumCircuit — the universal input type Most public APIs (:func:`~uniqc.compile.compile`, :class:`~uniqc.simulator.Simulator`, :func:`~uniqc.submit_task`) accept :data:`AnyQuantumCircuit`, which is a union of: * :class:`Circuit` — this class * ``str`` — OriginIR or OpenQASM 2.0 (auto-detected from content) * ``qiskit.QuantumCircuit`` — converted via QASM round-trip * ``pyqpanda3.QProg`` — converted via OriginIR round-trip Use :meth:`to_qiskit_circuit` or :meth:`to_pyqpanda3_circuit` to convert back to external formats. """ used_qubit_list: list[int] circuit_str: str max_qubit: int qubit_num: int cbit_num: int measure_list: list[int] opcode_list: list[OpCode] _qregs: dict[str, QReg] def __init__( self, qregs: dict[str, int] | list[QReg] | int | None = None, param_dict: dict[str, object] | None = None, ) -> None: """Initialize a quantum circuit. Args: qregs: Optional qubit register specification. Can be: - dict[str, int]: Mapping of register names to sizes, e.g., {"a": 4, "b": 2} - list[QReg]: List of QReg objects - int: Total number of qubits (backward compatible) - None: No predefined registers (backward compatible) param_dict: Optional mapping of parameter names to ``torch.Tensor`` values. Gate methods accept parameter names (strings) which are resolved through this dict and auto-registered in :pyattr:`param_map`. Examples: >>> # Backward compatible - no registers >>> c = Circuit() >>> # Backward compatible - fixed qubit count >>> c = Circuit(4) >>> # Named registers >>> c = Circuit(qregs={"data": 4, "ancilla": 2}) >>> # Using QReg objects >>> from uniqc.circuit_builder import QReg >>> qr_a = QReg(name="a", size=4) >>> c = Circuit(qregs=[qr_a]) >>> # Differentiable circuit with named parameters >>> params = {"theta": nn.Parameter(torch.tensor(0.5))} >>> c = Circuit(1, param_dict=params) >>> c.ry(0, "theta") """ from .qubit import QReg as QRegClass self.used_qubit_list = [] self.max_qubit = 0 self.qubit_num = 0 self.cbit_num = 0 self.measure_list = [] self.opcode_list = [] self.circuit_str = "" # Named register storage self._qregs = {} # Active-context state: accumulated control qubits and dagger flag for # gates added inside with-control / with-dagger blocks. self._active_controls: list[int] = [] self._active_dagger: bool = False # Stack used by set_control / unset_control to remember each push size. self._control_stack: list[tuple[int, ...]] = [] # Named parameters attached to this circuit (for parametric circuits) self._params: Parameters | None = None # Tensor parameter map: opcode index -> torch.Tensor # Enables differentiable circuit execution (no hard torch dependency). self.param_map: dict = {} # Named parameter dict (optional) for name-based gate param references. self._param_dict: dict[str, object] | None = param_dict # Auto-created nn.Parameters keyed by ("GATE_NAME", opcode_idx). self._auto_params: dict[tuple[str, int], object] = {} # QRAM declarations: name -> (addr_size, data_size) self.qram_declarations: dict[str, tuple[int, int]] = {} # Handle qregs parameter if qregs is not None: if isinstance(qregs, int): # Backward compatible: Circuit(4) sets qubit_num directly self.qubit_num = qregs self.max_qubit = max(0, qregs - 1) elif isinstance(qregs, dict): # Create QReg objects from dict base_index = 0 for name, size in qregs.items(): qreg = QRegClass(name=name, size=size, base_index=base_index) self._qregs[name] = qreg base_index += size self.qubit_num = base_index self.max_qubit = max(0, base_index - 1) elif isinstance(qregs, list): # Use provided QReg objects, updating base_index base_index = 0 for qreg in qregs: qreg.base_index = base_index self._qregs[qreg.name] = qreg base_index += qreg.size self.qubit_num = base_index self.max_qubit = max(0, base_index - 1) @property def qregs(self) -> dict[str, QReg]: """Return the named quantum registers.""" return self._qregs
[docs] def get_qreg(self, name: str) -> QReg: """Get a named quantum register by name. Args: name: Register name Returns: QReg object Raises: KeyError: If register name not found """ if name not in self._qregs: raise KeyError(f"QReg '{name}' not found. Available: {list(self._qregs.keys())}") return self._qregs[name]
def _resolve_qubit(self, qubit: QubitInput) -> int | list[int]: """Resolve a qubit reference to integer index(es). Args: qubit: Qubit reference - can be int, Qubit, QReg, QRegSlice, or list Returns: Integer qubit index or list of indices """ from .qubit import QReg as QRegClass from .qubit import QRegSlice as QRegSliceClass from .qubit import Qubit as QubitClass if isinstance(qubit, int): return qubit elif isinstance(qubit, QubitClass): return int(qubit) elif isinstance(qubit, QRegClass): # QReg - return all qubit indices return [int(q) for q in qubit.qubits] elif isinstance(qubit, QRegSliceClass): return [int(q) for q in qubit] elif isinstance(qubit, list): # Recursively resolve list elements resolved = [] for q in qubit: if isinstance(q, int): resolved.append(q) elif isinstance(q, QubitClass): resolved.append(int(q)) elif isinstance(q, QRegClass): resolved.extend(int(qi) for qi in q.qubits) elif isinstance(q, QRegSliceClass): resolved.extend(int(qi) for qi in q) else: raise TypeError(f"Unsupported qubit type in list: {type(q)}") return resolved else: raise TypeError(f"Unsupported qubit type: {type(qubit)}")
[docs] def copy(self) -> Circuit: """Return a deep copy of this circuit.""" new_circuit = Circuit() new_circuit.used_qubit_list = self.used_qubit_list.copy() new_circuit.max_qubit = self.max_qubit new_circuit.qubit_num = self.qubit_num new_circuit.cbit_num = self.cbit_num new_circuit.measure_list = self.measure_list.copy() new_circuit.opcode_list = self.opcode_list.copy() new_circuit.circuit_str = self.circuit_str new_circuit._active_controls = self._active_controls.copy() new_circuit._active_dagger = self._active_dagger new_circuit._control_stack = list(self._control_stack) new_circuit.param_map = dict(self.param_map) new_circuit._param_dict = self._param_dict # shared reference new_circuit._auto_params = dict(self._auto_params) return new_circuit
def _make_originir_circuit(self) -> str: qram_header = "" for name, (addr_size, data_size) in self.qram_declarations.items(): qram_header += f"QRAMDECL {name} {addr_size},{data_size}\n" header = qram_header + make_header_originir(self.qubit_num, self.cbit_num) circuit_str = "\n".join([opcode_to_line_originir(op) for op in self.opcode_list]) measure = make_measure_originir(self.measure_list) return header + circuit_str + "\n" + measure def _make_qasm_circuit(self) -> str: if self.qram_declarations: raise CircuitTranslationError( "Circuit contains QRAM operations which cannot be exported to " "OpenQASM 2.0. QRAM is an OriginIR-ext-only feature.", source_format="originir-ext", target_format="qasm2", ) from .translate_qasm2_oir import collect_qasm2_custom_gates custom_gates = collect_qasm2_custom_gates(self.opcode_list) header = make_header_qasm(self.qubit_num, self.cbit_num, custom_gates=custom_gates) circuit_str = "\n".join([opcode_to_line_qasm(op, self.qubit_num) for op in self.opcode_list]) measure = make_measure_qasm(self.measure_list) return header + circuit_str + "\n" + measure def _make_originir_official_circuit(self) -> str: """Generate strict official OriginIR — decompose ext gates, block format.""" if self.qram_declarations: raise CircuitTranslationError( "Circuit contains QRAM operations which cannot be converted to " "official OriginIR. QRAM is an OriginIR-ext-only feature.", source_format="originir-ext", target_format="originir", ) from uniqc.compile.decompose import decompose_for_originir decomposed = decompose_for_originir(self) header = make_header_originir(decomposed.qubit_num, decomposed.cbit_num) circuit_str = "\n".join( [opcode_to_line_originir_official(op) for op in decomposed.opcode_list] ) measure = make_measure_originir(decomposed.measure_list) return header + circuit_str + "\n" + measure @property def circuit(self) -> str: """Generate the circuit in OriginIR format.""" return self._make_originir_circuit() @property def originir(self) -> str: """Generate the circuit in OriginIR format.""" return self._make_originir_circuit() @property def qasm(self) -> str: """Generate the circuit in OpenQASM format.""" return self._make_qasm_circuit()
[docs] @classmethod def from_qasm(cls, qasm_str: str) -> Circuit: """Create a Circuit from an OpenQASM 2.0 string. Args: qasm_str: OpenQASM 2.0 formatted circuit string. Returns: A new Circuit instance. """ from uniqc.compile.qasm.qasm_base_parser import OpenQASM2_BaseParser parser = OpenQASM2_BaseParser() parser.parse(qasm_str) return parser.to_circuit()
[docs] @classmethod def from_originir(cls, originir_str: str) -> Circuit: """Create a Circuit from an OriginIR string. Args: originir_str: OriginIR formatted circuit string. Returns: A new Circuit instance. """ from uniqc.compile.originir.originir_base_parser import OriginIR_BaseParser parser = OriginIR_BaseParser() parser.parse(originir_str) return parser.to_circuit()
[docs] def to_qasm(self) -> str: """Export the circuit as an OpenQASM 2.0 string.""" return self.qasm
[docs] def to_originir(self) -> str: """Export the circuit as an OriginIR string.""" return self.originir
[docs] def to_extended_originir(self) -> str: """Export the circuit in extended OriginIR format (full form with QINIT/CREG/MEASURE).""" return self.originir
[docs] def to_originir_official(self) -> str: """Export the circuit as strict official OriginIR. Extended gates are decomposed to the official gate set, and inline ``dagger`` / ``controlled_by`` syntax is replaced with block-level ``DAGGER`` / ``CONTROL`` delimiters. The output is suitable for submission to OriginQ cloud. """ return self._make_originir_official_circuit()
@property def originir_official(self) -> str: """Generate the circuit in strict official OriginIR format.""" return self._make_originir_official_circuit()
[docs] @classmethod def from_originir_ext(cls, originir_ext_str: str) -> Circuit: """Create a Circuit from an OriginIR-ext string. Equivalent to :meth:`from_originir` — both parse the same superset syntax. This alias makes the intent explicit when working with OriginIR-ext source. """ return cls.from_originir(originir_ext_str)
[docs] def to_qiskit_circuit(self): """Convert to a ``qiskit.QuantumCircuit``. Returns: qiskit.QuantumCircuit equivalent of this circuit. Raises: ImportError: If qiskit is not installed. """ try: from qiskit import qasm2 except ImportError: raise ImportError( "qiskit is required for to_qiskit_circuit(). Install it with: pip install qiskit" ) from None return qasm2.loads(self.qasm)
[docs] def to_pyqpanda3_circuit(self): """Convert to a pyqpanda3 ``QProg``. Returns: pyqpanda3 QProg equivalent of this circuit. Raises: ImportError: If pyqpanda3 is not installed. """ try: from pyqpanda3.intermediate_compiler import ( convert_originir_string_to_qprog, ) except ImportError: raise ImportError( "pyqpanda3 is required for to_pyqpanda3_circuit(). Install it with: pip install pyqpanda3" ) from None return convert_originir_string_to_qprog(self.originir)
[docs] def record_qubit(self, qubits: int | list[int]) -> None: """Record the qubits used in the circuit.""" for qubit in qubits if isinstance(qubits, list) else [qubits]: if qubit not in self.used_qubit_list: self.used_qubit_list.append(qubit) self.max_qubit = max(self.max_qubit, qubit) self.qubit_num = self.max_qubit + 1
def _resolve_param_strings(self, params: ParamSpec) -> ParamSpec: """Resolve string parameter names via ``_param_dict``. Returns the params with any name strings replaced by the corresponding tensor/value from ``_param_dict``. If no ``_param_dict`` is set or params contains no strings, returns *params* unchanged. """ if params is None or self._param_dict is None: return params if isinstance(params, str): return self._param_dict[params] if isinstance(params, (list, tuple)): resolved = [self._param_dict[p] if isinstance(p, str) else p for p in params] return type(params)(resolved) # preserve list/tuple type return params
[docs] def add_gate( self, operation: str, qubits: QubitInput, cbits: CbitSpec = None, params: ParamSpec = None, dagger: bool = False, control_qubits: QubitInput = None, has_param: bool = False, trainable: bool = True, init_params: object = None, ) -> None: """Add a gate to the circuit. Args: operation: Gate name (e.g., "H", "CNOT", "RX") qubits: Target qubit(s) - can be int, Qubit, QRegSlice, or list cbits: Classical bit(s) for measurement params: Gate parameters dagger: Whether to apply dagger (adjoint) control_qubits: Control qubit(s) has_param: If *True*, automatically create an ``nn.Parameter`` for this gate's rotation angle(s). The created parameter is stored in :pyattr:`_auto_params` and registered in :pyattr:`param_map`. Requires PyTorch. trainable: Whether the auto-created parameter is trainable (``requires_grad``). Only used when *has_param=True*. init_params: Custom initial value(s) for the auto-created parameter. A scalar or list/tuple matching the gate's *num_params*. Defaults to ``Uniform(-π, π)`` (TorchQuantum convention). Only used when *has_param=True*. """ # Resolve qubit references to integers resolved_qubits = self._resolve_qubit(qubits) resolved_controls = self._resolve_qubit(control_qubits) if control_qubits is not None else None if operation in {"BARRIER", "I"} or operation in self.qram_declarations: # These gates have no controlled / dagger semantics; store as-is. merged_controls: QubitSpec = resolved_controls merged_dagger = dagger else: # Merge explicit control_qubits with any active context controls. base: list[int] = list(resolved_controls) if resolved_controls is not None else [] if self._active_controls: overlap = set(base) & set(self._active_controls) if overlap: raise ValueError( f"Qubit(s) {sorted(overlap)} appear in both " "control_qubits and an enclosing control() context block." ) base = base + list(self._active_controls) merged_controls = base if base else None # type: ignore[assignment] # XOR active-dagger with the explicit dagger flag. merged_dagger = dagger ^ self._active_dagger # Resolve string param names via _param_dict, then detect tensors. opcode_params = self._resolve_param_strings(params) _has_torch = "torch" in __import__("sys").modules if _has_torch and opcode_params is not None: import torch as _torch if isinstance(opcode_params, _torch.Tensor): opcode_idx = len(self.opcode_list) self.param_map[opcode_idx] = opcode_params opcode_params = float(opcode_params.detach().cpu().item()) elif isinstance(opcode_params, (list, tuple)) and any(isinstance(p, _torch.Tensor) for p in opcode_params): opcode_idx = len(self.opcode_list) self.param_map[opcode_idx] = _torch.stack( [p if isinstance(p, _torch.Tensor) else _torch.tensor(float(p)) for p in opcode_params] ) opcode_params = [float(p.detach().cpu().item()) if isinstance(p, _torch.Tensor) else float(p) for p in opcode_params] # has_param: auto-create nn.Parameter and register in param_map. if has_param: opcode_idx = len(self.opcode_list) if opcode_params is None: opcode_params = self._auto_init_param( operation, opcode_idx, trainable=trainable, init_params=init_params, ) elif not _has_torch: raise ImportError("has_param requires PyTorch. Install with: pip install unified-quantum[pytorch]") opcode: OpCode = (operation, resolved_qubits, cbits, opcode_params, merged_dagger, merged_controls) # type: ignore[assignment] self.opcode_list.append(opcode) self.record_qubit(resolved_qubits if isinstance(resolved_qubits, list) else [resolved_qubits])
[docs] def add_circuit(self, other: Circuit) -> None: """Add all gates from another circuit into this circuit.""" for op in other.opcode_list: self.add_gate(*op)
# ------------------------------------------------------------------ # Tensor parameter support (for differentiable circuit execution) # ------------------------------------------------------------------
[docs] def set_param(self, opcode_idx: int, tensor) -> None: """Register a differentiable tensor for the parametric gate at *opcode_idx*. Args: opcode_idx: Index into :pyattr:`opcode_list`. tensor: A ``torch.Tensor`` (typically with ``requires_grad=True``). Raises: IndexError: If *opcode_idx* is out of range. """ if opcode_idx < 0 or opcode_idx >= len(self.opcode_list): raise IndexError( f"opcode_idx {opcode_idx} out of range [0, {len(self.opcode_list)})" ) self.param_map[opcode_idx] = tensor
[docs] def set_param_last(self, tensor) -> int: """Register a tensor for the most recently added gate. Convenience wrapper around :pymeth:`set_param` for the common pattern of registering a parameter immediately after adding a gate. Returns: The opcode index that was registered. Raises: IndexError: If the circuit has no gates. """ if not self.opcode_list: raise IndexError("Cannot set_param_last on an empty circuit (no gates)") idx = len(self.opcode_list) - 1 self.param_map[idx] = tensor return idx
[docs] def get_param(self, opcode_idx: int): """Get the tensor parameter registered for *opcode_idx*. Raises: KeyError: If no tensor is registered for this opcode. """ return self.param_map[opcode_idx]
@property def tensor_params(self) -> list: """Return all registered tensor parameters (for passing to an optimizer).""" return list(self.param_map.values())
[docs] def has_tensor_params(self) -> bool: """Check whether this circuit has any registered tensor parameters.""" return len(self.param_map) > 0
@property def has_param(self) -> bool: """TorchQuantum-aligned alias for :meth:`has_tensor_params`. Returns ``True`` only when at least one parameter is a tensor (i.e., actually trainable). Pure Python-float parameters return ``False``. This is a no-argument property, distinct from the ``has_param`` keyword argument on :meth:`add_gate` / convenience gate methods (which opts-in to auto-creating an ``nn.Parameter`` for that gate). """ return self.has_tensor_params() # ---- has_param auto-initialization --------------------------------------- # Number of rotation angles per parametric gate. _PARAM_COUNTS: dict[str, int] = { "RX": 1, "RY": 1, "RZ": 1, "U1": 1, "RPhi": 2, "U2": 2, "U3": 3, "XX": 1, "YY": 1, "ZZ": 1, } def _auto_init_param( self, gate_name: str, opcode_idx: int, trainable: bool = True, init_params: object = None, ): """Create an ``nn.Parameter`` for *gate_name* and register it. Shape follows TorchQuantum convention: ``[1, num_params]``. Default initialization: ``Uniform(-π, π)`` when *init_params* is *None*. Returns a float placeholder for the opcode ``params`` field. """ import math as _math import torch as _torch n = self._PARAM_COUNTS.get(gate_name, 1) if init_params is not None: if isinstance(init_params, (list, tuple)): p = _torch.nn.Parameter(_torch.tensor(init_params, dtype=_torch.float32).reshape(1, -1)) else: p = _torch.nn.Parameter(_torch.full((1, n), float(init_params), dtype=_torch.float32)) else: p = _torch.nn.Parameter(_torch.empty(1, n).uniform_(-_math.pi, _math.pi)) p.requires_grad = trainable self._auto_params[(gate_name, opcode_idx)] = p # param_map stores the squeezed view for the simulator self.param_map[opcode_idx] = p.squeeze(0) if n > 1 else p[0, 0] vals = p.detach().cpu().flatten().tolist() return vals[0] if n == 1 else vals @property def params(self) -> list: """All auto-created ``nn.Parameter`` tensors (flat list for optimizers).""" return list(self._auto_params.values())
[docs] def get_params_by_gate(self, gate_name: str) -> list: """Return auto-created parameters for gates named *gate_name*. Example:: >>> c = Circuit(2) >>> c.ry(0, has_param=True) >>> c.ry(1, has_param=True) >>> c.rz(0, has_param=True) >>> len(c.get_params_by_gate("RY")) 2 """ gate_upper = gate_name.upper() return [p for (g, _), p in self._auto_params.items() if g == gate_upper]
@property def param_dict(self) -> dict[str, object] | None: """The named parameter dictionary, if provided at construction.""" return self._param_dict @param_dict.setter def param_dict(self, value: dict[str, object] | None) -> None: self._param_dict = value @property def depth(self) -> int: """Calculate the depth of the quantum circuit.""" qubit_depths: dict[int, int] = {} for opcode in self.opcode_list: op_name, qubits, _, _, _, control_qubits = opcode if op_name in ("I", "BARRIER") or op_name in self.qram_declarations: continue if not isinstance(qubits, list): qubits = [qubits] all_qubits = qubits + list(control_qubits) if control_qubits else qubits current_max_depth = 0 for q in all_qubits: current_max_depth = max(current_max_depth, qubit_depths.get(q, 0)) for q in all_qubits: qubit_depths[q] = current_max_depth + 1 if not qubit_depths: return 0 return max(qubit_depths.values())
[docs] def get_matrix(self): """Return the full unitary matrix of this circuit as ``np.ndarray``. Qubit 0 is treated as the least-significant bit of the statevector index. The returned matrix uses the convention ``state_out = U @ state_in`` and gates are applied in the same order as ``opcode_list``. Raises: NotMatrixableError: If the circuit contains MEASURE / CONTROL / DAGGER scope opcodes that have no unitary representation. """ from .matrix import get_matrix as _get_matrix return _get_matrix(self)
# ─────────────────── Single-qubit gates (no parameters) ───────────────────
[docs] def identity(self, qn: QubitInput) -> None: """Apply the identity (no-op) gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("I", qn)
[docs] def h(self, qn: QubitInput) -> None: """Apply single-qubit Hadamard gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("H", qn)
[docs] def x(self, qn: QubitInput) -> None: """Apply Pauli-X (NOT) gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("X", qn)
[docs] def y(self, qn: QubitInput) -> None: """Apply Pauli-Y gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("Y", qn)
[docs] def z(self, qn: QubitInput) -> None: """Apply Pauli-Z gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("Z", qn)
[docs] def sx(self, qn: QubitInput) -> None: """Apply square-root-of-X (SX) gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("SX", qn)
[docs] def sxdg(self, qn: QubitInput) -> None: """Apply conjugate-transpose of SX gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("SX", qn, dagger=True)
[docs] def s(self, qn: QubitInput) -> None: """Apply S (phase) gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("S", qn)
[docs] def sdg(self, qn: QubitInput) -> None: """Apply S-dagger (inverse phase) gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("S", qn, dagger=True)
[docs] def t(self, qn: QubitInput) -> None: """Apply T gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("T", qn)
[docs] def tdg(self, qn: QubitInput) -> None: """Apply T-dagger (inverse T) gate to qubit. Args: qn: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("T", qn, dagger=True)
# ─────────────────── Single-qubit parametric gates ───────────────────
[docs] def rx(self, qn: QubitInput, theta: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply RX rotation gate. Args: qn: Target qubit - can be int, Qubit, or QRegSlice theta: Rotation angle in radians. Omit when *has_param=True*. has_param: Auto-create an ``nn.Parameter`` for this gate. trainable: Whether the parameter is trainable (only with *has_param*). init_params: Custom initial value. Default: ``Uniform(-π, π)``. """ self.add_gate("RX", qn, params=theta, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def ry(self, qn: QubitInput, theta: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply RY rotation gate.""" self.add_gate("RY", qn, params=theta, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def rz(self, qn: QubitInput, theta: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply RZ rotation gate.""" self.add_gate("RZ", qn, params=theta, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def rphi(self, qn: QubitInput, theta: float = None, phi: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply RPhi rotation gate.""" params = [theta, phi] if not has_param else None self.add_gate("RPhi", qn, params=params, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def p(self, qn: QubitInput, lam: float) -> None: """Apply phase gate P(λ), equivalent to U1. Args: qn: Target qubit - can be int, Qubit, or QRegSlice lam: Phase angle in radians. """ self.add_gate("U1", qn, params=lam)
# ─────────────────── Two-qubit gates ───────────────────
[docs] def cnot(self, controller: QubitInput, target: QubitInput) -> None: """Apply CNOT (controlled-X) gate. Args: controller: Control qubit - can be int, Qubit, or QRegSlice target: Target qubit - can be int, Qubit, or QRegSlice """ self.add_gate("CNOT", [controller, target])
[docs] def cx(self, controller: QubitInput, target: QubitInput) -> None: """Apply CX gate (alias for CNOT). Args: controller: Control qubit - can be int, Qubit, or QRegSlice target: Target qubit - can be int, Qubit, or QRegSlice """ self.cnot(controller, target)
[docs] def cz(self, q1: QubitInput, q2: QubitInput) -> None: """Apply controlled-Z gate to two qubits. Args: q1: First qubit - can be int, Qubit, or QRegSlice q2: Second qubit - can be int, Qubit, or QRegSlice """ self.add_gate("CZ", [q1, q2])
[docs] def iswap(self, q1: QubitInput, q2: QubitInput) -> None: """Apply iSWAP gate to two qubits. Args: q1: First qubit - can be int, Qubit, or QRegSlice q2: Second qubit - can be int, Qubit, or QRegSlice """ self.add_gate("ISWAP", [q1, q2])
[docs] def swap(self, q1: QubitInput, q2: QubitInput) -> None: """Apply SWAP gate to two qubits. Args: q1: First qubit - can be int, Qubit, or QRegSlice q2: Second qubit - can be int, Qubit, or QRegSlice """ self.add_gate("SWAP", [q1, q2])
# ─────────────────── Controlled parametric gates ───────────────────
[docs] def crx(self, control: QubitInput, target: QubitInput, theta: float) -> None: """Apply controlled-RX gate. Args: control: Control qubit. target: Target qubit. theta: Rotation angle in radians. """ self.add_gate("RX", target, params=theta, control_qubits=[control])
[docs] def cry(self, control: QubitInput, target: QubitInput, theta: float) -> None: """Apply controlled-RY gate. Args: control: Control qubit. target: Target qubit. theta: Rotation angle in radians. """ self.add_gate("RY", target, params=theta, control_qubits=[control])
[docs] def crz(self, control: QubitInput, target: QubitInput, theta: float) -> None: """Apply controlled-RZ gate. Args: control: Control qubit. target: Target qubit. theta: Rotation angle in radians. """ self.add_gate("RZ", target, params=theta, control_qubits=[control])
[docs] def cp(self, control: QubitInput, target: QubitInput, lam: float) -> None: """Apply controlled-phase gate (equivalent to CU1). Args: control: Control qubit. target: Target qubit. lam: Phase angle in radians. """ self.add_gate("U1", target, params=lam, control_qubits=[control])
[docs] def cu(self, control: QubitInput, target: QubitInput, theta: float, phi: float, lam: float) -> None: """Apply controlled-U3 gate. Args: control: Control qubit. target: Target qubit. theta: Rotation angle in radians. phi: Phi angle in radians. lam: Lambda angle in radians. """ self.add_gate("U3", target, params=[theta, phi, lam], control_qubits=[control])
# ─────────────────── Three-qubit gates ───────────────────
[docs] def cswap(self, q1: QubitInput, q2: QubitInput, q3: QubitInput) -> None: """Apply CSWAP (Fredkin) gate to three qubits. Args: q1: Control qubit - can be int, Qubit, or QRegSlice q2: First target qubit q3: Second target qubit """ self.add_gate("CSWAP", [q1, q2, q3])
[docs] def toffoli(self, q1: QubitInput, q2: QubitInput, q3: QubitInput) -> None: """Apply Toffoli (CCNOT) gate to three qubits. Args: q1: First control qubit q2: Second control qubit q3: Target qubit """ self.add_gate("TOFFOLI", [q1, q2, q3])
# ─────────────────── Parametric gates ───────────────────
[docs] def u1(self, qn: QubitInput, lam: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply U1 single-parameter unitary gate.""" self.add_gate("U1", qn, params=lam, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def u2(self, qn: QubitInput, phi: float = None, lam: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply U2 two-parameter unitary gate.""" params = [phi, lam] if not has_param else None self.add_gate("U2", qn, params=params, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def u3(self, qn: QubitInput, theta: float = None, phi: float = None, lam: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply U3 three-parameter unitary gate.""" params = [theta, phi, lam] if not has_param else None self.add_gate("U3", qn, params=params, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def xx(self, q1: QubitInput, q2: QubitInput, theta: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply XX Ising interaction gate.""" self.add_gate("XX", [q1, q2], params=theta, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def yy(self, q1: QubitInput, q2: QubitInput, theta: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply YY Ising interaction gate.""" self.add_gate("YY", [q1, q2], params=theta, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def zz(self, q1: QubitInput, q2: QubitInput, theta: float = None, *, has_param: bool = False, trainable: bool = True, init_params: object = None) -> None: """Apply ZZ Ising interaction gate.""" self.add_gate("ZZ", [q1, q2], params=theta, has_param=has_param, trainable=trainable, init_params=init_params)
[docs] def xy(self, q1: QubitInput, q2: QubitInput, theta: float) -> None: """Apply XY Ising interaction gate. Args: q1: First qubit - can be int, Qubit, or QRegSlice q2: Second qubit - can be int, Qubit, or QRegSlice theta: Interaction angle in radians. """ self.add_gate("XY", [q1, q2], params=theta)
[docs] def phase2q(self, q1: QubitInput, q2: QubitInput, theta1: float, theta2: float, thetazz: float) -> None: """Apply two-qubit phase gate with local and ZZ terms. Args: q1: First qubit - can be int, Qubit, or QRegSlice q2: Second qubit - can be int, Qubit, or QRegSlice theta1: Local phase angle for q1 in radians. theta2: Local phase angle for q2 in radians. thetazz: ZZ interaction angle in radians. """ self.add_gate("PHASE2Q", [q1, q2], params=[theta1, theta2, thetazz])
[docs] def uu15(self, q1: QubitInput, q2: QubitInput, params: list[float]) -> None: """Apply general two-qubit UU15 gate with 15 parameters. Args: q1: First qubit - can be int, Qubit, or QRegSlice q2: Second qubit - can be int, Qubit, or QRegSlice params: List of 15 rotation parameters in radians. """ self.add_gate("UU15", [q1, q2], params=params)
[docs] def barrier(self, *qubits: QubitInput) -> None: """Insert a barrier across the specified qubits. Args: *qubits: Qubits to include in the barrier. """ self.add_gate("BARRIER", list(qubits))
[docs] def qram_declare(self, name: str, addr_size: int, data_size: int) -> None: """Declare a QRAM with the given address and data sizes. Args: name: Unique name for this QRAM. addr_size: Number of address qubits. data_size: Number of data qubits. """ if name in self.qram_declarations: raise ValueError(f"QRAM '{name}' is already declared in this circuit.") self.qram_declarations[name] = (addr_size, data_size)
[docs] def qram_call(self, name: str, *qubits: QubitInput) -> None: """Add a QRAM call to the circuit. Args: name: Name of a previously declared QRAM. *qubits: Qubit list (addr bits followed by data bits). """ if name not in self.qram_declarations: raise ValueError(f"QRAM '{name}' has not been declared. Call qram_declare() first.") addr_size, data_size = self.qram_declarations[name] total = addr_size + data_size resolved = self._resolve_qubit(list(qubits)) if isinstance(resolved, list) and len(resolved) != total: raise ValueError( f"QRAM '{name}' expects {total} qubits ({addr_size} addr + {data_size} data), " f"got {len(resolved)}." ) self.add_gate(name, list(qubits))
# ─────────────────── Measurement ───────────────────
[docs] def measure(self, *qubits: QubitInput) -> None: """Schedule qubits for measurement. Each qubit may be measured **at most once** per circuit. Calling ``measure(0)`` and then ``measure(0)`` again — or passing the same qubit twice in a single call (``measure(0, 0)``) — raises ``ValueError``. This guards against the common mistake of using ``measure(0, 1)`` to measure two qubits when ``cbit`` is meant to be implicit; use one ``measure(q)`` call per qubit instead, or pass distinct qubit indices. Args: *qubits: One or more qubits to measure — can be int, Qubit, or QRegSlice. Raises: ValueError: Called inside an active CONTROL or DAGGER context block, or any qubit would be measured more than once. """ if self._active_controls: raise ValueError("measure() cannot be called inside a control() context block.") if self._active_dagger: raise ValueError("measure() cannot be called inside a dagger() context block.") # Resolve all qubits to integers resolved_qubits = [] for q in qubits: resolved = self._resolve_qubit(q) if isinstance(resolved, list): resolved_qubits.extend(resolved) else: resolved_qubits.append(resolved) # Reject duplicate measurements (within this call AND against any # previously-recorded measurements). This catches `measure(0, 0)` # and silent failures like `measure(0); measure(0)` that previously # accumulated into the measurement list. existing = set(self.measure_list or []) seen: set[int] = set() for q in resolved_qubits: if q in seen or q in existing: raise ValueError( f"Qubit {q} is already measured in this circuit. " f"Each qubit may be measured at most once. " f"Did you mean `c.measure({q}); c.measure({q + 1})` " f"instead of `c.measure({q}, {q})`?" ) seen.add(q) self.record_qubit(resolved_qubits) if self.measure_list is None: self.measure_list = [] self.measure_list.extend(resolved_qubits) self.cbit_num = len(self.measure_list)
# ─────────────────── Control / Dagger context managers ───────────────────
[docs] def control(self, *args: QubitInput) -> CircuitControlContext: """Return a context manager that wraps gates in a CONTROL block. All gates added inside the ``with`` block will be executed only when all specified control qubits are in state ``|1>``. Args: *args: One or more control qubits - can be int, Qubit, or QRegSlice Returns: A :class:`CircuitControlContext` context manager. Raises: ValueError: No control qubits were supplied. """ # Resolve qubits to integers resolved = [] for q in args: r = self._resolve_qubit(q) if isinstance(r, list): resolved.extend(r) else: resolved.append(r) self.record_qubit(resolved) if len(resolved) == 0: raise ValueError("Controller qubit must not be empty.") return CircuitControlContext(self, tuple(resolved))
[docs] def set_control(self, *args: QubitInput) -> None: """Manually open a CONTROL block (low-level API; prefer :meth:`control`). Args: *args: Control qubits - can be int, Qubit, or QRegSlice """ # Resolve qubits to integers resolved = [] for q in args: r = self._resolve_qubit(q) if isinstance(r, list): resolved.extend(r) else: resolved.append(r) self.record_qubit(resolved) ret = "CONTROL " for q in resolved: ret += f"q[{q}], " self.circuit_str += ret[:-2] + "\n" # Update active-context state so add_gate picks up these controls. self._control_stack.append(tuple(resolved)) self._active_controls = self._active_controls + resolved
[docs] def unset_control(self) -> None: """Manually close a CONTROL block (low-level API; prefer :meth:`control`).""" self.circuit_str += "ENDCONTROL\n" if self._control_stack: popped = self._control_stack.pop() self._active_controls = self._active_controls[: len(self._active_controls) - len(popped)]
[docs] def dagger(self) -> CircuitDagContext: """Return a context manager that wraps gates in a DAGGER block. All gates added inside the ``with`` block will be conjugate-transposed (adjoint). Returns: A :class:`CircuitDagContext` context manager. """ return CircuitDagContext(self)
[docs] def set_dagger(self) -> None: """Manually open a DAGGER block (low-level API; prefer :meth:`dagger`).""" self.circuit_str += "DAGGER\n" self._active_dagger = not self._active_dagger
[docs] def unset_dagger(self) -> None: """Manually close a DAGGER block (low-level API; prefer :meth:`dagger`).""" self.circuit_str += "ENDDAGGER\n" self._active_dagger = not self._active_dagger
# ─────────────────── Remapping ───────────────────
[docs] def remapping(self, mapping: dict[int, int]) -> Circuit: """Create a new circuit with qubits remapped according to *mapping*.""" if not all(isinstance(k, int) and isinstance(v, int) and k >= 0 and v >= 0 for k, v in mapping.items()): raise TypeError("All keys and values in mapping must be non-negative integers.") if len(set(mapping.values())) != len(mapping.values()): raise ValueError("A physical qubit is assigned more than once.") for qubit in self.used_qubit_list: if qubit not in mapping: raise ValueError(f"At least one qubit is not appeared in mapping. (qubit : {qubit})") unique_qubit_set: set[int] = set() for qubit in mapping: if qubit in unique_qubit_set: raise ValueError(f"Qubit is used twice in the mapping. Given mapping : ({mapping})") unique_qubit_set.add(qubit) c = deepcopy(self) def remap_opcode(opcode: OpCode, mp: dict[int, int]) -> OpCode: op_name, qubits, cbits, params, dagger, control_qubits = opcode new_qubits = [mp[q] for q in qubits] if isinstance(qubits, list) else mp[qubits] if control_qubits is not None: new_control_qubits = ( [mp[q] for q in control_qubits] if isinstance(control_qubits, list) else mp[control_qubits] ) else: new_control_qubits = None return (op_name, new_qubits, cbits, params, dagger, new_control_qubits) c.opcode_list = [remap_opcode(op, mapping) for op in self.opcode_list] for i, old_qubit in enumerate(self.used_qubit_list): c.used_qubit_list[i] = mapping[old_qubit] for i, old_qubit in enumerate(self.measure_list): c.measure_list[i] = mapping[old_qubit] c.max_qubit = max(c.used_qubit_list) c.qubit_num = c.max_qubit + 1 c.cbit_num = len(c.measure_list) return c