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]>
232 lines
11 KiB
Python
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))
|