"""Circuit adapter layer for converting UnifiedQuantum circuits to provider-native formats.
This module provides adapter classes for converting UnifiedQuantum Circuit objects
to native circuit formats used by different quantum computing platforms:
- OriginQ (pyqpanda)
- QuarkStudio (OpenQASM 2.0)
- IBM (qiskit)
- TianYan (QCIS text, via cqlib)
- LogicalQubit (lqcloud QuantumCircuit)
Usage::
from uniqc.backend_adapter.circuit_adapter import OriginQCircuitAdapter, QuarkCircuitAdapter, IBMCircuitAdapter
from uniqc.circuit_builder import Circuit
# Create a UnifiedQuantum circuit
circuit = Circuit()
circuit.h(0)
circuit.cnot(0, 1)
circuit.measure(0, 1)
# Convert to provider-native circuits
originq_adapter = OriginQCircuitAdapter()
pyqpanda_circuit = originq_adapter.adapt(circuit)
quark_adapter = QuarkCircuitAdapter()
qasm2 = quark_adapter.adapt(circuit)
ibm_adapter = IBMCircuitAdapter()
qiskit_circuit = ibm_adapter.adapt(circuit)
"""
from __future__ import annotations
__all__ = [
"CircuitAdapter",
"OriginQCircuitAdapter",
"QuarkCircuitAdapter",
"IBMCircuitAdapter",
"TianyanCircuitAdapter",
"LogicalQubitCircuitAdapter",
"originir_to_qcis",
"originir_to_lqcloud_circuit",
]
import abc
from typing import TYPE_CHECKING, Any, Generic, TypeVar
if TYPE_CHECKING:
from uniqc.circuit_builder.qcircuit import Circuit
# Type variable for provider-native circuit types
T = TypeVar("T")
[docs]
class CircuitAdapter(abc.ABC, Generic[T]):
"""Abstract base class for circuit adapters.
Provides a unified interface for converting UnifiedQuantum Circuit
objects to provider-native circuit formats.
"""
[docs]
@abc.abstractmethod
def adapt(self, circuit: Circuit) -> T:
"""Convert a UnifiedQuantum Circuit to the provider's native circuit format.
Args:
circuit: UnifiedQuantum Circuit object.
Returns:
Provider-native circuit object.
"""
...
[docs]
def adapt_batch(self, circuits: list[Circuit]) -> list[T]:
"""Convert multiple UnifiedQuantum Circuits to provider-native format.
Args:
circuits: List of UnifiedQuantum Circuit objects.
Returns:
List of provider-native circuit objects.
"""
return [self.adapt(c) for c in circuits]
[docs]
@abc.abstractmethod
def get_supported_gates(self) -> list[str]:
"""Return the list of gate names supported by this adapter.
Returns:
List of supported gate names (uppercase strings).
"""
...
def _get_originir(self, circuit: Circuit) -> str:
"""Extract OriginIR string from a UnifiedQuantum Circuit.
Args:
circuit: UnifiedQuantum Circuit object.
Returns:
OriginIR string representation of the circuit.
"""
return circuit.originir
[docs]
class OriginQCircuitAdapter(CircuitAdapter[Any]):
"""Adapter for converting UnifiedQuantum Circuit to pyqpanda (OriginQ) format.
Uses pyqpanda3's intermediate compiler to convert OriginIR to QProg.
"""
# Gate mapping from OriginIR names to pyqpanda supported gates
SUPPORTED_GATES = [
"H",
"X",
"Y",
"Z",
"S",
"T",
"SX",
"RX",
"RY",
"RZ",
"RPhi",
"RPhi90",
"RPhi180",
"U1",
"U2",
"U3",
"U4",
"CNOT",
"CZ",
"SWAP",
"ISWAP",
"TOFFOLI",
"CSWAP",
"XX",
"YY",
"ZZ",
"XY",
"PHASE2Q",
"UU15",
"I",
"BARRIER",
"MEASURE",
]
def __init__(self) -> None:
self._pyqpanda3: Any = None
self._convert_originir: Any = None
def _ensure_imports(self) -> None:
"""Lazily import pyqpanda3 modules."""
if self._pyqpanda3 is None or self._convert_originir is None:
try:
from pyqpanda3 import core as pyqpanda3_core
from pyqpanda3.intermediate_compiler import (
convert_originir_string_to_qprog,
)
self._pyqpanda3 = pyqpanda3_core
self._convert_originir = convert_originir_string_to_qprog
except ImportError as e:
raise RuntimeError(
"pyqpanda3 is required for OriginQCircuitAdapter. Install it with: pip install pyqpanda3"
) from e
[docs]
def adapt(self, circuit: Circuit) -> str:
"""Convert UnifiedQuantum Circuit to OriginIR string.
The OriginQAdapter.submit() receives this string and converts it
to QProg internally via translate_circuit(). Returning QProg here
would cause submit() to double-convert, breaking translate_circuit().
Args:
circuit: UnifiedQuantum Circuit object.
Returns:
OriginIR format string.
"""
return self._get_originir(circuit)
[docs]
def get_supported_gates(self) -> list[str]:
"""Return the list of gate names supported by this adapter."""
return self.SUPPORTED_GATES.copy()
[docs]
class QuarkCircuitAdapter(CircuitAdapter[str]):
"""Adapter for converting UnifiedQuantum Circuit to OpenQASM 2.0 for QuarkStudio.
QuarkStudio's interface accepts an OpenQASM 2.0 string in the
task dictionary, so this adapter intentionally returns text rather than a
provider-specific circuit object.
"""
SUPPORTED_GATES = [
"H",
"X",
"Y",
"Z",
"S",
"T",
"SX",
"RX",
"RY",
"RZ",
"U1",
"U2",
"U3",
"CNOT",
"CX",
"CZ",
"SWAP",
"ISWAP",
"TOFFOLI",
"CCX",
"CSWAP",
"MEASURE",
"BARRIER",
"I",
"ID",
]
[docs]
def adapt(self, circuit: Circuit) -> str:
"""Convert UnifiedQuantum Circuit to OpenQASM 2.0 text."""
return circuit.qasm
[docs]
def get_supported_gates(self) -> list[str]:
"""Return the list of gate names supported by this adapter."""
return self.SUPPORTED_GATES.copy()
[docs]
class IBMCircuitAdapter(CircuitAdapter[Any]):
"""Adapter for converting UnifiedQuantum Circuit to qiskit (IBM) format.
Converts Circuit -> OriginIR -> QASM -> Qiskit QuantumCircuit.
"""
# QASM 2.0 standard gates supported by qiskit
SUPPORTED_GATES = [
"H",
"X",
"Y",
"Z",
"S",
"T",
"SX",
"RX",
"RY",
"RZ",
"U1",
"U2",
"U3",
"CNOT",
"CX",
"CZ",
"SWAP",
"ISWAP",
"TOFFOLI",
"CCX",
"CSWAP",
"Fredkin",
"MEASURE",
"BARRIER",
"I",
"ID",
]
def __init__(self) -> None:
self._qiskit: Any = None
def _ensure_imports(self) -> None:
"""Lazily import qiskit modules."""
if self._qiskit is None:
try:
import qiskit
self._qiskit = qiskit
except ImportError as e:
raise RuntimeError(
"qiskit is required for IBMCircuitAdapter. Install it with: pip install qiskit"
) from e
[docs]
def adapt(self, circuit: Circuit) -> Any:
"""Convert UnifiedQuantum Circuit to qiskit QuantumCircuit.
The conversion path is:
UnifiedQuantum Circuit -> OriginIR -> QASM -> Qiskit QuantumCircuit
Args:
circuit: UnifiedQuantum Circuit object.
Returns:
qiskit.QuantumCircuit object.
"""
self._ensure_imports()
# Get QASM representation (via OriginIR -> QASM conversion)
qasm_str = circuit.qasm
# Parse QASM to Qiskit QuantumCircuit
return self._qiskit.QuantumCircuit.from_qasm_str(qasm_str)
[docs]
def adapt_with_transpilation(
self,
circuit: Circuit,
backend: Any = None,
optimization_level: int = 1,
**kwargs: Any,
) -> Any:
"""Convert and transpile the circuit for a specific backend.
Args:
circuit: UnifiedQuantum Circuit object.
backend: Qiskit backend to transpile for.
optimization_level: Transpiler optimization level (0-3).
**kwargs: Additional arguments for qiskit.compiler.transpile.
Returns:
Transpiled qiskit.QuantumCircuit object.
"""
self._ensure_imports()
qiskit_circuit = self.adapt(circuit)
if backend is not None:
return self._qiskit.compiler.transpile(
qiskit_circuit, backend=backend, optimization_level=optimization_level, **kwargs
)
return qiskit_circuit
[docs]
def get_supported_gates(self) -> list[str]:
"""Return the list of gate names supported by this adapter."""
return self.SUPPORTED_GATES.copy()
# -----------------------------------------------------------------------------
# TianYan (天衍) — OriginIR -> QCIS text
# -----------------------------------------------------------------------------
def _format_gate_angle(parameter: Any, gate: str, *, dagger: bool = False) -> str:
"""Render a numeric gate angle for a text IR; reject symbolic parameters."""
try:
value = float(parameter)
except (TypeError, ValueError) as exc:
raise NotImplementedError(
f"Gate '{gate}' has a non-numeric parameter ({parameter!r}); "
"cloud platforms require concrete numeric angles."
) from exc
if dagger:
value = -value
return repr(value)
def _parse_originir_line(line: str) -> tuple[Any, ...]:
"""Parse one OriginIR line, raising a clear error on failure."""
from uniqc.compile.originir.originir_line_parser import OriginIR_LineParser
try:
return OriginIR_LineParser.parse_line(line)
except Exception as exc:
raise ValueError(f"Cannot parse OriginIR line {line!r}: {exc}") from exc
def _reject_control_flow(operation: str | None, control_qubits: Any, line: str, target: str) -> None:
"""Reject OriginIR control-flow constructs the target IR cannot express."""
if operation in ("CONTROL", "ENDCONTROL", "DAGGER", "ENDDAGGER"):
raise NotImplementedError(
f"OriginIR {operation} blocks are not supported by the {target} adapter; "
f"use inline 'dagger' suffixes on S/T/SX or rotation gates instead ({line!r})."
)
if control_qubits:
raise NotImplementedError(
f"controlled_by(...) is not supported by the {target} adapter ({line!r})."
)
[docs]
def originir_to_qcis(originir: str) -> str:
"""Convert an OriginIR string to QCIS text for the TianYan platform.
QCIS is a line-based format, e.g. ``H Q0``, ``RX Q0 0.12``,
``CZ Q0 Q6``, ``M Q0``. Measurement lines carry no classical-bit
index — the measurement order *is* the cbit order, so ``MEASURE``
lines must assign cbits sequentially (the uniqc convention).
Args:
originir: Circuit in OriginIR format.
Returns:
QCIS text (one instruction per line).
Raises:
ValueError: If a line cannot be parsed or a qubit index is out of
range for the ``QINIT`` size.
NotImplementedError: If the circuit uses a gate or construct with
no QCIS equivalent.
"""
lines: list[str] = []
n_qubits: int | None = None
n_measures = 0
def _q(index: int) -> str:
if n_qubits is None:
raise ValueError("QINIT must appear before any gate operation.")
if not 0 <= index < n_qubits:
raise ValueError(f"Qubit index q[{index}] is out of range for QINIT {n_qubits}.")
return f"Q{index}"
for raw_line in originir.splitlines():
line = raw_line.strip()
if not line:
continue
operation, qubit, cbit, parameter, dagger_flag, control_qubits = _parse_originir_line(line)
if operation == "QINIT":
n_qubits = int(qubit)
continue
if operation == "CREG":
continue
_reject_control_flow(operation, control_qubits, line, "TianYan QCIS")
if operation == "MEASURE":
if cbit is not None and int(cbit) != n_measures:
raise NotImplementedError(
f"QCIS records measurements in order; MEASURE must assign cbits "
f"sequentially (expected c[{n_measures}], got c[{cbit}] in {line!r})."
)
lines.append(f"M {_q(int(qubit))}")
n_measures += 1
continue
if operation == "BARRIER":
# QCIS has no barrier instruction; barriers carry no semantics
# for cloud sampling, so they are dropped.
continue
if operation in ("H", "X", "Y", "Z"):
# Self-inverse: dagger is a no-op.
lines.append(f"{operation} {_q(int(qubit))}")
elif operation == "S":
lines.append(f"{'SD' if dagger_flag else 'S'} {_q(int(qubit))}")
elif operation == "T":
lines.append(f"{'TD' if dagger_flag else 'T'} {_q(int(qubit))}")
elif operation == "SX":
lines.append(f"{'X2M' if dagger_flag else 'X2P'} {_q(int(qubit))}")
elif operation in ("RX", "RY", "RZ"):
angle = _format_gate_angle(parameter, operation, dagger=bool(dagger_flag))
lines.append(f"{operation} {_q(int(qubit))} {angle}")
elif operation == "CNOT":
lines.append(f"CX {_q(int(qubit[0]))} {_q(int(qubit[1]))}")
elif operation == "CZ":
lines.append(f"CZ {_q(int(qubit[0]))} {_q(int(qubit[1]))}")
elif operation == "SWAP":
lines.append(f"SWAP {_q(int(qubit[0]))} {_q(int(qubit[1]))}")
elif operation == "XY":
angle = _format_gate_angle(parameter, operation, dagger=bool(dagger_flag))
lines.append(f"XY {_q(int(qubit[0]))} {_q(int(qubit[1]))} {angle}")
elif operation == "TOFFOLI":
lines.append(f"CCX {_q(int(qubit[0]))} {_q(int(qubit[1]))} {_q(int(qubit[2]))}")
else:
raise NotImplementedError(
f"Gate '{operation}' is not supported by the TianYan QCIS adapter. "
f"Supported gates: {TianyanCircuitAdapter.SUPPORTED_GATES}"
)
if n_qubits is None:
raise ValueError("OriginIR is missing the QINIT header.")
return "\n".join(lines)
[docs]
def originir_to_lqcloud_circuit(originir: str, circuit_cls: Any) -> Any:
"""Convert an OriginIR string to an lqcloud ``QuantumCircuit``.
Gate methods follow qiskit conventions: parameterised gates take the
angle first (``rx(theta, qubit)``), measurements take
``measure(qubit, clbit)``.
Args:
originir: Circuit in OriginIR format.
circuit_cls: The ``lqcloud.QuantumCircuit`` class (passed in so
this function never imports lqcloud itself).
Returns:
An lqcloud QuantumCircuit object.
Raises:
ValueError: If a line cannot be parsed.
NotImplementedError: If the circuit uses a gate or construct with
no lqcloud equivalent.
"""
n_qubits: int | None = None
n_cbits: int | None = None
for raw_line in originir.splitlines():
line = raw_line.strip()
if line.startswith("QINIT"):
n_qubits = int(line.split()[1])
elif line.startswith("CREG"):
n_cbits = int(line.split()[1])
if n_qubits is None:
raise ValueError("OriginIR is missing the QINIT header.")
qc = circuit_cls(n_qubits, n_cbits if n_cbits is not None else n_qubits)
n_measures = 0
for raw_line in originir.splitlines():
line = raw_line.strip()
if not line:
continue
operation, qubit, cbit, parameter, dagger_flag, control_qubits = _parse_originir_line(line)
if operation in ("QINIT", "CREG"):
continue
_reject_control_flow(operation, control_qubits, line, "LogicalQubit")
if operation == "MEASURE":
qc.measure(int(qubit), int(cbit) if cbit is not None else n_measures)
n_measures += 1
continue
if operation == "BARRIER":
qc.barrier()
continue
if operation in ("H", "X", "Y", "Z"):
# Self-inverse: dagger is a no-op.
getattr(qc, operation.lower())(int(qubit))
elif operation == "I":
qc.id(int(qubit))
elif operation == "S":
(qc.sdg if dagger_flag else qc.s)(int(qubit))
elif operation == "T":
(qc.tdg if dagger_flag else qc.t)(int(qubit))
elif operation == "SX":
(qc.sxdg if dagger_flag else qc.sx)(int(qubit))
elif operation in ("RX", "RY", "RZ"):
angle = float(_format_gate_angle(parameter, operation, dagger=bool(dagger_flag)))
getattr(qc, operation.lower())(angle, int(qubit))
elif operation == "U1":
angle = float(_format_gate_angle(parameter, operation, dagger=bool(dagger_flag)))
qc.p(angle, int(qubit))
elif operation == "CNOT":
qc.cx(int(qubit[0]), int(qubit[1]))
elif operation == "CZ":
qc.cz(int(qubit[0]), int(qubit[1]))
elif operation == "SWAP":
qc.swap(int(qubit[0]), int(qubit[1]))
elif operation == "ISWAP":
qc.iswap(int(qubit[0]), int(qubit[1]))
elif operation == "TOFFOLI":
qc.ccx(int(qubit[0]), int(qubit[1]), int(qubit[2]))
else:
raise NotImplementedError(
f"Gate '{operation}' is not supported by the LogicalQubit adapter. "
f"Supported gates: {LogicalQubitCircuitAdapter.SUPPORTED_GATES}"
)
return qc
[docs]
class TianyanCircuitAdapter(CircuitAdapter[str]):
"""Adapter for converting UnifiedQuantum Circuit to QCIS text (TianYan).
Returns the QCIS string; :class:`TianyanAdapter` submits it via
``cqlib.TianYanPlatform.submit_job``.
"""
# OriginIR gate names accepted by originir_to_qcis().
SUPPORTED_GATES = [
"H",
"X",
"Y",
"Z",
"S",
"T",
"SX",
"RX",
"RY",
"RZ",
"CNOT",
"CZ",
"SWAP",
"XY",
"TOFFOLI",
"MEASURE",
"BARRIER",
]
[docs]
def adapt(self, circuit: Circuit) -> str:
"""Convert UnifiedQuantum Circuit to QCIS text."""
return originir_to_qcis(self._get_originir(circuit))
[docs]
def get_supported_gates(self) -> list[str]:
"""Return the list of gate names supported by this adapter."""
return self.SUPPORTED_GATES.copy()
[docs]
class LogicalQubitCircuitAdapter(CircuitAdapter[Any]):
"""Adapter for converting UnifiedQuantum Circuit to an lqcloud QuantumCircuit."""
# OriginIR gate names accepted by originir_to_lqcloud_circuit().
SUPPORTED_GATES = [
"H",
"X",
"Y",
"Z",
"S",
"T",
"SX",
"I",
"RX",
"RY",
"RZ",
"U1",
"CNOT",
"CZ",
"SWAP",
"ISWAP",
"TOFFOLI",
"MEASURE",
"BARRIER",
]
def __init__(self) -> None:
self._QuantumCircuit: Any = None
def _ensure_imports(self) -> None:
"""Lazily import lqcloud."""
if self._QuantumCircuit is None:
from uniqc.backend_adapter.task.optional_deps import require
lqcloud = require("lqcloud", "logicalqubit")
self._QuantumCircuit = lqcloud.QuantumCircuit
[docs]
def adapt(self, circuit: Circuit) -> Any:
"""Convert UnifiedQuantum Circuit to an lqcloud QuantumCircuit."""
self._ensure_imports()
return originir_to_lqcloud_circuit(self._get_originir(circuit), self._QuantumCircuit)
[docs]
def get_supported_gates(self) -> list[str]:
"""Return the list of gate names supported by this adapter."""
return self.SUPPORTED_GATES.copy()