The Course pane lists the 21 lessons, shows a lesson's text and exercise, loads its starter, and checks the program in the editor with the engine (engine_course_check in the page, the same commands in the desktop app): each case's verdict, each rule, What went wrong? from the diagnostics, the verdict and the critique, the hints one at a time with the engine's pointer, and a hint from the assistant when a model is connected, any program in its answer held back. Progress is kept only in this browser and says so. The interface's words are data, locales/<locale>/<area>.json for every part, English the source and Simplified Chinese marked machine-assisted; the Studio follows the browser's language until the reader picks one with the switch. test/locales.test.mjs holds every locale to every key and placeholder. The e2e checks the course in English and in Chinese. wai-web's engine tests pin 23 native and 16 wasm32, a course check the same bytes in both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
377 lines
27 KiB
JavaScript
377 lines
27 KiB
JavaScript
// The Studio's assistant: a language model on this machine, with the engine
|
||
// as its tools. The model is any server with the chat-completions interface,
|
||
// tool calls included: the JouleClaw runtime, a llama.cpp server, others. By default
|
||
// it is local, and nothing leaves the machine.
|
||
//
|
||
// Tools first: the model asks the engine (lint, critique, draw, plan, run, the
|
||
// equivalence check, what is new) and may state only numbers the engine returned. Every program it proposes is checked by the
|
||
// engine before the Studio offers it: read as the engine reads it, and run on
|
||
// this device. A proposal that does not parse goes back to the model once, with
|
||
// the engine's reason, to be corrected.
|
||
//
|
||
// The reader is the author. The assistant speaks only when asked, and by default
|
||
// it asks a question before it proposes (the stance, which the reader sets). For
|
||
// Alternatives it is a source of candidates, never the judge: each one it writes
|
||
// is read, measured and compared by the engine before the Studio shows it.
|
||
//
|
||
// Only the words the reader sees (the stances' labels, the errors) follow the
|
||
// reader's language; every word the model reads stays in English.
|
||
import { t as tr } from './i18n.js';
|
||
|
||
const SYSTEM = `You are the assistant in Quantum Studio, an editor for OpenQASM programs.
|
||
Tools run the Studio's own engine:
|
||
- get_program: the program in the editor, the job's settings and the last result.
|
||
- lint_qasm: read a program as the engine reads it (its qubits and gates, or the first problem and its line).
|
||
- critique_qasm: the program's measures (gates, two-qubit gates, depth, T gates, whether it is Clifford, a modelled fidelity) and its smells, each with its lines.
|
||
- check_equivalent: whether two programs are the same circuit, and how the engine knows.
|
||
- classical_shortcuts: every exact classical route to simulating a program and its cost; a cheap route proves it classically easy, none being cheap proves nothing.
|
||
- novelty_qasm: how far a program's build is from the known constructions and from the editor's program, the nearest named, and each known construction the engine proves it to be, built alike or by a different route.
|
||
- draw_circuit: the circuit as text.
|
||
- plan_job: the method that would run a job, why, and the memory it needs.
|
||
- run_job: run a job on this device; counts, expectation values, amplitudes and the accuracy of each, and the engine's verdict: the level it checked to (proof, evidence, executed, parsed, refused) and whether the intent you gave it holds.
|
||
Use them. Never state a probability, count, amplitude, expectation value or cost that a tool did not return; quote the tool's figure.
|
||
To say what a program must do, give run_job an intent (an exact distribution as {"distribution": {"00": 0.5, "11": 0.5}}, or a reference program's text as {"equivalent": "…"} or {"prepares": "…"}). Quote the verdict's level and statement; never call a program valid or correct beyond it.
|
||
Never call a program new, novel or a discovery unless novelty_qasm says so, and then say that it is the engine's structural distance from what it knows, not a search of the literature. When it is a known construction, name it.
|
||
The engine reads OpenQASM 3 and the OpenQASM 2 forms in common use: registers, the standard gates, gate definitions, ctrl and negctrl modifiers, measurement and reset anywhere, and if on measured bits. It refuses while, inv and pow. A program that declares integer inputs (input uint n;) is a generator: for loops over integer ranges and if on integer conditions write out one circuit for each value, and the tools read it at the values in the Studio's inputs bar.
|
||
When you write a program, run it with run_job and also show the whole program in one \`\`\`qasm block; the Studio checks it and runs it before offering it.
|
||
For a state, a probability or an amplitude, run the program and quote the result rather than working it out yourself.
|
||
Write plain text: no Markdown headings, no LaTeX. Write states as |00⟩ and roots as √2. Qubit 0 is the rightmost bit of a basis state.
|
||
Answer briefly.`;
|
||
|
||
// How the assistant helps, which the reader chooses. Asking first is the default:
|
||
// a question the reader answers teaches more than an answer they read.
|
||
export const STANCES = {
|
||
questions: { label: tr('assistant.stance_questions'), text: 'The user is the author and is learning. Before you propose a program or a change, ask one short question that helps them see it themselves: what they expect a line to do, or what a result shows. Answer a direct question from the tools, then ask one question back that moves them forward. Write a whole program only when they ask you to.' },
|
||
suggest: { label: tr('assistant.stance_suggest'), text: 'The user is the author. Suggest changes as small edits to their program, each naming its line and the reason the tools gave; write a whole program only when they ask for one.' },
|
||
write: { label: tr('assistant.stance_write'), text: 'When a change is wanted, write the whole program as described above.' },
|
||
};
|
||
|
||
export const TOOLS = [
|
||
{ type: 'function', function: { name: 'get_program', description: 'The program in the editor, the job settings, and a summary of the last result.', parameters: { type: 'object', properties: {} } } },
|
||
{ type: 'function', function: { name: 'lint_qasm', description: 'Read an OpenQASM program as the engine reads it: its qubits, gates and measurements, or the first problem with its line.', parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' } } } } },
|
||
{ type: 'function', function: { name: 'critique_qasm', description: 'A program\'s measures (gates by kind, two-qubit gates, depth, two-qubit depth, T count, whether it is a Clifford circuit, a fidelity under reference error rates, which is a model and not a run) and its smells: gates that cancel, rotations that merge or do nothing, idle or unmeasured qubits, each with its lines.', parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' } } } } },
|
||
{ type: 'function', function: { name: 'check_equivalent', description: 'Whether two programs are the same circuit up to a global phase, and how the engine knows: equal tableaus (Clifford, any size), every basis state (to 8 qubits), checked rewrites to nothing (a proof at any width), random inputs (to 22 qubits, evidence rather than proof), or unchecked. b defaults to the program in the editor.', parameters: { type: 'object', required: ['a'], properties: { a: { type: 'string' }, b: { type: 'string' } } } } },
|
||
{ type: 'function', function: { name: 'classical_shortcuts', description: 'Every exact classical route the engine knows for simulating a program (stabilizer tableau, cut-rank sum, exact matrix product state at a rigorous bond bound, light cone, tensor network when thorough, state vector), each with its memory and the log2 of its operations, and the cheapest for each question. A cheap route proves the program classically easy; none being cheap proves nothing about hardness.', parameters: { type: 'object', properties: { qasm: { type: 'string' }, thorough: { type: 'boolean' } } } } },
|
||
{ type: 'function', function: { name: 'novelty_qasm', description: 'What is new in a program: its structural distance, 0 (built alike) to 1 (nothing shared), from the known constructions (GHZ by a chain, by doubling and by fan-out; the uniform superposition; the quantum Fourier transform) and from the program in the editor, the nearest named; and each known construction the engine proves it to be (the same state, or the same circuit), built alike or by a different route. A high distance is a reason to look, not a claim that nobody has built it. qasm defaults to the program in the editor.', parameters: { type: 'object', properties: { qasm: { type: 'string' } } } } },
|
||
{ type: 'function', function: { name: 'draw_circuit', description: 'A program\'s circuit drawn as text.', parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' } } } } },
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'plan_job',
|
||
description: 'The method a job would run on and why, with the memory it needs, without running it.',
|
||
parameters: { type: 'object', required: ['qasm'], properties: { qasm: { type: 'string' }, shots: { type: 'integer' }, observables: { type: 'array', items: { type: 'string' } }, method: { type: 'string' } } },
|
||
},
|
||
},
|
||
{
|
||
type: 'function',
|
||
function: {
|
||
name: 'run_job',
|
||
description: 'Run a job on this device: measurement counts, Pauli expectation values (as "Z0 Z1") and/or amplitudes (bit strings, highest qubit first), with the method used and its accuracy.',
|
||
parameters: {
|
||
type: 'object',
|
||
required: ['qasm'],
|
||
properties: { qasm: { type: 'string' }, shots: { type: 'integer' }, observables: { type: 'array', items: { type: 'string' } }, amplitudes: { type: 'array', items: { type: 'string' } }, method: { type: 'string' }, intent: { description: 'What the program must do: {"distribution": {...}}, {"equivalent": "<a reference program>"}, {"prepares": "<a reference program>"}, or a list of them. The verdict says how far the engine checked it.' } },
|
||
},
|
||
},
|
||
},
|
||
];
|
||
|
||
const clip = (s, n = 6000) => (s.length > n ? s.slice(0, n) + `… (${s.length - n} more characters)` : s);
|
||
|
||
// A result as a model reads it: what was computed, not the receipt.
|
||
export function summary(job) {
|
||
const r = job?.result || {};
|
||
const counts = r.counts ? Object.entries(r.counts).sort((a, b) => b[1] - a[1]).slice(0, 16) : undefined;
|
||
return {
|
||
method: job?.method, why: job?.why, accuracy: r.accuracy, qubits: r.qubits, gates: r.gates,
|
||
counts: counts && Object.fromEntries(counts), distinct_outcomes: r.distinct_outcomes,
|
||
expectations: r.expectations, amplitudes: r.amplitudes?.slice(0, 16), dynamic: r.dynamic, warning: r.warning,
|
||
verdict: r.verdict ? { level: r.verdict.level, holds: r.verdict.holds, statement: r.verdict.statement, intent: r.verdict.intent?.map((i) => ({ kind: i.kind, outcome: i.outcome, strength: i.strength, how: i.how, distance: i.distance })) } : undefined,
|
||
};
|
||
}
|
||
|
||
// The ```qasm (or ```openqasm) blocks of an answer.
|
||
export function proposals(text) {
|
||
const out = [];
|
||
const re = /```(?:qasm|openqasm|openqasm3)?\s*\n([\s\S]*?)```/gi;
|
||
for (let m; (m = re.exec(text)); ) if (/\b(qubit|qreg|OPENQASM)\b/.test(m[1])) out.push(m[1].trim() + '\n');
|
||
return out;
|
||
}
|
||
|
||
// A model behind a chat-completions endpoint, called from this page.
|
||
export class HttpModel {
|
||
constructor(endpoint, name) { this.endpoint = endpoint.replace(/\/+$/, ''); this.name = name; }
|
||
async chat(body) {
|
||
const res = await fetch(`${this.endpoint}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
||
const v = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(v.error?.message || v.error || tr('assistant.model_answered', { status: res.status }));
|
||
return v;
|
||
}
|
||
async models() {
|
||
const res = await fetch(`${this.endpoint}/models`);
|
||
if (!res.ok) throw new Error(tr('assistant.server_answered', { status: res.status }));
|
||
return (await res.json()).data?.map((m) => m.id) ?? [];
|
||
}
|
||
// The same call, streamed: `onText(piece)` hears the answer's text as it
|
||
// arrives (server-sent events); the whole text is returned at the end.
|
||
async stream(body, onText) {
|
||
const res = await fetch(`${this.endpoint}/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...body, stream: true }) });
|
||
if (!res.ok) {
|
||
const v = await res.json().catch(() => ({}));
|
||
throw new Error(v.error?.message || v.error || `the model answered ${res.status}`);
|
||
}
|
||
const reader = res.body.getReader();
|
||
const dec = new TextDecoder();
|
||
let buf = '', all = '';
|
||
for (;;) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
buf += dec.decode(value, { stream: true });
|
||
for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) {
|
||
const line = buf.slice(0, i).trim();
|
||
buf = buf.slice(i + 1);
|
||
if (!line.startsWith('data:')) continue;
|
||
const data = line.slice(5).trim();
|
||
if (data === '[DONE]') return all;
|
||
let d;
|
||
try { d = JSON.parse(data); } catch (e) { continue; }
|
||
const t = d.choices?.[0]?.delta?.content;
|
||
if (t) { all += t; await onText(t); }
|
||
}
|
||
}
|
||
return all;
|
||
}
|
||
}
|
||
|
||
// The same, called by the desktop app (no browser origin rules in the way).
|
||
export class DesktopModel {
|
||
constructor(endpoint, name) { this.endpoint = endpoint.replace(/\/+$/, ''); this.name = name; this.invoke = window.__TAURI__.core.invoke; }
|
||
async chat(body) {
|
||
const v = JSON.parse(await this.invoke('chat', { endpoint: this.endpoint, body: JSON.stringify(body) }));
|
||
if (v.error && !v.choices) throw new Error(v.error.message || v.error);
|
||
return v;
|
||
}
|
||
async models() {
|
||
const v = JSON.parse(await this.invoke('models', { endpoint: this.endpoint }));
|
||
if (v.error && !v.data) throw new Error(v.error.message || v.error);
|
||
return v.data?.map((m) => m.id) ?? [];
|
||
}
|
||
// The app calls the model whole; the answer is heard at once.
|
||
async stream(body, onText) {
|
||
const v = await this.chat(body);
|
||
const t = v.choices?.[0]?.message?.content || '';
|
||
await onText(t);
|
||
return t;
|
||
}
|
||
}
|
||
|
||
export class Assistant {
|
||
// engine: the Studio's local engine; context(): {qasm, job, last} as the editor holds them;
|
||
// stance(): a key of STANCES; rules(): the reader's rules for proposals, as sentences.
|
||
constructor({ engine, model, context, stance = () => 'questions', rules = async () => [] }) {
|
||
this.engine = engine;
|
||
this.model = model;
|
||
this.context = context;
|
||
this.stance = stance;
|
||
this.rules = rules;
|
||
this.history = [];
|
||
}
|
||
|
||
// Candidates for Alternatives, from one call without tools and outside the
|
||
// conversation: [{idea, qasm}]. The engine checks each; nothing here does.
|
||
async muse(prompt, temperature, brief = MUSE) {
|
||
const res = await this.model.chat({ model: this.model.name || undefined, temperature, messages: [{ role: 'system', content: brief }, { role: 'user', content: prompt }] });
|
||
return ideas(unlatex((res.choices?.[0]?.message?.content || '').replace(/<think>[\s\S]*?<\/think>\s*/g, '')));
|
||
}
|
||
// The reader's rules, as a paragraph for a prompt, or nothing.
|
||
async ruled() {
|
||
const r = await Promise.resolve(this.rules()).catch(() => []);
|
||
return r?.length ? `\n\nThe reader's rules, which the Studio checks every change against (a change that breaks one is blocked):\n- ${r.join('\n- ')}` : '';
|
||
}
|
||
async improve(qasm, k = 3) {
|
||
return this.muse(`Write ${k} programs that are the same circuit as this one (the same unitary up to a global phase, and the same measurements) but cheaper: fewer two-qubit gates, less depth or fewer T gates. Before each, one line that starts "Idea:" and says what it changes. Each program whole, in its own \`\`\`qasm block.${await this.ruled()}\n\n\`\`\`qasm\n${qasm}\n\`\`\``, 0.4);
|
||
}
|
||
async explore(qasm, aim, k = 4) {
|
||
return this.muse(`Imagine ${k} different programs in the spirit of this one${aim ? `, toward this aim: ${aim}` : ''}. Make them differ from each other in kind, not only in their angles: another structure, another entangler, another symmetry, another use of the qubits. Before each, one line that starts "Idea:" and says what it tries. Each program whole, in its own \`\`\`qasm block.${await this.ruled()}\n\n\`\`\`qasm\n${qasm}\n\`\`\``, 0.9);
|
||
}
|
||
// Generators for the Workbench, from its specification and the cheapest it
|
||
// has found (`shown`, entries of the archive, cheapest first). The archive
|
||
// judges each; the model sees costs the engine measured, never its own.
|
||
propose(spec, shown, k = 3) {
|
||
const found = shown.map((e, i) => `${i ? 'Also found' : 'The cheapest found'}: cost ${Math.round(e.cost * 1000) / 1000}${e.proposer === 'reference' ? ', the reference' : e.note ? `, from the idea "${e.note}"` : ''}.\n\`\`\`qasm\n${e.source.trim()}\n\`\`\``).join('\n\n');
|
||
return this.muse(`Write ${k} generators that meet this specification at a lower cost than any found so far. ${specText(spec)}\n\n${found}\n\nMake the ideas differ from each other in kind: another structure, another order of the gates, another way to share the work between the qubits. Before each, one line that starts "Idea:" and says what it tries. Each generator whole, with its input declarations, in its own \`\`\`qasm block.`, 0.9, PROPOSE);
|
||
}
|
||
|
||
async tool(name, args) {
|
||
const ctx = this.context();
|
||
const job = (a) => {
|
||
const j = { qasm: a.qasm };
|
||
for (const k of ['shots', 'observables', 'amplitudes', 'method', 'intent']) if (a[k] != null && !(Array.isArray(a[k]) && !a[k].length)) j[k] = a[k];
|
||
if (!j.shots && !j.observables && !j.amplitudes) j.shots = 1000;
|
||
// Every run states how far the engine checked it.
|
||
if (!j.intent) j.verdict = true;
|
||
return j;
|
||
};
|
||
switch (name) {
|
||
case 'get_program': return { qasm: ctx.qasm, job: ctx.job, last_result: ctx.last ? summary(ctx.last) : null };
|
||
case 'lint_qasm': return this.engine.lint(args.qasm ?? ctx.qasm);
|
||
case 'critique_qasm': return this.engine.critique(args.qasm ?? ctx.qasm);
|
||
case 'check_equivalent': return this.engine.equivalent(args.a ?? ctx.qasm, args.b ?? ctx.qasm);
|
||
case 'classical_shortcuts': return this.engine.shortcuts(args.qasm ?? ctx.qasm, !!args.thorough);
|
||
case 'novelty_qasm': {
|
||
const qasm = args.qasm ?? ctx.qasm;
|
||
return this.engine.novelty({ qasm, against: qasm.trim() === ctx.qasm.trim() ? [] : [{ name: 'the program in the editor', qasm: ctx.qasm }] });
|
||
}
|
||
case 'draw_circuit': return { text: (await this.engine.draw(args.qasm ?? ctx.qasm, 80)).text };
|
||
case 'plan_job': return this.engine.plan(job({ ...args, qasm: args.qasm ?? ctx.qasm }));
|
||
case 'run_job': return summary(await this.engine.run(job({ ...args, qasm: args.qasm ?? ctx.qasm })));
|
||
default: throw new Error(`no tool ${name}`);
|
||
}
|
||
}
|
||
|
||
// Ask; `on(event)` hears each step: {type: 'tool', name, args, result} and
|
||
// finally {type: 'answer', text, proposals: [{qasm, lint, job}]}.
|
||
async ask(text, on = () => {}) {
|
||
const ctx = this.context();
|
||
const used = []; // programs the model passed to the tools, other than the editor's
|
||
const ranWith = new Map(); // program → the settings the model ran it with
|
||
const heard = [ctx.qasm, JSON.stringify(ctx.job ?? {}), JSON.stringify(ctx.last ? summary(ctx.last) : {})]; // what the engine and the editor told it
|
||
const messages = [
|
||
{ role: 'system', content: `${SYSTEM}\n${(STANCES[this.stance()] || STANCES.questions).text}` },
|
||
...this.history,
|
||
{ role: 'user', content: `${text}${await this.ruled()}\n\n(The program in the editor:)\n\`\`\`qasm\n${ctx.qasm}\n\`\`\`` },
|
||
];
|
||
let retried = false;
|
||
for (let round = 0; round < 8; round++) {
|
||
// The first turn must ask the engine something, so every answer starts from it.
|
||
const res = await this.model.chat({ model: this.model.name || undefined, messages, tools: TOOLS, tool_choice: round === 0 ? 'required' : 'auto', temperature: 0.2 });
|
||
const msg = res.choices?.[0]?.message;
|
||
if (!msg) throw new Error(tr('assistant.no_answer'));
|
||
messages.push({ role: 'assistant', content: msg.content ?? '', ...(msg.tool_calls?.length ? { tool_calls: msg.tool_calls } : {}) });
|
||
if (msg.tool_calls?.length) {
|
||
for (const call of msg.tool_calls) {
|
||
let args = {};
|
||
try { args = JSON.parse(call.function?.arguments || '{}'); } catch (e) {}
|
||
let result;
|
||
try { result = await this.tool(call.function?.name, args); } catch (e) { result = { error: e.message }; }
|
||
if (typeof args.qasm === 'string' && args.qasm.trim() && args.qasm.trim() !== ctx.qasm.trim()) used.push(args.qasm.trim() + '\n');
|
||
if (call.function?.name === 'run_job') {
|
||
const eff = pick(args, ['shots', 'observables', 'amplitudes', 'method', 'intent']);
|
||
if (!eff.shots && !eff.observables && !eff.amplitudes) eff.shots = 1000; // as the tool ran it
|
||
ranWith.set((args.qasm ?? ctx.qasm).trim(), eff);
|
||
}
|
||
on({ type: 'tool', name: call.function?.name, args, result });
|
||
messages.push({ role: 'tool', tool_call_id: call.id, content: clip(JSON.stringify(result)) });
|
||
heard.push(JSON.stringify(args), JSON.stringify(result)); // what it asked, and what it was told
|
||
}
|
||
continue;
|
||
}
|
||
const answer = unlatex((msg.content || '').replace(/<think>[\s\S]*?<\/think>\s*/g, '').trim());
|
||
// Check each proposal as the engine reads it; a broken one goes back once.
|
||
// The answer's programs, and the last one the model ran or checked through a tool
|
||
// when the answer shows none: either way, read by the engine here.
|
||
const written = proposals(answer);
|
||
const shown = written.length ? written : used.slice(-1);
|
||
const checked = [];
|
||
for (const qasm of shown) checked.push({ qasm, lint: await this.engine.lint(qasm) });
|
||
// What the engine says of each before anything else: refused when it does not read.
|
||
const verdictOf = async (job) => { try { return await this.engine.verdict?.(job); } catch (e) { return null; } };
|
||
const broken = checked.find((p) => !p.lint.ok);
|
||
if (broken && !retried) {
|
||
retried = true;
|
||
on({ type: 'retry', error: broken.lint.error });
|
||
messages.push({ role: 'user', content: `The engine could not read the program you proposed: ${broken.lint.error}. Give the corrected whole program in one \`\`\`qasm block.` });
|
||
continue;
|
||
}
|
||
for (const p of checked) {
|
||
if (!p.lint.ok) continue;
|
||
// Run as the model ran it, so the counts it quoted are the counts shown; else
|
||
// with the editor's settings.
|
||
const theirs = ranWith.get(p.qasm.trim());
|
||
const settings = theirs ?? { ...(ctx.job?.shots ? {} : { shots: 1000 }), ...pick(ctx.job, ['shots', 'observables']) };
|
||
const request = { qasm: p.qasm, ...settings };
|
||
if (!request.intent) request.verdict = true;
|
||
try { p.job = await this.engine.run(request); } catch (e) { p.error = e.message; }
|
||
p.verdict = p.job?.result?.verdict ?? (p.error ? await verdictOf({ request, error: p.error }) : null);
|
||
p.sealed = !!p.job?.result?.verdict;
|
||
}
|
||
// One the engine refused to read has the verdict of its refusal.
|
||
for (const p of checked) if (!p.lint.ok) { p.verdict = await verdictOf({ request: { qasm: p.qasm }, error: p.lint.error }); p.sealed = false; }
|
||
this.history.push({ role: 'user', content: text }, { role: 'assistant', content: answer });
|
||
this.history = this.history.slice(-12);
|
||
on({ type: 'answer', text: answer, proposals: checked, usage: res.usage, unsourced: unsourced(answer, heard.join(' ')) });
|
||
return;
|
||
}
|
||
throw new Error(tr('assistant.kept_calling'));
|
||
}
|
||
}
|
||
|
||
// The muse's brief: candidates, not answers, and no figures.
|
||
const MUSE = `You propose OpenQASM 3 programs for Quantum Studio's Alternatives. The Studio's engine reads, measures and compares every program you write; you do not judge them. Write each program whole, in its own \`\`\`qasm block, each after one line that starts "Idea:". State no counts, probabilities or costs. Use only what the engine reads: registers, the standard gates, gate definitions, ctrl and negctrl modifiers, measurement and reset, and if on measured bits; no while, inv or pow, and loops only in a program that declares an integer input.`;
|
||
|
||
// The proposer's brief for the Workbench: generators, judged at every size.
|
||
const PROPOSE = `You propose OpenQASM 3 generators for Quantum Studio's Workbench: programs with integer inputs (input uint n;) that write out one circuit for each value. The Studio's engine checks every generator you write at every size of the specification and keeps the cheapest; you do not judge them. Write each generator whole, in its own \`\`\`qasm block, each after one line that starts "Idea:". State no counts or costs. Use what the engine reads: integer inputs, registers sized by an input (qubit[n] q;), for loops over inclusive integer ranges (for uint i in [0:n-2] { … }), if on integer conditions (< <= == != && || % **), and the standard gates; no while, inv or pow.`;
|
||
|
||
const TASKS = { prepares: 'prepare the same state from |0…0⟩ as the reference, up to a global phase', equivalent: 'be the same circuit as the reference (the same unitary up to a global phase)' };
|
||
|
||
// What a specification asks, in the words a proposer reads.
|
||
export function specText(spec) {
|
||
const sizes = Object.entries(spec.inputs ?? {}).map(([n, vs]) => `${n} = ${vs.join(', ')}`).join('; ') || 'one size';
|
||
const cost = Object.entries(spec.cost ?? {}).map(([k, w]) => `${w} × ${k.replace(/_/g, ' ')}`).join(' + ');
|
||
return [
|
||
`Every member of the family must ${TASKS[spec.task] ?? spec.task}, at every size: ${sizes}.`,
|
||
spec.gates?.length ? `It may use only these gates: ${spec.gates.join(', ')}.` : 'It may use any standard gate.',
|
||
spec.connectivity === 'line' ? 'A two-qubit gate may join only neighbours on a line: q[i] and q[i + 1].' : 'A gate may join any qubits.',
|
||
`The cost, summed over the sizes: ${cost}.`,
|
||
].join(' ');
|
||
}
|
||
|
||
// The ideas in a muse's answer: each \`\`\`qasm block with the "Idea:" line
|
||
// before it.
|
||
export function ideas(text) {
|
||
const out = [];
|
||
const re = /\`\`\`(?:qasm|openqasm|openqasm3)?\s*\n([\s\S]*?)\`\`\`/gi;
|
||
let from = 0;
|
||
for (let m; (m = re.exec(text)); ) {
|
||
const before = text.slice(from, m.index);
|
||
from = m.index + m[0].length;
|
||
if (!/\b(qubit|qreg|OPENQASM)\b/.test(m[1])) continue;
|
||
const said = [...before.matchAll(/^[\s>*#-]*(?:\d+[.)]\s*)?\**idea\**\s*[:.]\**\s*(.+)$/gim)].pop();
|
||
out.push({ idea: said ? said[1].replace(/\*+/g, '').trim().replace(/\.$/, '') : '', qasm: m[1].trim() + '\n' });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// The figures in an answer's prose that no tool returned and the program does not
|
||
// hold: a decimal, a percentage, or a whole number of two digits or more.
|
||
export function unsourced(answer, heard) {
|
||
const prose = answer.replace(/```[\s\S]*?```/g, ' ');
|
||
const figures = prose.match(/(?<![\w.])\d+(?:\.\d+)?%?|(?<![\w.])\.\d+%?/g) ?? [];
|
||
const out = [];
|
||
for (const f of figures) {
|
||
const bare = f.replace(/%$/, '');
|
||
if (!/[.%]/.test(f) && bare.length < 2) continue; // a qubit's index, a step's number
|
||
if (heard.includes(bare) || out.includes(f)) continue;
|
||
out.push(f);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// LaTeX a model writes despite being asked not to, as plain Unicode.
|
||
export function unlatex(t) {
|
||
return t
|
||
.replace(/\$+/g, '')
|
||
.replace(/\\frac\{([^{}]*)\}\{\\sqrt\{([^{}]*)\}\}/g, '$1/√$2')
|
||
.replace(/\\frac\{([^{}]*)\}\{([^{}]*)\}/g, '($1)/($2)')
|
||
.replace(/\\sqrt\{([^{}]*)\}/g, '√$1')
|
||
.replace(/\\rangle/g, '⟩').replace(/\\langle/g, '⟨')
|
||
.replace(/\\(Phi|Psi|psi|phi|theta|pi|alpha|beta|gamma|otimes|cdot|pm)\b/g, (_, w) => ({ Phi: 'Φ', Psi: 'Ψ', psi: 'ψ', phi: 'φ', theta: 'θ', pi: 'π', alpha: 'α', beta: 'β', gamma: 'γ', otimes: '⊗', cdot: '·', pm: '±' })[w])
|
||
.replace(/\^\{?\+\}?/g, '⁺').replace(/\^\{?-\}?/g, '⁻')
|
||
.replace(/\\(left|right)/g, '');
|
||
}
|
||
|
||
function pick(o, keys) {
|
||
const out = {};
|
||
for (const k of keys) if (o && o[k] != null && !(Array.isArray(o[k]) && !o[k].length)) out[k] = o[k];
|
||
return out;
|
||
}
|