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

229 lines
9.9 KiB
JavaScript

// The widgets' front ends (python/wai_quantum/static/*.js), run under node on the
// data the Python package builds (written by test_widgets.py), against a small
// document of their own: elements, SVG elements, attributes, classes, events and
// the selectors the front ends use. The engine's SVG, which the Circuit widget
// places as markup, is read by a small XML reader, so its gates can be clicked.
//
// node tests/front_ends.mjs payloads.json
import { readFileSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import assert from 'node:assert/strict';
const here = dirname(fileURLToPath(import.meta.url));
const staticDir = join(here, '..', 'python', 'wai_quantum', 'static');
const payloads = JSON.parse(readFileSync(process.argv[2], 'utf8'));
class Text {
constructor(s) { this.text = String(s); this.parent = null; }
get textContent() { return this.text; }
}
class ClassList {
constructor(e) { this.e = e; }
get set() { return new Set((this.e.attrs.class || '').split(/\s+/).filter(Boolean)); }
contains(c) { return this.set.has(c); }
add(c) { const s = this.set; s.add(c); this.e.attrs.class = [...s].join(' '); }
remove(c) { const s = this.set; s.delete(c); this.e.attrs.class = [...s].join(' '); }
toggle(c, on) { const want = on === undefined ? !this.contains(c) : !!on; if (want) this.add(c); else this.remove(c); return want; }
}
class Element {
constructor(tag, ns = null) {
this.tagName = tag; this.ns = ns; this.attrs = {}; this.children = []; this.parent = null;
this.listeners = {}; this.classList = new ClassList(this); this.style = {};
}
get className() { return this.attrs.class || ''; }
set className(v) { this.attrs.class = v; }
get dataset() {
const d = {};
for (const [k, v] of Object.entries(this.attrs)) if (k.startsWith('data-')) d[k.slice(5).replace(/-(\w)/g, (_, c) => c.toUpperCase())] = v;
return d;
}
setAttribute(k, v) { this.attrs[k] = String(v); }
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
append(...kids) {
for (const k of kids) {
const n = typeof k === 'string' ? new Text(k) : k;
if (n.parent) n.parent.children = n.parent.children.filter((c) => c !== n);
n.parent = this;
this.children.push(n);
}
}
replaceChildren(...kids) { for (const c of this.children) c.parent = null; this.children = []; this.append(...kids); }
replaceWith(n) {
const p = this.parent; if (!p) return;
const i = p.children.indexOf(this);
n.parent = p; p.children[i] = n; this.parent = null;
}
get textContent() { return this.children.map((c) => c.textContent).join(''); }
set textContent(s) { this.replaceChildren(String(s)); }
set innerHTML(s) { this.replaceChildren(...parseXml(s)); }
get checked() { return !!this._checked; }
set checked(v) { this._checked = !!v; }
get value() { return this.attrs.value ?? ''; }
set value(v) { this.attrs.value = String(v); }
addEventListener(ev, f) { (this.listeners[ev] ||= []).push(f); }
dispatch(ev, detail = {}) { for (const f of this.listeners[ev] || []) f({ target: this, pointerId: 1, ...detail }); }
setPointerCapture() {}
*walk() { for (const c of this.children) if (c instanceof Element) { yield c; yield* c.walk(); } }
querySelectorAll(sel) { return [...this.walk()].filter((e) => matches(e, sel)); }
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; }
}
// Selectors: `tag`, `.a.b`, `[attr]`, and a child step `x > y`; enough for the front ends.
function matchesOne(e, s) {
const m = /^([a-z]*)((?:\.[\w-]+)*)((?:\[[\w-]+\])*)$/i.exec(s.trim());
if (!m) throw new Error(`selector not handled here: ${s}`);
const [, tag, classes, attrs] = m;
if (tag && e.tagName !== tag) return false;
for (const c of classes.split('.').filter(Boolean)) if (!e.classList.contains(c)) return false;
for (const a of attrs.match(/[\w-]+/g) || []) if (!(a in e.attrs)) return false;
return true;
}
function matches(e, sel) {
return sel.split(',').some((part) => {
const steps = part.split('>').map((x) => x.trim());
if (!matchesOne(e, steps.at(-1))) return false;
return steps.length === 1 || (e.parent instanceof Element && matchesOne(e.parent, steps[0]));
});
}
// The engine's SVG is XML it writes itself: elements, attributes in double quotes,
// text with the five entities.
function parseXml(s) {
const root = new Element('#root');
const stack = [root];
const unesc = (t) => t.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
const re = /<(\/?)([\w:-]+)((?:\s+[\w:-]+="[^"]*")*)\s*(\/?)>|([^<]+)/g;
let m;
while ((m = re.exec(s))) {
const [, close, tag, attrs, self, text] = m;
if (text !== undefined) { stack.at(-1).append(unesc(text)); continue; }
if (close) { const top = stack.pop(); assert.equal(top.tagName, tag, `unbalanced </${tag}>`); continue; }
const e = new Element(tag, 'svg');
for (const a of attrs.matchAll(/([\w:-]+)="([^"]*)"/g)) e.attrs[a[1]] = unesc(a[2]);
stack.at(-1).append(e);
if (!self) stack.push(e);
}
assert.equal(stack.length, 1, 'unclosed elements in the SVG');
return root.children.slice();
}
globalThis.document = {
createElement: (t) => new Element(t),
createElementNS: (ns, t) => new Element(t, ns),
};
function model(data, traits = {}) {
const state = { data, ...traits };
const on = {};
let saves = 0;
return {
get: (k) => state[k],
set: (k, v) => { state[k] = v; for (const f of on[`change:${k}`] || []) f(); },
save_changes: () => { saves += 1; },
on: (ev, f) => (on[ev] ||= []).push(f),
off: (ev, f) => { on[ev] = (on[ev] || []).filter((g) => g !== f); },
get saves() { return saves; },
};
}
async function widget(name) {
return (await import(pathToFileURL(join(staticDir, `${name}.js`)).href)).default;
}
const checks = [];
function check(what, f) { checks.push([what, f]); }
check('circuit', async () => {
const w = await widget('circuit');
const m = model(payloads.circuit, { selected: null });
const host = new Element('div');
const cleanup = w.render({ model: m, el: host });
const svg = host.querySelector('svg');
assert.ok(svg, 'the engine drawing is placed');
const gates = host.querySelectorAll('.qc-item[data-op]');
assert.equal(gates.length, 2);
assert.equal(host.querySelectorAll('.qc-item').length, 4);
gates[1].dispatch('click');
assert.equal(m.get('selected'), 1);
assert.match(host.textContent, /operation 1: ● on q0, ⊕ on q1/);
assert.ok(host.querySelector('.qc-item.wq-selected[data-op]'));
host.querySelectorAll('.qc-item[data-op]')[1].dispatch('click');
assert.equal(m.get('selected'), null, 'a second click clears it');
assert.equal(host.querySelectorAll('input').length, 0, 'no authorship toggle without marks');
cleanup();
// With marks: the toggle hides and shows them.
const mh = model(payloads.circuit_hands, { selected: null });
const h2 = new Element('div');
w.render({ model: mh, el: h2 });
const box = h2.querySelector('input');
assert.ok(box && box.checked);
assert.ok(h2.querySelector('.qc-item.qc-by-assistant'));
box.checked = false; box.dispatch('change');
assert.ok(h2.querySelector('svg').classList.contains('wq-unmarked'));
});
check('result', async () => {
const w = await widget('result');
const d = payloads.result;
const m = model(d, { frequencies: false, top: 16 });
const host = new Element('div');
w.render({ model: m, el: host });
const text = host.textContent;
assert.match(text, /stabilizer/); assert.match(text, /exact/); assert.match(text, /verified/);
const bars = host.querySelectorAll('rect');
assert.equal(bars.length, Object.keys(d.counts).length);
const values = () => host.querySelector('svg').children.filter((c) => c.tagName === 'text' && c.attrs.class === 'wq-small').map((c) => c.textContent);
assert.deepEqual(values(), Object.values(d.counts).map(String));
m.set('frequencies', true);
assert.deepEqual(values(), Object.values(d.counts).map((v) => (v / d.shots).toFixed(4)));
m.set('top', 1);
assert.equal(host.querySelectorAll('rect').length, 1);
assert.match(host.textContent, /more shots fell on outcomes not drawn/);
});
check('receipt', async () => {
const w = await widget('receipt');
const host = new Element('div');
w.render({ model: model(payloads.receipt), el: host });
assert.match(host.textContent, /verified/);
assert.ok(host.textContent.includes(payloads.receipt.signer));
assert.ok(host.textContent.includes(payloads.receipt.digest));
assert.match(host.textContent, /published demo key/);
const bad = new Element('div');
w.render({ model: model(payloads.receipt_forged), el: bad });
assert.match(bad.textContent, /INVALID/);
assert.doesNotMatch(bad.textContent, /verified/);
});
check('bloch', async () => {
const w = await widget('bloch');
const d = payloads.bloch;
const m = model(d, { view: [-35, 20] });
const host = new Element('div');
w.render({ model: m, el: host });
assert.equal(host.querySelectorAll('svg').length, d.qubits.length);
assert.match(host.textContent, /q0/); assert.match(host.textContent, /length 1/);
// q0 is |+⟩: from the view straight on, its tip is a radius to the right of the centre.
m.set('view', [0, 0]);
const tip = host.querySelectorAll('svg')[0].children.filter((c) => c.tagName === 'circle')[1];
const r = 150 * 0.36;
assert.ok(Math.abs(Number(tip.attrs.cx) - (75 + r)) < 0.02 && Math.abs(Number(tip.attrs.cy) - 79) < 0.02, JSON.stringify(tip.attrs));
// A drag turns the view and keeps it in the model.
const grid = host.querySelector('.wq-grid');
grid.dispatch('pointerdown', { clientX: 0, clientY: 0 });
grid.dispatch('pointermove', { clientX: 50, clientY: 25 });
grid.dispatch('pointerup', {});
assert.deepEqual(m.get('view'), [30, 10]);
assert.ok(m.saves >= 1);
});
let n = 0;
for (const [what, f] of checks) {
await f();
n += 1;
console.log(`ok ${what}`);
}
console.log(`front ends: ${n} widgets, every check passed`);