open-standards/wai/wai-quantum-py/tests/test_widgets.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

158 lines
7.8 KiB
Python

"""The widgets: the data each draws (the engine's answers, nothing else), their
static fallbacks, their mime bundle, and their front ends run on that data."""
from __future__ import annotations
import json
import subprocess
import sys
import xml.etree.ElementTree as ET
import pytest
import wai_quantum as wq
from wai_quantum import widgets
from conftest import BELL, HERE, node
DEMO = wq.Key.demo()
SVG = "{http://www.w3.org/2000/svg}"
def test_circuit_data_is_the_engines_drawing():
d = widgets.circuit_data(BELL)
assert d["svg"] == wq.draw(BELL, format="svg") and d["text"] == wq.draw(BELL) and d["layout"] == wq.layout(BELL)
assert d["hands"] is None and d["key"] == []
c = wq.Circuit(BELL)
assert c.data == d and c._repr_svg_() == d["svg"] and d["svg"] in c._repr_html_()
assert repr(c) == "Circuit(2 qubits, 4 operations in 3 columns)"
assert getattr(c, "selected", None) is None
def test_circuit_marks_each_gate_with_its_hand_from_a_history():
src = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n"
k = wq.Key.generate()
h = wq.history("start", {"source": src, "by": {"kind": "origin", "name": "example"}, "at": 1}, key=k)
h = wq.history("propose", {"history": h["history"], "parent": "v0", "proposed": src + "x q[1];\n", "accepted": [0], "by": {"kind": "assistant"}, "at": 2}, key=k)
d = widgets.circuit_data(src + "x q[1];\n", history=h["history"])
assert d["hands"] == ["origin", "origin", "assistant"]
assert d["key"] == [{"hand": "assistant", "label": "the assistant", "gates": 1}, {"hand": "origin", "label": "where it came from", "gates": 2}]
root = ET.fromstring(d["svg"])
marked = {g.get("data-op"): g.get("class") for g in root.iter(SVG + "g") if g.get("data-op")}
assert marked == {"0": "qc-item qc-by-origin", "1": "qc-item qc-by-origin", "2": "qc-item qc-by-assistant"}
with pytest.raises(ValueError):
widgets.circuit_data(src, hands=["person"] * 2, history=h["history"])
def test_result_data_is_the_jobs_counts_method_accuracy_and_receipt_check():
job = wq.run(BELL, shots=1000, seed=3, key=DEMO)
d = widgets.result_data(job)
assert d["counts"] == job.counts and list(d["counts"]) == sorted(job.counts, key=lambda k: -job.counts[k])
assert (d["method"], d["accuracy"], d["shots"], d["distinct_outcomes"], d["omitted_shots"]) == ("stabilizer", "exact", 1000, 2, 0)
assert d["receipt"]["valid"] is True and d["receipt"]["demo"] is True and d["receipt"]["signer"] == DEMO.did
r = wq.Result(job)
assert r.data == d and "stabilizer" in r._repr_html_() and "verified" in r._repr_html_()
# The histogram draws one bar per outcome.
html = r._repr_html_()
svg = html[html.index("<svg") : html.index("</svg>") + 6]
assert len(ET.fromstring(svg).findall(SVG + "rect")) == 2
# A job's own card is the same drawing.
assert job._repr_html_() == html
forged = json.loads(json.dumps(job))
forged["result"]["counts"]["00"] += 1
assert widgets.result_data(forged)["receipt"]["valid"] is False and "INVALID" in wq.Result(forged)._repr_html_()
def test_result_data_carries_expectations_with_their_errors_and_amplitudes():
job = wq.run("OPENQASM 3.0; qubit[2] q; h q[0]; ry(0.3) q[1];", observables=["Z1", "X0"], method="pauli", key=DEMO)
d = widgets.result_data(job)
assert [e["observable"] for e in d["expectations"]] == ["Z1", "X0"] and all("bound" in e for e in d["expectations"])
assert "(bound)" in wq.Result(job)._repr_html_()
amp = wq.run("OPENQASM 3.0; qubit[1] q; h q[0];", amplitudes=["0", "1"], key=DEMO)
assert [a["bits"] for a in widgets.result_data(amp)["amplitudes"]] == ["0", "1"]
def test_receipt_data_is_the_engines_check():
k = wq.Key.generate()
job = wq.run(BELL, shots=10, key=k)
d = widgets.receipt_data(job)
bare_result = {x: v for x, v in job["result"].items() if x != "receipt"}
assert d["valid"] is True and d["key"] == k.public_key and d["signer"] == k.did and d["demo"] is False
assert d["digest"] == wq.digest(job["request"], bare_result) and d["alg"] == "ed25519" and d["canon"] == "rfc8785"
w = wq.Receipt(job)
assert w.valid and "verified" in w._repr_html_() and k.did in w._repr_html_()
forged = json.loads(json.dumps(job))
forged["request"]["shots"] = 11
assert not wq.Receipt(forged).valid and "INVALID" in wq.Receipt(forged)._repr_html_()
with pytest.raises(ValueError):
widgets.receipt_data({"request": {}, "result": {}})
def test_bloch_data_is_the_engines_vectors():
d = widgets.bloch_data("OPENQASM 3.0; qubit[2] q; h q[0]; ry(0.5) q[1];", key=DEMO)
assert d["method"] == "statevector" and d["accuracy"] == "exact" and "job" not in d
q0, q1 = d["qubits"]
assert (q0["x"], q0["z"]) == pytest.approx((1, 0), abs=1e-12) and q1["x"] == pytest.approx(0.479425538604203, abs=1e-12)
b = wq.Bloch("OPENQASM 3.0; qubit[2] q; h q[0]; ry(0.5) q[1];", key=DEMO)
assert b.data == d and b._repr_html_().count("<svg") == 2
assert widgets.bloch_data(wq.bloch("OPENQASM 3.0; qubit[1] q;", key=DEMO)) == widgets.bloch_data("OPENQASM 3.0; qubit[1] q;", key=DEMO)
def test_the_static_bloch_projection_puts_the_poles_and_the_equator_where_they_belong():
from wai_quantum._render import project
assert project((0, 0, 1), -35, 20)[1] == pytest.approx(0.9396926, abs=1e-6)
assert project((0, 0, 1), 0, 0) == pytest.approx((0, 1))
assert project((1, 0, 0), 0, 0) == pytest.approx((1, 0))
# Seen from the equator straight on, +y points at the reader.
assert project((0, 1, 0), 0, 0) == pytest.approx((0, 0))
@pytest.mark.skipif(not widgets.HAVE_WIDGETS, reason="the widgets extra is not installed")
def test_each_widget_offers_its_view_and_its_static_html():
from IPython.core.formatters import DisplayFormatter
job = wq.run(BELL, shots=100, key=DEMO)
for w in (wq.Circuit(BELL), wq.Result(job), wq.Receipt(job), wq.Bloch(BELL, key=DEMO)):
bundle, _ = DisplayFormatter().format(w)
assert "application/vnd.jupyter.widget-view+json" in bundle, type(w)
assert "text/html" in bundle and bundle["text/html"] == w._repr_html_()
assert "export default" in w._esm and w.data
c = wq.Circuit(BELL)
c.selected = 1
assert c.selected == 1
def test_the_widgets_render_without_their_library():
code = (
"import sys; sys.modules['anywidget'] = None\n"
"import wai_quantum as wq\n"
"from wai_quantum import widgets\n"
"assert not widgets.HAVE_WIDGETS\n"
f"bell = {BELL!r}\n"
"job = wq.run(bell, shots=50, key=wq.Key.demo())\n"
"parts = [wq.Circuit(bell)._repr_html_(), wq.Result(job)._repr_html_(), wq.Receipt(job)._repr_html_(), wq.Bloch(bell, key=wq.Key.demo())._repr_html_()]\n"
"assert all(parts) and 'verified' in parts[2]\n"
"print('ok')\n"
)
p = subprocess.run([sys.executable, "-I", "-c", code], capture_output=True, text=True, timeout=120)
assert p.returncode == 0 and p.stdout.strip() == "ok", p.stderr
def test_the_front_ends_draw_the_data_they_are_given(tmp_path):
exe = node()
job = wq.run(BELL, shots=1000, seed=3, key=DEMO)
forged = json.loads(json.dumps(job))
forged["result"]["counts"]["00"] += 1
payloads = {
"circuit": widgets.circuit_data(BELL),
"circuit_hands": widgets.circuit_data("OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", hands=["person", "assistant"]),
"result": widgets.result_data(job),
"receipt": widgets.receipt_data(job),
"receipt_forged": widgets.receipt_data(forged),
"bloch": widgets.bloch_data("OPENQASM 3.0; qubit[2] q; h q[0]; ry(0.5) q[1];", key=DEMO),
}
f = tmp_path / "payloads.json"
f.write_text(json.dumps(payloads))
p = subprocess.run([exe, str(HERE / "front_ends.mjs"), str(f)], capture_output=True, text=True, timeout=120)
assert p.returncode == 0, p.stdout + p.stderr
assert "front ends: 4 widgets," in p.stdout, p.stdout