Source code for uniqc.visualization.circuit_render.layout

"""Column layout engine for circuit rendering (shared by all output modes).

An ASAP (as-soon-as-possible) greedy layering assigns every op a column:
each op lands on the first column where all of its lanes (qubit wires, cbit
lanes, and for control-flow markers *every* lane) are free.  Column widths are
content-driven; the columns may then be folded into bands (rows of the final
figure) according to a per-mode fold policy.
"""

from __future__ import annotations

import shutil
from dataclasses import dataclass, field

from .labels import classify, cop_label, label_lines
from .model import FLOW_OPS, DrawCircuit, DrawOp

__all__ = ["Layout", "FlowSpan", "layout_circuit", "estimate_text_width"]


[docs] @dataclass class FlowSpan: """A QIF/ENDQIF or QWHILE/ENDQWHILE bracket, in column coordinates.""" kind: str # "QIF" | "QWHILE" cond: str col_begin: int col_end: int
[docs] @dataclass class Layout: """Laid-out circuit: ops carry ``col``; bands fold columns into rows.""" ops: list[DrawOp] n_cols: int col_widths: list[float] spans: list[FlowSpan] = field(default_factory=list) bands: list[tuple[int, int]] = field(default_factory=list) # inclusive col ranges
[docs] def estimate_text_width(text: str, font_size: float) -> float: """Heuristic text width (px) — mirrors what the SVG browser render shows.""" w = 0.0 for ch in text: cp = ord(ch) w += font_size * (0.95 if 0x2000 < cp < 0x3300 or cp > 0x2E7F else 0.62) return w
def _lane_keys(op: DrawOp, n_qubits: int, n_cbits: int) -> list[str]: if op.op == "COP": return [f"c:{c}" for c in op.cbits] if op.op in FLOW_OPS: # Control-flow brackets span the whole figure: they must occupy every # lane so they are placed after all preceding content. return [f"q:{q}" for q in range(n_qubits)] + [f"c:{c}" for c in range(n_cbits)] keys = [f"q:{q}" for q in op.qubits] if op.op == "MEASURE": keys += [f"c:{c}" for c in op.cbits] keys += [f"q:{q}" for q in op.controls] return keys def _assign_columns(draw: DrawCircuit) -> list[FlowSpan]: next_free: dict[str, int] = {} n_cols = 0 for op in draw.ops: keys = _lane_keys(op, draw.n_qubits, draw.n_cbits) col = max((next_free.get(k, 0) for k in keys), default=0) op.col = col for k in keys: next_free[k] = col + 1 n_cols = max(n_cols, col + 1) spans: list[FlowSpan] = [] stack: list[DrawOp] = [] for op in draw.ops: if op.op in ("QIF", "QWHILE"): stack.append(op) elif op.op in ("ENDIF", "ENDWHILE") and stack: begin = stack.pop() spans.append(FlowSpan(kind=begin.op, cond=begin.cond or "", col_begin=begin.col, col_end=op.col)) return spans _SYMBOL_OPS = {"CNOT", "CX", "CZ", "CP", "CU1", "P", "SWAP", "TOFFOLI", "CCX", "CCNOT", "CSWAP", "FREDKIN"} def _column_widths(draw: DrawCircuit, geom, param_mode: str) -> list[float]: widths: list[float] = [] for op in draw.ops: kind = classify(op) if op.op == "BARRIER": w = 22.0 elif op.op in FLOW_OPS: w = 14.0 elif kind == "measure": w = max(48.0, geom.min_col) elif kind == "cop": w = estimate_text_width(cop_label(op), geom.fs) + 18 elif op.op.upper() in _SYMBOL_OPS and not op.controls: w = min(52.0, max(38.0, geom.min_col * 0.8)) else: name, params = label_lines(op, param_mode) w = ( max( estimate_text_width(name, geom.fs), estimate_text_width(params, geom.pfs) if params else 0.0, ) + geom.pad ) if op.op == "QRAM": w = estimate_text_width("QRAM", geom.fs) + 44 elif op.op == "DEF": w = max(w, estimate_text_width(f"DEF {op.name}", geom.fs) + 30) elif op.op == "CHANNEL": w = max(w, 34.0) while op.col >= len(widths): widths.append(geom.min_col) if w > widths[op.col]: widths[op.col] = w return widths
[docs] def resolve_fold(fold, mode: str, n_cols: int) -> int: """Resolve the ``fold`` option to a concrete column count (0 = no fold). ``fold="auto"``: for text output, fit the current terminal width; for graphical outputs (no fixed viewport) fold at 16 columns. ``fold=None`` is treated as ``"auto"``; an int ``<= 0`` disables folding. """ if fold is None or fold == "auto": if mode == "text": term_cols = shutil.get_terminal_size(fallback=(110, 24)).columns # ~7 characters per layout column is a good average density return max(4, min(n_cols, (term_cols - 8) // 7)) return 16 if n_cols > 16 else 0 fold = int(fold) return fold if fold > 0 else 0
[docs] def layout_circuit(draw: DrawCircuit, geom, param_mode: str, fold, mode: str = "svg") -> Layout: """Assign columns, compute column widths, and fold into bands.""" for i, op in enumerate(draw.ops): op.index = i spans = _assign_columns(draw) n_cols = max((op.col + 1 for op in draw.ops), default=0) widths = _column_widths(draw, geom, param_mode) while len(widths) < n_cols: widths.append(geom.min_col) fold_n = resolve_fold(fold, mode, n_cols) if fold_n <= 0 or n_cols <= fold_n: bands = [(0, n_cols - 1)] if n_cols else [] else: bands = [] start = 0 while start < n_cols: end = min(start + fold_n, n_cols) - 1 bands.append((start, end)) start = end + 1 return Layout(ops=draw.ops, n_cols=n_cols, col_widths=widths, spans=spans, bands=bands)