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]>
110 lines
5.5 KiB
JavaScript
110 lines
5.5 KiB
JavaScript
// The Result widget: a job's result as the engine gave it. Its counts as bars, the
|
|
// most frequent first (or their sampled frequencies: each count over the shots),
|
|
// its expectation values with the bound or standard error the method gives, its
|
|
// amplitudes, the method and the accuracy class it names, and the receipt's check
|
|
// made by the engine in Python. The model's `frequencies` and `top` choose what
|
|
// the bars show.
|
|
|
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
const BAR = '#7aa0ff';
|
|
|
|
function el(tag, attrs = {}, ...kids) {
|
|
const e = document.createElement(tag);
|
|
for (const [k, v] of Object.entries(attrs)) {
|
|
if (k === 'text') e.textContent = v;
|
|
else if (v != null) e.setAttribute(k, String(v));
|
|
}
|
|
for (const k of kids) if (k != null) e.append(k);
|
|
return e;
|
|
}
|
|
|
|
function svgEl(tag, attrs = {}, text) {
|
|
const e = document.createElementNS(SVG, tag);
|
|
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
|
|
if (text != null) e.textContent = text;
|
|
return e;
|
|
}
|
|
|
|
const num = (x) => (typeof x === 'number' ? (Number.isInteger(x) ? String(x) : String(Number(x.toPrecision(6)))) : String(x));
|
|
|
|
function badge(ok) {
|
|
return el('span', { class: `wq-badge ${ok ? 'wq-good' : 'wq-bad'}`, text: ok ? 'verified' : 'INVALID' });
|
|
}
|
|
|
|
function histogram(counts, shots, top, frequencies) {
|
|
const rows = Object.entries(counts).slice(0, top);
|
|
if (!rows.length) return null;
|
|
const labelW = Math.max(7.5 * Math.max(...rows.map(([k]) => k.length)) + 12, 40);
|
|
const barW = 260, rowH = 18;
|
|
const peak = Math.max(...rows.map(([, v]) => v)) || 1;
|
|
const W = labelW + barW + 70, H = rowH * rows.length + 4;
|
|
const s = svgEl('svg', { width: W, height: H, viewBox: `0 0 ${W} ${H}`, class: 'wq-histogram', role: 'img', 'aria-label': `Counts of ${rows.length} outcomes` });
|
|
rows.forEach(([k, v], i) => {
|
|
const y = 2 + i * rowH;
|
|
const w = Math.max((barW * v) / peak, 1);
|
|
s.append(svgEl('text', { x: labelW - 6, y: y + 9, 'text-anchor': 'end', 'dominant-baseline': 'central', class: 'wq-mono' }, k));
|
|
const r = svgEl('rect', { x: labelW, y: y + 2, width: w.toFixed(1), height: rowH - 4, rx: 2, fill: BAR });
|
|
r.append(svgEl('title', {}, `${k}: ${v} of ${shots}`));
|
|
s.append(r);
|
|
s.append(svgEl('text', { x: (labelW + w + 6).toFixed(1), y: y + 9, 'dominant-baseline': 'central', class: 'wq-small' }, frequencies && shots ? (v / shots).toFixed(4) : String(v)));
|
|
});
|
|
return s;
|
|
}
|
|
|
|
function table(head, rows) {
|
|
const t = el('table', { class: 'wq-table' }, el('tr', {}, ...head.map((h) => el('th', { text: h }))));
|
|
for (const r of rows) t.append(el('tr', {}, ...r.map((c) => el('td', { class: 'wq-mono', text: c }))));
|
|
return t;
|
|
}
|
|
|
|
function render({ model, el: host }) {
|
|
const root = el('div', { class: 'wq wq-result-widget' });
|
|
host.append(root);
|
|
|
|
function draw() {
|
|
const d = model.get('data') || {};
|
|
const top = Math.max(1, model.get('top') || 16);
|
|
const frequencies = !!model.get('frequencies');
|
|
const head = el('div', {}, el('b', { text: d.method || '?' }), ' · accuracy ', el('b', { text: d.accuracy || '?' }));
|
|
if (d.accuracy_note) head.append(' ', el('span', { class: 'wq-muted', text: `(${d.accuracy_note})` }));
|
|
const parts = [head];
|
|
if (d.why) parts.push(el('div', { class: 'wq-muted', text: d.why }));
|
|
const counts = d.counts || {};
|
|
const n = Object.keys(counts).length;
|
|
if (n) {
|
|
const toggle = el('input', { type: 'checkbox' });
|
|
toggle.checked = frequencies;
|
|
toggle.addEventListener('change', () => { model.set('frequencies', toggle.checked); model.save_changes(); });
|
|
const topBox = el('input', { type: 'number', min: 1, max: Math.max(n, 1), value: Math.min(top, n), class: 'wq-num' });
|
|
topBox.addEventListener('change', () => { model.set('top', Math.max(1, Number(topBox.value) || 1)); model.save_changes(); });
|
|
parts.push(el('div', { class: 'wq-bar' },
|
|
el('span', { text: `${d.shots} shots, ${d.distinct_outcomes ?? n} distinct outcomes; showing` }), topBox,
|
|
el('label', { class: 'wq-check' }, toggle, el('span', { text: 'as sampled frequencies' }))));
|
|
parts.push(histogram(counts, d.shots, top, frequencies));
|
|
const rest = Object.values(counts).slice(top).reduce((a, b) => a + b, 0) + (d.omitted_shots || 0);
|
|
if (rest) parts.push(el('div', { class: 'wq-muted', text: `${rest} more shots fell on outcomes not drawn` }));
|
|
}
|
|
const exps = d.expectations || [];
|
|
if (exps.length) {
|
|
parts.push(table(['observable', 'value', 'error'], exps.map((e) => [
|
|
e.observable, num(e.value),
|
|
e.bound != null ? `± ${num(e.bound)} (bound)` : e.stderr != null ? `± ${num(e.stderr)} (std. error)` : '',
|
|
])));
|
|
}
|
|
const amps = d.amplitudes || [];
|
|
if (amps.length) parts.push(table(['basis state', 're', 'im', '|a|²'], amps.map((a) => [a.bits, num(a.re), num(a.im), num(a.re * a.re + a.im * a.im)])));
|
|
if (d.receipt) {
|
|
const r = d.receipt;
|
|
const line = el('div', { class: 'wq-line' }, 'receipt ', badge(r.valid === true), ' signed by ', el('span', { class: 'wq-mono', title: r.signer || '', text: r.signer || r.key || '?' }));
|
|
if (r.demo) line.append(' ', el('span', { class: 'wq-muted', text: '(the published demo key: it shows the result is unaltered, not who signed it)' }));
|
|
parts.push(line);
|
|
}
|
|
root.replaceChildren(...parts.filter(Boolean));
|
|
}
|
|
|
|
draw();
|
|
for (const ev of ['change:data', 'change:frequencies', 'change:top']) model.on(ev, draw);
|
|
return () => { for (const ev of ['change:data', 'change:frequencies', 'change:top']) model.off(ev, draw); };
|
|
}
|
|
|
|
export default { render };
|