open-standards/wai/wai-quantum-py/python/wai_quantum/_render.py
Transaction Science 22fdc7a3cb wai-quantum-py: WAI Quantum in Python and notebooks, in-process
A new crate, wai/wai-quantum-py, built by maturin 1.15.0 into one abi3 wheel
per platform (pyo3 0.29.3, Python 3.9 on), module wai_quantum. Its extension
module wraps the shared job engine and nothing else: run, plan, lint, draw
(text and the engine SVG), layout, critique, improve, shortcuts,
equivalent, verify, seal, canonical, digest, methods, accuracy_classes and
history, values crossing one to one with JSON (NaN, integers past 64 bits
and non-string keys refused). A job is the service form as a plain dict
with helpers (counts, probabilities, expectations, amplitudes, method,
accuracy, receipt); Key reads and writes the command line key files and
finds the same user key; bloch gives each qubit reduced state from the
engine expectation values. A refusal raises EngineError with the engine
message, its line, and a code slot for the engine diagnostic codes.

Service is a client in the standard library alone for the job service
(submit, status, result, cancel, run), naming itself
wai-quantum-python/<version>, its receipts checked here offline.

Widgets (the widgets extra): Circuit (the engine drawing, gates clickable,
authorship marks from a history), Result, Receipt (the engine verdict) and
Bloch (drag to turn), each a plain ES module, each with static HTML (and
SVG) fallbacks that render without the widget library.

Five doc notebooks run in CI and are held to their stored outputs.
ci/check.sh is the whole check; 62 pytest tests hold the package to the
command line (same results, plans, lints, critiques, drawings, methods,
equivalence verdicts; receipts verified both ways; the same user key; the
digest recomputed from Python canonical bytes), to the service run locally,
and run the front ends under node on the payloads; Python 3.9 smoke with
the wheel alone. Workflow wai-quantum-py.yml runs it on linux and macos.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 19:51:08 -04:00

232 lines
11 KiB
Python

"""Static HTML and SVG for a job, a receipt, a circuit and Bloch vectors.
What a notebook shows when no widget front end is there (a static viewer, a
converted page, a front end without the widget extension), and what the
widgets show before their script runs. Everything here is drawn from the
engine's answers; nothing is computed that the engine did not say. The
output is deterministic: the same answer writes the same bytes.
"""
from __future__ import annotations
import math
from html import escape
from typing import Any, Dict, List, Mapping, Optional, Sequence
GOOD = "#10b981"
BAD = "#ef4444"
BAR = "#7aa0ff"
_CARD = (
"font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;"
"border:1px solid var(--jp-border-color2,rgba(127,127,127,.35));border-radius:8px;padding:10px 12px;margin:2px 0;max-width:100%;overflow-x:auto"
)
_MUTED = "opacity:.7"
_MONO = "font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px"
#: The most outcomes a histogram lists.
TOP = 16
def _num(x: Any) -> str:
if isinstance(x, bool) or not isinstance(x, (int, float)):
return escape(str(x))
if isinstance(x, int):
return str(x)
if x == 0:
return "0"
return f"{x:.6g}"
def _badge(ok: bool, yes: str = "verified", no: str = "INVALID") -> str:
colour = GOOD if ok else BAD
return (
f'<span class="wq-badge" style="display:inline-block;padding:0 7px;border-radius:9px;font-size:11px;font-weight:600;'
f'color:#fff;background:{colour}">{escape(yes if ok else no)}</span>'
)
def _short(s: Optional[str], keep: int = 10) -> str:
if not s:
return ""
return s if len(s) <= 2 * keep + 1 else f"{s[:keep]}…{s[-keep:]}"
def histogram_svg(counts: Mapping[str, int], shots: int, *, top: int = TOP, frequencies: bool = False) -> str:
"""Counts as horizontal bars, the most frequent first: the outcome, its
bar, its count (or its sampled frequency)."""
rows = list(counts.items())[:top]
if not rows:
return ""
label_w = max(7.5 * max(len(k) for k, _ in rows) + 12, 40)
bar_w = 260.0
row_h = 18
peak = max(v for _, v in rows) or 1
w = label_w + bar_w + 70
h = row_h * len(rows) + 4
out = [
f'<svg xmlns="http://www.w3.org/2000/svg" class="wq-histogram" width="{w:g}" height="{h:g}" viewBox="0 0 {w:g} {h:g}" '
f'role="img" aria-label="Counts of {len(rows)} outcomes">'
]
for i, (k, v) in enumerate(rows):
y = 2 + i * row_h
bw = max(bar_w * v / peak, 1.0)
value = f"{v / shots:.4f}" if frequencies and shots else str(v)
out.append(
f'<text x="{label_w - 6:g}" y="{y + 9}" text-anchor="end" dominant-baseline="central" fill="currentColor" '
f'style="{_MONO}">{escape(k)}</text>'
f'<rect x="{label_w:g}" y="{y + 2}" width="{bw:.1f}" height="{row_h - 4}" rx="2" fill="{BAR}"><title>{escape(k)}: {v} of {shots}</title></rect>'
f'<text x="{label_w + bw + 6:.1f}" y="{y + 9}" dominant-baseline="central" fill="currentColor" style="font-size:11px">{value}</text>'
)
out.append("</svg>")
return "".join(out)
def _table(head: Sequence[str], rows: List[Sequence[str]]) -> str:
th = "".join(f'<th style="text-align:left;padding:1px 10px 1px 0;font-weight:600">{escape(h)}</th>' for h in head)
body = "".join("<tr>" + "".join(f'<td style="padding:1px 10px 1px 0;{_MONO}">{c}</td>' for c in r) + "</tr>" for r in rows)
return f'<table style="border-collapse:collapse;margin-top:6px"><tr>{th}</tr>{body}</table>'
def result_html(data: Mapping[str, Any]) -> str:
"""A result's card from the Result widget's data: its method and accuracy
class, its counts as a histogram, its expectation values and amplitudes."""
parts = [f'<div class="wq-result" style="{_CARD}">']
acc = data.get("accuracy") or "?"
parts.append(f'<div><b>{escape(str(data.get("method") or "?"))}</b> · accuracy <b>{escape(str(acc))}</b>')
if data.get("accuracy_note"):
parts.append(f' <span style="{_MUTED}">({escape(str(data["accuracy_note"]))})</span>')
parts.append("</div>")
if data.get("why"):
parts.append(f'<div style="{_MUTED}">{escape(str(data["why"]))}</div>')
counts = data.get("counts") or {}
if counts:
shots = int(data.get("shots") or 0)
listed = min(len(counts), TOP)
parts.append(f'<div style="margin-top:6px">{shots} shots, {data.get("distinct_outcomes", len(counts))} distinct outcomes; the {listed} most frequent:</div>')
parts.append(histogram_svg(counts, shots))
rest = sum(list(counts.values())[TOP:]) + int(data.get("omitted_shots") or 0)
if rest:
parts.append(f'<div style="{_MUTED}">{rest} more shots fell on outcomes not drawn</div>')
exps = data.get("expectations") or []
if exps:
rows = []
for e in exps:
err = ""
if e.get("bound") is not None:
err = f"± {_num(e['bound'])} (bound)"
elif e.get("stderr") is not None:
err = f"± {_num(e['stderr'])} (std. error)"
rows.append([escape(str(e.get("observable"))), _num(e.get("value")), err])
parts.append(_table(["observable", "value", "error"], rows))
amps = data.get("amplitudes") or []
if amps:
parts.append(_table(["basis state", "re", "im", "|a|²"], [[escape(str(a["bits"])), _num(a["re"]), _num(a["im"]), _num(a["re"] ** 2 + a["im"] ** 2)] for a in amps]))
if data.get("receipt"):
r = data["receipt"]
parts.append(f'<div style="margin-top:6px">receipt {_badge(bool(r.get("valid")))} signed by <span style="{_MONO}" title="{escape(str(r.get("signer") or ""))}">{escape(_short(r.get("signer"), 14))}</span>')
if r.get("demo"):
parts.append(f' <span style="{_MUTED}">(the published demo key: it shows the result is unaltered, not who signed it)</span>')
parts.append("</div>")
parts.append("</div>")
return "".join(parts)
def receipt_html(data: Mapping[str, Any]) -> str:
"""A receipt's card from the Receipt widget's data."""
ok = bool(data.get("valid"))
rows = [
["signer", f'<span title="{escape(str(data.get("signer") or ""))}">{escape(str(data.get("signer") or "—"))}</span>'],
["key", escape(str(data.get("key") or "—"))],
["digest", escape(str(data.get("digest") or "—"))],
["signature", escape(_short(data.get("signature"), 16) or "—")],
["form", escape(f"{data.get('alg') or '?'} over {data.get('canon') or 'the first form'} canonical JSON")],
]
body = "".join(f'<tr><td style="padding:1px 12px 1px 0;{_MUTED}">{k}</td><td style="{_MONO};word-break:break-all">{v}</td></tr>' for k, v in rows)
note = ""
if data.get("demo"):
note = f'<div style="{_MUTED}">Signed with the published demo key: it shows the result is unaltered, not who signed it.</div>'
what = escape(str(data.get("binds") or "the request and the result as given"))
return (
f'<div class="wq-receipt" style="{_CARD}"><div>receipt {_badge(ok)} <span style="{_MUTED}">checked here, binding {what}</span></div>'
f'<table style="border-collapse:collapse;margin-top:6px">{body}</table>{note}</div>'
)
def verification_html(v: Mapping[str, Any]) -> str:
return (
f'<div class="wq-verification" style="{_CARD}">receipt {_badge(v.get("valid") is True)} '
f'signed by <span style="{_MONO}">{escape(str(v.get("signer") or v.get("key") or "?"))}</span></div>'
)
def circuit_html(data: Mapping[str, Any]) -> str:
"""The engine's drawing, in a card that scrolls when it is wide."""
return f'<div class="wq-circuit-card" style="{_CARD}">{data["svg"]}</div>'
# -- Bloch vectors ----------------------------------------------------------
#: The view a drawing of Bloch vectors starts from, degrees: turned about z,
#: then tilted towards the reader.
VIEW = (-35.0, 20.0)
def project(v: Sequence[float], azimuth: float, elevation: float) -> tuple:
"""A point on or in the unit sphere as the reader sees it, in the
sphere's radii: (right, up). The same projection the widget draws."""
a, e = math.radians(azimuth), math.radians(elevation)
x, y, z = v
xr = x * math.cos(a) - y * math.sin(a)
yr = x * math.sin(a) + y * math.cos(a)
return (xr, z * math.cos(e) - yr * math.sin(e))
def bloch_svg(q: Mapping[str, Any], *, size: int = 150, view: Sequence[float] = VIEW) -> str:
"""One qubit's Bloch vector on its sphere, seen from ``view``."""
r = size * 0.36
cx, cy = size / 2, size / 2 + 4
az, el = view
def pt(v):
px, py = project(v, az, el)
return cx + r * px, cy - r * py
out = [
f'<svg xmlns="http://www.w3.org/2000/svg" class="wq-bloch" width="{size}" height="{size + 10}" viewBox="0 0 {size} {size + 10}" role="img" '
f'aria-label="Qubit {q["qubit"]}: Bloch vector ({_num(q["x"])}, {_num(q["y"])}, {_num(q["z"])})">',
f'<circle cx="{cx:g}" cy="{cy:g}" r="{r:.2f}" fill="none" stroke="currentColor" stroke-opacity=".35"/>',
]
# The equator, as the projection draws it.
eq = " ".join(f"{x:.2f},{y:.2f}" for x, y in (pt((math.cos(t), math.sin(t), 0.0)) for t in (2 * math.pi * k / 48 for k in range(49))))
out.append(f'<polyline points="{eq}" fill="none" stroke="currentColor" stroke-opacity=".25" stroke-dasharray="3 2"/>')
for v, label in (((0, 0, 1), "|0⟩"), ((0, 0, -1), "|1⟩"), ((1, 0, 0), "+x"), ((0, 1, 0), "+y")):
x0, y0 = pt(tuple(-c for c in v))
x1, y1 = pt(v)
out.append(f'<line x1="{x0:.2f}" y1="{y0:.2f}" x2="{x1:.2f}" y2="{y1:.2f}" stroke="currentColor" stroke-opacity=".2"/>')
lx, ly = pt(tuple(1.22 * c for c in v))
out.append(f'<text x="{lx:.2f}" y="{ly:.2f}" text-anchor="middle" dominant-baseline="central" fill="currentColor" style="font-size:10px;opacity:.65">{escape(label)}</text>')
tx, ty = pt((q["x"], q["y"], q["z"]))
out.append(f'<line x1="{cx:g}" y1="{cy:g}" x2="{tx:.2f}" y2="{ty:.2f}" stroke="{BAR}" stroke-width="2.4" stroke-linecap="round"/>')
out.append(f'<circle cx="{tx:.2f}" cy="{ty:.2f}" r="3.6" fill="{BAR}"/>')
out.append(f'<text x="{cx:g}" y="10" text-anchor="middle" fill="currentColor" style="font-size:11px;font-weight:600">q{q["qubit"]}</text>')
out.append("</svg>")
return "".join(out)
def bloch_html(data: Mapping[str, Any]) -> str:
"""Each qubit's sphere and vector, with its components and length."""
cells = []
for q in data.get("qubits") or []:
nums = f'x {_num(q["x"])} · y {_num(q["y"])} · z {_num(q["z"])}<br>length {_num(q["length"])}'
cells.append(f'<div style="display:inline-block;text-align:center;margin:0 8px 6px 0;vertical-align:top">{bloch_svg(q)}<div style="{_MONO};font-size:11px">{nums}</div></div>')
head = f'<div><b>Bloch vectors</b> · {escape(str(data.get("method") or "?"))} · accuracy <b>{escape(str(data.get("accuracy") or "?"))}</b></div>'
note = f'<div style="{_MUTED}">Each qubit\'s reduced state: length 1 is pure, shorter is mixed (entangled with the others, or noisy).</div>'
return f'<div class="wq-bloch-card" style="{_CARD}">{head}<div style="margin-top:6px">{"".join(cells)}</div>{note}</div>'
def job_html(job: Any) -> str:
"""A job's card, as the Result widget's data draws it."""
from ._data import result_data
return result_html(result_data(job))