"""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, Optional, Union
from .opcode import (
make_header_originir,
make_header_qasm,
make_measure_originir,
make_measure_qasm,
opcode_to_line_originir,
opcode_to_line_qasm,
)
if TYPE_CHECKING:
from .qubit import Qubit, QReg, QRegSlice
# 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]
__all__ = ["Circuit", "OpcodeType"]
# 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).
"""
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: Optional[dict[str, int] | list["QReg"] | int] = 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)
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])
"""
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, ...]] = []
# 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 Qubit as QubitClass
from .qubit import QReg as QRegClass
from .qubit import QRegSlice as QRegSliceClass
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)
return new_circuit
def _make_originir_circuit(self) -> str:
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:
header = make_header_qasm(self.qubit_num, self.cbit_num)
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
@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]
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
[docs]
def add_gate(
self,
operation: str,
qubits: QubitInput,
cbits: CbitSpec = None,
params: ParamSpec = None,
dagger: bool = False,
control_qubits: QubitInput = 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)
"""
# 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"}:
# 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
opcode: OpCode = (operation, resolved_qubits, cbits, 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)
@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"):
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())
# ─────────────────── 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:
"""Apply RX rotation gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
theta: Rotation angle in radians.
"""
self.add_gate("RX", qn, params=theta)
[docs]
def ry(self, qn: QubitInput, theta: float) -> None:
"""Apply RY rotation gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
theta: Rotation angle in radians.
"""
self.add_gate("RY", qn, params=theta)
[docs]
def rz(self, qn: QubitInput, theta: float) -> None:
"""Apply RZ rotation gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
theta: Rotation angle in radians.
"""
self.add_gate("RZ", qn, params=theta)
[docs]
def rphi(self, qn: QubitInput, theta: float, phi: float) -> None:
"""Apply RPhi rotation gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
theta: Polar rotation angle in radians.
phi: Azimuthal angle in radians.
"""
self.add_gate("RPhi", qn, params=[theta, phi])
# ─────────────────── 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])
# ─────────────────── 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:
"""Apply U1 single-parameter unitary gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
lam: Phase angle lambda in radians.
"""
self.add_gate("U1", qn, params=lam)
[docs]
def u2(self, qn: QubitInput, phi: float, lam: float) -> None:
"""Apply U2 two-parameter unitary gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
phi: Phi angle in radians.
lam: Lambda angle in radians.
"""
self.add_gate("U2", qn, params=[phi, lam])
[docs]
def u3(self, qn: QubitInput, theta: float, phi: float, lam: float) -> None:
"""Apply U3 three-parameter unitary gate.
Args:
qn: Target qubit - can be int, Qubit, or QRegSlice
theta: Theta angle in radians.
phi: Phi angle in radians.
lam: Lambda angle in radians.
"""
self.add_gate("U3", qn, params=[theta, phi, lam])
[docs]
def xx(self, q1: QubitInput, q2: QubitInput, theta: float) -> None:
"""Apply XX 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("XX", [q1, q2], params=theta)
[docs]
def yy(self, q1: QubitInput, q2: QubitInput, theta: float) -> None:
"""Apply YY 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("YY", [q1, q2], params=theta)
[docs]
def zz(self, q1: QubitInput, q2: QubitInput, theta: float) -> None:
"""Apply ZZ 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("ZZ", [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))
# ─────────────────── Measurement ───────────────────
[docs]
def measure(self, *qubits: QubitInput) -> None:
"""Schedule qubits for measurement.
Appends the given qubits to the measurement list. Multiple calls
accumulate measurements; classical bit indices are assigned in the
order qubits are added.
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.
"""
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)
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