"Open" takes .ll, .qir and .jeff beside .qasm: the engine reads the file to OpenQASM 3 in the page (engine_convert, in the worker), the program goes into the editor as a version from the file, and the line under it names the format and its version, the check (written back to its format and read again) and each of the reader's notes with its code. A module the reader refuses leaves the editor as it was, the refusal and its code in the status line. The desktop app answers the same request with a convert command. The Studio e2e opens a QIR module, a jeff module and a module its profile forbids; the desktop's test is pinned in CI. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1360 lines
121 KiB
JavaScript
1360 lines
121 KiB
JavaScript
// The Studio in the installed browser, as a reader uses it:
|
||
//
|
||
// node build.mjs --pkg PATH/TO/ENGINE/PKG && node test/studio.test.mjs
|
||
//
|
||
// Serves dist/ (with the engine package in dist/pkg/); the page's index.html names the
|
||
// live job service, https://wai.transaction.science/quantum-jobs, which the service checks use.
|
||
// - the first example loads; the program is linted, drawn and planned as it is typed;
|
||
// - it runs in this browser: the stabilizer tableau, only 00 and 11, ⟨Z0 Z1⟩ = 1, the
|
||
// receipt checked here and signed by this browser's key, and the run's verdict shown as
|
||
// a badge (executed, nothing checked against an intent), sealed by its receipt;
|
||
// - a run of a program the engine refuses shows the verdict refused, with its code;
|
||
// - "Your hardware" says, in a browser tab, that such a run goes through the desktop
|
||
// app or the command line, since a tab must not hold a provider's credential;
|
||
// - an error in the program is marked on its line;
|
||
// - a QIR module and a jeff module open as OpenQASM 3 through the engine, with its
|
||
// notes and check, and a module its profile forbids is refused with its code;
|
||
// - a rotation runs on the state vector, ⟨Z0⟩ = cos 0.3;
|
||
// - a device target: its coupling map with each pair and qubit coloured by its error,
|
||
// a run placed on it and simulated under its noise with the target's hash in the
|
||
// receipt and the layout drawn on the map, a snapshot of another format imported;
|
||
// - the same Bell job on the service gives the counts run here, shot for shot, and its
|
||
// receipt checks here as the service's (left out with STUDIO_SKIP_SERVICE=1);
|
||
// - Ctrl+Enter in the editor runs; the program and the history survive a reload;
|
||
// - Versions: each change recorded with who made it; a proposal taken change by
|
||
// change; whose hand each line and gate is in; the receipt binding it; a line
|
||
// of work from an older version merged back; kept and downloaded;
|
||
// - Rules: held lines marked, shaded and drawn; a gate set and a region; a
|
||
// proposal's breaking changes shown with their rule and never taken; the
|
||
// model told the rules;
|
||
// - the critique: its measures as the program changes and their change since the last
|
||
// run; a smell named with its lines, flagged on the diagram, shown, and fixed by the
|
||
// engine's rewrite, checked the same circuit;
|
||
// - Alternatives: Improve offers the engine's rewrite checked equivalent, plotted and
|
||
// applied; Explore without a model says it needs one; a rewrite that reaches the GHZ
|
||
// state by a different route is labelled so and flagged in Found, saved there with a
|
||
// note, and kept with the page; an alternative that is the same circuit, taken whole, is
|
||
// a choice the reader's taste learns from, and the card says how the draft becomes it, as
|
||
// a chain of checked rewrites;
|
||
// - the rewrite lens: each rewrite the program admits, its gates marked on the diagram,
|
||
// one taken as the engine's proposal, and the list following the program;
|
||
// - your own way: a while on one's own, kept in this browser, Explore waiting for it, a
|
||
// version made then noted so, the convergence line, and its end;
|
||
// - the study: a feature turned off and on, nothing kept until asked, a choice among
|
||
// Improve's candidates kept with the engine's best, an ownership answer, a probe done with
|
||
// the assistant paused and scored by the engine, the measures, a sealed protocol joined
|
||
// (its condition set by the key, the features held), the record exported sealed, and
|
||
// leaving it;
|
||
// - the classical-shortcut lens: a Clifford ring with a closing rotation layer priced by
|
||
// the cut-rank sum, a Clifford circuit easy at any width, a contraction searched only
|
||
// when thorough, and a check marked stale once the program changes;
|
||
// - the compile lens: a rotation compiled to Clifford+T through checked passes, each with
|
||
// its counts and check, the certificate verified here, the circuit after the last pass
|
||
// and after a chosen one drawn, and a compile marked stale once the program changes;
|
||
// - the estimate pane: a program with a gate under three controls and a rotation counted
|
||
// (an ancilla, three Toffolis, the rotation at its word's T count), its estimate on a
|
||
// chosen qubit, the frontier plotted and listed with the headline marked, a point
|
||
// pointed at read out, every seal verified and the request re-run here, and an
|
||
// estimate marked stale once the program changes;
|
||
// - predict first: the form stops a run until the reader predicts, refuses an outcome of
|
||
// the wrong width, and sets the prediction against the counts;
|
||
// - the correctness record: off by default, and while off nothing about the model's
|
||
// answers is kept, not even a key in storage; turned on, a proposal is recorded with
|
||
// the level the engine checked it to, the reader's "Not this" kept, and the failures
|
||
// exported as an eval corpus in the conformance format;
|
||
// - answer views: an answer in the answer view layer drawn from its text, each view's
|
||
// numbers from the engine here, a number the model wrote in a view refused with its
|
||
// code, the seal checked here; and an answer the scripted model streams, each view
|
||
// drawn as its statement arrives, before the stream has ended;
|
||
// - the assistant, against a scripted model server: a tool call run on the engine, a
|
||
// proposal the engine cannot read sent back with its reason, the corrected one checked
|
||
// and run before it is offered, each proposal with its verdict's badge, and Apply; it asks
|
||
// questions first by default and speaks
|
||
// only when asked; offering a hint after refused runs only when the reader allows it,
|
||
// and never asking the model itself; Improve and Explore with the model, each candidate
|
||
// read, measured and compared by the engine, a rewrite that is not the same circuit
|
||
// left out with its reason;
|
||
// - generators: a program with an integer input shows it under the editor, is linted and
|
||
// drawn at its value, and runs there; the Workbench starts from it, judges a hundred
|
||
// blind edits, keeps the reader's log-depth tree as the cheapest, opens it in the
|
||
// editor, and comes back with the page; the tree is named a known construction, GHZ by
|
||
// doubling, with what it beats the reference on that the cost does not weigh; on a
|
||
// line, the tree is an aberration, offered with the rule to drop, and flagged; your taste:
|
||
// two kept generators compared and one chosen for its economy, the archive ranked by the
|
||
// profile with each one's typicality, the dial turned, a second profile kept apart, and
|
||
// the profiles kept with the page; with the
|
||
// model, its generators are judged at every size, a wrong one and one with a gate the
|
||
// specification forbids left out (an aberration), and it is told the specification and
|
||
// shown the cheapest found; its CZ idea in Explore is a different route to GHZ;
|
||
// - step through: the engine's debugger on the bit-exact method, a slider forward and
|
||
// back to the same bits, the next gate named with its line and marked on the
|
||
// diagram, each qubit on its Bloch sphere and phase disk, the amplitudes, the reduced
|
||
// state of chosen qubits, teleportation's four branches a quarter each exactly, one
|
||
// chosen and q2 then holding the state q0 held, and under noise the density matrix
|
||
// beside its noiseless state;
|
||
// - editing by the drawing: a gate dropped from the palette onto a wire, a gate dragged
|
||
// to another wire, moved and deleted from the keyboard, each an edit to the program's
|
||
// text recorded as the reader's, and a gate of a broadcast refused with its line;
|
||
// - the circuit in words for a screen reader, and the drawing saved as SVG and LaTeX,
|
||
// each the engine's;
|
||
// - error correction: a pane closed until opened; a code shown with its certified distance and its seal checked,
|
||
// qubits flipped on the lattice and their syndrome lit, the colour decoder's
|
||
// correction marked and the residual judged, the certificate's witness shown, a
|
||
// threshold measured and plotted one line per distance; and a run mitigated by
|
||
// virtual distillation under its noise, raw and mitigated with their errors;
|
||
// - the course, in English: a lesson's starter loaded and checked, not passed, What went
|
||
// wrong? from the engine's verdict, a hint with the engine's pointer, the solution passed
|
||
// and marked, a rule and a generator's cases each with their verdict, the assistant's hint
|
||
// asked for without a model, the solution never on the page, and the progress kept in this
|
||
// browser across a reload;
|
||
// - the course in Chinese, from the browser's language: the interface, the lesson, the check
|
||
// and what went wrong in Chinese, marked machine-assisted; the switch to English and back,
|
||
// the choice kept; nothing scrolls sideways at a phone's width with the course open;
|
||
// - no console error and no failed request; at a phone's width nothing scrolls sideways.
|
||
// Exit 0 iff every check passes.
|
||
import { createServer } from 'node:http';
|
||
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
||
import { dirname, extname, join, normalize, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default;
|
||
const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
||
const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots');
|
||
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm', '.json': 'application/json' };
|
||
const server = createServer((req, res) => {
|
||
let file = normalize(join(DIST, decodeURIComponent(new URL(req.url, 'http://x').pathname)));
|
||
if (!file.startsWith(DIST)) { res.writeHead(403).end(); return; }
|
||
if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html');
|
||
if (!existsSync(file)) { res.writeHead(404).end('not found'); return; }
|
||
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' });
|
||
res.end(readFileSync(file));
|
||
});
|
||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||
const ORIGIN = `http://127.0.0.1:${server.address().port}`;
|
||
let fails = 0;
|
||
const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
|
||
const LONG = 120_000;
|
||
// The current version's line once it says all of `res`: a version is recorded
|
||
// after the editor changes, so a check waits for the list rather than reading
|
||
// it at once (false if it never does).
|
||
const settles = (page, ...res) => page.waitForFunction((srcs) => {
|
||
const text = document.querySelector('#qs-vs-list li.here')?.textContent ?? '';
|
||
return srcs.every((s) => new RegExp(s).test(text));
|
||
}, res.map((r) => r.source), { timeout: LONG }).then(() => true, () => false);
|
||
const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
|
||
|
||
async function open(viewport = { width: 1400, height: 950 }, locale = 'en-US', path = '/') {
|
||
const ctx = await browser.newContext({ viewport, colorScheme: 'dark', locale });
|
||
const page = await ctx.newPage();
|
||
const seen = { errors: [], failed: [] };
|
||
page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); });
|
||
page.on('pageerror', (e) => seen.errors.push(String(e)));
|
||
page.on('requestfailed', (r) => seen.failed.push(`${r.url()} (${r.failure()?.errorText})`));
|
||
page.on('response', (r) => { if (r.status() >= 400) seen.failed.push(`${r.url()} (${r.status()})`); });
|
||
await page.goto(ORIGIN + path, { waitUntil: 'domcontentloaded' });
|
||
return { ctx, page, seen };
|
||
}
|
||
const done = (page) => page.waitForFunction(() => !/^Running|^On the service/.test(document.querySelector('.qs-status')?.textContent ?? '') && document.querySelector('#qs-run') && !document.querySelector('#qs-run').disabled, null, { timeout: LONG });
|
||
const verified = (page) => page.waitForFunction(() => /verifies|does not verify|not checked/.test(document.querySelector('#qs-verified')?.textContent ?? ''), null, { timeout: LONG });
|
||
const counts = (page) => page.$$eval('.qs-bar', (bs) => bs.map((b) => [b.querySelector('.qs-key').textContent, b.querySelector('.qs-n').textContent]));
|
||
|
||
const { ctx, page, seen } = await open();
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.textContent('#qs-lint')) === '2 qubits · 2 gates', `the first example is linted as the engine reads it (${await page.textContent('#qs-lint')})`);
|
||
await page.waitForFunction(() => document.querySelector('#qs-circuit')?.textContent.includes('⊕'), null, { timeout: LONG });
|
||
const drawing = await page.textContent('#qs-circuit');
|
||
check(/q0: .*H.*●/.test(drawing) && /q1: .*⊕/.test(drawing), 'the circuit is drawn as the program is typed');
|
||
const svg = await page.$eval('#qs-diagram svg', (g) => ({ label: g.getAttribute('aria-label'), rings: g.querySelectorAll('.qc-ring').length, dots: g.querySelectorAll('.qc-dot').length, joins: g.querySelectorAll('.qc-join').length, meters: g.querySelectorAll('.qc-measure').length }));
|
||
check(svg.rings === 1 && svg.dots === 1 && svg.joins === 1 && svg.meters === 2 && /2 qubits/.test(svg.label), `the circuit is drawn as a diagram with a text alternative (${JSON.stringify(svg)})`);
|
||
await page.click('#qs-astext');
|
||
check(await page.isVisible('#qs-circuit') && await page.isHidden('#qs-diagram'), '“Text” shows the circuit as text to copy');
|
||
await page.click('#qs-astext');
|
||
await page.waitForFunction(() => /Stabilizer tableau/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the plan is shown before running (${await page.textContent('#qs-plan')})`);
|
||
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
await verified(page);
|
||
check((await page.textContent('.qs-method')) === 'Stabilizer tableau', 'run in this browser: the stabilizer tableau');
|
||
const here = await counts(page);
|
||
check(here.length === 2 && here.every(([k]) => k === '00' || k === '11'), `only 00 and 11 (${JSON.stringify(here)})`);
|
||
const zz = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
|
||
check(zz.some((t) => t.startsWith('⟨Z0 Z1⟩') && t.includes('1.000000')), `⟨Z0 Z1⟩ = 1 (${zz.slice(0, 2).join(' | ')})`);
|
||
check((await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the receipt verifies, checked here (${await page.textContent('#qs-verified')})`);
|
||
check((await page.textContent('#qs-signed-by')).startsWith('this browser’s key did:key:z6Mk'), `signed by this browser's key (${await page.textContent('#qs-signed-by')})`);
|
||
check((await page.$$('.qs-hitem')).length === 1, 'the run is in the history');
|
||
{
|
||
const badge = await page.$eval('#qs-verdict', (b) => ({ level: b.dataset.level, text: b.textContent, line: b.parentElement.textContent }));
|
||
check(badge.level === 'executed' && /executed/.test(badge.text) && /no intent was given/.test(badge.line) && /sealed by the receipt/.test(badge.line), `the run states its verdict as a badge, sealed by its receipt (${badge.line})`);
|
||
}
|
||
// Your own hardware: in a browser tab the switch says where such a run goes, and stays here.
|
||
await page.click('#qs-where-hardware');
|
||
check(/desktop app or the command line/.test(await page.textContent('.qs-status')) && /a browser tab must not hold the credentials/.test(await page.textContent('.qs-status')), `“Your hardware” says a run on your own hardware goes through the desktop app or the command line (${await page.textContent('.qs-status')})`);
|
||
check((await page.getAttribute('[data-where=local]', 'aria-pressed')) === 'true' && await page.isHidden('#qs-provider'), 'and the Studio still runs in this browser');
|
||
// What it looks like, for a reader of the change: the dark scheme and the light.
|
||
mkdirSync(SHOTS, { recursive: true });
|
||
await page.screenshot({ path: join(SHOTS, 'studio-dark.png'), fullPage: true });
|
||
await page.emulateMedia({ colorScheme: 'light' });
|
||
await page.screenshot({ path: join(SHOTS, 'studio-light.png'), fullPage: true });
|
||
await page.emulateMedia({ colorScheme: 'dark' });
|
||
|
||
// An error is marked on its line.
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
await page.keyboard.type('\nfoo q[0];');
|
||
await page.waitForFunction(() => document.querySelector('#qs-lint')?.textContent === 'problem', null, { timeout: LONG });
|
||
await page.waitForSelector('.cm-lintRange-error', { timeout: LONG });
|
||
check(true, 'an unknown gate is marked in the editor');
|
||
// Run as it is: refused, and the verdict says so with the reader's code.
|
||
await page.click('#qs-run');
|
||
await page.waitForSelector('#qs-verdict[data-level=refused]', { timeout: LONG });
|
||
check(/Q103/.test(await page.$eval('#qs-verdict', (b) => b.parentElement.textContent)), `a refused run states the verdict refused, with its code (${await page.$eval('#qs-verdict', (b) => b.parentElement.textContent)})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
for (let i = 0; i < 10; i++) await page.keyboard.press('Backspace');
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
|
||
// A program in another format: QIR and jeff opened, read by the engine to
|
||
// OpenQASM 3 with its notes and its check; a module the reader refuses keeps
|
||
// the editor as it was and says why, with its code.
|
||
{
|
||
const formats = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'quantum-conformance', 'formats');
|
||
await page.setInputFiles('#qs-open-file', { name: 'bell.ll', mimeType: 'text/plain', buffer: readFileSync(join(formats, 'qir', 'bell_base.ll')) });
|
||
await page.waitForFunction(() => /read from QIR/.test(document.querySelector('#qs-shows')?.textContent ?? ''), null, { timeout: LONG });
|
||
const fromQir = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||
check(/^\/\/ Read from QIR 2\.0, base profile/m.test(fromQir) && /OPENQASM 3\.0;/.test(fromQir) && /cx q\[0\], q\[1\];/.test(fromQir) && /checked: equivalent/.test(await page.textContent('#qs-shows')), `a QIR module opens as OpenQASM 3, checked (${(await page.textContent('#qs-shows')).slice(0, 120)}…)`);
|
||
await page.setInputFiles('#qs-open-file', { name: 'ghz.jeff', mimeType: 'application/octet-stream', buffer: readFileSync(join(formats, 'jeff', 'upstream', 'ghz-linear_3.jeff')) });
|
||
await page.waitForFunction(() => /read from jeff/.test(document.querySelector('#qs-shows')?.textContent ?? ''), null, { timeout: LONG });
|
||
const fromJeff = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||
check(/Read from jeff 0\.3/.test(fromJeff) && /qubit\[3\] q;/.test(fromJeff) && /\[Q715\]/.test(await page.textContent('#qs-shows')), `a jeff module opens as OpenQASM 3 with the reader's notes (${(await page.textContent('#qs-shows')).slice(0, 160)}…)`);
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.setInputFiles('#qs-open-file', { name: 'forbidden.ll', mimeType: 'text/plain', buffer: readFileSync(join(formats, 'qir', 'refuse', 'base_branch.ll')) });
|
||
await page.waitForFunction(() => /does not read as QIR/.test(document.querySelector('.qs-status')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(/\[Q704\]/.test(await page.textContent('.qs-status')) && (await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === fromJeff, `a module its profile forbids is refused with its code, the editor kept (${await page.textContent('.qs-status')})`);
|
||
}
|
||
|
||
// A rotation: any angle, on the state vector.
|
||
await page.selectOption('#qs-example', 'rotation');
|
||
await page.waitForFunction(() => /State vector/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
const z = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
|
||
check((await page.textContent('.qs-method')) === 'State vector, double precision' && z.some((t) => t.startsWith('⟨Z0⟩') && t.includes(Math.cos(0.3).toFixed(6))), `rx(0.3) on the state vector, ⟨Z0⟩ = cos 0.3 (${z[0]})`);
|
||
|
||
// A device target: its coupling map drawn with each pair and qubit coloured by its
|
||
// error; a run placed on its most reliable qubits, simulated under its noise, the
|
||
// target's hash in the receipt, and the layout drawn on the map; a snapshot brought in
|
||
// another format read and kept; and back to no device, the noise fields free again.
|
||
{
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.waitForFunction(() => document.querySelector('#qs-lint')?.textContent === '2 qubits · 2 gates' && document.querySelector('#qs-circuit')?.textContent.includes('⊕'), null, { timeout: LONG });
|
||
await page.click('#qs-device > summary');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-target option').length >= 5, null, { timeout: LONG });
|
||
await page.selectOption('#qs-target', 'synthetic-heavy-hex-27');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-device-map .qs-dev-q').length === 27, null, { timeout: LONG });
|
||
const map = await page.$eval('#qs-device-map svg', (g) => ({ q: g.querySelectorAll('.qs-dev-q').length, pairs: g.querySelectorAll('.qs-dev-pair').length, label: g.getAttribute('aria-label'), bins: new Set([...g.querySelectorAll('.qs-dev-pair')].map((p) => p.getAttribute('data-bin'))).size, tips: g.querySelectorAll('title').length }));
|
||
check(map.q === 27 && map.pairs === 28 && map.bins >= 4 && map.tips === 55 && /27 qubits and 28 coupled pairs/.test(map.label), `the coupling map: 27 qubits and 28 pairs, each coloured by its error in bins, each with its numbers on hover (${JSON.stringify(map)})`);
|
||
check(await page.isDisabled('#qs-noise-readout') && await page.$eval('#qs-noise-note', (n) => !n.hidden), 'with a device chosen, the noise fields wait: the noise is the device’s own');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-device-map .qs-dev-placed').length === 2, null, { timeout: LONG });
|
||
check(/^Placed on \d+, \d+; 0 swaps; expected success 0\.\d+ against 0\.\d+ for the fewest swaps/.test(await page.textContent('#qs-dev-placed')), `the plan places the program before it runs, and draws the layout on the map (${await page.textContent('#qs-dev-placed')})`);
|
||
check(/Density matrix/.test(await page.textContent('#qs-plan')), `and plans it on the density matrix (${await page.textContent('#qs-plan')})`);
|
||
await page.click('#qs-run'); await done(page); await verified(page);
|
||
const res = await page.$eval('#qs-device-result', (b) => b.textContent);
|
||
const hash = await page.textContent('#qs-dev-hash');
|
||
check(/synthetic-heavy-hex-27/.test(res) && /^[0-9a-f]{64}$/.test(hash) && /A model of the device, not a run on it/.test(res) && /total variation distance 0\.\d+/.test(res), `the result names the target, its hash, the placement and its distance from the ideal, and that it is a model (${res.slice(0, 140)}…)`);
|
||
check((await page.textContent('.qs-method')) === 'Density matrix' && (await page.textContent('#qs-verified')).startsWith('✓ verifies'), 'run here on the density matrix, the receipt checked here');
|
||
const here = await counts(page);
|
||
check(here.some(([k]) => k === '00') && here.some(([k]) => k === '11') && here.some(([k]) => k === '01' || k === '10'), `the device's noise moves some shots off 00 and 11 (${JSON.stringify(here)})`);
|
||
await page.selectOption('#qs-dev-colour', 't1');
|
||
check(/T1 \(shorter is worse\)/.test(await page.$eval('#qs-device-map svg', (g) => g.getAttribute('aria-label'))) && /≥ \d/.test(await page.textContent('.qs-dev-legend')), 'the qubits can be coloured by T1, the legend reading longer as better');
|
||
await page.screenshot({ path: join(SHOTS, 'studio-device.png'), fullPage: true });
|
||
// A snapshot in the device-capabilities shape, imported and kept in this browser.
|
||
const snapshot = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'wai-quantum', 'src', 'engine', 'targets', 'snapshots', 'synthetic-ring-8.device-capabilities.json'));
|
||
await page.setInputFiles('#qs-target-file', { name: 'ring.json', mimeType: 'application/json', buffer: snapshot });
|
||
await page.waitForFunction(() => document.querySelector('#qs-target')?.value === 'imported:synthetic-ring-8' && document.querySelectorAll('#qs-device-map .qs-dev-q').length === 8, null, { timeout: LONG });
|
||
check(/device-capabilities/.test(await page.textContent('.qs-status')), `a snapshot in another format is read and kept here (${await page.textContent('.qs-status')})`);
|
||
await page.selectOption('#qs-target', '');
|
||
check(!(await page.isDisabled('#qs-noise-readout')) && (await page.$$('#qs-device-map svg')).length === 0, 'with no device, the noise fields are free again and no map is drawn');
|
||
}
|
||
|
||
// The same Bell job on the service, and here: the same counts.
|
||
// (STUDIO_SKIP_SERVICE=1 leaves it out, where a build may not reach the service.)
|
||
if (process.env.STUDIO_SKIP_SERVICE) {
|
||
console.log('SKIP the service checks (STUDIO_SKIP_SERVICE)');
|
||
} else {
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.click('#qs-run'); await done(page); await verified(page);
|
||
const local = await counts(page);
|
||
await page.click('.qs-seg[data-where=service]');
|
||
await page.click('#qs-run'); await done(page); await verified(page);
|
||
const remote = await counts(page);
|
||
check(JSON.stringify(remote) === JSON.stringify(local), `the service gives the counts run here, shot for shot (${JSON.stringify(remote)})`);
|
||
check((await page.textContent('#qs-signed-by')).startsWith('the service’s key') && (await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the service's receipt checks here, as the service's (${await page.textContent('#qs-signed-by')})`);
|
||
await page.click('.qs-seg[data-where=local]');
|
||
|
||
}
|
||
|
||
// Ctrl+Enter runs; the program and the history survive a reload. (A job run again
|
||
// unchanged gives the same signed result, which the history keeps once; so this one
|
||
// asks for other shots.)
|
||
const before = (await page.$$('.qs-hitem')).length;
|
||
await page.fill('#qs-shots', '321');
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+Enter');
|
||
// The run starts after the key, so its entry is waited for: the run button can still be
|
||
// enabled, and the status not yet running, before the run has begun.
|
||
const ranByKey = await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length === n + 1, before, { timeout: LONG }).then(() => true, () => false);
|
||
await done(page);
|
||
check(ranByKey, 'Ctrl+Enter in the editor runs the program');
|
||
const program = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === program, 'the program survives a reload');
|
||
await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length >= n, before + 1, { timeout: LONG });
|
||
check(true, 'the history survives a reload');
|
||
|
||
// The critique, as the program changes.
|
||
const doc = () => page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||
const setProgram = async (q) => { await page.evaluate((q) => window.quantumStudio.fill({ qasm: q }), q); await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, q); };
|
||
const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textContent).catch(() => '');
|
||
{
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG });
|
||
const m = await page.textContent('#qs-measures');
|
||
check(/2 qubits/.test(m) && /1 two-qubit/.test(m) && await page.isVisible('#qs-clifford') && /fidelity, modelled/.test(m) && (await page.$$('#qs-smells li')).length === 0,
|
||
`the critique gives the Bell pair's measures, that it is Clifford, a modelled fidelity, and no smells (${m})`);
|
||
const SMELLY = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(SMELLY);
|
||
await page.waitForSelector('.qs-smell[data-kind=cancels]', { timeout: LONG });
|
||
const smell = await page.textContent('.qs-smell[data-kind=cancels] .qs-smell-text');
|
||
check(smell === 'x on qubit 1 twice in a row (lines 5 and 6): together they do nothing', `a smell is named with its lines (${smell})`);
|
||
check((await page.$$('#qs-diagram .qc-item.qc-flag')).length === 2, 'its two gates are flagged on the diagram');
|
||
await page.hover('.qs-smell[data-kind=cancels]');
|
||
check((await page.$$('#qs-diagram .qc-item.qc-smell')).length === 2 && (await page.$$('#qs-diagram .qc-wire.qc-smell')).length === 0, 'pointing at the smell marks its gates, and not their wires');
|
||
await page.click('.qs-smell[data-kind=cancels] button >> text=Show');
|
||
const sel = await page.evaluate(() => { const v = window.quantumStudio.view, d = v.state.doc, r = v.state.selection.main; return [r.from === d.line(5).from, r.to === d.line(6).to]; });
|
||
check(sel[0] && sel[1], 'Show selects its lines in the editor');
|
||
await page.click('#qs-run'); await done(page);
|
||
await page.click('.qs-smell[data-kind=cancels] .qs-fix');
|
||
await page.waitForFunction(() => !/x q\[1\];/.test(window.quantumStudio.view.state.doc.toString()) && /checked the result/.test(document.querySelector('#qs-shows')?.textContent ?? ''), null, { timeout: LONG });
|
||
const shows = await page.textContent('#qs-shows');
|
||
check(/^Fixed: x on qubit 1/.test(shows) && /same tableau/.test(shows), `Fix applies the engine's rewrite, checked the same circuit (${shows})`);
|
||
await page.waitForFunction(() => /−2/.test(document.querySelector('#qs-measures [data-k=gates]')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the measures show their change since the last run (${await chip('gates')})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+z');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, SMELLY, { timeout: LONG });
|
||
check(true, 'Undo brings the fixed lines back');
|
||
await setProgram('OPENQASM 3.0;\nqubit[3] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\n');
|
||
await page.waitForSelector('.qs-smell[data-kind=idle]', { timeout: LONG });
|
||
check((await page.$$('#qs-diagram .qc-wire.qc-flag')).length === 2 && !(await page.$('.qs-smell[data-kind=idle] .qs-fix')), 'qubits nothing uses are flagged on their wires, with no Fix');
|
||
}
|
||
|
||
// Alternatives: Improve, without a model; Explore says it needs one.
|
||
{
|
||
const MERGE = 'OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nrz(0.25) q[0];\nrz(0.5) q[0];\nc[0] = measure q[0];\n';
|
||
await setProgram(MERGE);
|
||
await page.click('#qs-improve');
|
||
await page.waitForSelector('.qs-cand', { timeout: LONG });
|
||
const card = await page.textContent('.qs-cand');
|
||
check(/the engine/.test(card) && /same circuit · on every basis state/.test(card) && /2 gates/.test(card), `Improve offers the engine's rewrite, checked on every basis state (${card.slice(0, 160)})`);
|
||
check(await page.isVisible('#qs-atlas') && (await page.$$('#qs-atlas .qs-dot')).length === (await page.$$('.qs-cand')).length && (await page.$$('#qs-atlas .qs-ring')).length === 1, 'the atlas plots the draft and each alternative');
|
||
check(/Connect a model/.test(await page.textContent('#qs-alt-body')), 'without a model, Improve says the engine’s are the only ones');
|
||
check(/How: 1 rewrite, each checked/.test(card) && card.includes('rz(0.25) q[0] and rz(0.5) q[0] merge into rz(0.75) q[0]'), `the card says how the draft becomes it, a chain of checked rewrites (${card.slice(0, 220)})`);
|
||
check((await doc()) === MERGE, 'nothing changes the program until an alternative is applied');
|
||
await page.click('.qs-cand .qs-apply');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
check((await doc()) === MERGE && /A proposal from the engine \(Alternatives\)/.test(await page.textContent('#qs-review')), 'Apply offers the alternative as the engine’s proposal, to take change by change');
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => /rz\(0\.25 \+ 0\.5\) q\[0\];/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG });
|
||
check(true, 'taking it puts the alternative in the editor');
|
||
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '1', null, { timeout: LONG });
|
||
check(/^1 choice\b/.test(await page.textContent('#qs-taste-summary')), 'taken whole, an alternative that is the same circuit is a choice the reader’s taste learns from');
|
||
await page.click('#qs-explore');
|
||
await page.waitForSelector('#qs-alt-nomodel', { timeout: LONG });
|
||
check(true, 'Explore without a model says it needs one');
|
||
}
|
||
|
||
// What is new: a rewrite that is the GHZ state, built unlike the known chain, is a
|
||
// different route to it, and Found keeps it until the reader saves or dismisses it.
|
||
{
|
||
check((await page.textContent('#qs-found-count')) === '0 flagged · 0 saved' && await page.isVisible('#qs-found-empty'), 'Found starts empty');
|
||
await setProgram('OPENQASM 3.0;\nqubit[3] q;\nh q[2];\nx q[0];\nx q[0];\ncx q[2], q[1];\ncx q[1], q[0];\n');
|
||
await page.click('#qs-improve');
|
||
await page.waitForSelector('.qs-cand .qs-novelty', { timeout: LONG });
|
||
const labels = await page.$$eval('.qs-cand .qs-novelty', (bs) => bs.map((b) => [b.textContent, b.title]));
|
||
const route = labels.find(([t]) => t === 'a different route to GHZ, by a chain of CNOTs');
|
||
check(route && await page.$('.qs-cand.find'), `a rewrite that makes the GHZ state, built unlike the chain, is labelled a different route (${labels.map(([t]) => t).join(' | ')})`);
|
||
check(/0\.\d\d apart/.test(route?.[1] ?? ''), 'the label says how far apart they are built');
|
||
await page.waitForSelector('#qs-found-list .qs-found-item[data-kind=route]', { timeout: LONG });
|
||
check((await page.textContent('#qs-found-count')) === '1 flagged · 0 saved' && /different route/.test(await page.textContent('#qs-found-list')) && /Improve/.test(await page.textContent('#qs-found-list')), 'Found flags it, and says where it came from');
|
||
await page.click('#qs-found-list .qs-found-save');
|
||
await page.fill('#qs-found-list .qs-found-item input', 'the ladder, read backwards');
|
||
await page.dispatchEvent('#qs-found-list .qs-found-item input', 'change');
|
||
check((await page.textContent('#qs-found-count')) === '1 flagged · 1 saved' && (await page.textContent('#qs-found-list .qs-found-save')) === 'Saved', 'Save keeps it, and the count says so');
|
||
}
|
||
|
||
// The rewrite lens: each rewrite that keeps the meaning, checked, taken one at a time.
|
||
{
|
||
const LENS = 'OPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\nt q[0];\nt q[0];\n';
|
||
await setProgram(LENS);
|
||
// The diagram is drawn a moment after the edit; marks go on the one drawn.
|
||
// (The measurements it draws at the end carry no gate.)
|
||
await page.waitForFunction(() => /5 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && [...document.querySelectorAll('#qs-diagram .qc-item')].filter((g) => /^\d+$/.test(g.dataset.op ?? '')).length === 5, null, { timeout: LONG });
|
||
await page.click('#qs-rewrites');
|
||
await page.waitForSelector('#qs-rewrite-list li', { timeout: LONG });
|
||
const rows = await page.$$eval('#qs-rewrite-list li', (ls) => ls.map((l) => [l.dataset.rule, l.textContent]));
|
||
check(rows.some(([r, t]) => r === 'conjugate' && t.includes('cx q[0], q[1] between two h q[1] is cz q[0], q[1]')) && rows.some(([r, t]) => r === 'merge' && t.includes('t q[0] and t q[0] merge into s q[0]')),
|
||
`the lens lists each rewrite the program admits, in words (${rows.map(([r]) => r).join(', ')})`);
|
||
await page.hover('#qs-rewrite-list li[data-rule=conjugate]');
|
||
const marked = await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-rewrite').length === 3, null, { timeout: 5000 }).then(() => true, () => false);
|
||
check(marked, `pointing at one marks its gates on the diagram (${(await page.$$('#qs-diagram .qc-item.qc-rewrite')).length} marked)`);
|
||
await page.click('#qs-rewrite-list li[data-rule=conjugate] .qs-rewrite-take');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
check(/A proposal from the engine \(Rewrite lens\)/.test(await page.textContent('#qs-review')) && (await doc()) === LENS, 'Take offers it as the engine’s proposal, to review');
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => /cz q\[0\], q\[1\];/.test(window.quantumStudio.view.state.doc.toString()) && !/h q\[1\]/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG });
|
||
await page.waitForFunction(() => !document.querySelector('#qs-rewrite-list li[data-rule=conjugate]') && document.querySelector('#qs-rewrite-list li[data-rule=merge]'), null, { timeout: LONG });
|
||
check(true, 'taken, the lens follows the program: the conjugation is gone and the merge is still offered');
|
||
}
|
||
|
||
// Your own way: whether the versions converge on the model's, and a while on one's own.
|
||
{
|
||
check(/^Explore on my own/.test(await page.textContent('#qs-organic')) && await page.isHidden('#qs-organic-chip'), 'Versions offers a while on one’s own');
|
||
await page.click('#qs-organic');
|
||
check(await page.isVisible('#qs-organic-chip') && (await page.getAttribute('#qs-organic', 'aria-pressed')) === 'true' && (await page.evaluate(() => localStorage.getItem('wai.quantum-studio.organic'))) === '1', 'on one’s own: marked, and kept in this browser');
|
||
await page.click('#qs-explore');
|
||
await page.waitForSelector('#qs-alt-paused', { timeout: LONG });
|
||
check(true, 'Explore, which asks the model, waits');
|
||
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nz q[1];\n');
|
||
await page.evaluate(() => window.quantumStudio.versions.record());
|
||
const note = await page.evaluate(() => window.quantumStudio.versions.history().versions.at(-1).note);
|
||
check(note === 'organic exploration', `a version made on one’s own is noted so (${note})`);
|
||
await page.waitForFunction(() => /\.$/.test(document.querySelector('#qs-converge')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the line says whether the versions converge on the model’s (${await page.textContent('#qs-converge')})`);
|
||
await page.click('#qs-organic');
|
||
check(await page.isHidden('#qs-organic-chip') && /^Explore on my own/.test(await page.textContent('#qs-organic')), 'ended, the model may be asked again');
|
||
}
|
||
|
||
// The study: features, a record kept, an ownership answer, a probe, a protocol joined, the record exported.
|
||
{
|
||
const SEALED = {"protocol":{"study":"the Studio's e2e study","hypotheses":["Without the taste profile, unassisted probe scores do not change."],"primary":"unassisted","conditions":[{"name":"everything"},{"name":"no taste","flags":{"taste":false}}],"control":"everything","analysis":{"resamples":1000,"seed":1,"level":0.95}},"seal":{"alg":"ed25519","canon":"rfc8785","key":"197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61","digest":"a3d028f00e263d6ac33bfe61283f86e193316812f0f6bd16727108f937e39257","signature":"3f957f63a602925c49fa7fa010e29cea055ba4051180818f41d1d84539051027c4db992b5598ac36dd1145d6a0f408830743080b59ca6e80c129dbbdb0069d0a"}};
|
||
if (!(await page.$eval('#qs-features', (d) => d.open))) await page.click('#qs-features > summary');
|
||
await page.uncheck('#qs-flag-rewrites');
|
||
check(await page.isHidden('#qs-rewrites-panel'), 'a feature turned off is not there: the rewrite lens');
|
||
await page.check('#qs-flag-rewrites');
|
||
check(await page.isVisible('#qs-rewrites-panel'), 'and turned on, it is back');
|
||
check(await page.isHidden('#qs-study-tools'), 'nothing is kept until the reader asks');
|
||
await page.check('#qs-study-keep');
|
||
await page.waitForSelector('#qs-study-tools:not([hidden]) #qs-study-own', { timeout: LONG });
|
||
// A choice among Improve's candidates is kept, with the engine's best of them.
|
||
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\n');
|
||
await page.click('#qs-improve');
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-cand').length >= 2 && !/looking/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('.qs-cand >> nth=0 >> .qs-apply');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
await page.click('#qs-review-apply');
|
||
const kept = await page.evaluate(() => JSON.parse(localStorage.getItem('wai.quantum-studio.study')).choices);
|
||
check(kept.length === 1 && kept[0].kind === 'alternatives' && kept[0].k >= 2 && kept[0].picked === 0 && Number.isInteger(kept[0].best), `the choice is kept, with how many were shown and the engine's best (${JSON.stringify(kept)})`);
|
||
await page.click('#qs-study-own');
|
||
await page.waitForSelector('#qs-own-4', { timeout: LONG });
|
||
for (let i = 0; i < 5; i++) await page.selectOption(`#qs-own-${i}`, '6');
|
||
await page.click('#qs-own-save');
|
||
await page.waitForSelector('#qs-own-saved', { timeout: LONG });
|
||
check(true, 'an answer on ownership is kept');
|
||
await page.click('#qs-study-probe');
|
||
await page.waitForSelector('#qs-probe-answer', { timeout: LONG });
|
||
check(await page.isVisible('#qs-organic-chip'), 'during a probe the assistant is paused');
|
||
await page.selectOption('#qs-probe-pick', 'swap');
|
||
await page.fill('#qs-probe-answer', 'OPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n');
|
||
await page.click('#qs-probe-score');
|
||
await page.waitForFunction(() => /^Verified\. Score 1\b/.test(document.querySelector('#qs-probe-result')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the engine scores the probe (${await page.textContent('#qs-probe-result')})`);
|
||
await page.click('#qs-probe-close');
|
||
check(await page.isHidden('#qs-organic-chip'), 'closed, the assistant is back');
|
||
await page.click('#qs-study-record');
|
||
await page.waitForSelector('#qs-study-measures', { timeout: LONG });
|
||
const cell = (k) => page.textContent(`#qs-study-measures tr[data-measure=${k}] td.num`);
|
||
check((await cell('ownership')) === '6' && (await cell('unassisted')) === '1' && /^(0|1|0\.5)$/.test(await cell('selection')), `the record's measures are the engine's (ownership ${await cell('ownership')}, unassisted ${await cell('unassisted')}, selection ${await cell('selection')})`);
|
||
// A study: the protocol is read, joined, and its condition holds the features.
|
||
await page.setInputFiles('#qs-study-file', { name: 'study.sealed.json', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify(SEALED)) });
|
||
await page.waitForSelector('#qs-study-join', { timeout: LONG });
|
||
const offer = await page.textContent('#qs-study-offer');
|
||
check(/Without the taste profile/.test(offer) && /digest a3d028f00e263d6a/.test(offer), 'the protocol is shown before joining: what it studies, and its digest');
|
||
await page.click('#qs-study-join');
|
||
await page.waitForFunction(() => /You take part in/.test(document.querySelector('#qs-study-status')?.textContent ?? ''), null, { timeout: LONG });
|
||
const cond = (await page.textContent('#qs-study-status')).match(/condition (everything|no taste)/)?.[1];
|
||
check(cond && (cond === 'no taste') === (await page.isDisabled('#qs-flag-taste')) && (cond === 'no taste') === (await page.isHidden('#qs-taste')) && !(await page.isDisabled('#qs-flag-rewrites')) && !(await page.isChecked('#qs-predict-first')),
|
||
`joined: the key's condition (${cond}) sets the features it names and holds them; the others stay the reader's`);
|
||
const files = [];
|
||
page.on('download', (d) => files.push(d));
|
||
await page.click('#qs-study-export');
|
||
await page.waitForSelector('#qs-study-exported', { timeout: LONG });
|
||
await page.waitForTimeout(500);
|
||
const sealedRecord = files.length ? JSON.parse(readFileSync(await files.at(-1).path(), 'utf8')) : null;
|
||
const r = sealedRecord?.record;
|
||
check(r?.protocol === SEALED.seal.digest && /^did:key:z6Mk/.test(r?.participant ?? '') && r.condition === cond && sealedRecord.seal?.signature?.length === 128 && r.ownership.length === 1 && r.probes.length === 1 && r.choices.length >= 1,
|
||
`Export my record saves it sealed with this browser's key, for the researcher (${files.at(-1)?.suggestedFilename()})`);
|
||
await page.click('#qs-study-leave');
|
||
check(!(await page.isDisabled('#qs-flag-taste')) && await page.isVisible('#qs-taste') && !(await page.isChecked('#qs-predict-first')), 'left, the features are the reader’s again, as they set them');
|
||
// The Study pane is the last of the side column: back to the top, where the editor's lines are drawn.
|
||
await page.evaluate(() => window.scrollTo(0, 0));
|
||
}
|
||
|
||
// Versions: each change and who made it; a proposal taken change by change;
|
||
// whose hand each line and gate is in; the run's receipt binding the count; a
|
||
// line of work from an older version, brought back in; and all of it kept.
|
||
{
|
||
const here = () => page.textContent('#qs-vs-list li.here');
|
||
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
|
||
const BASE = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(BASE);
|
||
await record();
|
||
check(/edited by you/.test(await here()), `what the reader types is recorded as theirs (${await here()})`);
|
||
const first = await page.textContent('#qs-vs-list li:last-child');
|
||
check(/^v0started from the example /.test(first), `the first version is named for where the program came from (${first})`);
|
||
const mine = await page.$eval('#qs-vs-list li.here', (l) => l.dataset.id);
|
||
const PROPOSED = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[1];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n';
|
||
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'assistant', name: 'scripted' }, 'entangle the third qubit'); }, PROPOSED);
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-1', { timeout: LONG });
|
||
const offered = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent));
|
||
check(offered.length === 2 && /h q\[0\]/.test(offered[0]) && /\+ cx q\[1\], q\[2\]/.test(offered[1]) && await page.evaluate(() => window.quantumStudio.view.state.readOnly),
|
||
`a proposal is shown change by change, and the program waits (${offered.map((t) => t.replace(/\s+/g, ' ')).join(' | ')})`);
|
||
await page.uncheck('#qs-review-0');
|
||
check((await page.textContent('#qs-review-apply')) === 'Take 1 of 2', 'the reader takes the changes they want');
|
||
await page.click('#qs-review-apply');
|
||
const TAKEN = BASE.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\ncx q[1], q[2];\n');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q && !window.quantumStudio.view.state.readOnly, TAKEN, { timeout: LONG });
|
||
check(await settles(page, /proposed by the assistant \(scripted\)/, /1 of 2 taken/), `only the change taken reaches the program, recorded as the assistant's (${await here()})`);
|
||
check(/1 of 2 changes taken; the rest were not taken/.test(await page.textContent('#qs-vs-detail')), 'the version keeps what was rejected');
|
||
await page.check('#qs-vs-hands');
|
||
// The editor draws gutter marks only for the lines in view, and it is as tall as the
|
||
// side column: its first lines are brought into view.
|
||
await page.evaluate(() => document.querySelector('.cm-editor').scrollIntoView({ block: 'start' }));
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-hand-gutter .qs-hand:not(.qs-hand-spacer)').length === 7, null, { timeout: LONG });
|
||
const gutter = await page.$$eval('.qs-hand-gutter .qs-hand:not(.qs-hand-spacer)', (ms) => ms.map((m) => m.className.replace('qs-hand qs-hand-', '')));
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-by-assistant').length === 1, null, { timeout: LONG });
|
||
check(gutter[5] === 'assistant' && gutter.filter((h) => h === 'assistant').length === 1 && gutter.slice(1, 3).every((h) => h === 'person'),
|
||
`“Who wrote it” marks each line in the gutter and each gate on the diagram (${gutter.join(', ')})`);
|
||
// The reader edits the model's line: in both hands.
|
||
await setProgram(TAKEN.replace('cx q[1], q[2];', 'cx q[0], q[2];'));
|
||
await record();
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-hand-gutter .qs-hand-assistant_then_person').length === 1, null, { timeout: LONG });
|
||
check(/the assistant, then you \(scripted\): 1 gate, 1 line/.test(await page.textContent('#qs-vs-share')), `a model's line the reader edits is in both hands (${await page.textContent('#qs-vs-share')})`);
|
||
await page.click('#qs-run'); await done(page);
|
||
await page.waitForFunction(() => /recomputed/.test(document.querySelector('#qs-account')?.textContent ?? ''), null, { timeout: LONG });
|
||
const wrote = await page.textContent('#qs-written-by');
|
||
check(/1 by the assistant, then you \(scripted\)/.test(wrote) && /of 3 gates/.test(wrote) && (await page.textContent('#qs-verified')).startsWith('✓ verifies'),
|
||
`the run's receipt binds whose hand each gate is in, recomputed here (${wrote}; ${await page.textContent('#qs-account')})`);
|
||
// A line of work from an older version, then brought back in.
|
||
await page.click(`#qs-vs-list li[data-id=${mine}] .qs-vs-item`);
|
||
await page.click('#qs-vs-open');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, BASE, { timeout: LONG });
|
||
await setProgram(BASE.replace('h q[0];\n', 'h q[0];\nx q[2];\n'));
|
||
await record();
|
||
const heads = await page.$$eval('#qs-vs-list li', (ls) => ls.filter((l) => /a head|in the editor/.test(l.textContent)).length);
|
||
check(heads === 2 && (await page.$$('.qs-vs-svg .qs-vs-edge')).length >= 4, `an edit from an older version is a line of work of its own (${heads} heads)`);
|
||
const other = await page.$eval('#qs-vs-list li:not(.here)', (l) => l.dataset.id);
|
||
const otherHead = await page.$$eval('#qs-vs-list li', (ls) => ls.find((l) => /a head/.test(l.textContent))?.dataset.id);
|
||
await page.click(`#qs-vs-list li[data-id=${otherHead}] .qs-vs-item`);
|
||
await page.click('#qs-vs-merge');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
const merging = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent));
|
||
// Keep this line's x q[2]; take the other's edited CX.
|
||
for (const [i, t] of merging.entries()) if (/− x q\[2\]/.test(t)) await page.uncheck(`#qs-review-${i}`);
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => /merged by you/.test(document.querySelector('#qs-vs-list li.here')?.textContent ?? ''), null, { timeout: LONG });
|
||
const merged = await doc();
|
||
check(/x q\[2\];/.test(merged) && /cx q\[0\], q\[2\];/.test(merged) && /, from v\d+ and v\d+/.test(await page.textContent('#qs-vs-detail')),
|
||
`bringing a line of work back in is a merge of both (${other}, ${otherHead})`);
|
||
check(/the assistant, then you \(scripted\): 1 gate/.test(await page.textContent('#qs-vs-share')), 'the merge keeps each line’s author');
|
||
const count = (await page.$$('#qs-vs-list li')).length;
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForSelector('#qs-vs-list li.here', { timeout: LONG });
|
||
const back = (await page.$$('#qs-vs-list li')).length;
|
||
check(back === count && /merged by you/.test(await here()) && (await doc()) === merged, `the versions come back with the page (${back} of ${count}; ${await here()})`);
|
||
const files = [];
|
||
page.on('download', (d) => files.push(d));
|
||
await page.click('#qs-vs-download');
|
||
await page.waitForFunction(() => /program\.history\.json/.test(document.querySelector('.qs-status')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.waitForTimeout(500);
|
||
const hist = files.find((d) => d.suggestedFilename() === 'program.history.json');
|
||
const saved = hist && JSON.parse(readFileSync(await hist.path(), 'utf8'));
|
||
const prog = files.find((d) => d.suggestedFilename() === 'program.qasm');
|
||
check(saved?.versions?.length === count && saved.versions.every((v) => v.seal?.key?.length === 64 && v.digest.length === 64) && prog && readFileSync(await prog.path(), 'utf8') === merged,
|
||
`Download saves the program and its history, every version sealed, as wai-quantum history verify reads them (${files.map((d) => d.suggestedFilename()).join(', ')})`);
|
||
await page.uncheck('#qs-vs-hands');
|
||
}
|
||
|
||
// Rules for proposals: held lines, a gate set and a region bind every proposal,
|
||
// change by change; a change that breaks one is shown with it and cannot be taken.
|
||
{
|
||
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
|
||
const BASE = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(BASE);
|
||
await record();
|
||
await page.evaluate(() => { const v = window.quantumStudio.view, l = v.state.doc.line(4); v.dispatch({ selection: { anchor: l.from, head: l.to } }); });
|
||
await page.click('#qs-rules > summary');
|
||
await page.click('#qs-rule-hold');
|
||
const HELD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\n// @lock\nh q[0];\n// @unlock\ncx q[0], q[1];\nc = measure q;\n';
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, HELD, { timeout: LONG });
|
||
check((await page.$$('.cm-line.qs-held-line')).length === 3, 'holding the selected lines marks them in the program and shades them');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-held').length === 1, null, { timeout: LONG });
|
||
check(true, 'the held gate is marked on the diagram');
|
||
await page.fill('#qs-rule-gates', 'h, cx, x');
|
||
await page.dispatchEvent('#qs-rule-gates', 'change');
|
||
await record();
|
||
// Three changes: a cz the gate set does not allow, the held line, and an x it does.
|
||
const PROPOSED = HELD.replace('bit[2] c;\n', 'bit[2] c;\ncz q[0], q[1];\n').replace('h q[0];', 'h q[1];').replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n');
|
||
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'assistant', name: 'scripted' }, 'three changes'); }, PROPOSED);
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-2', { timeout: LONG });
|
||
const rev = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent.replace(/\s+/g, ' ')));
|
||
check(await page.isDisabled('#qs-review-0') && await page.isDisabled('#qs-review-1') && !(await page.isDisabled('#qs-review-2')) && /only cx, h, x may be used/.test(rev[0]) && /are held/.test(rev[1]),
|
||
`a change that breaks a rule is shown with it and cannot be taken (${rev.map((t) => t.slice(0, 80)).join(' | ')})`);
|
||
check((await page.textContent('#qs-review-apply')) === 'Take 1 of 1', 'only the change the rules allow can be taken');
|
||
await page.click('#qs-review-apply');
|
||
const KEPT = HELD.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, KEPT, { timeout: LONG });
|
||
check(await settles(page, /1 of 3 taken, 2 blocked/), `the version records what the rules blocked (${await page.textContent('#qs-vs-list li.here')})`);
|
||
// A region: the next proposal keeps to the selected line.
|
||
await page.evaluate(() => { const v = window.quantumStudio.view, l = v.state.doc.line(8); v.dispatch({ selection: { anchor: l.from, head: l.to } }); });
|
||
await page.click('#qs-rule-region');
|
||
check(/Only lines 8 to 8/.test(await page.textContent('#qs-rule-region-chip')), 'a region is kept to the selected lines');
|
||
const OUTSIDE = KEPT.replace('qubit[2] q;\nbit[2] c;', 'qubit[3] q;\nbit[3] c;').replace('x q[1];', 'x q[0];');
|
||
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'agent', name: 'test agent' }, 'two changes'); }, OUTSIDE);
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-1', { timeout: LONG });
|
||
const out = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent.replace(/\s+/g, ' ')));
|
||
check(await page.isDisabled('#qs-review-0') && /outside lines 8 to 8/.test(out[0]) && !(await page.isDisabled('#qs-review-1')), `a change outside the region is blocked (${out[0].slice(0, 90)})`);
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => document.querySelector('#qs-rule-region-chip')?.hidden, null, { timeout: LONG });
|
||
check((await doc()) === KEPT.replace('x q[1];', 'x q[0];'), 'the region holds for that proposal and is then let go');
|
||
await page.fill('#qs-rule-gates', '');
|
||
await page.dispatchEvent('#qs-rule-gates', 'change');
|
||
}
|
||
|
||
// The classical-shortcut lens.
|
||
{
|
||
let RING = 'OPENQASM 3.0;\nqubit[16] q;\nbit[16] c;\nh q;\n';
|
||
for (let i = 0; i < 16; i++) RING += `cz q[${i}], q[${(i + 1) % 16}];\n`;
|
||
RING += 'rz(0.3) q;\nc = measure q;\n';
|
||
await setProgram(RING);
|
||
await page.click('#qs-shortcuts');
|
||
await page.waitForSelector('#qs-shortcuts-summary', { timeout: LONG });
|
||
const summary = await page.textContent('#qs-shortcuts-summary');
|
||
const cut = await page.textContent('#qs-routes tr[data-route=cutrank]');
|
||
check(/cut-rank sum/.test(summary) && /widest cut 2 bits/.test(cut) && await page.$('#qs-routes tr.best[data-route=cutrank]') && /does not apply/.test(await page.textContent('#qs-routes tr[data-route=stabilizer]')),
|
||
`the lens prices a Clifford ring with a closing layer by the cut-rank sum (${summary})`);
|
||
check(/not searched/.test(await page.textContent('#qs-routes tr[data-route=tensor_network]')), 'the quick check leaves the contraction search for a thorough one');
|
||
await page.click('#qs-shortcuts-thorough');
|
||
await page.waitForFunction(() => /2\^/.test(document.querySelector('#qs-routes tr[data-route=tensor_network]')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `a thorough check searches a contraction tree (${(await page.textContent('#qs-routes tr[data-route=tensor_network]')).trim()})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
await page.keyboard.type('\nh q[0];');
|
||
await page.waitForSelector('#qs-shortcuts-panel.stale .qs-stale-note', { timeout: LONG });
|
||
check(true, 'a check is marked stale once the program changes');
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-shortcuts');
|
||
await page.waitForFunction(() => /^Classically easy at any width/.test(document.querySelector('#qs-shortcuts-summary')?.textContent ?? '') && !document.querySelector('#qs-shortcuts-panel.stale'), null, { timeout: LONG });
|
||
check(true, 'a Clifford circuit is classically easy at any width');
|
||
}
|
||
|
||
// The compile lens: the passes with their counts and checks, the certificate
|
||
// checked here, the circuit after any pass, and the compiled program offered.
|
||
{
|
||
const ANGLES = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nrz(0.3) q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(ANGLES);
|
||
await page.fill('#qs-compile-eps', '1e-4');
|
||
await page.click('#qs-compile');
|
||
await page.waitForSelector('#qs-passes tbody tr', { timeout: LONG });
|
||
const passes = await page.$$eval('#qs-passes tbody tr', (rs) => rs.map((r) => r.getAttribute('data-pass')));
|
||
const summary = await page.textContent('#qs-compile-summary');
|
||
check(passes[0] === 'simplify' && passes.includes('approximate') && passes.at(-1) === 'lower' && /^Approximate: within/.test(summary) && /verifies/.test(summary),
|
||
`the program compiles through ${passes.length} checked passes to Clifford+T, the certificate checked here (${summary})`);
|
||
const row = await page.textContent('#qs-passes tr[data-pass=approximate]');
|
||
check(/1→0/.test(row) && /approximate/.test(row), `the approximation writes the one rotation as T gates (${row.trim()})`);
|
||
await page.waitForSelector('#qs-compile-diagram svg', { timeout: LONG });
|
||
check(/After lower \(pass \d+ of \d+\)/.test(await page.textContent('#qs-compile-caption')), 'the circuit after the last pass is drawn');
|
||
await page.click('#qs-passes tr[data-pass=simplify] button');
|
||
await page.waitForFunction(() => /After simplify \(pass 1 of/.test(document.querySelector('#qs-compile-caption')?.textContent ?? ''), null, { timeout: LONG });
|
||
const first = await page.$eval('#qs-compile-diagram svg', (g) => g.getAttribute('aria-label'));
|
||
check(/2 qubits/.test(first) && await page.$('#qs-passes tr.on[data-pass=simplify]'), `choosing a pass draws the circuit after it (${first})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
await page.keyboard.type('\nh q[1];');
|
||
await page.waitForSelector('#qs-compile-panel.stale .qs-stale-note', { timeout: LONG });
|
||
check(true, 'a compile is marked stale once the program changes');
|
||
}
|
||
|
||
// The estimate pane: the counts, the estimate on a chosen qubit, the frontier
|
||
// plotted and listed, the seals checked and the request re-run here.
|
||
{
|
||
const LADDER = 'OPENQASM 3.0;\nqubit[4] q;\nh q;\nctrl(3) @ x q[0], q[1], q[2], q[3];\nrz(0.3) q[0];\nt q[1];\n';
|
||
await setProgram(LADDER);
|
||
await page.selectOption('#qs-estimate-qubit', 'us-e3');
|
||
await page.fill('#qs-estimate-budget', '1e-4');
|
||
await page.click('#qs-estimate');
|
||
await page.waitForSelector('#qs-estimate-summary', { timeout: LONG });
|
||
const summary = await page.textContent('#qs-estimate-summary');
|
||
const counts = await page.textContent('#qs-estimate-counts');
|
||
check(/^On us-e3: [\d,]+ physical qubits \(\d+ T factor(y|ies), [\d.]+% of them\) for .+, at code distance \d+ \(surface code \(fit: exact matching\)\)\.$/.test(summary), `the program is estimated on the chosen qubit (${summary})`);
|
||
check(/^5 logical qubits \(1 ancilla\), 4 measurements, 1 T, 3 Toffolis, 1 rotations \(\d+ T in their words\); T depth \d+, depth \d+\.$/.test(counts), `the counts take the gate under three controls through an ancilla (${counts})`);
|
||
const eps = await page.textContent('#qs-estimate-eps');
|
||
check(/Each rotation within .* T apiece\. Code distance from P\(d\) .* fitted to this engine’s exact matching decoder/.test(eps), `the synthesis precision and the calibration are stated (${eps})`);
|
||
const rows = await page.$$eval('#qs-frontier-table tbody tr', (rs) => rs.map((r) => r.textContent));
|
||
const dots = await page.$$eval('#qs-frontier svg .qs-frontier-point', (g) => g.length);
|
||
check(rows.length >= 2 && dots === rows.length && rows.some((r) => r.includes('the estimate')), `the frontier is plotted and listed, the headline marked (${rows.length} estimates)`);
|
||
await (await page.$('#qs-estimate-panel')).screenshot({ path: join(SHOTS, 'studio-estimate.png') });
|
||
await page.focus(`#qs-frontier svg .qs-frontier-point[data-i="${rows.length - 1}"]`);
|
||
const read = await page.textContent('#qs-estimate-point');
|
||
check(/physical qubits \(\d+ factor(y|ies)\), .+, code distance \d+\.$/.test(read), `a point pointed at is read out (${read})`);
|
||
const receipts = await page.textContent('#qs-estimate-receipts');
|
||
check(/estimates? sealed with did:key:.+: the seals verify, and the request re-run here gives every figure\./.test(receipts), `every estimate is sealed and re-run here (${receipts})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
await page.keyboard.type('\nt q[2];');
|
||
await page.waitForSelector('#qs-estimate-panel.stale .qs-stale-note', { timeout: LONG });
|
||
check(true, 'an estimate is marked stale once the program changes');
|
||
}
|
||
|
||
// Predict first.
|
||
{
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.check('#qs-predict-first');
|
||
const ran = (await page.$$('.qs-hitem')).length;
|
||
await page.click('#qs-run');
|
||
await page.waitForSelector('#qs-predict', { timeout: LONG });
|
||
check((await page.$$('.qs-hitem')).length === ran && !(await page.$('.qs-method')), 'with Predict first, the run waits for a prediction');
|
||
await page.fill('#qs-predict-bits', '000');
|
||
await page.fill('#qs-predict-share', '50');
|
||
await page.click('#qs-predict-run');
|
||
check(/2 classical bits; the outcome has 3/.test(await page.textContent('#qs-predict-note')), 'an outcome of the wrong width is refused, with the width');
|
||
await page.fill('#qs-predict-bits', '00');
|
||
await page.click('#qs-predict-run');
|
||
await done(page);
|
||
await page.waitForSelector('#qs-predicted', { timeout: LONG });
|
||
const pred = await page.textContent('#qs-predicted');
|
||
check(/you said00 in 50%/.test(pred) && /the run00 in \d/.test(pred) && /(came out on top|Another outcome)/.test(pred) && /first prediction/.test(pred), `the prediction is set against the counts (${pred})`);
|
||
await page.uncheck('#qs-predict-first');
|
||
}
|
||
// Generators: a program with integer inputs, read at the inputs bar's values. The
|
||
// Workbench checks a family at every size and keeps the cheapest of each cell.
|
||
const CHAIN = 'OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] {\n cx q[i], q[i + 1];\n}\n';
|
||
const TREE = 'OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint k in [0:n] {\n for uint i in [0:2**k - 1] {\n if (2**k < n && i + 2**k < n) {\n cx q[i], q[i + 2**k];\n }\n }\n}\n';
|
||
const lintSays = (re) => page.waitForFunction((re) => new RegExp(re).test(document.querySelector('#qs-lint')?.textContent ?? ''), re.source, { timeout: LONG });
|
||
const wbLog = () => page.$$eval('#qs-wb-log li', (ls) => ls.map((l) => l.textContent));
|
||
const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-start').disabled, null, { timeout: LONG });
|
||
{
|
||
await setProgram(CHAIN.replace('qubit[n] q;\n', 'qubit[n] q;\nbit[n] c;\n') + 'c = measure q;\n');
|
||
await page.waitForSelector('#qs-inputs:not([hidden]) #qs-input-n', { timeout: LONG });
|
||
await lintSays(/4 qubits · 4 gates at n = 4/);
|
||
check(true, 'a generator shows its input under the editor, and the lint reads it at n = 4');
|
||
await page.fill('#qs-input-n', '3');
|
||
await page.dispatchEvent('#qs-input-n', 'change');
|
||
await lintSays(/3 qubits · 3 gates at n = 3/);
|
||
await page.waitForFunction(() => /^The circuit: 3 qubits/.test(document.querySelector('#qs-diagram svg')?.getAttribute('aria-label') ?? ''), null, { timeout: LONG });
|
||
check(true, 'a new value re-reads it: linted and drawn at n = 3');
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
const got = await counts(page);
|
||
check(got.length === 2 && got.every(([k]) => k === '000' || k === '111'), `it runs at its value: a GHZ state on 3 qubits (${JSON.stringify(got)})`);
|
||
await page.fill('#qs-input-n', '4');
|
||
await page.dispatchEvent('#qs-input-n', 'change');
|
||
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-start');
|
||
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
|
||
const st = await page.textContent('#qs-wb-stats');
|
||
check(/Best e0: cost 17\.3 against the reference’s 17\.3, by reference/.test(st) && (await page.$$('#qs-wb-map .qs-ring')).length === 1,
|
||
`Start checks the reference at n = 3, 5 and 8 and seeds the archive with it (${st})`);
|
||
await page.click('#qs-wb-search');
|
||
await page.waitForFunction(() => /The blind search: 100 edits judged/.test(document.querySelector('#qs-wb-log')?.textContent ?? ''), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const after = await page.textContent('#qs-wb-stats');
|
||
check(/^101 evaluated/.test(after), `the blind search judges a hundred edits, each by the engine (${after})`);
|
||
await setProgram(TREE);
|
||
await page.click('#qs-wb-submit');
|
||
// What is new in it is said after the outcome, above it.
|
||
await page.waitForFunction(() => [...document.querySelectorAll('#qs-wb-log li')].some((l) => /^Your program: /.test(l.textContent)), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const said = (await wbLog()).find((t) => /^Your program: /.test(t));
|
||
check(/^Your program: kept as e\d+, cheaper than every generator before it \(cost 12\.3\)/.test(said) && /by person/.test(await page.textContent('#qs-wb-stats')),
|
||
`the reader's log-depth tree is kept as the cheapest, and named as theirs (${said})`);
|
||
const named = (await wbLog())[0];
|
||
check(/^e\d+: a known construction: GHZ, by doubling\.$/.test(named), `the tree is named for the known construction it is (${named})`);
|
||
await page.click('#qs-wb-map .qs-wb-best');
|
||
await page.waitForSelector('#qs-wb-open', { timeout: LONG });
|
||
const card = await page.textContent('#qs-wb-entry');
|
||
check(/cost 12\.3/.test(card) && /checked by \w+, sealed/.test(card) && /person/.test(card) && !/null/.test(card), `an entry shows its cost, how it was checked, its seal and who wrote it (${card.slice(0, 140)})`);
|
||
await page.waitForFunction(() => /a known construction/.test(document.querySelector('#qs-wb-novelty')?.textContent ?? ''), null, { timeout: LONG });
|
||
const novel = await page.textContent('#qs-wb-novelty');
|
||
check(/GHZ, by doubling/.test(novel) && /Better than the reference on what the cost does not weigh: two-qubit depth \d+ against \d+/.test(novel), `the entry says what it is and what it beats the reference on unasked (${novel})`);
|
||
// Your taste: two kept generators, which the engine proves make the same state, compared.
|
||
if (!(await page.$eval('#qs-taste', (d) => d.open))) await page.click('#qs-taste > summary');
|
||
// The choices the profile already holds: the alternatives taken whole above.
|
||
const tasteBefore = Number((await page.textContent('#qs-taste-count')) || 0);
|
||
const tasteNow = String(tasteBefore + 1);
|
||
await page.click('#qs-taste-compare');
|
||
await page.waitForSelector('.qs-taste-pick', { timeout: LONG });
|
||
const pair = await page.$$eval('.qs-taste-side', (ss) => ss.map((s) => s.dataset.name));
|
||
check(pair.length === 2 && pair[0] !== pair[1] && /proves they do the same thing \(state\)/.test(await page.textContent('#qs-taste-pair')), `Compare two offers the pair the profile is least sure of, proved equal (${pair.join(', ')})`);
|
||
await page.selectOption('#qs-taste-part', 'economy');
|
||
await page.click('.qs-taste-side >> nth=0 >> .qs-taste-pick');
|
||
await page.waitForSelector('#qs-taste-chosen', { timeout: LONG });
|
||
check(new RegExp(`^Kept: ${pair[0]} over ${pair[1]}, for its economy`).test(await page.textContent('#qs-taste-chosen')), `the choice is kept, with what it was about (${await page.textContent('#qs-taste-chosen')})`);
|
||
await page.waitForFunction((n) => document.querySelector('#qs-taste-count')?.textContent === n, tasteNow, { timeout: LONG });
|
||
check(new RegExp(`^${tasteNow} choices \\(economy 1\\)`).test(await page.textContent('#qs-taste-summary')), `the profile counts its choices and their parts (${await page.textContent('#qs-taste-summary')})`);
|
||
await page.click('#qs-taste-rank');
|
||
await page.waitForSelector('#qs-taste-list li', { timeout: LONG });
|
||
const ranked = await page.$$eval('#qs-taste-list li', (ls) => ls.map((l) => l.textContent));
|
||
const kept = await page.evaluate(() => Object.keys(JSON.parse(localStorage.getItem('wai.quantum-studio.workbench')).archive.cells).length);
|
||
check(ranked.length === kept && ranked.every((t) => /typicality \d\.\d\d/.test(t) && /score -?\d/.test(t) && /cost \d/.test(t)), `Rank by my taste orders every kept generator, each with its typicality, score and cost (${ranked.length} of ${kept}: ${ranked[0]})`);
|
||
check(kept <= 3 || /The first 3 are \d\.\d\d apart in how they are built; by cost, \d\.\d\d/.test(await page.textContent('#qs-taste-ranked')), 'the ranking says whether it narrows what comes first, against the cost');
|
||
await page.evaluate(() => { window.qsListBefore = document.querySelector('#qs-taste-list'); });
|
||
await page.$eval('#qs-taste-dial', (d) => { d.value = '1'; d.dispatchEvent(new Event('change')); });
|
||
await page.waitForFunction(() => document.querySelector('#qs-taste-list') && document.querySelector('#qs-taste-list') !== window.qsListBefore, null, { timeout: LONG });
|
||
check((await page.$$('#qs-taste-list li')).length === kept, 'turning the dial ranks them again');
|
||
await page.$eval('#qs-taste-dial', (d) => { d.value = '0'; d.dispatchEvent(new Event('change')); });
|
||
await page.fill('#qs-taste-profile', 'teaching');
|
||
await page.dispatchEvent('#qs-taste-profile', 'change');
|
||
await page.waitForFunction(() => /^No choices yet/.test(document.querySelector('#qs-taste-summary')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.textContent('#qs-taste-count')) === '', 'a second profile is kept apart: it has no choices');
|
||
await page.fill('#qs-taste-profile', 'mine');
|
||
await page.dispatchEvent('#qs-taste-profile', 'change');
|
||
await page.waitForFunction((n) => document.querySelector('#qs-taste-count')?.textContent === n, tasteNow, { timeout: LONG });
|
||
check(true, 'and the first comes back with its choices');
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-open');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, TREE, { timeout: LONG });
|
||
check(await settles(page, /proposed by you/), 'Open in editor offers the kept generator, recorded as its proposer’s, the reader’s');
|
||
await page.click('#qs-wb-ask');
|
||
check(/needs one/.test((await wbLog())[0]), 'Ask the model, with none connected, says it needs one');
|
||
const files = [];
|
||
page.on('download', (d) => files.push(d));
|
||
await page.click('#qs-wb-save');
|
||
await page.waitForFunction(() => /^Saved workbench-/.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.waitForTimeout(500);
|
||
const read = async (d) => JSON.parse(readFileSync(await d.path(), 'utf8'));
|
||
const [specFile, archiveFile] = [files.find((d) => !/archive/.test(d.suggestedFilename())), files.find((d) => /\.archive\.json$/.test(d.suggestedFilename()))];
|
||
const savedSpec = specFile && await read(specFile), savedArchive = archiveFile && await read(archiveFile);
|
||
check(savedSpec?.reference === CHAIN && savedArchive?.spec?.length === 64 && savedArchive.entries.length >= 2 && savedArchive.entries.every((e) => e.sealed?.receipt)
|
||
&& archiveFile.suggestedFilename() === specFile.suggestedFilename().replace(/\.json$/, '.archive.json'),
|
||
`Download saves the specification and its archive side by side, every entry sealed, as the command line reads them (${files.map((d) => d.suggestedFilename()).join(', ')})`);
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => /Taken up again: 102 evaluated/.test(document.querySelector('#qs-wb-log')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(/by person/.test(await page.textContent('#qs-wb-stats')) && (await page.inputValue('#qs-wb-sizes')) === 'n = 3, 5, 8', 'the archive and its specification come back with the page');
|
||
await lintSays(/qubits/);
|
||
check((await page.textContent('#qs-taste-count')) === tasteNow && (await page.inputValue('#qs-taste-profile')) === 'mine', 'the taste profiles come back with the page');
|
||
check(/^\d+ flagged · 1 saved$/.test(await page.textContent('#qs-found-count')) && (await page.inputValue('#qs-found-list .qs-found-item.saved input')) === 'the ladder, read backwards', 'what Found keeps comes back with the page, with its note');
|
||
// On a line, the tree breaks only the rule that gates join neighbours: an aberration.
|
||
if (!(await page.$eval('#qs-wb-spec', (d) => d.open))) await page.click('#qs-wb-spec > summary');
|
||
await page.selectOption('#qs-wb-conn', 'line');
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-start');
|
||
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
|
||
await setProgram(TREE);
|
||
await page.click('#qs-wb-submit');
|
||
await page.waitForFunction(() => /an aberration/.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const ab = (await wbLog())[0];
|
||
check(/^Your program: an aberration\. It breaks only one rule \(at n = 3: `cx` acts on qubits \[0, 2\], which are not neighbours on the line\); without it, it would be verified \(\w+\) at cost 12\.3/.test(ab) && /Try without that rule/.test(ab),
|
||
`a generator that breaks only the line's rule is an aberration, with the cost it would have (${ab})`);
|
||
check(/not verified/.test((await wbLog())[1]) && /\b1 kept\b/.test(await page.textContent('#qs-wb-stats')), 'an aberration is never kept');
|
||
check(await page.$('#qs-found-list .qs-found-item[data-kind=aberration]'), 'Found flags the aberration');
|
||
await page.click('#qs-wb-log .qs-wb-try');
|
||
check((await page.inputValue('#qs-wb-conn')) === 'all' && /lets a gate join any pair/.test(await page.textContent('#qs-wb-stats')), 'Try without that rule loosens the specification, to start again under it');
|
||
}
|
||
|
||
// The assistant, against a scripted model server: it calls a tool, proposes a program
|
||
// the engine cannot read, gets the engine's reason back, and proposes a corrected one,
|
||
// which the Studio checks and runs before offering it.
|
||
{
|
||
const asked = [];
|
||
const GOOD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
const STREAMED_FIRST = `root = Answer([bell, counts, check, seal])\n\`\`\`qasm bell\n${GOOD}\`\`\`\ncounts = Histogram(@Sample(bell, 500))\n`;
|
||
const STREAMED_REST = 'check = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n';
|
||
let release;
|
||
const streamHeld = new Promise((r) => { release = r; });
|
||
const model = createServer((req, res) => {
|
||
const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET, POST, OPTIONS' };
|
||
if (req.method === 'OPTIONS') { res.writeHead(204, cors).end(); return; }
|
||
if (req.url.endsWith('/models')) { res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ data: [{ id: 'scripted' }] })); return; }
|
||
let body = '';
|
||
req.on('data', (c) => (body += c));
|
||
req.on('end', async () => {
|
||
const r = JSON.parse(body);
|
||
asked.push(r);
|
||
const msgs = r.messages;
|
||
// An answer in the answer view layer, streamed: its first part at once, the rest
|
||
// only once the test has seen the first part drawn.
|
||
if (r.stream) {
|
||
res.writeHead(200, { ...cors, 'content-type': 'text/event-stream' });
|
||
const send = (t) => res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: t } }] })}\n\n`);
|
||
for (const piece of STREAMED_FIRST.match(/[\s\S]{1,9}/g)) send(piece);
|
||
await streamHeld;
|
||
for (const piece of STREAMED_REST.match(/[\s\S]{1,9}/g)) send(piece);
|
||
res.end('data: [DONE]\n\n');
|
||
return;
|
||
}
|
||
// Alternatives' calls carry no tools: candidates, one of them wrong on purpose.
|
||
if (!r.tools) {
|
||
const want = msgs[1].content;
|
||
const content = /^Write \d+ generators/.test(want)
|
||
? `Idea: double the qubits that hold the state each layer.\n\`\`\`qasm\n${TREE}\`\`\`\nIdea: forget the Hadamard.\n\`\`\`qasm\n${CHAIN.replace('h q[0];\n', '')}\`\`\`\nIdea: use CZ.\n\`\`\`qasm\n${CHAIN.replace('cx q[i], q[i + 1];', 'h q[i + 1];\n cz q[i], q[i + 1];\n h q[i + 1];')}\`\`\``
|
||
: /^Write \d+ programs that are the same circuit/.test(want)
|
||
? 'Idea: the two Hadamards cancel, and the X moves past the CNOT.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nx q[1];\nc = measure q;\n```\nIdea: drop the X.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nc = measure q;\n```'
|
||
: 'Idea: a GHZ state on three qubits.\n```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\n**Idea:** entangle with CZ instead.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[1];\nc = measure q;\n```';
|
||
res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }] }));
|
||
return;
|
||
}
|
||
const told = msgs.some((m) => m.role === 'user' && /could not read the program you proposed/.test(m.content));
|
||
const toolDone = msgs.some((m) => m.role === 'tool');
|
||
let message;
|
||
if (!toolDone) message = { role: 'assistant', content: '', tool_calls: [{ id: 'c1', type: 'function', function: { name: 'lint_qasm', arguments: JSON.stringify({ qasm: GOOD }) } }] };
|
||
else if (!told) message = { role: 'assistant', content: 'It entangles two qubits. A variant:\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nfoo q[0];\n```' };
|
||
else message = { role: 'assistant', content: 'Corrected:\n```qasm\n' + GOOD + '```' };
|
||
res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message, finish_reason: message.tool_calls ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } }));
|
||
});
|
||
});
|
||
await new Promise((r) => model.listen(0, '127.0.0.1', r));
|
||
await page.click('#qs-ai-gear');
|
||
await page.fill('#qs-ai-endpoint', `http://127.0.0.1:${model.address().port}/v1`);
|
||
await page.click('#qs-ai-connect');
|
||
await page.waitForFunction(() => /connected · scripted/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, 'the assistant connects to a model server and reads its models');
|
||
await page.click('.qs-ai-quick button >> text=Explain this circuit');
|
||
await page.waitForSelector('.qs-ai-apply', { timeout: LONG });
|
||
const log = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => l.textContent));
|
||
check(log.some((t) => t.includes('used lint_qasm')), 'the model\'s tool call ran on the engine, and the log shows it');
|
||
check(log.some((t) => /could not read the proposed program \(line 3: unknown gate foo \[Q103\]\)/.test(t)), `a proposal the engine cannot read goes back with the engine's reason (${log.find((t) => /could not read/.test(t)) ?? 'none'})`);
|
||
const proposal = await page.textContent('.qs-ai-proposal');
|
||
check(/checked: 2 qubits, 2 gates/.test(proposal) && /ran here on Stabilizer tableau/.test(proposal), `the corrected proposal is checked and run before it is offered (${proposal})`);
|
||
const level = await page.$eval('.qs-ai-proposal .qs-level', (b) => ({ level: b.dataset.level, title: b.title }));
|
||
check(level.level === 'executed' && /Executed on stabilizer/.test(level.title), `the proposal states the level the engine checked it to, as a badge (${JSON.stringify(level)})`);
|
||
check(asked.length === 3 && asked.every((r) => r.tools?.some((t) => t.function.name === 'run_job') && r.messages[0].role === 'system' && /Never state a probability/.test(r.messages[0].content)), 'every call to the model carries the engine\'s tools and the rule that numbers come from them');
|
||
const toolMsg = asked[1].messages.find((m) => m.role === 'tool');
|
||
check(toolMsg && JSON.parse(toolMsg.content).qubits === 2, 'the tool\'s answer went back to the model');
|
||
await page.click('.qs-ai-apply');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
check(/A proposal from the assistant \(scripted\)/.test(await page.textContent('#qs-review')), 'Apply offers the checked program as the assistant’s proposal, named');
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, GOOD, { timeout: LONG });
|
||
check(true, 'taken, it is the program in the editor');
|
||
check(await page.evaluate(() => localStorage.getItem('wai.quantum-studio.telemetry')) === null && /0 answers recorded; recording is off/.test(await page.textContent('#qs-tel-count')),
|
||
'with the correctness record off, as it is by default, nothing about the model’s answer is kept, not even a key in storage');
|
||
check((await page.inputValue('#qs-ai-stance')) === 'questions' && (await page.inputValue('#qs-ai-initiative')) === 'asked' && /ask one short question/.test(asked[0].messages[0].content),
|
||
'by default the assistant asks questions first and speaks only when asked');
|
||
const quick = await page.$$eval('.qs-ai-quick button', (bs) => bs.map((b) => b.textContent));
|
||
check(quick.includes('Ask me a question') && quick.includes('Hint'), `it can be asked for a question or a hint (${quick.join(', ')})`);
|
||
check(asked.every((r) => !r.tools || r.tools.some((t) => t.function.name === 'critique_qasm') && r.tools.some((t) => t.function.name === 'check_equivalent')), 'its tools include the critique and the equivalence check');
|
||
// Runs refused twice: no hint offered while it speaks only when asked; offered, not sent, when allowed.
|
||
const calls = asked.length;
|
||
await setProgram('OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n');
|
||
for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); }
|
||
check(await page.isHidden('#qs-ai-stuck'), 'while it speaks only when asked, refused runs offer no hint');
|
||
await page.selectOption('#qs-ai-initiative', 'hint');
|
||
for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); }
|
||
check(await page.isVisible('#qs-ai-stuck') && asked.length === calls, 'allowed to, it offers a hint after refused runs, and asks the model nothing');
|
||
await page.selectOption('#qs-ai-initiative', 'asked');
|
||
// Improve and Explore with the model: the engine reads, measures and compares each.
|
||
// A rule the model is told: text to avoid.
|
||
if (!(await page.$eval('#qs-rules', (d) => d.open))) await page.click('#qs-rules > summary');
|
||
await page.fill('#qs-rule-text', 'measure q[5]');
|
||
await page.dispatchEvent('#qs-rule-text', 'change');
|
||
const DRAFT = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[0];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(DRAFT);
|
||
await page.click('#qs-improve');
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-cand').length && !/looking/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
|
||
const cards = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent));
|
||
const refused = await page.textContent('#qs-alt-refused').catch(() => '');
|
||
check(cards.some((t) => /the model/.test(t) && /the two Hadamards cancel/.test(t) && /same circuit · equal tableaus/.test(t)) && /drop the X: not the same circuit/.test(refused),
|
||
`Improve with the model: its rewrite checked the same circuit, its wrong one left out with the reason (${refused})`);
|
||
await page.fill('#qs-explore-aim', 'another entangler');
|
||
await page.click('#qs-explore');
|
||
await page.waitForFunction(() => /a GHZ state/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
|
||
const ideas = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent));
|
||
const muse = asked.filter((r) => !r.tools).map((r) => r.messages[1].content);
|
||
check(ideas.length === 2 && ideas.some((t) => /3 qubits/.test(t) && /a different circuit/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t)) && muse.some((m) => /toward this aim: another entangler/.test(m)),
|
||
`Explore: the model's ideas, each read and measured by the engine (${ideas.map((t) => t.slice(0, 60)).join(' | ')})`);
|
||
check(ideas.some((t) => /a GHZ state on three qubits/.test(t) && /a known construction: GHZ, by a chain of CNOTs/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t) && /a different route to GHZ, by a chain of CNOTs/.test(t)),
|
||
'each idea says what is new in it: the chain is a known construction, the CZ one a different route to it');
|
||
check(muse.length >= 2 && muse.every((m) => m.includes('Add no line holding `measure q[5]`.')), 'the model is told the reader’s rules with every request for ideas');
|
||
await page.fill('#qs-rule-text', '');
|
||
await page.dispatchEvent('#qs-rule-text', 'change');
|
||
check((await doc()) === DRAFT, 'neither changes the program until one is applied');
|
||
// The Workbench asks the model for generators, and the engine judges each at every size.
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-start');
|
||
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-wb-ask');
|
||
await page.waitForFunction(() => /^use CZ: /.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const wlog = await wbLog();
|
||
check(wlog.some((t) => /^double the qubits that hold the state each layer: kept as e\d+, cheaper than every generator before it/.test(t))
|
||
&& wlog.some((t) => /^forget the Hadamard: not verified: at n = 3: different/.test(t))
|
||
&& wlog.some((t) => /^use CZ: not verified: at n = 3: it uses `cz`, which the specification does not allow/.test(t)),
|
||
`the model's generators, each judged at every size: one kept, a wrong one and a forbidden gate left out (${wlog.slice(0, 3).join(' | ')})`);
|
||
check(wlog.some((t) => /^use CZ: an aberration\. It breaks only one rule \(at n = 3: it uses `cz`/.test(t)) && !wlog.some((t) => /^forget the Hadamard: an aberration/.test(t)),
|
||
'the forbidden gate that would be verified without the rule is an aberration; the wrong one is not');
|
||
const brief = asked.filter((r) => !r.tools).map((r) => r.messages[1].content).find((m) => /^Write 3 generators/.test(m)) ?? '';
|
||
check(/prepare the same state from \|0…0⟩/.test(brief) && /n = 3, 5, 8/.test(brief) && /only these gates: h, cx/.test(brief) && brief.includes('cx q[i], q[i + 1];') && /The cheapest found: cost 17\.3, the reference\./.test(brief),
|
||
'the model is told the specification and shown the cheapest found, with the cost the engine measured');
|
||
check(/by model: scripted/.test(await page.textContent('#qs-wb-stats')), 'the best is the model’s, and named as its');
|
||
await setProgram(DRAFT);
|
||
// The candidates come back within the editor's redraw delay: wait for the draft's diagram.
|
||
await page.waitForFunction(() => /4 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && document.querySelectorAll('#qs-diagram .qc-flag').length === 2, null, { timeout: LONG });
|
||
check(!/null/.test(await page.textContent('#qs-alt-body')) && !/null/.test(await page.textContent('#qs-measures')), 'no empty part is written out as “null”');
|
||
await page.screenshot({ path: join(SHOTS, 'studio-alternatives.png'), fullPage: true });
|
||
// The correctness record, turned on: the proposal recorded, "Not this" kept, the failures exported.
|
||
await setProgram(GOOD);
|
||
await page.click('#qs-tel > summary');
|
||
await page.check('#qs-tel-on');
|
||
await page.click('.qs-ai-quick button >> text=Explain this circuit');
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-ai-reject').length >= 2 && /^1 answer recorded/.test(document.querySelector('#qs-tel-count')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-ai-log > li:last-child .qs-ai-reject');
|
||
const kept = await page.evaluate(() => JSON.parse(localStorage.getItem('wai.quantum-studio.telemetry')));
|
||
const rec = kept?.records?.[0];
|
||
check(kept?.on === true && kept.records.length === 1 && rec.format === 'wai.quantum.telemetry/1' && rec.verdict?.level === 'executed' && rec.decision === 'rejected' && /^[0-9a-f]{64}$/.test(rec.receipt ?? '') && rec.program === GOOD,
|
||
`turned on, the proposal is one record: the level the engine checked it to, its receipt, and the reader's "Not this" (${JSON.stringify({ level: rec?.verdict?.level, decision: rec?.decision })})`);
|
||
const telFiles = [];
|
||
page.on('download', (d) => telFiles.push(d));
|
||
await page.click('#qs-tel-corpus');
|
||
await page.waitForTimeout(800);
|
||
const corpus = telFiles.length ? JSON.parse(readFileSync(await telFiles.at(-1).path(), 'utf8')) : null;
|
||
check(corpus?.format === 'wai.quantum-intent-conformance-bundle/1' && corpus.cases.length === 1 && corpus.cases[0].covers[0] === 'the person rejected it' && corpus.cases[0].qasm === GOOD,
|
||
`the failures export as an eval corpus in the conformance format (${telFiles.at(-1)?.suggestedFilename()})`);
|
||
// Answer views: an answer drawn from its text, a number the model wrote refused.
|
||
await page.click('#qs-answers > summary');
|
||
const PASTED = `root = Answer([bell, counts, made_up, check, seal])\n\`\`\`qasm bell\n${GOOD}\`\`\`\ncounts = Histogram(@Sample(bell, 1000))\nmade_up = Histogram(512)\ncheck = Verdict(@Check(bell, {"00": 0.5, "11": 0.5}))\nseal = Receipt(@Seal([bell, counts, check]))\n`;
|
||
await page.fill('#qs-av-input', PASTED);
|
||
await page.click('#qs-av-draw');
|
||
await page.waitForSelector('#qs-av[data-done="true"]', { timeout: LONG });
|
||
await page.waitForFunction(() => document.querySelector('.qs-av-card[data-name="seal"] [data-receipt]')?.dataset.receipt !== 'checking', null, { timeout: LONG });
|
||
const av = await page.evaluate(() => Object.fromEntries([...document.querySelectorAll('.qs-av-card')].map((c) => [c.dataset.name, c.dataset.state])));
|
||
const bars = await page.$$eval('.qs-av-card[data-name="counts"] .qs-bar', (bs) => bs.map((b) => Number(b.dataset.count)));
|
||
check(av.counts === 'drawn' && bars.length === 2 && bars.reduce((a, b) => a + b, 0) === 1000, `the histogram's counts are the engine's, run here (${bars.join(' + ')})`);
|
||
check(await page.$eval('.qs-av-card[data-name="check"] .qs-level', (b) => b.dataset.level) === 'proof', 'the verdict is the engine’s: checked as proof');
|
||
check(await page.$eval('.qs-av-card[data-name="seal"] [data-receipt]', (b) => b.dataset.receipt) === 'valid', 'the seal over the program, the counts and the verdict is checked here');
|
||
const diag = await page.$$eval('#qs-av-diags li', (ls) => ls.map((l) => [l.dataset.code, l.dataset.severity, l.textContent]));
|
||
check(av.made_up === 'dropped' && diag.some(([c, s, t]) => c === 'J104' && s === 'error' && /comes from the engine/.test(t)), `a number the model wrote in a view is refused, with its code (${diag.map(([c]) => c).join(', ')})`);
|
||
check((await page.evaluate(() => JSON.parse(localStorage.getItem('wai.quantum-studio.telemetry')))).records.at(-1)?.codes?.includes('J104'), 'the answer is recorded with the codes it drew');
|
||
// An answer the model streams: each view drawn as its statement arrives.
|
||
await page.fill('#qs-av-q', 'Show me what this program does.');
|
||
await page.click('#qs-av-ask');
|
||
await page.waitForFunction(() => document.querySelector('.qs-av-card[data-name="counts"]')?.dataset.state === 'drawn', null, { timeout: LONG });
|
||
const before = await page.evaluate(() => ({ done: document.querySelector('#qs-av').dataset.done ?? null, seal: document.querySelector('.qs-av-card[data-name="seal"]')?.dataset.state }));
|
||
check(before.done === null && before.seal === 'waiting', `the histogram is drawn while the model is still writing, the seal's card waiting for its statement (${JSON.stringify(before)})`);
|
||
release();
|
||
await page.waitForSelector('#qs-av[data-done="true"]', { timeout: LONG });
|
||
await page.waitForFunction(() => document.querySelector('.qs-av-card[data-name="seal"] [data-receipt]')?.dataset.receipt === 'valid', null, { timeout: LONG }).catch(() => {});
|
||
const streamed = asked.find((r) => r.stream);
|
||
check(await page.$eval('.qs-av-card[data-name="seal"] [data-receipt]', (b) => b.dataset.receipt).catch(() => null) === 'valid' && /Engine calls:/.test(streamed?.messages?.[0]?.content ?? '') && /never write a count/.test(streamed.messages[0].content),
|
||
'the rest arrives and is drawn, its seal checked here; the model was told the layer, and that it writes no numbers');
|
||
await page.screenshot({ path: join(SHOTS, 'studio-answer-views.png'), fullPage: true });
|
||
model.close();
|
||
}
|
||
// Step through: the engine's debugger, forward and back, on the branch the reader chooses;
|
||
// the state's views; editing by the drawing; the circuit in words; the drawing saved.
|
||
{
|
||
const TELE = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ns q[0];\nh q[1];\ncx q[1], q[2];\ncx q[0], q[1];\nh q[0];\nc[0] = measure q[0];\nc[1] = measure q[1];\nif (c[1]) x q[2];\nif (c[0]) z q[2];\nc[2] = measure q[2];\n';
|
||
const posIs = (re) => page.waitForFunction((re) => new RegExp(re).test(document.querySelector('#qs-debug-pos')?.textContent ?? ''), re.source, { timeout: LONG });
|
||
const at = async (n) => {
|
||
await page.$eval('#qs-debug-slider', (s, n) => { s.value = String(n); s.dispatchEvent(new Event('input')); }, n);
|
||
await posIs(new RegExp(`^position ${n} of`));
|
||
};
|
||
const digest = async () => (await page.textContent('.qs-dbg-amps .qs-note')).match(/digest ([0-9a-f]+)/)?.[1];
|
||
await setProgram(TELE);
|
||
await page.waitForFunction(() => /3 qubits · 8 gates/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-debug-start');
|
||
await posIs(/^position 0 of 9$/);
|
||
check(/^exact · bit-exact$/.test(await page.textContent('#qs-debug-method')), `the debugger steps the program on the bit-exact method (${await page.textContent('#qs-debug-method')})`);
|
||
check((await page.$$('#qs-debug-qubits .qs-bloch')).length === 3 && (await page.$$('#qs-debug-qubits .qs-disk')).length === 3, 'each qubit on its Bloch sphere and its phase disk');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-debug-tree .qs-dbg-leaf').length === 4, null, { timeout: LONG });
|
||
const leaves = await page.$$eval('#qs-debug-tree .qs-dbg-leaf > button', (bs) => bs.map((b) => b.textContent));
|
||
check(leaves.length === 4 && leaves.every((t) => /weight 1\/4$/.test(t)), `teleportation branches four ways, each weighing 1/4 exactly (${leaves.join(' | ')})`);
|
||
await at(6);
|
||
const amps = await page.$$eval('#qs-debug-amps tbody tr', (rs) => rs.map((r) => r.children[3].textContent));
|
||
check(amps.length === 8 && amps.every((p) => p === '0.1250'), `before the measurements, eight amplitudes of probability 1/8 each (${amps.join(' ')})`);
|
||
check(/^Next: measure q0 into bit 0 \(line 10\)$/.test(await page.textContent('#qs-debug-next')), `the next step is named with its line (${await page.textContent('#qs-debug-next')})`);
|
||
check((await page.$$('#qs-diagram .qc-step')).length === 0, 'a measurement is no gate to mark');
|
||
const d6 = await digest();
|
||
await at(4);
|
||
check((await page.$$('#qs-diagram .qc-step')).length === 1, 'the next gate is marked on the diagram');
|
||
await at(8);
|
||
await at(6);
|
||
check(d6 && (await digest()) === d6, `forward and back again, the same state bit for bit (digest ${d6})`);
|
||
// The reduced state of q0 and q1 there: as mixed as q2's, which is maximally mixed.
|
||
await page.click('#qs-debug-density > summary');
|
||
check(/purity 0\.500000, entropy 1\.000000 bits/.test(await page.textContent('#qs-debug-rho-note')), `the reduced state of q0 and q1, one bit of entropy (${await page.textContent('#qs-debug-rho-note')})`);
|
||
// The branch both measurements read 1: both corrections run, and q2 holds |+i⟩.
|
||
await page.click('#qs-debug-tree button[data-path="11"]');
|
||
// On that path both corrections run: two steps more.
|
||
await posIs(/^position 6 of 11$/);
|
||
await at(10);
|
||
check(/outcomes 1 1 · weight 0\.25/.test(await page.textContent('#qs-debug-path')), `the path takes the branch chosen (${await page.textContent('#qs-debug-path')})`);
|
||
check(/y \+1\.000/.test(await page.textContent('#qs-debug-qubits [data-q="2"] figcaption')), `q2 then holds the state q0 held, Bloch y = 1 (${await page.textContent('#qs-debug-qubits [data-q="2"] figcaption')})`);
|
||
check((await page.getAttribute('#qs-debug-tree button[data-path="11"]', 'aria-pressed')) === 'true', 'and the branch is marked as the path');
|
||
// Under noise: the density matrix, beside the noiseless state.
|
||
await page.$eval('#qs-noise-depolarizing_2q', (i) => { i.value = '0.05'; i.dispatchEvent(new Event('input')); });
|
||
await page.click('#qs-debug-start');
|
||
await page.waitForFunction(() => /^density_matrix/.test(document.querySelector('#qs-debug-method')?.textContent ?? '') && /Fidelity to it/.test(document.querySelector('#qs-debug-overlay')?.textContent ?? ''), null, { timeout: LONG });
|
||
await at(6);
|
||
check(/Fidelity to it 0\.9/.test(await page.textContent('#qs-debug-overlay')) && (await page.$$('#qs-debug-overlay .qs-ideal')).length === 8, `under noise, the noisy state against the noiseless one (${(await page.textContent('#qs-debug-overlay')).slice(-40)})`);
|
||
await page.$eval('#qs-noise-depolarizing_2q', (i) => { i.value = ''; i.dispatchEvent(new Event('input')); });
|
||
|
||
// Editing by the drawing.
|
||
const BELL = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
const gates = (n) => page.waitForFunction((n) => document.querySelectorAll('#qs-diagram .qc-item[data-op]').length === n, n, { timeout: LONG });
|
||
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
|
||
await setProgram(BELL);
|
||
await record();
|
||
await gates(2);
|
||
const box = await page.$eval('#qs-diagram svg', (s) => ({ w: Number(s.getAttribute('width')) }));
|
||
await page.dragAndDrop('.qs-gate-chip[data-gate="x"]', '#qs-diagram svg', { targetPosition: { x: box.w - 8, y: 65 } });
|
||
await page.waitForFunction(() => window.quantumStudio.view.state.doc.toString().includes('x q[1];'), null, { timeout: LONG });
|
||
check((await doc()) === BELL.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n'), `a gate dropped on a wire is a statement in the program (${JSON.stringify(await doc())})`);
|
||
await record();
|
||
check(/edited by you/.test(await page.textContent('#qs-vs-list li.here')), 'recorded as the reader’s edit');
|
||
await gates(3);
|
||
await page.waitForFunction(() => document.querySelector('#qs-diagram .qc-item[data-op="2"]')?.dataset.qubits === '1', null, { timeout: LONG });
|
||
const g = await page.$eval('#qs-diagram .qc-item[data-op="2"]', (e) => { const r = e.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; });
|
||
await page.mouse.move(g.x, g.y);
|
||
await page.mouse.down();
|
||
await page.mouse.move(g.x, g.y - 20, { steps: 4 });
|
||
await page.mouse.move(g.x, g.y - 40, { steps: 4 });
|
||
await page.mouse.up();
|
||
await page.waitForFunction(() => window.quantumStudio.view.state.doc.toString().includes('x q[0];'), null, { timeout: LONG });
|
||
check(/^Moved x q1 onto q0/.test(await page.textContent('.qs-status')), `a gate dragged to another wire (${await page.textContent('.qs-status')})`);
|
||
// The diagram is drawn again from the new text, the moved gate selected.
|
||
const drawnOn = (op, qs) => page.waitForFunction(([op, qs]) => document.querySelector(`#qs-diagram .qc-item[data-op="${op}"]`)?.dataset.qubits === qs, [op, qs], { timeout: LONG });
|
||
await drawnOn(2, '0');
|
||
await page.focus('#qs-diagram .qc-item[data-op="2"]');
|
||
await page.keyboard.press('ArrowDown');
|
||
await page.waitForFunction(() => window.quantumStudio.view.state.doc.toString().includes('x q[1];'), null, { timeout: LONG });
|
||
await drawnOn(2, '1');
|
||
await page.waitForFunction(() => document.activeElement?.dataset?.op === '2', null, { timeout: LONG });
|
||
await page.keyboard.press('Delete');
|
||
await page.waitForFunction((b) => window.quantumStudio.view.state.doc.toString() === b, BELL, { timeout: LONG });
|
||
check(true, 'moved and deleted from the keyboard, the program as it was');
|
||
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q;\n');
|
||
await drawnOn(1, '1');
|
||
await page.focus('#qs-diagram .qc-item[data-op="1"]');
|
||
await page.keyboard.press('Delete');
|
||
await page.waitForFunction(() => /which makes 2 gates/.test(document.querySelector('.qs-status')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await doc()) === 'OPENQASM 3.0;\nqubit[2] q;\nh q;\n', `a gate of a broadcast is refused with its line (${await page.textContent('.qs-status')})`);
|
||
|
||
// The circuit in words, and the drawing saved as the engine writes it.
|
||
await setProgram(BELL);
|
||
await page.waitForFunction(() => /CNOT, control qubit 0, target qubit 1/.test(document.querySelector('#qs-describe')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.getAttribute('#qs-diagram', 'aria-describedby')) === 'qs-describe' && (await page.textContent('#qs-describe')).startsWith('A circuit on 2 qubits and 2 classical bits, 2 gates'), `the diagram is described in the engine's words (${await page.textContent('#qs-describe')})`);
|
||
const saved = async (button) => {
|
||
const [d] = await Promise.all([page.waitForEvent('download'), page.click(button)]);
|
||
return [d.suggestedFilename(), readFileSync(await d.path(), 'utf8')];
|
||
};
|
||
const [svgName, svgText] = await saved('#qs-export-svg');
|
||
check(svgName === 'circuit.svg' && svgText.startsWith('<svg') && svgText.includes('wq-circuit'), `the drawing saved as the engine's SVG (${svgName}, ${svgText.slice(0, 40)})`);
|
||
const [texName, texText] = await saved('#qs-export-tex');
|
||
check(texName === 'circuit.tex' && texText.includes('\\begin{tikzpicture}') && texText.includes('$H$'), `and as LaTeX (${texName})`);
|
||
await page.screenshot({ path: join(SHOTS, 'studio-debugger.png'), fullPage: true });
|
||
}
|
||
|
||
// Error correction: a code drawn with its certified distance, qubits flipped on
|
||
// the lattice, the syndrome lit, the decoder's correction marked and the residual
|
||
// judged, the certificate's witness shown, a threshold plotted; and a run that
|
||
// mitigates its value under its noise.
|
||
{
|
||
check(!(await page.$eval('#qs-qec', (d) => d.open)), 'the error-correction pane waits closed until it is opened');
|
||
await page.click('#qs-qec > summary');
|
||
await page.waitForFunction(() => /\[\[9, 1, 3\]\]/.test(document.querySelector('#qs-qec-summary')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(/Every logical operator has weight at least 3/.test(await page.textContent('#qs-qec-cert')) && /seal verifies here/.test(await page.textContent('#qs-qec-cert')), `the surface code is shown with its certified distance, sealed (${await page.textContent('#qs-qec-cert')})`);
|
||
await page.selectOption('#qs-qec-code', 'color:5');
|
||
await page.waitForFunction(() => /\[\[19, 1, 5\]\]/.test(document.querySelector('#qs-qec-summary')?.textContent ?? ''), null, { timeout: LONG });
|
||
const faces = await page.$$eval('#qs-qec-lattice .qs-qec-face', (f) => [f.length, f.filter((x) => /c[012]/.test(x.getAttribute('class'))).length]);
|
||
check(faces[0] === 9 && faces[1] === 9, `the colour code's nine faces are drawn in their colours (${faces})`);
|
||
await page.click('#qs-qec-lattice [data-q="3"]');
|
||
await page.click('#qs-qec-lattice [data-q="11"]');
|
||
const lit = await page.$$eval('#qs-qec-lattice .qs-qec-face.on', (f) => f.length);
|
||
check(lit > 0 && /2 qubits flipped/.test(await page.textContent('#qs-qec-result')), `two flipped qubits light their syndrome (${lit} faces; ${await page.textContent('#qs-qec-result')})`);
|
||
check((await page.$$eval('#qs-qec-decoder option', (o) => o.map((x) => x.value))).join(',') === 'color,relay_bp,search', 'the decoders the colour code takes are offered');
|
||
await page.click('#qs-qec-decode');
|
||
await page.waitForFunction(() => /unharmed|logical error/.test(document.querySelector('#qs-qec-result')?.textContent ?? ''), null, { timeout: LONG });
|
||
const res = await page.textContent('#qs-qec-result');
|
||
check(/explains the syndrome/.test(res) && /unharmed/.test(res) && /seal verifies here/.test(res) && (await page.$$('#qs-qec-lattice .qs-qec-qubit.fix')).length >= 1, `the colour decoder corrects them and the residual is judged (${res})`);
|
||
await page.click('#qs-qec-witness');
|
||
check((await page.$$('#qs-qec-lattice .qs-qec-qubit.wit')).length === 5, 'the certificate’s witness, a logical of weight 5, is shown on the lattice');
|
||
await (await page.$('#qs-qec-pane')).screenshot({ path: join(SHOTS, 'studio-qec.png') });
|
||
await page.fill('#qs-qec-shots', '600');
|
||
await page.click('#qs-qec-measure');
|
||
await page.waitForSelector('#qs-qec-plot svg', { timeout: LONG });
|
||
const series = await page.$$eval('#qs-qec-plot svg .qs-qec-line', (l) => l.length);
|
||
const reading = await page.textContent('#qs-qec-reading');
|
||
check(series === 3 && /code capacity/.test(reading) && /seal verifies here/.test(reading), `a threshold is measured and plotted, one line per distance (${series}; ${reading})`);
|
||
// Pulse: a program that is not a pulse program is refused by the pulse
|
||
// reader; the example's schedule is drawn on its frames, and its run
|
||
// integrated, bounded and sealed.
|
||
check(!(await page.$eval('#qs-pulse', (d) => d.open)), 'the pulse pane waits closed until it is opened');
|
||
await setProgram('OPENQASM 3.0;\nqubit q;\nh q;\n');
|
||
await page.click('#qs-pulse > summary');
|
||
await page.waitForFunction(() => /\[Q460\]/.test(document.querySelector('#qs-pulse-summary')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.$$('#qs-pulse-timeline svg')).length === 0, `a circuit with declared qubits is refused by the pulse reader, with its code (${await page.textContent('#qs-pulse-summary')})`);
|
||
await page.click('#qs-pulse-example');
|
||
await page.waitForFunction(() => /seal verifies here/.test(document.querySelector('#qs-pulse-summary')?.textContent ?? ''), null, { timeout: LONG });
|
||
const lanes = await page.$$eval('#qs-pulse-timeline .qs-pulse-lane', (l) => l.map((x) => x.getAttribute('data-frame')));
|
||
const plays = await page.$$eval('#qs-pulse-timeline .qs-pulse-play', (p) => p.length);
|
||
const caps = await page.$$eval('#qs-pulse-timeline .qs-pulse-capture', (p) => p.length);
|
||
const calls = await page.$$eval('#qs-pulse-timeline .qs-pulse-call-label', (c) => c.map((x) => x.textContent));
|
||
check(lanes.join(',') === 'q0,r0,c0' && plays === 3 && caps === 1 && calls.join(',') === 'sx $0,x $0,measure $0', `the example's schedule is drawn: a lane per frame, its plays, its capture and its gate calls (${lanes}; ${plays} plays; ${caps} capture; ${calls})`);
|
||
check(/328 samples \(164 ns\)/.test(await page.textContent('#qs-pulse-summary')), `the schedule's length is stated (${await page.textContent('#qs-pulse-summary')})`);
|
||
await page.fill('#qs-pulse-shots', '500');
|
||
await page.click('#qs-pulse-run');
|
||
await page.waitForFunction(() => /seal verifies here/.test(document.querySelector('#qs-pulse-result')?.textContent ?? ''), null, { timeout: LONG });
|
||
const pr = await page.textContent('#qs-pulse-result');
|
||
const pb = await page.textContent('#qs-pulse-binds');
|
||
check(/\$0 → c: P\(1\) = /.test(pr) && /Accuracy bounded/.test(pr) && /The receipt binds the program/.test(pb), `the pulse run gives the capture's reading, its bound and what its receipt binds, sealed (${pr.slice(0, 200)}; ${pb.slice(0, 120)})`);
|
||
await (await page.$('#qs-pulse-pane')).screenshot({ path: join(SHOTS, 'studio-pulse.png') });
|
||
// A run that mitigates its value under its noise.
|
||
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n');
|
||
await page.fill('#qs-obs', 'Z0 Z1');
|
||
await page.evaluate(() => { document.querySelector('.qs-noise').open = true; });
|
||
await page.fill('#qs-noise-depolarizing_2q', '0.03');
|
||
await page.selectOption('#qs-mitigate', 'vd');
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
await page.waitForSelector('#qs-mitigation', { timeout: LONG });
|
||
const m = await page.textContent('#qs-mitigation');
|
||
check(/Mitigated: vd/.test(m) && /⟨Z0 Z1⟩/.test(m) && /Bias: Assumes the error-free state/.test(m), `the run carries its mitigation, raw and mitigated with their errors and the method's assumptions (${m.slice(0, 160)})`);
|
||
await page.selectOption('#qs-mitigate', 'none');
|
||
await page.fill('#qs-noise-depolarizing_2q', '');
|
||
await page.fill('#qs-obs', '');
|
||
}
|
||
|
||
check(seen.errors.length === 0, `no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
|
||
check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`);
|
||
await ctx.close();
|
||
|
||
// ---- the course ----
|
||
// A link to it: `#course=<lesson>` opens the pane at that lesson.
|
||
{
|
||
const { ctx, page, seen } = await open(undefined, 'en-US', '/#course=bell');
|
||
const at = await page.waitForFunction(() => !document.querySelector('#qs-course')?.hidden && document.querySelector('#qs-course-title')?.textContent === 'Two qubits, one answer', null, { timeout: LONG }).then(() => true, () => false);
|
||
check(at && (await page.getAttribute('#qs-course-open', 'aria-expanded')) === 'true', `a link opens the course at its lesson (#course=bell: ${await page.textContent('#qs-course-title')})`);
|
||
check(seen.errors.length === 0, `the course link: no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
|
||
await ctx.close();
|
||
}
|
||
const COURSE = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'wai-quantum', 'course', 'lessons');
|
||
const exercise = (id) => JSON.parse(readFileSync(join(COURSE, id, 'exercise.json'), 'utf8'));
|
||
const lessonProgram = (v) => (Array.isArray(v) ? v.join('\n') + '\n' : v);
|
||
const putProgram = (page, src) => page.evaluate((s) => { const v = window.quantumStudio.view; v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: s } }); }, src);
|
||
const courseChecked = (page) => page.waitForFunction(() => !document.querySelector('#qs-course-check')?.disabled && /\S/.test(document.querySelector('#qs-course-statement')?.textContent ?? ''), null, { timeout: LONG });
|
||
async function checkCourse(page) {
|
||
await page.evaluate(() => { document.querySelector('#qs-course-statement').textContent = ''; });
|
||
await page.click('#qs-course-check');
|
||
await courseChecked(page);
|
||
return page.textContent('#qs-course-passed');
|
||
}
|
||
{
|
||
const { ctx, page, seen } = await open();
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-course-open');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-course-list [data-lesson]').length === 21 && /\S/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.textContent('#qs-course-title')) === 'A qubit, a gate, a measurement' && /Lesson 1 of 21/.test(await page.textContent('#qs-course-where')), `the course opens at its first lesson (${await page.textContent('#qs-course-title')})`);
|
||
await page.click('[data-lesson="bell"]');
|
||
await page.waitForFunction(() => document.querySelector('#qs-course-title')?.textContent === 'Two qubits, one answer', null, { timeout: LONG });
|
||
const bell = exercise('bell');
|
||
const courseText = await page.textContent('#qs-course');
|
||
check(!courseText.includes('cx q[0], q[1];') && /00 with probability 0.5/.test(await page.textContent('#qs-course-must')), 'the lesson states what the program must do, and not the solution');
|
||
await page.click('#qs-course-starter');
|
||
await page.waitForFunction((s) => window.quantumStudio.view.state.doc.toString() === s, lessonProgram(bell.starter), { timeout: LONG });
|
||
check(true, 'the starter program is loaded into the editor');
|
||
check((await checkCourse(page)) === 'not yet', `the starter is checked and not passed (${await page.textContent('#qs-course-statement')})`);
|
||
const levels = await page.$$eval('#qs-course-cases .qs-level', (b) => b.map((x) => x.dataset.level));
|
||
check(levels.join() === 'proof', `its verdict is the engine's, checked as proof (${levels})`);
|
||
await page.click('#qs-course-why');
|
||
await page.waitForSelector('#qs-course-wrong:not([hidden]) li', { timeout: LONG });
|
||
const wrongText = await page.textContent('#qs-course-wrong');
|
||
check(/00: your program gives 1, the exercise asks for 0\.5/.test(wrongText) && /Where to look:/.test(wrongText), `What went wrong? names the gap the engine measured (${wrongText.slice(0, 160)})`);
|
||
await page.click('#qs-course-hint');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-course-hints li').length === 1 && !document.querySelector('#qs-course-pointer').hidden, null, { timeout: LONG });
|
||
check(/Hint 1 of 2/.test(await page.textContent('#qs-course-hints')) && /outcome/.test(await page.textContent('#qs-course-pointer')), `a hint, with the engine's pointer (${await page.textContent('#qs-course-pointer')})`);
|
||
await page.click('#qs-course-ask');
|
||
await page.waitForFunction(() => /needs a model/.test(document.querySelector('#qs-course-asked')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, 'the assistant is asked for a hint only with a model connected, and says so');
|
||
await putProgram(page, lessonProgram(bell.reference));
|
||
check((await checkCourse(page)) === 'passed', `the solution passes (${await page.textContent('#qs-course-statement')})`);
|
||
check(await page.$eval('[data-lesson="bell"]', (b) => b.classList.contains('done')), 'and the lesson is marked passed');
|
||
check(/1 of 21 lessons passed · kept only in this browser/.test(await page.textContent('#qs-course-progress')), `the progress says where it is kept (${await page.textContent('#qs-course-progress')})`);
|
||
// A rule the engine measures, and a generator's cases each with their verdict.
|
||
await page.click('[data-lesson="phase"]');
|
||
await page.waitForFunction(() => /^A phase you cannot see/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG });
|
||
await putProgram(page, lessonProgram(exercise('phase').wrong.find((w) => w.fails === 'constraint').qasm));
|
||
await checkCourse(page);
|
||
const rules = await page.$$eval('#qs-course-rules li', (l) => l.map((x) => x.className + ' ' + x.textContent));
|
||
check(rules.some((r) => /^bad ✗ only these gates: h, z/.test(r)), `a gate the exercise does not allow breaks its rule (${rules})`);
|
||
await page.click('[data-lesson="bit-flip-code"]');
|
||
await page.waitForFunction(() => /^Three copies/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG });
|
||
await putProgram(page, lessonProgram(exercise('bit-flip-code').wrong[1].qasm));
|
||
await checkCourse(page);
|
||
const cases = await page.$$eval('#qs-course-cases li', (l) => l.map((x) => x.className));
|
||
check(cases.join() === 'ok,bad,ok', `a generator is checked in each case, each with its verdict (${cases})`);
|
||
// Kept in this browser: the course reopens where it was, the pass still marked.
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => !document.querySelector('#qs-course')?.hidden && document.querySelectorAll('#qs-course-list [data-lesson]').length === 21 && /^Three copies/.test(document.querySelector('#qs-course-title')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(await page.$eval('[data-lesson="bell"]', (b) => b.classList.contains('done')), 'after a reload the course reopens where it was, the pass kept');
|
||
await page.screenshot({ path: join(SHOTS, 'studio-course.png'), fullPage: true });
|
||
check(seen.errors.length === 0, `the course: no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
|
||
check(seen.failed.length === 0, `the course: no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`);
|
||
await ctx.close();
|
||
}
|
||
{
|
||
// The browser's language is Chinese: so is the Studio, until the reader picks another.
|
||
const { ctx, page, seen } = await open(undefined, 'zh-CN');
|
||
await page.waitForFunction(() => /量子比特/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.evaluate(() => document.documentElement.lang)) === 'zh-CN' && (await page.textContent('#qs-run')) === '▶ 运行', `the Studio follows the browser's language (${await page.textContent('#qs-run')}, ${await page.textContent('#qs-lint')})`);
|
||
check(/机器辅助翻译/.test(await page.textContent('#qs-translation-note')), 'and says its words are a machine-assisted translation');
|
||
await page.click('#qs-course-open');
|
||
await page.waitForFunction(() => document.querySelector('#qs-course-title')?.textContent === '一个量子比特、一个门、一次测量', null, { timeout: LONG });
|
||
check(/第 1\/21 课/.test(await page.textContent('#qs-course-where')) && /机器辅助翻译/.test(await page.textContent('#qs-course-translation')), `the lesson in Chinese (${await page.textContent('#qs-course-where')})`);
|
||
await page.click('#qs-course-starter');
|
||
await page.waitForFunction((s) => window.quantumStudio.view.state.doc.toString() === s, lessonProgram(exercise('first-circuit').starter), { timeout: LONG });
|
||
check((await checkCourse(page)) === '尚未通过', `checked in Chinese (${await page.textContent('#qs-course-statement')})`);
|
||
await page.click('#qs-course-why');
|
||
await page.waitForSelector('#qs-course-wrong:not([hidden]) li', { timeout: LONG });
|
||
check(/1:你的程序给出 0,练习要求 1/.test(await page.textContent('#qs-course-wrong')), `What went wrong? in Chinese (${(await page.textContent('#qs-course-wrong')).slice(0, 120)})`);
|
||
// At a phone's width, the course open, nothing scrolls sideways.
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
||
check(over <= 0, `in Chinese at 390 px, the course open, nothing scrolls sideways (overflow ${over} px)`);
|
||
await page.setViewportSize({ width: 1400, height: 950 });
|
||
await page.screenshot({ path: join(SHOTS, 'studio-course-zh.png'), fullPage: true });
|
||
// The switch: English, kept over the browser's language; then Chinese again.
|
||
await Promise.all([page.waitForEvent('load'), page.selectOption('#qs-lang', 'en')]);
|
||
await page.waitForFunction(() => document.querySelector('#qs-run')?.textContent === '▶ Run', null, { timeout: LONG });
|
||
check((await page.evaluate(() => document.documentElement.lang)) === 'en', 'the switch picks English over the browser’s language');
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => document.querySelector('#qs-run')?.textContent === '▶ Run', null, { timeout: LONG });
|
||
check(true, 'and the choice is kept in this browser');
|
||
await Promise.all([page.waitForEvent('load'), page.selectOption('#qs-lang', 'zh-CN')]);
|
||
await page.waitForFunction(() => document.querySelector('#qs-run')?.textContent === '▶ 运行', null, { timeout: LONG });
|
||
check(true, 'and back to Chinese');
|
||
check(seen.errors.length === 0, `in Chinese: no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
|
||
// A switch reloads the page, which aborts what was still loading; nothing else may fail.
|
||
const failed = seen.failed.filter((f) => !f.endsWith('(net::ERR_ABORTED)'));
|
||
check(failed.length === 0, `in Chinese: no failed request${failed.length ? ': ' + failed.join(' | ') : ''}`);
|
||
await ctx.close();
|
||
}
|
||
|
||
{
|
||
const { ctx, page } = await open({ width: 390, height: 844 });
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
||
check(over <= 0, `at 390 px nothing scrolls sideways (overflow ${over} px)`);
|
||
await ctx.close();
|
||
}
|
||
|
||
await browser.close();
|
||
server.close();
|
||
console.log(`\n${fails === 0 ? 'every check passed' : `${fails} check(s) failed`}`);
|
||
process.exit(fails === 0 ? 0 : 1);
|