"""Enhanced transpiler with chip-characterization-aware routing.
This module provides the canonical :func:`compile` entry point for chip-aware
circuit transpilation in UnifiedQuantum. It wraps the existing Qiskit-based
transpiler and adds fidelity-weighted routing, multiple output formats, and a
typed configuration object.
"""
from __future__ import annotations
__all__ = ["compile", "TranspilerConfig", "CompilationResult", "CompilationFailedError"]
import heapq
import re
from collections import defaultdict, deque
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal
from uniqc._error_hints import format_enriched_message
from ._utils import CompilationFailedError
from .converter import convert_oir_to_qasm, convert_qasm_to_oir
if TYPE_CHECKING:
from uniqc.backend_adapter.backend_info import BackendInfo
from uniqc.circuit_builder import Circuit
from uniqc.cli.chip_info import ChipCharacterization
OutputFormat = Literal["circuit", "originir", "qasm", "auto"]
# Default basis gates for superconducting qubit platforms
_DEFAULT_BASIS_GATES = ("cz", "sx", "rz")
# Default fidelity assumed for edges/qubits not in characterization
_DEFAULT_FIDELITY = 0.99
def _resolve_input(circuit: Any) -> tuple[Circuit, str]:
"""Normalize any input to (Circuit, detected_type).
Accepted formats: ``uniqc.Circuit``, OriginIR string, OpenQASM 2.0
string, ``qiskit.QuantumCircuit``.
"""
from uniqc.circuit_builder.normalize import normalize_circuit_input
result = normalize_circuit_input(circuit)
return result.circuit, result.type
[docs]
@dataclass(frozen=True)
class TranspilerConfig:
"""Configuration for the :func:`compile` function.
Parameters
----------
type : str
Transpiler backend. Currently only ``"qiskit"`` is supported.
The string is kept for future extensibility.
level : int
Qiskit optimization level (0–3). Default: 2.
0 = no optimization, 1 = light, 2 = heavy, 3 = heaviest.
basis_gates : tuple[str, ...]
Target basis gate set. Default: ``("cz", "sx", "rz")``.
chip_characterization : ChipCharacterization | None
Per-qubit and per-pair calibration data. When provided, the router
prefers higher-fidelity qubits and edges during SWAP insertion and
qubit routing. If ``None``, routing uses only the connectivity graph.
"""
type: str = "qiskit"
level: int = 2
basis_gates: tuple[str, ...] = _DEFAULT_BASIS_GATES
chip_characterization: ChipCharacterization | None = None
def __post_init__(self) -> None:
if self.type not in ("qiskit",):
raise ValueError(
format_enriched_message(
f"Unsupported transpiler type: {self.type!r}. Only 'qiskit' is supported.", "compilation"
)
)
if not 0 <= self.level <= 3:
raise ValueError(
format_enriched_message(f"optimization_level must be 0–3, got {self.level}", "compilation")
)
object.__setattr__(self, "basis_gates", tuple(self.basis_gates or _DEFAULT_BASIS_GATES))
[docs]
@dataclass
class CompilationResult:
"""Result of a :func:`compile` call.
Attributes
----------
output : Circuit | str
Compiled circuit. Type depends on ``output_format`` passed to :func:`compile`.
fidelity_estimate : float | None
Estimated success probability of the compiled circuit on the target chip,
computed from per-gate fidelities in the chip characterization.
``None`` if no chip_characterization was provided.
routing_overhead : int
Number of SWAP gates inserted by the router.
Zero if the circuit already fits the topology.
transpiler_messages : list[str]
Informational messages from the transpiler.
"""
output: Circuit | str
fidelity_estimate: float | None
routing_overhead: int
transpiler_messages: list[str] = field(default_factory=list)
[docs]
def compile(
circuit: Any,
backend_info: BackendInfo | None = None,
*,
type: str = "qiskit",
level: int = 2,
basis_gates: list[str] | None = None,
chip_characterization: ChipCharacterization | None = None,
output_format: OutputFormat = "circuit",
available_qubits: list[int] | tuple[int, ...] | None = None,
) -> Circuit | str:
"""Compile a circuit for a specific backend using chip characterization data.
This is the canonical entry point for chip-aware transpilation in UnifiedQuantum.
It supersedes :func:`transpile_originir <uniqc.compile.qiskit_transpiler.transpile_originir>`
by adding chip-characterization-aware routing, a typed configuration object,
and a Circuit return type.
.. important::
``compile`` (at every optimization ``level``, including ``level=0``)
requires Qiskit, which is a **core dependency** installed by default
with ``unified-quantum``. If ``import qiskit`` fails, the install is
broken — reinstall with ``pip install --upgrade unified-quantum``.
Without ``qiskit`` available every call raises ``CompilationFailedError``.
There is currently no pure-Python fallback path.
Parameters
----------
circuit :
The input circuit. Accepts a :class:`uniqc.Circuit`, an OriginIR
string, an OpenQASM 2.0 string, or a ``qiskit.QuantumCircuit``.
backend_info :
Target backend descriptor. Supplies the topology coupling map.
If omitted, the topology is taken from ``chip_characterization``.
type :
Transpiler backend. Currently only ``"qiskit"`` is supported.
level :
Qiskit optimization level (0–3). Default: 2.
basis_gates :
Target basis gate set. Default: ``["cz", "sx", "rz"]``.
chip_characterization :
Per-qubit and per-pair calibration data. When provided, the router
prefers high-fidelity qubits and edges during SWAP insertion.
If ``None``, routing uses only the connectivity graph.
output_format :
Return format. ``"circuit"`` (default) returns a new ``Circuit`` object;
``"originir"`` returns an OriginIR string;
``"qasm"`` returns a QASM2 string;
``"auto"`` matches the detected input format.
Returns
-------
Circuit | str
The compiled circuit in the requested format.
Raises
------
CompilationFailedError
If transpilation fails.
ValueError
If the transpiler type is unsupported, the optimization level is out
of range, or no topology information is available.
TypeError
If the input type is not recognized.
Example
-------
>>> from uniqc.circuit_builder import Circuit
>>> from uniqc.compile import compile
>>> circuit = Circuit()
>>> circuit.h(0); circuit.cnot(0, 1)
>>> compiled = compile(circuit, level=2)
>>> print(type(compiled).__name__)
Circuit
"""
config = TranspilerConfig(
type=type,
level=level,
basis_gates=tuple(basis_gates) if basis_gates else _DEFAULT_BASIS_GATES,
chip_characterization=chip_characterization,
)
return compile_with_config(circuit, backend_info, config, output_format, available_qubits=available_qubits)
[docs]
def compile_with_config(
circuit: Any,
backend_info: BackendInfo | None,
config: TranspilerConfig,
output_format: OutputFormat,
*,
available_qubits: list[int] | tuple[int, ...] | None = None,
) -> Circuit | str:
"""Internal compile implementation that accepts a :class:`TranspilerConfig`."""
messages: list[str] = []
# Normalize any input type to a Circuit and record the detected type
circuit_obj, input_type = _resolve_input(circuit)
# Resolve topology
if backend_info is not None and backend_info.topology:
topology = [(e.u, e.v) for e in backend_info.topology]
elif config.chip_characterization is not None and config.chip_characterization.connectivity:
topology = [(e.u, e.v) for e in config.chip_characterization.connectivity]
else:
raise ValueError(
format_enriched_message(
"compile() requires either backend_info.topology or "
"chip_characterization.connectivity to determine the coupling map.",
"compilation",
)
)
# If the caller restricted the chip to a subset of physical qubits
# (`available_qubits`), drop every edge that touches a forbidden qubit
# so neither the fidelity-aware mapper nor qiskit's downstream router
# can route onto excluded physical qubits (e.g. known-bad ones).
if available_qubits is not None:
allowed = {int(q) for q in available_qubits}
topology = [(u, v) for (u, v) in topology if u in allowed and v in allowed]
if not topology:
raise ValueError(
"compile(): available_qubits restriction yielded an empty "
"coupling map. Check that the requested physical qubits are "
"actually connected on the chip."
)
originir_input = circuit_obj.originir
# Fidelity-weighted *mapping* selection (no OriginIR rewriting)
routed_originir = originir_input
routing_overhead = 0
fidelity_estimate: float | None = None
initial_layout: list[int] | None = None
if config.chip_characterization is not None:
(
routed_originir,
routing_overhead,
fidelity_estimate,
initial_layout,
) = _route_with_fidelity(
routed_originir, topology, config.chip_characterization, available_qubits=available_qubits
)
layout_msg = f", initial_layout={initial_layout}" if initial_layout else ""
messages.append(
f"Mapping selected from chip characterization "
f"(fidelity estimate: {fidelity_estimate:.4f}{layout_msg}). "
f"SWAP routing deferred to qiskit transpile."
)
else:
messages.append("No chip characterization provided; routing uses topology only.")
# Convert to QASM for Qiskit
qasm_input = convert_oir_to_qasm(routed_originir)
# Qiskit transpilation (handles routing + SWAP insertion using full coupling map)
transpile_qasm = _load_transpile_qasm()
transpiled_qasm = transpile_qasm(
[qasm_input],
topology=topology,
optimization_level=config.level,
basis_gates=list(config.basis_gates),
initial_layout=initial_layout,
)[0]
# Build return value — resolve output format (auto matches input)
from uniqc.circuit_builder.normalize import resolve_output_format
target_format = resolve_output_format(output_format, input_type)
if target_format == "originir":
return convert_qasm_to_oir(transpiled_qasm)
if target_format == "qasm":
return transpiled_qasm
output_originir = convert_qasm_to_oir(transpiled_qasm)
return _originir_to_circuit(output_originir)
[docs]
def compile_full(
circuit: Any,
backend_info: BackendInfo | None = None,
*,
type: str = "qiskit",
level: int = 2,
basis_gates: list[str] | None = None,
chip_characterization: ChipCharacterization | None = None,
output_format: OutputFormat = "circuit",
available_qubits: list[int] | tuple[int, ...] | None = None,
) -> CompilationResult:
"""Full compile returning a :class:`CompilationResult` with metadata.
Equivalent to :func:`compile` but also returns routing overhead,
fidelity estimate, and informational messages.
Parameters
----------
circuit :
The input circuit. Accepts a :class:`uniqc.Circuit`, an OriginIR
string, an OpenQASM 2.0 string, or a ``qiskit.QuantumCircuit``.
backend_info :
Target backend descriptor.
type, level, basis_gates, chip_characterization, output_format,
available_qubits :
Same as :func:`compile`.
Returns
-------
CompilationResult
The compiled output plus metadata.
"""
config = TranspilerConfig(
type=type,
level=level,
basis_gates=tuple(basis_gates) if basis_gates else _DEFAULT_BASIS_GATES,
chip_characterization=chip_characterization,
)
messages: list[str] = []
# Normalize any input type to a Circuit and record the detected type
circuit_obj, input_type = _resolve_input(circuit)
# Resolve topology
if backend_info is not None and backend_info.topology:
topology = [(e.u, e.v) for e in backend_info.topology]
elif config.chip_characterization is not None and config.chip_characterization.connectivity:
topology = [(e.u, e.v) for e in config.chip_characterization.connectivity]
else:
raise ValueError(
format_enriched_message(
"compile_full() requires either backend_info.topology or chip_characterization.connectivity.",
"compilation",
)
)
# Restrict the coupling map to user-allowed physical qubits, if any.
if available_qubits is not None:
allowed = {int(q) for q in available_qubits}
topology = [(u, v) for (u, v) in topology if u in allowed and v in allowed]
if not topology:
raise ValueError("compile_full(): available_qubits restriction yielded an empty coupling map.")
originir_input = circuit_obj.originir
routed_originir = originir_input
routing_overhead = 0
fidelity_estimate: float | None = None
initial_layout: list[int] | None = None
if config.chip_characterization is not None:
(
routed_originir,
routing_overhead,
fidelity_estimate,
initial_layout,
) = _route_with_fidelity(
routed_originir, topology, config.chip_characterization, available_qubits=available_qubits
)
layout_msg = f", initial_layout={initial_layout}" if initial_layout else ""
messages.append(
f"Mapping selected from chip characterization "
f"(fidelity estimate: {fidelity_estimate:.4f}{layout_msg}). "
f"SWAP routing deferred to qiskit transpile."
)
else:
messages.append("No chip characterization; routing uses topology only.")
qasm_input = convert_oir_to_qasm(routed_originir)
transpile_qasm = _load_transpile_qasm()
transpiled_qasm = transpile_qasm(
[qasm_input],
topology=topology,
optimization_level=config.level,
basis_gates=list(config.basis_gates),
initial_layout=initial_layout,
)[0]
from uniqc.circuit_builder.normalize import resolve_output_format
target_format = resolve_output_format(output_format, input_type)
if target_format == "originir":
output = convert_qasm_to_oir(transpiled_qasm)
elif target_format == "qasm":
output = transpiled_qasm
else:
output = _originir_to_circuit(convert_qasm_to_oir(transpiled_qasm))
return CompilationResult(
output=output,
fidelity_estimate=fidelity_estimate,
routing_overhead=routing_overhead,
transpiler_messages=messages,
)
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _load_transpile_qasm():
try:
from .qiskit_transpiler import transpile_qasm
except ImportError as exc:
raise CompilationFailedError(
format_enriched_message(
"compile() requires qiskit, which is a core dependency of "
"unified-quantum. The install appears broken; reinstall with "
"`pip install --upgrade unified-quantum`.",
"compilation",
)
) from exc
return transpile_qasm
def _route_with_fidelity(
originir: str,
topology: list[tuple[int, int]],
chip: ChipCharacterization,
*,
available_qubits: list[int] | tuple[int, ...] | None = None,
) -> tuple[str, int, float, list[int] | None]:
"""Chip-aware *mapping* selection + fidelity estimate.
Picks an initial logical→physical mapping that prefers high-fidelity
qubits/edges on the chip, and returns it as a Qiskit-style
``initial_layout`` list (``initial_layout[i]`` is the physical qubit
on which logical qubit ``i`` should be placed). Routing (SWAP
insertion) is left to the downstream Qiskit transpile pass — it sees
the full ``coupling_map`` and is much better at it than this function.
If ``available_qubits`` is given, every fidelity / topology entry that
references a forbidden physical qubit is dropped before selection so
the layout cannot land on excluded physical qubits.
Returns
-------
routed_originir : str
The OriginIR string passed in, unchanged. Returned as the first
element to keep the original (mapping, swap_count, fidelity)
contract callable.
swap_count : int
Always ``0`` from this layer; the actual SWAP count comes from
Qiskit transpile downstream.
fidelity_estimate : float
Product-of-fidelities estimate, computed against the chosen
initial mapping.
initial_layout : list[int] | None
Physical qubit list ordered by logical qubit. ``None`` if the
chip has no usable fidelity data (caller should fall back to
Qiskit's default layout selection).
"""
allowed = {int(q) for q in available_qubits} if available_qubits is not None else None
# Build best 2Q fidelity map per undirected edge (skip self-loops)
tq_fid: dict[tuple[int, int], float] = {}
for tq_data in chip.two_qubit_data:
if tq_data.qubit_u == tq_data.qubit_v:
continue
if allowed is not None and (tq_data.qubit_u not in allowed or tq_data.qubit_v not in allowed):
continue
for gate in tq_data.gates:
if gate.fidelity is not None:
edge = tuple(sorted((tq_data.qubit_u, tq_data.qubit_v)))
existing = tq_fid.get(edge)
if existing is None or gate.fidelity > existing:
tq_fid[edge] = gate.fidelity
# Build undirected adjacency (for region selection) — already filtered
# by `topology`, which the caller restricted via available_qubits.
undirected_adj: dict[int, set[int]] = defaultdict(set)
for u, v in topology:
if u == v:
continue
undirected_adj[u].add(v)
undirected_adj[v].add(u)
# Single-qubit fidelity map
sq_fid: dict[int, float] = {}
for sq_data in chip.single_qubit_data:
if allowed is not None and sq_data.qubit_id not in allowed:
continue
if sq_data.single_gate_fidelity is not None:
sq_fid[sq_data.qubit_id] = sq_data.single_gate_fidelity
# Determine n_qubits from QINIT
lines = originir.strip().splitlines()
n_qubits = 0
for line in lines:
s = line.strip()
if s.startswith("QINIT"):
n_qubits = int(s.split()[1])
break
# ---- Initial mapping selection (no OriginIR rewriting) ----
initial_layout: list[int] | None = None
if n_qubits == 0:
initial_layout = None
elif n_qubits == 1:
# Pick best single-qubit fidelity if known; else any chip qubit.
if sq_fid:
best_q = max(sq_fid.items(), key=lambda kv: kv[1])[0]
initial_layout = [best_q]
elif undirected_adj:
initial_layout = [next(iter(undirected_adj))]
elif n_qubits == 2:
# Mapping-only: pick the highest-fidelity adjacent physical pair
# (skipping self-loops just in case).
valid_edges = {e: f for e, f in tq_fid.items() if e[0] != e[1]}
if valid_edges:
best_edge = max(valid_edges.items(), key=lambda kv: kv[1])[0]
initial_layout = [best_edge[0], best_edge[1]]
else:
# For larger circuits, defer to RegionSelector to pick a connected
# high-fidelity region on the FULL chip topology, then use that
# region as the initial layout. SWAP insertion remains qiskit's job.
try:
from uniqc.backend_adapter.region_selector import RegionSelector
selector = RegionSelector(chip)
region = selector.find_best_1D_chain(n_qubits, max_search_seconds=2.0)
if region.qubits is not None and len(region.qubits) >= n_qubits:
# find_best_1D_chain returns a SET; recover order via the
# adjacency walk so neighbouring logicals stay adjacent.
ordered = _order_chain(region.qubits, undirected_adj)
initial_layout = ordered[:n_qubits]
except Exception:
initial_layout = None
# Fidelity estimate against the chosen layout.
#
# Defensive note: when ``initial_layout`` is shorter than ``n_qubits``
# (e.g. RegionSelector returned a partial region, or the chip is too
# small for the circuit), the previous ``min(n_qubits, len(...))`` slice
# left some logical qubits unmapped. Any subsequent lookup of those
# missing logicals would have raised ``KeyError`` from
# ``_estimate_circuit_fidelity_from_lines`` on tiny circuits against
# large chips. Pad with an identity fallback so every logical qubit
# has an entry; downstream qiskit transpile still owns final routing.
if initial_layout is not None:
l2p = {i: initial_layout[i] for i in range(min(n_qubits, len(initial_layout)))}
else:
l2p = {}
for i in range(n_qubits):
l2p.setdefault(i, i)
fidelity = _estimate_circuit_fidelity_from_lines(lines, sq_fid, tq_fid, l2p, {})
return originir, 0, fidelity, initial_layout
def _order_chain(qubits: set[int], adj: dict[int, set[int]]) -> list[int]:
"""Walk a connected qubit set into a path order."""
qset = set(qubits)
if not qset:
return []
# Start from any endpoint (degree 1 within the set), else any node.
endpoints = [q for q in qset if len(adj.get(q, set()) & qset) <= 1]
start = endpoints[0] if endpoints else next(iter(qset))
order: list[int] = [start]
visited = {start}
while True:
cur = order[-1]
nxt = None
for nb in adj.get(cur, set()):
if nb in qset and nb not in visited:
nxt = nb
break
if nxt is None:
break
order.append(nxt)
visited.add(nxt)
# Append any leftovers (shouldn't happen for a chain, but be safe)
for q in qset:
if q not in visited:
order.append(q)
return order
def _dijkstra_path(start: int, end: int, adj: dict[int, list[tuple[int, float]]]) -> list[int] | None:
"""Find minimum-weight path from start to end using Dijkstra."""
dist: dict[int, float] = {start: 0.0}
prev: dict[int, int] = {}
pq: list[tuple[float, int]] = [(0.0, start)]
while pq:
d, u = heapq.heappop(pq)
if u == end:
path = []
cur = end
while cur in prev:
path.append(cur)
cur = prev[cur]
path.append(start)
path.reverse()
return path
if d > dist.get(u, float("inf")):
continue
for v, w in adj.get(u, []):
nd = d + w
if nd < dist.get(v, float("inf")):
dist[v] = nd
prev[v] = u
heapq.heappush(pq, (nd, v))
return None
def _bfs_path(start: int, end: int, adj: dict[int, list[tuple[int, float]]]) -> list[int] | None:
"""Unweighted BFS fallback path finder."""
queue: deque[tuple[int, list[int]]] = deque([(start, [start])])
visited = {start}
while queue:
node, path = queue.popleft()
if node == end:
return path
for neighbor, _ in adj.get(node, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None
_QREG_RE = re.compile(r"q\[(\d+)\]")
def _rewrite_originir_qubits(line: str, old_qubits: list[int], new_qubits: list[int]) -> str:
"""Rewrite the ``q[i]`` references in an OriginIR line in order.
Replaces the *first* occurrence of ``q[old_qubits[0]]`` with
``q[new_qubits[0]]``, the second with ``q[new_qubits[1]]``, etc.
Substitution is done positionally, not value-based, so swapping
operands (e.g. CNOT q[1] q[0] → CNOT q[0] q[1]) works correctly
even when source and target indices overlap.
"""
if old_qubits == list(new_qubits):
return line
parts = _QREG_RE.split(line)
# parts alternates [text, qidx_str, text, qidx_str, ...]
out: list[str] = []
consumed = 0
for i, p in enumerate(parts):
if i % 2 == 0:
out.append(p)
else:
if consumed < len(new_qubits):
out.append(f"q[{new_qubits[consumed]}]")
consumed += 1
else:
out.append(f"q[{p}]")
return "".join(out)
def _remap_measure_line(line: str, l2p: dict[int, int]) -> str:
"""Rewrite ``MEASURE q[L], c[k]`` so that L → l2p[L]."""
def _sub(m: re.Match[str]) -> str:
lq = int(m.group(1))
pq = l2p.get(lq, lq)
return f"q[{pq}]"
return _QREG_RE.sub(_sub, line)
def _estimate_circuit_fidelity_from_lines(
lines: list[str],
sq_fid: dict[int, float],
tq_fid: dict[tuple[int, int], float],
l2p: dict[int, int],
p2l: dict[int, int],
) -> float:
"""Compute product-of-fidelities estimate from OriginIR lines.
Defensive lookups:
* ``l2p.get(qubit, qubit)`` — logical qubits absent from the mapping
fall back to identity (treat the logical index as a physical index).
* ``sq_fid.get(p, _DEFAULT_FIDELITY)`` / ``tq_fid.get(edge, _DEFAULT_FIDELITY)`` —
chip qubits / edges without calibration data fall back to
``_DEFAULT_FIDELITY``. This keeps the estimate optimistic instead of
crashing with ``KeyError`` on tiny circuits routed onto large chips
(where most calibration data is for unrelated qubits) or when the
caller's ``available_qubits`` restriction filtered out an edge.
* Two-qubit edge keys are always built with ``tuple(sorted(...))`` to
match how ``tq_fid`` is constructed in :func:`_route_with_fidelity`
(line 468), so ``CNOT q[1], q[0]`` and ``CNOT q[0], q[1]`` resolve
to the same fidelity entry.
"""
from uniqc.compile.originir.originir_line_parser import OriginIR_LineParser
fidelity = 1.0
measured_qubits: set[int] = set()
for line in lines:
stripped = line.strip()
if not stripped:
continue
if stripped.startswith("MEASURE"):
measured_qubits.update(int(q) for q in re.findall(r"q\[(\d+)\]", stripped))
continue
try:
op, qubit, *_ = OriginIR_LineParser.parse_line(stripped)
except Exception:
continue
if op is None or op in (
"QINIT",
"CREG",
"CONTROL",
"ENDCONTROL",
"DAGGER",
"ENDDAGGER",
"BARRIER",
"SWAP",
"DEF",
"ENDDEF",
):
continue
if isinstance(qubit, int):
p = l2p.get(qubit, qubit)
fidelity *= sq_fid.get(p, _DEFAULT_FIDELITY)
elif isinstance(qubit, list) and len(qubit) >= 2:
p0 = l2p.get(qubit[0], qubit[0])
p1 = l2p.get(qubit[1], qubit[1])
edge = tuple(sorted((p0, p1)))
fidelity *= tq_fid.get(edge, _DEFAULT_FIDELITY)
return max(0.0, min(1.0, fidelity))
def _originir_to_circuit(originir_str: str) -> Circuit:
"""Parse an OriginIR string into a Circuit object."""
from uniqc.circuit_builder import Circuit
from uniqc.compile.originir.originir_line_parser import OriginIR_LineParser
circuit: Circuit = Circuit()
lines = originir_str.strip().splitlines()
pending_measurements: dict[int, int] = {}
GATE_MAP: dict[str, str] = {
"H": "h",
"X": "x",
"Y": "y",
"Z": "z",
"S": "s",
"T": "t",
"SX": "sx",
"I": "i",
"CNOT": "cnot",
"CZ": "cz",
"SWAP": "swap",
"ISWAP": "iswap",
"TOFFOLI": "toffoli",
"CSWAP": "cswap",
"ECR": "ecr",
}
for line in lines:
stripped = line.strip()
if not stripped:
continue
try:
op, qubit, cbit, param, dagger, ctrl = OriginIR_LineParser.parse_line(stripped)
except Exception:
# Try to handle SWAP, QINIT, CREG, MEASURE naively
if stripped.startswith("SWAP"):
m = re.findall(r"q\[(\d+)\]", stripped)
if len(m) == 2:
circuit.swap(int(m[0]), int(m[1]))
continue
if stripped.startswith("QINIT"):
n = int(stripped.split()[1])
# Set circuit size; Circuit will populate itself from opcodes
circuit.qubit_num = n
circuit.max_qubit = max(0, n - 1)
continue
if stripped.startswith("CREG"):
circuit.cbit_num = int(stripped.split()[1])
continue
if "MEASURE" in stripped:
q_match = re.search(r"q\[(\d+)\]", stripped)
c_match = re.search(r"c\[(\d+)\]", stripped)
if q_match is not None and c_match is not None:
pending_measurements[int(c_match.group(1))] = int(q_match.group(1))
continue
# Skip unparseable lines
continue
if op == "QINIT":
circuit.qubit_num = int(qubit)
circuit.max_qubit = max(0, circuit.qubit_num - 1)
continue
if op == "CREG":
circuit.cbit_num = int(cbit or 0)
continue
if op is None or op in ("CONTROL", "ENDCONTROL", "DAGGER", "ENDDAGGER", "DEF", "ENDDEF"):
continue
if op == "MEASURE":
pending_measurements[int(cbit)] = int(qubit)
circuit.record_qubit(int(qubit))
continue
if op == "BARRIER":
q_ids = [int(q) for q in re.findall(r"q\[(\d+)\]", stripped)]
if q_ids:
circuit.barrier(*q_ids)
continue
# Map gate name to Circuit method
method_name = GATE_MAP.get(op, op.lower())
if hasattr(circuit, method_name):
fn = getattr(circuit, method_name)
try:
if isinstance(qubit, list):
args = qubit[:]
if param is not None:
if isinstance(param, (list, tuple)):
args.extend(param)
else:
args.append(param)
fn(*args)
elif isinstance(qubit, int):
if param is not None:
if isinstance(param, (list, tuple)):
fn(qubit, *param)
else:
fn(qubit, param)
else:
fn(qubit)
except TypeError:
circuit.add_gate(op, qubit, cbit, param, bool(dagger), ctrl)
else:
circuit.add_gate(op, qubit, cbit, param, bool(dagger), ctrl)
if pending_measurements:
circuit.measure_list = [pending_measurements[cbit] for cbit in sorted(pending_measurements)]
circuit.cbit_num = max(pending_measurements) + 1
circuit.record_qubit(circuit.measure_list)
return circuit