"""Circuit scheduling and timeline visualization.
This module computes a left-compacted schedule from a compiled circuit and can
render both the existing table-style PDF timeline and static HTML/SVG views.
Logical circuits require gate-duration data from backend metadata,
chip-characterization data, or explicit ``gate_durations`` overrides. Pulse
data that already carries start times remains supported without duration data.
"""
from __future__ import annotations
__all__ = [
"TimelineDurationError",
"TimelineGate",
"TimelineSchedule",
"circuit_to_html",
"create_time_line_table",
"format_result",
"plot_time_line",
"plot_time_line_html",
"schedule_circuit",
]
import html
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from uniqc._error_hints import format_enriched_message
try:
import pandas as pd
except ImportError:
pd = None
_TWO_QUBIT_GATES = {"CNOT", "CX", "CZ", "SWAP", "ISWAP", "ECR", "XX", "YY", "ZZ", "XY", "PHASE2Q"}
_MEASURE_GATES = {"MEASURE", "MEAS", "MEASUREMENT"}
_ZERO_DURATION_GATES = {"I", "ID", "BARRIER"}
_VIRTUAL_Z_GATES = {"Z", "RZ", "U1", "P", "PHASE", "S", "T", "SDG", "TDG"}
_DEFAULT_BASIS_GATES = ["cz", "sx", "rz"]
# Re-export from central module to preserve the historical local name.
from uniqc.exceptions import TimelineDurationError as TimelineDurationError # noqa: F401, E501
[docs]
@dataclass(frozen=True, slots=True)
class TimelineGate:
"""One scheduled operation."""
index: int
name: str
qubits: tuple[int, ...]
params: tuple[Any, ...] = ()
cbits: tuple[int, ...] = ()
control_qubits: tuple[int, ...] = ()
start: float = 0.0
duration: float = 0.0
end: float = 0.0
layer: int = 0
raw: str | None = None
@property
def resources(self) -> tuple[int, ...]:
"""Return all quantum resources touched by this operation."""
return _unique_ints((*self.control_qubits, *self.qubits))
@property
def is_barrier(self) -> bool:
return self.name.upper() == "BARRIER"
@property
def is_virtual(self) -> bool:
return self.name.upper() in _VIRTUAL_Z_GATES
[docs]
@dataclass(frozen=True, slots=True)
class TimelineSchedule:
"""Scheduled circuit timeline."""
gates: tuple[TimelineGate, ...]
qubits: tuple[int, ...]
total_duration: float
unit: str = "ns"
gate_durations: dict[str, float] | None = None
@property
def time_points(self) -> tuple[int | float, ...]:
return tuple(_format_time(t) for t in sorted({gate.start for gate in self.gates}))
@dataclass(frozen=True, slots=True)
class _ProgramEntry:
name: str
qubits: tuple[int, ...]
params: tuple[Any, ...] = ()
cbits: tuple[int, ...] = ()
control_qubits: tuple[int, ...] = ()
explicit_start: float | None = None
raw: str | None = None
@property
def resources(self) -> tuple[int, ...]:
return _unique_ints((*self.control_qubits, *self.qubits))
@property
def is_barrier(self) -> bool:
return self.name.upper() == "BARRIER"
[docs]
def schedule_circuit(
compiled_prog: Any,
*,
backend_info: Any | None = None,
chip_characterization: Any | None = None,
gate_durations: dict[str, float] | None = None,
compile_to_basis: bool = True,
basis_gates: list[str] | None = None,
unit: str = "ns",
) -> TimelineSchedule:
"""Schedule a compiled circuit by left-compacting gates on qubit resources.
.. important::
Whenever ``compile_to_basis=True`` (the default), this function calls
:func:`uniqc.compile.compile`, which **requires** Qiskit. Qiskit is a
core dependency installed by default with ``unified-quantum``; if it
fails to import, the install is broken (reinstall with
``pip install --upgrade unified-quantum``). There is no native-only
bypass: even if the input circuit already uses only chip-native gates
(e.g. CZ/SX/RZ), ``schedule_circuit`` will still call ``compile()`` to
collect timing data unless every entry already carries an explicit
``start`` time. To skip ``compile()`` entirely you must pass pulse /
timeline data where every entry has ``start_time`` set, and pass
``compile_to_basis=False`` (otherwise ``TimelineDurationError`` is
raised).
Parameters
----------
compiled_prog:
A ``Circuit``-like object, OriginIR text, JSON pulse data, or a list of
gate dictionaries.
backend_info, chip_characterization:
Backend metadata used to resolve gate durations. ``BackendInfo.extra``
may contain ``gate_durations``, ``single_qubit_gate_time``,
``two_qubit_gate_time``, and ``measure_time``.
gate_durations:
Explicit duration overrides. Gate names are case-insensitive. Generic
keys ``"1q"``, ``"2q"``, and ``"measure"`` are supported.
compile_to_basis:
Logical circuits must be compiled to basis gates before scheduling.
This flag defaults to ``True`` and may only be disabled for inputs that
already carry explicit start times.
basis_gates:
Basis gate override forwarded to ``compile()`` when
``compile_to_basis=True``.
unit:
Display unit label for renderers. Numeric values are not converted.
Raises
------
TimelineDurationError
If the circuit lacks explicit start times and a non-virtual operation
cannot be assigned a duration from backend metadata or overrides.
"""
entries = _normalise_program(compiled_prog)
has_explicit_start_times = any(entry.explicit_start is not None for entry in entries)
if not has_explicit_start_times:
if not compile_to_basis:
raise TimelineDurationError(
format_enriched_message(
"Timeline scheduling requires compiling logical circuits to basis gates first. "
"Use compile_to_basis=True, or pass pulse/timeline data with explicit start times.",
"visualization",
)
)
compiled_prog = _compile_to_basis_for_timeline(
compiled_prog,
entries,
backend_info=backend_info,
chip_characterization=chip_characterization,
basis_gates=basis_gates,
)
entries = _normalise_program(compiled_prog)
durations = _resolve_gate_durations(
backend_info=backend_info,
chip_characterization=chip_characterization,
gate_durations=gate_durations,
)
needs_duration_data = any(entry.explicit_start is None for entry in entries if not entry.is_barrier)
all_qubits = sorted({q for entry in entries for q in entry.resources})
available_at: dict[int, float] = dict.fromkeys(all_qubits, 0.0)
scheduled: list[TimelineGate] = []
start_to_layer: dict[float, int] = {}
for index, entry in enumerate(entries):
resources = entry.resources
if entry.is_barrier:
barrier_qubits = resources or tuple(all_qubits)
boundary = max((available_at.get(q, 0.0) for q in barrier_qubits), default=0.0)
for q in barrier_qubits:
available_at[q] = boundary
layer = _layer_for_start(start_to_layer, boundary)
scheduled.append(
TimelineGate(
index=index,
name="BARRIER",
qubits=barrier_qubits,
start=boundary,
duration=0.0,
end=boundary,
layer=layer,
raw=entry.raw,
)
)
continue
if not resources:
continue
duration = _duration_for_gate(entry.name, resources, durations, strict=needs_duration_data)
start = (
float(entry.explicit_start)
if entry.explicit_start is not None
else max((available_at.get(q, 0.0) for q in resources), default=0.0)
)
end = start + duration
for q in resources:
available_at[q] = end
layer = _layer_for_start(start_to_layer, start)
scheduled.append(
TimelineGate(
index=index,
name=_display_gate_name(entry.name),
qubits=entry.qubits,
params=entry.params,
cbits=entry.cbits,
control_qubits=entry.control_qubits,
start=start,
duration=duration,
end=end,
layer=layer,
raw=entry.raw,
)
)
total_duration = max((gate.end for gate in scheduled), default=0.0)
qubits = tuple(sorted({q for gate in scheduled for q in gate.resources}))
return TimelineSchedule(
gates=tuple(scheduled),
qubits=qubits,
total_duration=total_duration,
unit=unit,
gate_durations=durations,
)
[docs]
def create_time_line_table(layer_dict, qubit_list, time_line):
"""Create a pandas DataFrame-like timeline table from legacy layer data."""
if pd is not None:
time_line_table = pd.DataFrame(columns=time_line, index=[f"qubit {i}" for i in qubit_list])
else:
time_line_table = _SimpleTimelineTable(columns=time_line, index=[f"qubit {i}" for i in qubit_list])
for gates in layer_dict.values():
for gate_name, qubit, angle, time in gates:
label = gate_name if gate_name.upper() in _MEASURE_GATES else f"{gate_name} {round(angle, 3)}"
if isinstance(qubit, int):
time_line_table.loc[f"qubit {qubit}", time] = label
else:
for q in qubit:
time_line_table.loc[f"qubit {q}", time] = label
return time_line_table.fillna("idle")
[docs]
def plot_time_line(
compiled_prog,
figure_save_path: str | Path = Path.cwd() / "timeline_plot",
*,
backend_info: Any | None = None,
chip_characterization: Any | None = None,
gate_durations: dict[str, float] | None = None,
compile_to_basis: bool = True,
basis_gates: list[str] | None = None,
):
"""Plot the quantum circuit timeline as table-style PDF files."""
import matplotlib.pyplot as plt
format_prog, qubit_list, time_line = format_result(
compiled_prog,
backend_info=backend_info,
chip_characterization=chip_characterization,
gate_durations=gate_durations,
compile_to_basis=compile_to_basis,
basis_gates=basis_gates,
)
time_line_table = create_time_line_table(format_prog, qubit_list, time_line)
depth = len(time_line)
if depth == 0:
return
figure_save_path = Path(figure_save_path)
figure_save_path.mkdir(parents=True, exist_ok=True)
split_table = depth // 20 + 1
width = min(20, depth)
cmap = {
"RPhi90": "lightblue",
"RPhi180": "orange",
"CZ": "mistyrose",
"CNOT": "mistyrose",
"CX": "mistyrose",
"SWAP": "mistyrose",
"idle": "white",
"MEASURE": "gray",
"Measure": "gray",
}
for i in range(1, split_table + 1):
start = (i - 1) * 20
end = min(i * 20, depth)
if start >= end:
continue
plt.figure(figsize=(width, max(1.0, len(qubit_list) / 2)))
plt.axis("off")
if pd is not None:
values = time_line_table.values[:, start:end]
else:
values = [row[start:end] for row in time_line_table.values]
columns = time_line_table.columns[start:end]
cell_colours = [[cmap.get(str(x).split(" ")[0], "white") for x in row] for row in values]
plt.table(
cellText=values,
colLabels=columns,
colWidths=[0.05] * len(columns),
rowLabels=time_line_table.index,
loc="center",
cellColours=cell_colours,
)
plt.savefig(figure_save_path / f"timeline_{i}.pdf")
plt.close()
[docs]
def plot_time_line_html(
compiled_prog: Any,
output_path: str | Path | None = None,
*,
backend_info: Any | None = None,
chip_characterization: Any | None = None,
gate_durations: dict[str, float] | None = None,
compile_to_basis: bool = True,
basis_gates: list[str] | None = None,
title: str = "Quantum circuit timeline",
unit: str = "ns",
) -> str:
"""Render a scheduled timeline as static HTML/SVG.
Each gate carries an SVG ``title`` tooltip with its qubits, parameters,
start time, duration, and end time.
"""
schedule = schedule_circuit(
compiled_prog,
backend_info=backend_info,
chip_characterization=chip_characterization,
gate_durations=gate_durations,
compile_to_basis=compile_to_basis,
basis_gates=basis_gates,
unit=unit,
)
document = _html_document(title, _schedule_to_svg(schedule, use_timing=True), schedule.unit)
if output_path is not None:
Path(output_path).write_text(document, encoding="utf-8")
return document
[docs]
def circuit_to_html(
circuit: Any,
output_path: str | Path | None = None,
*,
title: str = "Quantum circuit",
) -> str:
"""Render a static HTML/SVG circuit diagram without timing requirements."""
schedule = _layered_circuit_schedule(_normalise_program(circuit))
document = _html_document(title, _schedule_to_svg(schedule, use_timing=False), schedule.unit)
if output_path is not None:
Path(output_path).write_text(document, encoding="utf-8")
return document
def _compile_to_basis_for_timeline(
compiled_prog: Any,
entries: list[_ProgramEntry],
*,
backend_info: Any | None,
chip_characterization: Any | None,
basis_gates: list[str] | None,
) -> Any:
from uniqc.compile import compile as compile_circuit
target_backend = backend_info
if not _has_topology(target_backend) and not _has_connectivity(chip_characterization):
target_backend = _virtual_all_to_all_backend(entries)
return compile_circuit(
compiled_prog,
backend_info=target_backend,
basis_gates=basis_gates or _DEFAULT_BASIS_GATES,
chip_characterization=chip_characterization,
output_format="circuit",
)
def _has_topology(backend_info: Any | None) -> bool:
return bool(getattr(backend_info, "topology", None))
def _has_connectivity(chip_characterization: Any | None) -> bool:
return bool(getattr(chip_characterization, "connectivity", None))
def _virtual_all_to_all_backend(entries: list[_ProgramEntry]) -> Any:
from uniqc.backend_adapter.backend_info import BackendInfo, Platform, QubitTopology
qubits = sorted({q for entry in entries for q in entry.resources})
if not qubits:
qubits = [0]
if len(qubits) == 1:
topology = (QubitTopology(qubits[0], qubits[0]),)
else:
topology = tuple(QubitTopology(u, v) for u in qubits for v in qubits if u != v)
return BackendInfo(
platform=Platform.DUMMY,
name="timeline-virtual-all-to-all",
num_qubits=max(qubits) + 1,
topology=topology,
is_simulator=True,
extra={"basis_gates": list(_DEFAULT_BASIS_GATES)},
)
def _normalise_program(compiled_prog: Any) -> list[_ProgramEntry]:
if hasattr(compiled_prog, "opcode_list"):
return _opcode_list_to_entries(compiled_prog)
if hasattr(compiled_prog, "originir"):
return _originir_to_entries(compiled_prog.originir)
if isinstance(compiled_prog, str):
text = compiled_prog.strip()
if not text:
return []
try:
parsed = json.loads(text)
except json.JSONDecodeError:
return _originir_to_entries(text)
if isinstance(parsed, list):
return [_json_gate_to_entry(entry) for entry in parsed]
if isinstance(parsed, dict):
return [_json_gate_to_entry(parsed)]
return []
if isinstance(compiled_prog, list):
return [_json_gate_to_entry(entry) for entry in compiled_prog]
if isinstance(compiled_prog, dict):
return [_json_gate_to_entry(compiled_prog)]
return []
def _opcode_list_to_entries(circuit: Any) -> list[_ProgramEntry]:
entries: list[_ProgramEntry] = []
for opcode in getattr(circuit, "opcode_list", []):
operation, qubits, cbits, params, _dagger, control_qubits = opcode
all_qubits = _to_int_tuple(qubits)
entries.append(
_ProgramEntry(
name=str(operation),
qubits=all_qubits,
params=_to_tuple(params),
cbits=_to_int_tuple(cbits),
control_qubits=_to_int_tuple(control_qubits),
raw=str(opcode),
)
)
measure_list = getattr(circuit, "measure_list", None) or []
for cbit, qubit in enumerate(measure_list):
entries.append(
_ProgramEntry(
name="MEASURE",
qubits=(int(qubit),),
cbits=(cbit,),
raw=f"MEASURE q[{int(qubit)}], c[{cbit}]",
)
)
return entries
def _originir_to_entries(originir: str) -> list[_ProgramEntry]:
entries: list[_ProgramEntry] = []
try:
from uniqc.compile.originir.originir_line_parser import OriginIR_LineParser
except ImportError:
return entries
for line in originir.splitlines():
stripped = line.strip()
if not stripped:
continue
try:
op, qubit, cbit, param, dagger, ctrl = OriginIR_LineParser.parse_line(stripped)
except Exception:
continue
if op is None or op in {"QINIT", "CREG", "CONTROL", "ENDCONTROL", "DAGGER", "ENDDAGGER", "DEF", "ENDDEF"}:
continue
entries.append(
_ProgramEntry(
name=str(op),
qubits=_to_int_tuple(qubit),
params=_to_tuple(param),
cbits=_to_int_tuple(cbit),
control_qubits=_to_int_tuple(ctrl),
raw=stripped,
)
)
return entries
def _json_gate_to_entry(entry: Any) -> _ProgramEntry:
if not isinstance(entry, dict) or not entry:
return _ProgramEntry("UNKNOWN", ())
if "gate" in entry or "name" in entry:
name = str(entry.get("gate", entry.get("name")))
return _ProgramEntry(
name=name,
qubits=_to_int_tuple(entry.get("qubits", entry.get("q", ()))),
params=_to_tuple(entry.get("params", ())),
cbits=_to_int_tuple(entry.get("cbits", entry.get("c", ()))),
control_qubits=_to_int_tuple(entry.get("controls", entry.get("control_qubits", ()))),
explicit_start=_optional_float(entry.get("start", entry.get("t", None))),
raw=json.dumps(entry, ensure_ascii=False),
)
gate_name = str(next(iter(entry.keys())))
values = next(iter(entry.values()))
values = values if isinstance(values, list) else [values]
upper = gate_name.upper()
if upper == "RPHI":
qubit = int(values[0])
params: tuple[Any, ...] = tuple(values[1:3])
start = float(values[3]) if len(values) > 3 and _is_number(values[3]) else None
name = "RPhi90" if len(values) > 2 and float(values[2]) == 90.0 else "RPhi180"
return _ProgramEntry(name=name, qubits=(qubit,), params=params, explicit_start=start, raw=str(entry))
if upper in _MEASURE_GATES:
qubit_param = values[0] if values else []
start = float(values[1]) if len(values) > 1 and _is_number(values[1]) else None
return _ProgramEntry(
name="MEASURE",
qubits=_to_int_tuple(qubit_param),
explicit_start=start,
raw=str(entry),
)
arity = _gate_arity(upper, values)
qubits = tuple(int(q) for q in values[:arity])
params = tuple(values[arity:])
start = None
if params and _is_number(params[-1]) and (upper in _TWO_QUBIT_GATES or len(params) > 1):
start = float(params[-1])
params = params[:-1]
return _ProgramEntry(name=gate_name, qubits=qubits, params=params, explicit_start=start, raw=str(entry))
def _resolve_gate_durations(
*,
backend_info: Any | None,
chip_characterization: Any | None,
gate_durations: dict[str, float] | None,
) -> dict[str, float]:
durations: dict[str, float] = {}
if backend_info is not None:
extra = getattr(backend_info, "extra", {}) or {}
durations.update(_normalise_duration_dict(extra.get("gate_durations", {})))
if extra.get("single_qubit_gate_time") is not None:
durations["1Q"] = float(extra["single_qubit_gate_time"])
if extra.get("two_qubit_gate_time") is not None:
durations["2Q"] = float(extra["two_qubit_gate_time"])
if extra.get("measure_time") is not None:
durations["MEASURE"] = float(extra["measure_time"])
if chip_characterization is not None:
global_info = getattr(chip_characterization, "global_info", None)
one_q = getattr(global_info, "single_qubit_gate_time", None)
two_q = getattr(global_info, "two_qubit_gate_time", None)
if one_q is not None:
durations["1Q"] = float(one_q)
if two_q is not None:
durations["2Q"] = float(two_q)
if gate_durations:
durations.update(_normalise_duration_dict(gate_durations))
return durations
def _duration_for_gate(gate_name: str, qubits: tuple[int, ...], durations: dict[str, float], *, strict: bool) -> float:
upper = gate_name.upper()
if upper in _ZERO_DURATION_GATES or upper in _VIRTUAL_Z_GATES:
return 0.0
if upper in durations:
return durations[upper]
if upper in _MEASURE_GATES:
if "MEASURE" in durations:
return durations["MEASURE"]
if strict:
raise TimelineDurationError(
format_enriched_message(_missing_duration_message(gate_name, "measure"), "visualization")
)
return 0.0
if len(qubits) >= 2 or upper in _TWO_QUBIT_GATES:
if "2Q" in durations:
return durations["2Q"]
if strict:
raise TimelineDurationError(
format_enriched_message(_missing_duration_message(gate_name, "2q"), "visualization")
)
return 0.0
if "1Q" in durations:
return durations["1Q"]
if strict:
raise TimelineDurationError(
format_enriched_message(_missing_duration_message(gate_name, "1q"), "visualization")
)
return 0.0
def _missing_duration_message(gate_name: str, generic_key: str) -> str:
return (
f"No duration is available for gate {gate_name!r}. Provide backend_info.extra timing data, "
"chip_characterization.global_info gate times, or gate_durations with either the exact gate "
f"name or generic key {generic_key!r}."
)
def _layered_circuit_schedule(entries: list[_ProgramEntry]) -> TimelineSchedule:
all_qubits = sorted({q for entry in entries for q in entry.resources})
available_layer: dict[int, int] = dict.fromkeys(all_qubits, 0)
gates: list[TimelineGate] = []
for index, entry in enumerate(entries):
resources = entry.resources
if entry.is_barrier:
barrier_qubits = resources or tuple(all_qubits)
layer = max((available_layer.get(q, 0) for q in barrier_qubits), default=0)
for q in barrier_qubits:
available_layer[q] = layer + 1
gates.append(
TimelineGate(index, "BARRIER", barrier_qubits, start=float(layer), end=float(layer + 1), layer=layer)
)
continue
if not resources:
continue
layer = max((available_layer.get(q, 0) for q in resources), default=0)
for q in resources:
available_layer[q] = layer + 1
gates.append(
TimelineGate(
index=index,
name=_display_gate_name(entry.name),
qubits=entry.qubits,
params=entry.params,
cbits=entry.cbits,
control_qubits=entry.control_qubits,
start=float(layer),
duration=1.0,
end=float(layer + 1),
layer=layer,
raw=entry.raw,
)
)
total_duration = max((gate.end for gate in gates), default=0.0)
qubits = tuple(sorted({q for gate in gates for q in gate.resources}))
return TimelineSchedule(tuple(gates), qubits, total_duration, unit="layer")
def _schedule_to_svg(schedule: TimelineSchedule, *, use_timing: bool) -> str:
if not schedule.qubits:
return '<svg class="uniqc-circuit" width="640" height="120"><text x="24" y="64">Empty circuit</text></svg>'
lane_height = 54
top = 48
left = 76
right = 36
gate_h = 30
min_gate_w = 34
if use_timing:
span = max(schedule.total_duration, 1.0)
scale = min(8.0, max(0.8, 920.0 / span))
x_for = lambda gate: left + gate.start * scale
w_for = lambda gate: max(min_gate_w, gate.duration * scale if gate.duration > 0 else min_gate_w)
body_width = max(720, int(left + span * scale + right + min_gate_w))
else:
scale = 88.0
x_for = lambda gate: left + gate.layer * scale
w_for = lambda gate: min_gate_w
max_layer = max((gate.layer for gate in schedule.gates), default=0)
body_width = int(left + (max_layer + 1) * scale + right + min_gate_w)
q_to_y = {q: top + i * lane_height for i, q in enumerate(schedule.qubits)}
height = top + (len(schedule.qubits) - 1) * lane_height + 58
parts = [
f'<svg class="uniqc-circuit" width="{body_width}" height="{height}" '
f'viewBox="0 0 {body_width} {height}" xmlns="http://www.w3.org/2000/svg" role="img">',
"<defs>",
"<style>.wire{stroke:#677083;stroke-width:1.4}.qlabel{fill:#1f2937;font:13px ui-monospace,monospace}"
".gate{stroke:#243047;stroke-width:1.1;rx:5;ry:5}.gate-text{fill:#111827;font:12px Arial,sans-serif;"
"text-anchor:middle;dominant-baseline:middle}.connector{stroke:#384152;stroke-width:1.2}.barrier{stroke:#7a3e00;"
"stroke-width:1.5;stroke-dasharray:4 4}.tick{stroke:#d6dbe4;stroke-width:1}.tick-text{fill:#64748b;"
"font:11px ui-monospace,monospace}.virtual{stroke-dasharray:3 2}</style>",
"</defs>",
]
if use_timing and schedule.gates:
ticks = _timeline_ticks(schedule.total_duration)
for tick in ticks:
x = left + tick * scale
parts.append(f'<line class="tick" x1="{x:.2f}" x2="{x:.2f}" y1="20" y2="{height - 18}"/>')
parts.append(
f'<text class="tick-text" x="{x:.2f}" y="16" text-anchor="middle">{html.escape(str(_format_time(tick)))}</text>'
)
for q, y in q_to_y.items():
parts.append(f'<text class="qlabel" x="12" y="{y + 4}">q[{q}]</text>')
parts.append(f'<line class="wire" x1="{left - 8}" x2="{body_width - right}" y1="{y}" y2="{y}"/>')
for gate in schedule.gates:
resources = gate.resources
if not resources:
continue
ys = [q_to_y[q] for q in resources if q in q_to_y]
if not ys:
continue
x = x_for(gate)
width = w_for(gate)
center_x = x + width / 2
tooltip = html.escape(gate.tooltip(unit=schedule.unit))
if gate.is_barrier:
parts.append(f"<g><title>{tooltip}</title>")
for y in ys:
parts.append(
f'<line class="barrier" x1="{center_x:.2f}" x2="{center_x:.2f}" y1="{y - 20}" y2="{y + 20}"/>'
)
parts.append("</g>")
continue
if len(ys) > 1:
parts.append(
f'<line class="connector" x1="{center_x:.2f}" x2="{center_x:.2f}" y1="{min(ys):.2f}" y2="{max(ys):.2f}"/>'
)
label = html.escape(_gate_label(gate.name))
fill = _gate_color(gate)
css_class = "gate virtual" if gate.is_virtual else "gate"
for y in ys:
parts.append(f"<g><title>{tooltip}</title>")
parts.append(
f'<rect class="{css_class}" x="{x:.2f}" y="{y - gate_h / 2:.2f}" '
f'width="{width:.2f}" height="{gate_h}" fill="{fill}"/>'
)
parts.append(f'<text class="gate-text" x="{center_x:.2f}" y="{y + 1:.2f}">{label}</text>')
parts.append("</g>")
parts.append("</svg>")
return "\n".join(parts)
def _html_document(title: str, svg: str, unit: str) -> str:
escaped_title = html.escape(title)
escaped_unit = html.escape(unit)
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escaped_title}</title>
<style>
body {{ margin: 0; padding: 24px; background: #f8fafc; color: #111827; font-family: Arial, sans-serif; }}
.uniqc-wrap {{ overflow-x: auto; border: 1px solid #d8dee9; background: #ffffff; border-radius: 6px; padding: 16px; }}
h1 {{ font-size: 18px; margin: 0 0 12px; font-weight: 600; }}
.meta {{ color: #64748b; font-size: 12px; margin: 0 0 14px; }}
svg.uniqc-circuit {{ display: block; max-width: none; }}
</style>
</head>
<body>
<h1>{escaped_title}</h1>
<p class="meta">Static render. Hover a gate to inspect parameters and timing ({escaped_unit}).</p>
<div class="uniqc-wrap">
{svg}
</div>
</body>
</html>
"""
def _normalise_duration_dict(values: dict[str, Any]) -> dict[str, float]:
durations: dict[str, float] = {}
for key, value in values.items():
durations[_normalise_duration_key(str(key))] = float(value)
return durations
def _normalise_duration_key(key: str) -> str:
compact = key.strip().replace("-", "_").replace(" ", "_").upper()
aliases = {
"1Q": "1Q",
"1_Q": "1Q",
"ONE_QUBIT": "1Q",
"SINGLE": "1Q",
"SINGLE_QUBIT": "1Q",
"SINGLE_QUBIT_GATE_TIME": "1Q",
"2Q": "2Q",
"2_Q": "2Q",
"TWO_QUBIT": "2Q",
"TWO_QUBIT_GATE_TIME": "2Q",
"MEAS": "MEASURE",
"MEASUREMENT": "MEASURE",
"MEASURE_TIME": "MEASURE",
}
return aliases.get(compact, compact)
def _gate_arity(upper: str, values: list[Any]) -> int:
if upper in _TWO_QUBIT_GATES:
return 2
if upper in {"TOFFOLI", "CCX", "CSWAP"}:
return 3
return min(1, len(values))
def _display_gate_name(name: str) -> str:
if name.upper() in _MEASURE_GATES:
return "MEASURE"
return name
def _gate_label(name: str) -> str:
upper = name.upper()
if upper == "MEASURE":
return "M"
if upper == "BARRIER":
return "|"
return name[:8]
def _gate_color(gate: TimelineGate) -> str:
upper = gate.name.upper()
if upper in _MEASURE_GATES:
return "#d1d5db"
if gate.is_virtual:
return "#ecfdf5"
if len(gate.resources) >= 2:
return "#fee2e2"
return "#dbeafe"
def _timeline_ticks(total_duration: float) -> list[float]:
if total_duration <= 0:
return [0.0]
raw_step = total_duration / 8
magnitude = 10 ** max(0, len(str(int(raw_step))) - 1)
step = max(1.0, round(raw_step / magnitude) * magnitude)
ticks: list[float] = []
value = 0.0
while value <= total_duration + 1e-9:
ticks.append(value)
value += step
if ticks[-1] < total_duration:
ticks.append(total_duration)
return ticks
def _layer_for_start(layer_by_start: dict[float, int], start: float) -> int:
if start not in layer_by_start:
layer_by_start[start] = len(layer_by_start)
return layer_by_start[start]
def _is_explicit_pulse_data(compiled_prog: Any) -> bool:
return any(entry.explicit_start is not None for entry in _normalise_program(compiled_prog))
def _to_tuple(value: Any) -> tuple[Any, ...]:
if value is None:
return ()
if isinstance(value, tuple):
return value
if isinstance(value, list):
return tuple(value)
return (value,)
def _to_int_tuple(value: Any) -> tuple[int, ...]:
if value is None:
return ()
if isinstance(value, set):
value = sorted(value)
if isinstance(value, tuple | list):
return tuple(int(v) for v in value if v is not None)
return (int(value),)
def _unique_ints(values: tuple[int, ...]) -> tuple[int, ...]:
seen: set[int] = set()
result: list[int] = []
for value in values:
if value not in seen:
seen.add(value)
result.append(value)
return tuple(result)
def _optional_float(value: Any) -> float | None:
return float(value) if _is_number(value) else None
def _is_number(value: Any) -> bool:
return isinstance(value, int | float)
def _format_time(value: float) -> int | float:
return int(value) if float(value).is_integer() else round(float(value), 6)
class _SimpleTimelineTable:
"""Small pandas-like table used when pandas is not installed."""
def __init__(self, *, columns: list[int | float], index: list[str]) -> None:
self.columns = list(columns)
self.index = list(index)
self._data = [[None for _ in self.columns] for _ in self.index]
self.loc = _SimpleTimelineLoc(self)
@property
def values(self):
return self._data
@property
def shape(self) -> tuple[int, int]:
return (len(self.index), len(self.columns))
def fillna(self, value: str):
for r, row in enumerate(self._data):
for c, cell in enumerate(row):
if cell is None:
self._data[r][c] = value
return self
def __len__(self) -> int:
return len(self.index)
class _SimpleTimelineLoc:
def __init__(self, table: _SimpleTimelineTable) -> None:
self._table = table
def __setitem__(self, key, value) -> None:
row_key, col_key = key
row_idx = self._table.index.index(row_key)
col_idx = self._table.columns.index(col_key)
self._table.values[row_idx][col_idx] = value