Source code for uniqc.visualization.circuit_render.model

"""Data model and normalization for circuit rendering.

The render pipeline is::

    Circuit | OriginIR-ext str | OpenQASM str | JSON-ish list
        -> DrawCircuit          (this module)
        -> Layout               (layout.py)
        -> primitives           (svg.py)
        -> svg / text / latex / html / mpl / png

``DrawOp`` mirrors the OriginIR-ext opcode tuple
``(operation, qubits, cbits, params, dagger, controls)`` and adds the
constructs that only exist in the structured program tree
(``MeasureOp``/``ResetOp``/``ClassicalOp``/``IfBlock``/``WhileBlock``) or in
headers (``QRAMDECL``).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

__all__ = ["DrawOp", "DrawCircuit", "to_draw_circuit", "FLOW_OPS", "GREEK_SYMBOL_NAMES"]

FLOW_OPS = frozenset({"QIF", "QELSE", "ENDIF", "QWHILE", "ENDWHILE"})

#: Latin parameter names rendered as Greek letters when the output charset
#: supports it (SVG / HTML / matplotlib / quantikz).
GREEK_SYMBOL_NAMES = {
    "theta": "θ",
    "phi": "φ",
    "alpha": "α",
    "beta": "β",
    "gamma": "γ",
    "lambda": "λ",
    "lam": "λ",
    "mu": "μ",
    "delta": "δ",
    "pi": "π",
}

_SUBSCRIPT_DIGITS = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")


[docs] @dataclass class DrawOp: """One drawable operation (gate, marker, or classical construct).""" op: str qubits: tuple[int, ...] = () params: tuple = () cbits: tuple[int, ...] = () dagger: bool = False controls: tuple[int, ...] = () name: str | None = None # CHANNEL / QRAM / DEF / COP display name cond: str | None = None # QIF / QWHILE condition text note: str | None = None # DEF body summary addr: int = 0 # QRAM address-bit count data: int = 0 # QRAM data-bit count raw: str = "" # original text/IR line, when known col: int = -1 # layout column (assigned by layout.py) index: int = -1 # index into DrawCircuit.ops (tooltips / interactive)
[docs] @dataclass class DrawCircuit: """Normalized, render-ready circuit description.""" n_qubits: int n_cbits: int params: list[str] = field(default_factory=list) ops: list[DrawOp] = field(default_factory=list)
[docs] def display_param(p: Any, *, greek: bool = True) -> str: """Render one opcode parameter as display text. Numbers are returned unchanged (the caller formats / pi-converts them); sympy expressions are stringified with array elements ``alpha_2`` rendered as ``alpha[2]`` and common names mapped to Greek letters when *greek*. """ if isinstance(p, (int, float)): return p text = str(p) # sympy prints array elements as ``alpha[2]`` (see Circuit._display_subs); # normalize to subscript or bracket form for display. if "[" in text and "]" in text: base, _, idx = text.partition("[") idx = idx.rstrip("]") if greek and base in GREEK_SYMBOL_NAMES and idx.isdigit(): return GREEK_SYMBOL_NAMES[base] + idx.translate(_SUBSCRIPT_DIGITS) return f"{GREEK_SYMBOL_NAMES.get(base, base) if greek else base}[{idx}]" if greek: return GREEK_SYMBOL_NAMES.get(text, text) return text
[docs] def display_param_ascii(text: str) -> str: """ASCII-safe fallback for a display parameter string.""" table = { "π": "pi", "θ": "th", "φ": "ph", "α": "a", "β": "b", "γ": "g", "λ": "l", "μ": "u", "δ": "d", "†": "'", "₂": "2", "₀": "0", "₁": "1", "₃": "3", "₄": "4", "₅": "5", "₆": "6", "₇": "7", "₈": "8", "₉": "9", "…": "..", "≈": "~", "←": "<-", "⟩": ">", } for src, dst in table.items(): text = text.replace(src, dst) return text
def _to_int_tuple(value: Any) -> tuple[int, ...]: if value is None: return () if isinstance(value, (list, tuple, set)): return tuple(int(v) for v in value if v is not None) return (int(value),) def _from_opcode_tuple(opcode: tuple, *, qram_names: set[str], raw: str = "") -> DrawOp: operation, qubits, cbits, params, dagger, controls = opcode name = str(operation) if name in qram_names: return DrawOp( op="QRAM", qubits=_to_int_tuple(qubits), controls=_to_int_tuple(controls), name=name, raw=raw or name, ) from uniqc.circuit_builder.originir_spec import available_originir_error_channels if name in available_originir_error_channels: return DrawOp( op="CHANNEL", qubits=_to_int_tuple(qubits), params=tuple(params) if params is not None and hasattr(params, "__len__") else (() if params is None else (params,)), controls=_to_int_tuple(controls), name=name, raw=raw, ) return DrawOp( op=name, qubits=_to_int_tuple(qubits), params=tuple(params) if params is not None and hasattr(params, "__len__") else (() if params is None else (params,)), cbits=_to_int_tuple(cbits), dagger=bool(dagger), controls=_to_int_tuple(controls), raw=raw, ) def _walk_program(nodes: list, ops: list[DrawOp], qram_names: set[str]) -> None: """Flatten a structured ``dynamic_program`` tree into DrawOp markers.""" from uniqc.circuit_builder.classical_program import ( ClassicalOp, GateOp, IfBlock, MeasureOp, ResetOp, WhileBlock, ) for node in nodes: if isinstance(node, GateOp): ops.append(_from_opcode_tuple(node.opcode, qram_names=qram_names)) elif isinstance(node, MeasureOp): ops.append(DrawOp(op="MEASURE", qubits=(node.qubit,), cbits=(node.cbit,))) elif isinstance(node, ResetOp): ops.append(DrawOp(op="RESET", qubits=(node.qubit,))) elif isinstance(node, ClassicalOp): ops.append( DrawOp( op="COP", name=node.op, cbits=(node.dest,) + tuple(s.value for s in node.srcs), note=", ".join(s.to_str() for s in node.srcs), ) ) elif isinstance(node, IfBlock): ops.append(DrawOp(op="QIF", cond=node.cond.to_str())) _walk_program(node.then_body, ops, qram_names) if node.else_body is not None: ops.append(DrawOp(op="QELSE")) _walk_program(node.else_body, ops, qram_names) ops.append(DrawOp(op="ENDIF")) elif isinstance(node, WhileBlock): ops.append(DrawOp(op="QWHILE", cond=node.cond.to_str())) _walk_program(node.body, ops, qram_names) ops.append(DrawOp(op="ENDWHILE")) else: # pragma: no cover - defensive raise TypeError(f"Unknown program node: {node!r}") def _from_circuit(circuit: Any) -> DrawCircuit: qram_names = set(getattr(circuit, "qram_declarations", {}) or {}) ops: list[DrawOp] = [] dynamic = getattr(circuit, "dynamic_program", None) if dynamic is not None: _walk_program(dynamic, ops, qram_names) else: for opcode in getattr(circuit, "opcode_list", []) or []: ops.append(_from_opcode_tuple(opcode, qram_names=qram_names)) # Terminal measurements live only in measure_list (never in # dynamic_program), while mid-circuit measure_to() lives only in # dynamic_program — the two are disjoint, so always append both. measure_list = getattr(circuit, "measure_list", None) or [] for cbit, qubit in enumerate(measure_list): ops.append(DrawOp(op="MEASURE", qubits=(int(qubit),), cbits=(cbit,))) for name, (addr, data) in getattr(circuit, "qram_declarations", {}).items(): for op in ops: if op.op == "QRAM" and op.name == name: op.addr, op.data = addr, data import contextlib params = [] with contextlib.suppress(Exception): params = list(circuit.free_parameters) n_qubits = max(int(getattr(circuit, "qubit_num", 0) or 0), _max_qubit(ops) + 1) n_cbits = max(int(getattr(circuit, "cbit_num", 0) or 0), _max_cbit(ops) + 1) return DrawCircuit(n_qubits=n_qubits, n_cbits=n_cbits, params=params, ops=ops) def _max_qubit(ops: list[DrawOp]) -> int: best = -1 for op in ops: for q in op.qubits + op.controls: best = max(best, q) return best def _max_cbit(ops: list[DrawOp]) -> int: best = -1 for op in ops: for c in op.cbits: best = max(best, c) return best def _from_jsonable(items: list | dict) -> DrawCircuit: if isinstance(items, dict): items = [items] ops: list[DrawOp] = [] for entry in items: if not isinstance(entry, dict) or not entry: continue name = str(entry.get("gate", entry.get("op", entry.get("name", "UNKNOWN")))) ops.append( DrawOp( op=name.upper() if name.upper() in FLOW_OPS else name, qubits=_to_int_tuple(entry.get("qubits", entry.get("q"))), params=tuple(entry.get("params", entry.get("p", ()))), cbits=_to_int_tuple(entry.get("cbits", entry.get("c"))), dagger=bool(entry.get("dagger", False)), controls=_to_int_tuple(entry.get("controls", entry.get("ctrl"))), name=entry.get("name"), cond=entry.get("cond"), addr=int(entry.get("addr", 0)), data=int(entry.get("data", 0)), raw=str(entry), ) ) return DrawCircuit(n_qubits=_max_qubit(ops) + 1, n_cbits=_max_cbit(ops) + 1, ops=ops) def _from_originir_lenient(text: str) -> DrawCircuit: """Tolerant line-by-line OriginIR-ext parse; unparseable lines are skipped. Used as a fallback when the strict ``Circuit.from_originir`` parse fails — e.g. backend-stored IR with bare (unparenthesized) parameters like ``RX q[0], 1.5707963``. Visualization must never crash on slightly irregular IR (gateway serves whatever was stored at submit time). """ import re from uniqc.compile.originir.originir_line_parser import OriginIR_LineParser n_qubits = 0 n_cbits = 0 ops: list[DrawOp] = [] qram_names: set[str] = set() qram_sizes: dict[str, tuple[int, int]] = {} bare_param_re = re.compile(r"^([A-Za-z][A-Za-z0-9_]*)(\s+q\[[\d,\s]*\]),\s*([-+0-9.eE,\s]+?)\s*$") def parse_gate_line(line: str) -> None: m = bare_param_re.match(line) candidates = [line] if m: candidates.append(f"{m.group(1)}{m.group(2)}, ({m.group(3)})") for cand in candidates: try: operation, qubits, cbit, params, dagger, controls = OriginIR_LineParser.parse_line(cand) except Exception: continue if operation is None or operation in ("QINIT", "CREG", "CONTROL", "ENDCONTROL", "DAGGER", "ENDDAGGER"): return ops.append( _from_opcode_tuple((operation, qubits, cbit, params, dagger, controls), qram_names=qram_names, raw=line) ) return for raw in text.splitlines(): line = raw.strip() if not line: continue head = line.split(None, 1)[0].upper() if head == "QINIT": n_qubits = int(line.split()[1]) continue if head == "CREG": n_cbits = int(line.split()[1]) continue if head == "PARAM": continue if head == "QRAMDECL": parts = line.split() if len(parts) >= 3: name = parts[1] nums = re.findall(r"\d+", parts[2]) addr, data = (int(nums[0]), int(nums[1])) if len(nums) >= 2 else (1, 1) qram_names.add(name) qram_sizes[name] = (addr, data) continue m = re.match(r"^MEASURE\s+q\[(\d+)\]\s*,\s*c\[(\d+)\]", line) if m: ops.append(DrawOp(op="MEASURE", qubits=(int(m.group(1)),), cbits=(int(m.group(2)),), raw=line)) continue m = re.match(r"^RESET\s+q\[(\d+)\]", line) if m: ops.append(DrawOp(op="RESET", qubits=(int(m.group(1)),), raw=line)) continue m = re.match(r"^(AND|OR|XOR|MOV|NOT)\s+c\[(\d+)\]\s*,\s*(.+)$", line) if m: srcs = [int(x) for x in re.findall(r"c\[(\d+)\]", m.group(3))] ops.append(DrawOp(op="COP", name=m.group(1), cbits=(int(m.group(2)), *srcs), raw=line)) continue if head in ("QIF", "QWHILE"): ops.append(DrawOp(op=head, cond=line.split(None, 1)[1] if " " in line else "", raw=line)) continue if head in ("QELSE",): ops.append(DrawOp(op="QELSE", raw=line)) continue if head in ("ENDQIF", "ENDIF"): ops.append(DrawOp(op="ENDIF", raw=line)) continue if head in ("ENDQWHILE", "ENDWHILE"): ops.append(DrawOp(op="ENDWHILE", raw=line)) continue parse_gate_line(line) for op in ops: if op.op == "QRAM" and op.name in qram_sizes: op.addr, op.data = qram_sizes[op.name] return DrawCircuit(n_qubits=n_qubits, n_cbits=n_cbits, ops=ops)
[docs] def to_draw_circuit(circuit: Any) -> DrawCircuit: """Normalize a circuit-like object into a :class:`DrawCircuit`. Accepts a :class:`~uniqc.circuit_builder.Circuit`, an OriginIR(-ext) or OpenQASM 2.0 string, a JSON string of gate dicts, or a list of gate dicts. """ from uniqc.circuit_builder import Circuit if isinstance(circuit, DrawCircuit): return circuit if isinstance(circuit, Circuit): return _from_circuit(circuit) if isinstance(circuit, str): text = circuit.strip() if not text: return DrawCircuit(n_qubits=0, n_cbits=0) if text.startswith("[") or text.startswith("{"): import json return _from_jsonable(json.loads(text)) if text.upper().startswith("OPENQASM"): try: return _from_circuit(Circuit.from_qasm(text)) except Exception: from uniqc.compile.converter import convert_qasm_to_oir return _from_originir_lenient(convert_qasm_to_oir(text)) try: return _from_circuit(Circuit.from_originir(text)) except Exception: return _from_originir_lenient(text) if isinstance(circuit, (list, tuple)): return _from_jsonable(list(circuit)) raise TypeError( "Cannot render object of type " f"{type(circuit).__name__!r}. Expected a uniqc Circuit, an OriginIR/OpenQASM " "string, or a JSON list of gate dicts." )