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]>
98 lines
5.2 KiB
JavaScript
98 lines
5.2 KiB
JavaScript
// The Bloch widget: each qubit's reduced state as the engine measured it (the
|
|
// expectation values of X, Y and Z on that qubit), drawn as a vector on its
|
|
// sphere. Drag a sphere to turn the view; the model's `view` keeps it (azimuth and
|
|
// elevation, degrees). The projection is the one the static drawing uses
|
|
// (wai_quantum._render.project), in plain SVG.
|
|
|
|
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) => (Number.isInteger(x) ? String(x) : String(Number(x.toPrecision(6))));
|
|
|
|
// A point on or in the unit sphere as the reader sees it: (right, up), in radii.
|
|
function project([x, y, z], azimuth, elevation) {
|
|
const a = (azimuth * Math.PI) / 180, e = (elevation * Math.PI) / 180;
|
|
const xr = x * Math.cos(a) - y * Math.sin(a);
|
|
const yr = x * Math.sin(a) + y * Math.cos(a);
|
|
return [xr, z * Math.cos(e) - yr * Math.sin(e)];
|
|
}
|
|
|
|
function sphere(q, view, size = 150) {
|
|
const r = size * 0.36, cx = size / 2, cy = size / 2 + 4;
|
|
const pt = (v) => { const [px, py] = project(v, view[0], view[1]); return [cx + r * px, cy - r * py]; };
|
|
const s = svgEl('svg', { width: size, height: size + 10, viewBox: `0 0 ${size} ${size + 10}`, class: 'wq-bloch', role: 'img', 'aria-label': `Qubit ${q.qubit}: Bloch vector (${num(q.x)}, ${num(q.y)}, ${num(q.z)})` });
|
|
s.append(svgEl('circle', { cx, cy, r: r.toFixed(2), fill: 'none', stroke: 'currentColor', 'stroke-opacity': 0.35 }));
|
|
const eq = [];
|
|
for (let k = 0; k <= 48; k++) { const t = (2 * Math.PI * k) / 48; const [x, y] = pt([Math.cos(t), Math.sin(t), 0]); eq.push(`${x.toFixed(2)},${y.toFixed(2)}`); }
|
|
s.append(svgEl('polyline', { points: eq.join(' '), fill: 'none', stroke: 'currentColor', 'stroke-opacity': 0.25, 'stroke-dasharray': '3 2' }));
|
|
for (const [v, label] of [[[0, 0, 1], '|0⟩'], [[0, 0, -1], '|1⟩'], [[1, 0, 0], '+x'], [[0, 1, 0], '+y']]) {
|
|
const [x0, y0] = pt(v.map((c) => -c)), [x1, y1] = pt(v);
|
|
s.append(svgEl('line', { x1: x0.toFixed(2), y1: y0.toFixed(2), x2: x1.toFixed(2), y2: y1.toFixed(2), stroke: 'currentColor', 'stroke-opacity': 0.2 }));
|
|
const [lx, ly] = pt(v.map((c) => 1.22 * c));
|
|
s.append(svgEl('text', { x: lx.toFixed(2), y: ly.toFixed(2), 'text-anchor': 'middle', 'dominant-baseline': 'central', class: 'wq-axis' }, label));
|
|
}
|
|
const [tx, ty] = pt([q.x, q.y, q.z]);
|
|
s.append(svgEl('line', { x1: cx, y1: cy, x2: tx.toFixed(2), y2: ty.toFixed(2), stroke: BAR, 'stroke-width': 2.4, 'stroke-linecap': 'round' }));
|
|
s.append(svgEl('circle', { cx: tx.toFixed(2), cy: ty.toFixed(2), r: 3.6, fill: BAR }));
|
|
s.append(svgEl('text', { x: cx, y: 10, 'text-anchor': 'middle', class: 'wq-title' }, `q${q.qubit}`));
|
|
return s;
|
|
}
|
|
|
|
function render({ model, el: host }) {
|
|
const root = el('div', { class: 'wq wq-bloch-widget' });
|
|
host.append(root);
|
|
let view = (model.get('view') || [-35, 20]).slice(0, 2);
|
|
|
|
function draw() {
|
|
const d = model.get('data') || {};
|
|
const cells = (d.qubits || []).map((q) => {
|
|
const cell = el('div', { class: 'wq-bloch-cell' }, sphere(q, view));
|
|
const errs = q.bound ? ` · bound ${q.bound.map(num).join(', ')}` : q.stderr ? ` · std. error ${q.stderr.map(num).join(', ')}` : '';
|
|
cell.append(el('div', { class: 'wq-mono wq-small', text: `x ${num(q.x)} · y ${num(q.y)} · z ${num(q.z)}` }),
|
|
el('div', { class: 'wq-mono wq-small', text: `length ${num(q.length)}${errs}` }));
|
|
return cell;
|
|
});
|
|
const grid = el('div', { class: 'wq-grid' }, ...cells);
|
|
let drag = null;
|
|
grid.addEventListener('pointerdown', (e) => { drag = { x: e.clientX, y: e.clientY, view: view.slice() }; grid.setPointerCapture(e.pointerId); });
|
|
grid.addEventListener('pointermove', (e) => {
|
|
if (!drag) return;
|
|
view = [drag.view[0] + (e.clientX - drag.x) * 0.6, Math.max(-80, Math.min(80, drag.view[1] + (e.clientY - drag.y) * 0.4))];
|
|
const svgs = grid.querySelectorAll('svg');
|
|
(d.qubits || []).forEach((q, i) => svgs[i].replaceWith(sphere(q, view)));
|
|
});
|
|
const stop = () => { if (!drag) return; drag = null; model.set('view', view.map((v) => Math.round(v * 10) / 10)); model.save_changes(); };
|
|
grid.addEventListener('pointerup', stop);
|
|
grid.addEventListener('pointercancel', stop);
|
|
root.replaceChildren(
|
|
el('div', {}, el('b', { text: 'Bloch vectors' }), ` · ${d.method || '?'} · accuracy `, el('b', { text: d.accuracy || '?' })),
|
|
grid,
|
|
el('div', { class: 'wq-muted', text: "Each qubit's reduced state: length 1 is pure, shorter is mixed (entangled with the others, or noisy). Drag to turn." }),
|
|
);
|
|
}
|
|
|
|
const onView = () => { view = (model.get('view') || view).slice(0, 2); draw(); };
|
|
draw();
|
|
model.on('change:data', draw);
|
|
model.on('change:view', onView);
|
|
return () => { model.off('change:data', draw); model.off('change:view', onView); };
|
|
}
|
|
|
|
export default { render };
|