// 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 };