wai-quantum: what is new, what is a known construction found again, and aberrations #495
24 changed files with 900 additions and 44 deletions
5
.github/workflows/wai-web-wasm.yml
vendored
5
.github/workflows/wai-web-wasm.yml
vendored
|
|
@ -168,14 +168,15 @@ jobs:
|
|||
# vector, what does not run saying why, plan, lint, draw and methods as JSON,
|
||||
# critique, equivalence and variants as JSON, the classical shortcuts, the
|
||||
# workbench run and sealed in the page, a program's history recorded and
|
||||
# blamed there, and the reader's rules checking a proposal.
|
||||
# blamed there, the reader's rules checking a proposal, and what is new in
|
||||
# a program and in a workbench entry.
|
||||
- name: The job engine in a page (native unit tests)
|
||||
shell: bash
|
||||
working-directory: wai/wai-web
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log"
|
||||
grep -q "test result: ok. 9 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
|
||||
grep -q "test result: ok. 10 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
|
||||
# The crate's native unit tests, run on the host: the pure-Rust halves of the
|
||||
# bindings, which wasm_tests does not run. They decode the committed integer
|
||||
# payloads to their sidecars' SHA-256 (int_payload::tests) and check that the
|
||||
|
|
|
|||
|
|
@ -1562,6 +1562,28 @@ wire format may change and such changes are recorded here.
|
|||
- **`tools/ops-gpu-bench`** times each capability's decode with its graphs on the CPU and on each adapter, and reads the energy counters the host has.
|
||||
- **CI:** `wai-ops-gpu.yml` runs the Metal and browser legs on the `macos` runner, and requires on a runner with no GPU that the device tests skip without `WAI_OPS_GPU_REQUIRE` and fail with it.
|
||||
|
||||
### wai-quantum: what is new, what is a known construction found again, and aberrations
|
||||
|
||||
Feature 9 of the co-design report: a novelty, rediscovery and serendipity ledger.
|
||||
|
||||
- **What is new** (`engine::novelty`):
|
||||
- the inspiring set: GHZ by a chain of CNOTs, by doubling and by fan-out, the uniform superposition and the toolchain's quantum Fourier transform, each built at the program's width (to 12 qubits), and whatever programs the caller adds;
|
||||
- a structural distance, 0 to 1: the weighted Jaccard distance between two programs' multisets of gate tokens (a gate's kind and its qubits relative to its first) and of consecutive pairs of them;
|
||||
- rediscovery: a program the engine proves does what a known construction does (`equiv::same_state` for a preparation, `equiv::equivalent_programs` otherwise; never on random inputs alone) is that construction found again. Built unlike it (a distance of 0.5 or more) it is a different route to a known result. A program that is itself one known construction is not a different route to another;
|
||||
- novelty, the distance to the nearest of the set, named;
|
||||
- with a reference, the measures a program beats it on that the cost does not weigh.
|
||||
- **Aberrations** (Wiggins's term) in the workbench: a candidate that breaks only the specification's rules (its gates, its connectivity) is judged again without them. If it is verified there, its evaluation carries the rule it broke (`broke`: the gate, or the line), its reason, and the cost, axes and strength it would have. It is never kept.
|
||||
- `discover::novelty` reads one archive entry at the specification's largest size against the archive's other elites.
|
||||
- **The command line:** `wai-quantum novelty` (`--against`, `--reference`, `--weigh`, `--input`) and `discover novelty`; `discover eval` and `discover submit` say an aberration's rule and the cost it would have.
|
||||
- **For agents:** `novelty_qasm` and `discover_novelty`.
|
||||
- **The Studio:**
|
||||
- each Alternatives card says what is new in it, against the known constructions and the draft, and marks a different route to a known result;
|
||||
- the Workbench names each kept generator for what it is, shows it on the entry with what it beats the reference on unasked, and logs an aberration with "Try without that rule", which loosens the specification for a new archive;
|
||||
- Found keeps what is worth a second look (a different route, an aberration, a verified entry far from everything compared) to open, save with a note, or dismiss, and counts what was flagged and saved;
|
||||
- the assistant has the tool, and may call a program new only when the engine says so.
|
||||
- **For hosts:** `novelty::call`, `engine_novelty` and `engine_discover_novelty` in the page, `novelty` and `discover_novelty` in the desktop app, which now builds the toolchain's QFT into its inspiring set.
|
||||
- **CI:** wai-web's native engine tests count 10.
|
||||
|
||||
### wai-quantum: the person's rules for proposals, checked change by change
|
||||
|
||||
Feature 7 of the co-design report: region-scoped edits with locks and keep/avoid marks.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ The Workbench searches a family of circuits for its cheapest members, and the en
|
|||
|
||||
A proposal is verified only when it reads, keeps the rules and passes the check at every size. Each kept entry is sealed with this browser's key (the desktop app's, in the app). Clicking a point shows its cost, how it was checked, its seal and where it came from; "Open in editor" puts it in the editor. The archive is kept in this browser and comes back with the page. "Download" saves the specification and its archive side by side (`NAME.json` and `NAME.archive.json`), so `wai-quantum discover verify NAME.json` checks every seal. In the desktop app a search also counts its seconds and the joules its process spent.
|
||||
|
||||
## What is new, and Found
|
||||
|
||||
The engine says what is new in a program: the known construction it is, found again; a known result reached by a different route; or how far it is from the nearest program it was compared with. The known constructions are GHZ by a chain, by doubling and by fan-out, the uniform superposition and the quantum Fourier transform. The distance is structural, from 0 (built alike) to 1 (nothing shared), and it is the engine's, not a search of the literature.
|
||||
|
||||
- **Alternatives:** each card says what is new in it, against the known constructions and the draft. A card that reaches a known result by a different route is marked.
|
||||
- **The Workbench:** each kept generator is named for what it is, and its entry shows it with the measures it beats the reference on that the cost does not weigh. A candidate that breaks only one rule of the specification (a gate, or the line) and would be verified without it is an aberration. It is never kept: the log says what it would cost, and "Try without that rule" loosens the specification, for a new archive under it.
|
||||
- **Found** keeps what is worth a second look: a different route to a known result, an aberration, a verified entry far from everything it was compared with. Each can be opened in the editor, saved with a note, or dismissed. Its count says how many were flagged and how many saved. It is kept in this browser.
|
||||
|
||||
## Versions
|
||||
|
||||
The Versions pane records each change to the program and who made it:
|
||||
|
|
@ -76,7 +84,7 @@ The Assistant pane talks to a language model on this machine: any server with th
|
|||
|
||||
The engine is the model's tool, not an afterthought:
|
||||
|
||||
- **Its tools are the Studio's engine:** read the program, lint, draw, plan and run. Its first turn must call one of them, so every answer starts from the engine.
|
||||
- **Its tools are the Studio's engine:** read the program, lint, draw, plan, run, and say what is new in a program. Its first turn must call one of them, so every answer starts from the engine. It may call a program new only when the engine says so, and then as the engine's distance, not a search of the literature.
|
||||
- **Every program it proposes is checked before it is offered.** The engine reads it here. One that does not parse goes back to the model once, with the engine's reason, to be corrected. One that parses is run, with the settings the model used, so the counts it quoted are the counts shown. Then "Apply" puts it in the editor.
|
||||
- **It may state only figures the engine returned.** A figure in its prose that no tool returned is marked "Not from a run".
|
||||
- **Answers are plain text.** LaTeX a model writes anyway is shown as Unicode.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// it is local, and nothing leaves the machine.
|
||||
//
|
||||
// Tools first: the model asks the engine (lint, critique, draw, plan, run, the
|
||||
// equivalence check) and may state only numbers the engine returned. Every program it proposes is checked by 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.
|
||||
|
|
@ -21,10 +21,12 @@ Tools run the Studio's own engine:
|
|||
- 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.
|
||||
Use them. Never state a probability, count, amplitude, expectation value or cost that a tool did not return; quote the tool's figure.
|
||||
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.
|
||||
|
|
@ -45,6 +47,7 @@ export const TOOLS = [
|
|||
{ 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), 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',
|
||||
|
|
@ -171,6 +174,10 @@ export class Assistant {
|
|||
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 })));
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ export class TabEngine {
|
|||
history(name, args) { return this.call('history', { name, args }); }
|
||||
// The person's rules for proposals: `name` is check, held or brief.
|
||||
locks(name, args) { return this.call('locks', { name, args }); }
|
||||
// What is new in a program ({qasm, inputs, against, reference, weighed}), and
|
||||
// in one entry of an archive.
|
||||
novelty(args) { return this.call('novelty', { args }); }
|
||||
discoverNovelty(spec, archive, entry) { return this.call('discover_novelty', { spec, archive, entry }); }
|
||||
async signer() { return (await this.call('signer')).did; }
|
||||
cancel() {
|
||||
this.worker?.terminate();
|
||||
|
|
@ -156,6 +160,8 @@ export class DesktopEngine {
|
|||
discoverSearch(spec, archive, seed, evaluations) { return this.call('discover_search', { spec: JSON.stringify(spec), archive: JSON.stringify(archive), seed, evaluations }); }
|
||||
history(name, args) { return this.call('history', { name, args: JSON.stringify(args) }); }
|
||||
locks(name, args) { return this.call('locks', { name, args: JSON.stringify(args) }); }
|
||||
novelty(args) { return this.call('novelty', { args: JSON.stringify(args) }); }
|
||||
discoverNovelty(spec, archive, entry) { return this.call('discover_novelty', { spec: JSON.stringify(spec), archive: JSON.stringify(archive), id: entry }); }
|
||||
async signer() { return (await this.call('signer', {})).did; }
|
||||
saveJob(name, job) { return this.call('save_job', { name, job: JSON.stringify(job, null, 2) }); }
|
||||
saveProgram(name, text) { return this.call('save_program', { name, text }); }
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@
|
|||
// the model for different circuits toward an aim the reader names; the engine
|
||||
// reads, measures and compares each one, and says how far it is from the draft.
|
||||
// The model proposes, the engine judges, and the reader chooses: nothing is
|
||||
// applied until they press Apply.
|
||||
// applied until they press Apply. Each card says what is new in it: the known
|
||||
// construction it is, a known result reached by a different route (flagged in
|
||||
// Found), or how far it is from the nearest known construction or the draft.
|
||||
import { el, sameness } from './dom.js';
|
||||
import { chips } from './critique-view.js';
|
||||
import { noveltyLabel } from './novelty.js';
|
||||
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = (tag, attrs = {}, ...kids) => {
|
||||
|
|
@ -74,7 +77,8 @@ export function atlas(draft, cands, onPick) {
|
|||
// editor as `by`'s proposal: the engine's, or the model's.
|
||||
// `check(base, qasm)` gives the reader's rules' verdict on each change: a
|
||||
// candidate whose every change breaks one is left out with the rule.
|
||||
export function galleryPane({ engine, draft, model, apply, check = null }) {
|
||||
// `found` is the Found pane, which keeps a different route to a known result.
|
||||
export function galleryPane({ engine, draft, model, apply, check = null, found = null }) {
|
||||
const intent = el('input', { class: 'qs-input', id: 'qs-explore-aim', placeholder: 'Toward… fewer qubits, another entangler, more symmetry', autocomplete: 'off', 'aria-label': 'What Explore should aim for' });
|
||||
const improveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-improve', type: 'button', text: 'Improve', title: 'The same circuit, cheaper: each checked equivalent to the draft' });
|
||||
const exploreBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-explore', type: 'button', text: 'Explore', title: 'Different circuits toward the aim, from the model, each measured by the engine' });
|
||||
|
|
@ -102,14 +106,20 @@ export function galleryPane({ engine, draft, model, apply, check = null }) {
|
|||
const crit = await engine.critique(c.qasm);
|
||||
if (!crit.ok) return { ...c, refused: crit.error };
|
||||
const equivalence = c.equivalence ?? await engine.equivalent(src, c.qasm);
|
||||
return { ...c, measures: crit, equivalence, distance: lineDistance(src, c.qasm), base };
|
||||
// What is new in it, against the known constructions and the draft; an
|
||||
// engine without the call offers the candidate without it.
|
||||
const novelty = await engine.novelty?.({ qasm: c.qasm, against: [{ name: 'the draft', qasm: src }] }).catch(() => null);
|
||||
return { ...c, measures: crit, equivalence, novelty, distance: lineDistance(src, c.qasm), base };
|
||||
}
|
||||
|
||||
function show(mode, src, base, cands, refused, said) {
|
||||
const cards = cands.map((c, i) => {
|
||||
const s = sameness(c.equivalence);
|
||||
if (mode === 'explore' && s.cls === 'bad') s.cls = ''; // a different circuit is what Explore asks for
|
||||
const card = el('li', { class: 'qs-cand', 'data-i': i, id: `qs-cand-${i}`, tabindex: '-1' },
|
||||
const nv = noveltyLabel(c.novelty);
|
||||
const by = c.source === 'engine' ? { kind: 'engine', name: 'Alternatives' } : { kind: 'assistant', name: model()?.model?.name || undefined };
|
||||
if (nv?.flag === 'route') found?.flag({ kind: 'route', title: `${c.idea || c.label}: ${nv.text}`, why: nv.why, where: mode === 'improve' ? 'Improve' : 'Explore', source: c.qasm, by });
|
||||
const card = el('li', { class: `qs-cand${nv?.flag ? ' find' : ''}`, 'data-i': i, id: `qs-cand-${i}`, tabindex: '-1' },
|
||||
el('div', { class: 'qs-cand-head' },
|
||||
el('span', { class: 'qs-cand-n', text: String(i + 1) }),
|
||||
el('span', { class: 'qs-cand-title', text: c.idea || c.label }),
|
||||
|
|
@ -117,12 +127,12 @@ export function galleryPane({ engine, draft, model, apply, check = null }) {
|
|||
el('div', { class: 'qs-chips' }, ...chips(c.measures, base).filter((x) => x.dataset.k !== 'qubits' || c.measures.qubits !== base.qubits)),
|
||||
el('div', { class: 'qs-cand-facts' },
|
||||
el('span', { class: `qs-badge ${s.cls}`, title: s.why || '', text: s.text }),
|
||||
el('span', { class: 'qs-note', text: c.distance.changed ? `${c.distance.changed} lines changed` : 'the same lines' })),
|
||||
el('span', { class: 'qs-note', text: c.distance.changed ? `${c.distance.changed} lines changed` : 'the same lines' }),
|
||||
nv ? el('span', { class: `qs-badge qs-novelty ${nv.cls}`, title: nv.why, text: nv.text }) : null),
|
||||
c.changes?.length ? el('p', { class: 'qs-note', text: c.changes.join('; ') }) : null,
|
||||
el('details', { class: 'qs-cand-src' }, el('summary', { text: 'Program' }), el('pre', { class: 'qs-ai-code', text: c.qasm })),
|
||||
el('div', { class: 'qs-actions' },
|
||||
el('button', { class: 'qs-btn qs-small qs-apply', type: 'button', text: 'Apply', onclick: () => apply(c.qasm, `${mode === 'improve' ? 'An improvement' : 'An exploration'} from ${c.source === 'engine' ? 'the engine' : 'the model'}: ${c.idea || c.label}. ${s.text[0].toUpperCase() + s.text.slice(1)}.`,
|
||||
c.source === 'engine' ? { kind: 'engine', name: 'Alternatives' } : { kind: 'assistant', name: model()?.model?.name || undefined }) })));
|
||||
el('button', { class: 'qs-btn qs-small qs-apply', type: 'button', text: 'Apply', onclick: () => apply(c.qasm, `${mode === 'improve' ? 'An improvement' : 'An exploration'} from ${c.source === 'engine' ? 'the engine' : 'the model'}: ${c.idea || c.label}. ${s.text[0].toUpperCase() + s.text.slice(1)}.`, by) })));
|
||||
return card;
|
||||
});
|
||||
const pick = (i, go) => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export function withInputs(engine, values, local = engine) {
|
|||
equivalent: async (a, b) => engine.equivalent(await inst(a), await inst(b)),
|
||||
run: async (req, ...rest) => engine.run({ ...req, qasm: await inst(req.qasm) }, ...rest),
|
||||
plan: async (req, ...rest) => engine.plan({ ...req, qasm: await inst(req.qasm) }, ...rest),
|
||||
// Read at the inputs, so a generator is compared as the circuit it writes there.
|
||||
novelty: (args) => local.novelty({ inputs: values(), ...args }),
|
||||
instance: inst,
|
||||
};
|
||||
for (const m of ['draw', 'layout', 'variants', 'shortcuts']) over[m] = async (q, ...rest) => engine[m](await inst(q), ...rest);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { el, sameness } from './dom.js';
|
|||
import { critiquePanel } from './critique-view.js';
|
||||
import { galleryPane } from './gallery.js';
|
||||
import { workbenchPane } from './workbench.js';
|
||||
import { foundPane } from './novelty.js';
|
||||
import { versionsPane, HANDS, whoName } from './versions.js';
|
||||
import { rulesPane, heldLines, LOCK, UNLOCK } from './rules.js';
|
||||
import { declared, inputsBar, withInputs } from './generators.js';
|
||||
|
|
@ -190,6 +191,9 @@ function mount(root) {
|
|||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-circuit-h', text: 'Circuit' }), el('span', { class: 'qs-grow' }), el('span', { class: 'qs-note', id: 'qs-plan' }), asText),
|
||||
diagram, circuit, legend, critique.node, lens.node);
|
||||
|
||||
// ---- what was found worth a second look ----
|
||||
const found = foundPane({ open: (it) => offer(it.source, it.by, `${it.title}, from Found.`) });
|
||||
|
||||
// ---- the alternatives ----
|
||||
const gallery = galleryPane({
|
||||
engine: local,
|
||||
|
|
@ -197,6 +201,7 @@ function mount(root) {
|
|||
model: () => (aiConnected ? assistantFor() : null),
|
||||
apply: (qasm, note, by) => offer(qasm, by, note),
|
||||
check: (base, qasm) => local.locks('check', { base, proposed: qasm, locks: rules.locks(), inputs: genInputs() }),
|
||||
found,
|
||||
});
|
||||
|
||||
// ---- the workbench ----
|
||||
|
|
@ -206,6 +211,7 @@ function mount(root) {
|
|||
model: () => (aiConnected ? assistantFor() : null),
|
||||
apply: (qasm, note, by) => offer(qasm, by, note),
|
||||
save: (name, body) => saveFile(name, body, 'application/json'),
|
||||
found,
|
||||
});
|
||||
|
||||
// ---- the result ----
|
||||
|
|
@ -289,7 +295,7 @@ function mount(root) {
|
|||
status: (s, cls) => setStatus(s, cls),
|
||||
});
|
||||
|
||||
root.replaceChildren(header, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, gallery.node, workbench.node, resultPane, versions.node, aiPane, historyPane)), footer);
|
||||
root.replaceChildren(header, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, gallery.node, workbench.node, found.node, resultPane, versions.node, aiPane, historyPane)), footer);
|
||||
|
||||
// ---- the editor ----
|
||||
const style = HighlightStyle.define([
|
||||
|
|
|
|||
102
wai/quantum-studio/src/novelty.js
Normal file
102
wai/quantum-studio/src/novelty.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// What is new in a program, as the engine reads it (engine::novelty): the
|
||||
// known construction it is found to be, built alike or by a different route,
|
||||
// or how far it is from the nearest program it was compared with. The distance
|
||||
// is structural and the engine's: a reason to look, not a claim that nobody has
|
||||
// built it.
|
||||
//
|
||||
// Found keeps what is worth a second look until the reader saves or dismisses
|
||||
// it: a known result reached by a different route, a Workbench candidate that
|
||||
// breaks only one of its rules (an aberration), a verified entry far from
|
||||
// everything it was compared with. It counts what was flagged and what the
|
||||
// reader saved, in this browser.
|
||||
import { el } from './dom.js';
|
||||
|
||||
const FOUND = 'wai.quantum-studio.found';
|
||||
// The novelty at which a verified entry is worth a second look.
|
||||
export const HIGH = 0.75;
|
||||
const KEEP = 40; // the items kept unsaved; saved ones stay until dismissed
|
||||
|
||||
const two = (x) => (Math.round(x * 100) / 100).toFixed(2);
|
||||
|
||||
// A label for the engine's answer: {text, cls, why, flag}, where `flag` is
|
||||
// 'route' or 'novel' when it is worth a second look, or null. A program that
|
||||
// does not read, or that nothing was compared with, has no label.
|
||||
export function noveltyLabel(v, { verified = false } = {}) {
|
||||
if (!v || v.ok === false) return null;
|
||||
const r = v.rediscovers ?? [];
|
||||
const what = (x) => (x.prepares ? 'the same state from |0…0⟩' : 'the same circuit');
|
||||
const route = r.find((x) => x.route === 'a different route');
|
||||
if (route) {
|
||||
return { text: `a different route to ${route.name}`, cls: 'find', flag: 'route',
|
||||
why: `The engine finds ${what(route)} as ${route.name} (${route.how}), built unlike it: ${two(route.distance)} apart, where 0 is built alike and 1 shares nothing. Worth a second look.` };
|
||||
}
|
||||
const same = r.find((x) => x.route === 'the same construction') ?? r[0];
|
||||
if (same) return { text: `a known construction: ${same.name}`, cls: '', flag: null, why: `The engine finds ${what(same)} as ${same.name} (${same.how}), built alike.` };
|
||||
if (!v.nearest) return null;
|
||||
const far = verified && v.novelty >= HIGH;
|
||||
return { text: `new: ${two(v.novelty)} from the nearest (${v.nearest.name})`, cls: far ? 'find' : '', flag: far ? 'novel' : null,
|
||||
why: `Its gates and their order are ${two(v.novelty)} from ${v.nearest.name}, the nearest of the ${v.compared} programs compared (0 built alike, 1 nothing shared). A reason to look, not a claim that nobody has built it.` };
|
||||
}
|
||||
|
||||
// The measures a candidate beats its reference on that the cost did not weigh.
|
||||
export function unaskedText(v, words = {}) {
|
||||
const u = v?.unasked ?? [];
|
||||
if (!u.length) return null;
|
||||
return `Better than the reference on what the cost does not weigh: ${u.map((x) => `${words[x.measure] ?? x.measure} ${x.candidate} against ${x.reference}`).join('; ')}.`;
|
||||
}
|
||||
|
||||
const KINDS = { route: 'a different route', aberration: 'an aberration', novel: 'far from the known' };
|
||||
|
||||
// The pane. `open(item)` offers an item's program to the editor.
|
||||
export function foundPane({ open }) {
|
||||
let state = { items: [], flagged: 0, saved: 0 };
|
||||
try { Object.assign(state, JSON.parse(localStorage.getItem(FOUND) || '{}')); } catch (e) {}
|
||||
const keep = () => { try { localStorage.setItem(FOUND, JSON.stringify(state)); } catch (e) {} };
|
||||
const count = el('span', { class: 'qs-badge', id: 'qs-found-count' });
|
||||
const list = el('ol', { class: 'qs-found-list', id: 'qs-found-list' });
|
||||
const empty = el('p', { class: 'qs-empty', id: 'qs-found-empty', text: 'What the engine finds worth a second look comes here: a known result reached by a different route, a Workbench candidate that breaks only one of its rules, a verified entry far from everything it was compared with. Save what you mean to keep; dismiss the rest.' });
|
||||
const node = el('section', { class: 'qs-pane qs-found', 'aria-labelledby': 'qs-found-h' },
|
||||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-found-h', text: 'Found' }), el('span', { class: 'qs-grow' }), count),
|
||||
empty, list);
|
||||
|
||||
function render() {
|
||||
count.textContent = `${state.flagged} flagged · ${state.saved} saved`;
|
||||
empty.hidden = state.items.length > 0;
|
||||
list.replaceChildren(...state.items.map((it) => {
|
||||
const note = el('input', { class: 'qs-input qs-small', value: it.note ?? '', placeholder: 'A note, kept with it', 'aria-label': `A note on ${it.title}`, autocomplete: 'off' });
|
||||
note.addEventListener('change', () => { it.note = note.value; keep(); });
|
||||
const saveBtn = el('button', { class: 'qs-btn qs-small qs-found-save', type: 'button', text: it.saved ? 'Saved' : 'Save', 'aria-pressed': String(!!it.saved) });
|
||||
saveBtn.addEventListener('click', () => { it.saved = !it.saved; state.saved += it.saved ? 1 : -1; keep(); render(); });
|
||||
const dismiss = el('button', { class: 'qs-btn qs-small qs-found-dismiss', type: 'button', text: 'Dismiss' });
|
||||
dismiss.addEventListener('click', () => {
|
||||
if (it.saved) state.saved -= 1;
|
||||
state.items = state.items.filter((x) => x !== it);
|
||||
keep(); render();
|
||||
});
|
||||
const openBtn = it.source ? el('button', { class: 'qs-btn qs-small', type: 'button', text: 'Open in editor', onclick: () => open(it) }) : null;
|
||||
return el('li', { class: `qs-found-item${it.saved ? ' saved' : ''}`, 'data-kind': it.kind },
|
||||
el('div', { class: 'qs-cand-head' },
|
||||
el('span', { class: 'qs-badge find', text: KINDS[it.kind] ?? it.kind }),
|
||||
el('span', { class: 'qs-cand-title', text: it.title }),
|
||||
el('span', { class: 'qs-note', text: it.where })),
|
||||
el('p', { class: 'qs-note', text: it.why }),
|
||||
el('div', { class: 'qs-actions' }, ...[openBtn, saveBtn, dismiss, note].filter(Boolean)));
|
||||
}));
|
||||
}
|
||||
|
||||
// An item {kind, title, why, where, source, by, note?}, flagged once: the
|
||||
// same kind for the same program is not flagged again.
|
||||
function flag(item) {
|
||||
const key = `${item.kind}:${(item.source ?? item.title).trim()}`;
|
||||
if (state.items.some((x) => x.key === key)) return false;
|
||||
state.items.unshift({ ...item, key, saved: false });
|
||||
state.flagged += 1;
|
||||
let unsaved = 0;
|
||||
state.items = state.items.filter((x) => x.saved || ++unsaved <= KEEP);
|
||||
keep(); render();
|
||||
return true;
|
||||
}
|
||||
|
||||
render();
|
||||
return { node, flag, items: () => state.items };
|
||||
}
|
||||
|
|
@ -223,6 +223,23 @@
|
|||
.qs-wb-log { margin: 8px 12px 12px; padding: 0 0 0 18px; display: flex; flex-direction: column; gap: 3px; font: 11px/1.4 var(--font-mono, ui-monospace, monospace); color: var(--text-secondary); }
|
||||
.qs-wb-log li.ok { color: var(--ok-text); }
|
||||
.qs-wb-log li.bad { color: var(--bad-text); }
|
||||
.qs-wb-log li.find { color: var(--accent-text); }
|
||||
.qs-wb-log li .qs-wb-try { margin-left: 4px; font-family: var(--font-sans); }
|
||||
.qs-wb-novelty { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
|
||||
.qs-wb-novelty:empty { display: none; }
|
||||
.qs-wb-novelty .qs-note { margin: 0; flex-basis: 100%; }
|
||||
|
||||
/* What is new: a known result by a different route, an aberration, an entry far from the known. */
|
||||
.qs-badge.find { color: var(--accent-text); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
|
||||
.qs-cand.find { border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); }
|
||||
.qs-novelty { max-width: 100%; }
|
||||
.qs-found-list { list-style: none; margin: 0; padding: 8px 12px 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.qs-found > .qs-empty { padding: 8px 12px 12px; }
|
||||
.qs-found-list:empty { display: none; }
|
||||
.qs-found-item { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-base); }
|
||||
.qs-found-item.saved { border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); }
|
||||
.qs-found-item .qs-note { margin: 0; }
|
||||
.qs-found-item .qs-input { flex: 1 1 160px; min-width: 0; }
|
||||
/* Versions: who made each change, and whose hand each line and gate is in. */
|
||||
.qs-hand-person { --qs-hand: var(--qs-hand-person); }
|
||||
.qs-hand-assistant { --qs-hand: var(--qs-hand-assistant); }
|
||||
|
|
|
|||
|
|
@ -11,8 +11,16 @@
|
|||
// (the baseline any idea should beat) and the model, when one is connected;
|
||||
// each is named on what it found. Nothing changes the editor until the reader
|
||||
// opens an entry.
|
||||
//
|
||||
// What is new in a kept generator is said beside it: the known construction it
|
||||
// is, or a known result reached by a different route, or how far it is from
|
||||
// everything it was compared with. A candidate that breaks only one rule of
|
||||
// the specification and would be verified without it is an aberration: never
|
||||
// kept, it is offered as a reason to revise the rule. What is worth a second
|
||||
// look goes to Found.
|
||||
import { el } from './dom.js';
|
||||
import { declared } from './generators.js';
|
||||
import { noveltyLabel, unaskedText } from './novelty.js';
|
||||
|
||||
const SAVED = 'wai.quantum-studio.workbench';
|
||||
// A cost as the Studio shows it: a sum of weighted measures, to three places.
|
||||
|
|
@ -120,8 +128,8 @@ const byOf = (p) => (p === 'mutate' ? { kind: 'engine', name: 'the blind search'
|
|||
|
||||
// `save(name, text)` writes a file where the reader keeps them and gives back
|
||||
// where it went. `apply(source, note, by)` offers a generator to the editor as
|
||||
// `by`'s proposal.
|
||||
export function workbenchPane({ engine, draft, model, apply, save }) {
|
||||
// `by`'s proposal. `found` is the Found pane.
|
||||
export function workbenchPane({ engine, draft, model, apply, save, found = null }) {
|
||||
const startBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-start', type: 'button', text: 'Start', title: 'A new archive, seeded with the program in the editor as the reference' });
|
||||
const task = el('select', { class: 'qs-input qs-small', id: 'qs-wb-task', 'aria-label': 'What each member must do' },
|
||||
el('option', { value: 'prepares', text: 'prepare the same state from |0…0⟩' }), el('option', { value: 'equivalent', text: 'be the same circuit (unitary)' }));
|
||||
|
|
@ -157,7 +165,7 @@ export function workbenchPane({ engine, draft, model, apply, save }) {
|
|||
form, acts, stats, mapBox, entry, log);
|
||||
|
||||
let spec = null, archive = null, summary = null, selected = null, openedFrom = null, busy = false;
|
||||
const say = (text, cls = '') => { log.prepend(el('li', { class: cls, text })); while (log.children.length > 12) log.lastChild.remove(); };
|
||||
const say = (text, cls = '', ...more) => { log.prepend(el('li', { class: cls }, text, ...more)); while (log.children.length > 12) log.lastChild.remove(); };
|
||||
const persist = () => { try { localStorage.setItem(SAVED, JSON.stringify({ spec, archive })); } catch (e) {} };
|
||||
const setBusy = (b) => { busy = b; for (const x of [startBtn, searchBtn, askBtn, submitBtn]) x.disabled = b; };
|
||||
const entryById = (id) => archive?.entries.find((e) => e.id === id);
|
||||
|
|
@ -197,10 +205,19 @@ export function workbenchPane({ engine, draft, model, apply, save }) {
|
|||
const open = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-open', type: 'button', text: 'Open in editor' });
|
||||
open.addEventListener('click', () => { openedFrom = id; apply(e.source, `${id} from the Workbench (${e.proposer}).`, byOf(e.proposer)); });
|
||||
const chain = lineage(id).map((x, i) => el('button', { class: 'qs-chip', type: 'button', text: `${i ? '← ' : ''}${x.id} ${x.proposer}`, title: x.note ?? '', onclick: () => pick(x.id) }));
|
||||
const novel = el('div', { class: 'qs-wb-novelty', id: 'qs-wb-novelty' });
|
||||
engine.discoverNovelty?.(spec, archive, id).then((v) => {
|
||||
if (selected !== id) return;
|
||||
const nv = noveltyLabel(v, { verified: true });
|
||||
const u = unaskedText(v, MEASURES);
|
||||
novel.replaceChildren(...[nv ? el('span', { class: `qs-badge ${nv.cls}`, text: nv.text }) : null,
|
||||
nv ? el('p', { class: 'qs-note', text: nv.why }) : null, u ? el('p', { class: 'qs-note', text: u }) : null].filter(Boolean));
|
||||
}).catch(() => {});
|
||||
entry.replaceChildren(...[
|
||||
el('div', { class: 'qs-cand-head' }, el('span', { class: 'qs-cand-title', text: `${e.id} · cost ${num(e.cost)} · ${MEASURES[spec.axes[0]]} ${e.axes[0]}, ${MEASURES[spec.axes[1]]} ${e.axes[1]}` }),
|
||||
el('span', { class: 'qs-badge ok', text: `checked by ${e.strength}${e.sealed ? ', sealed' : ''}` }), el('span', { class: 'qs-grow' }), open),
|
||||
e.note ? el('p', { class: 'qs-wb-note', text: e.note }) : null,
|
||||
novel,
|
||||
el('div', { class: 'qs-wb-chain', 'aria-label': 'Where it came from' }, ...chain),
|
||||
el('details', {}, el('summary', { text: 'Its generator' }), el('pre', { class: 'qs-ai-code', text: e.source })),
|
||||
].filter(Boolean));
|
||||
|
|
@ -233,20 +250,59 @@ export function workbenchPane({ engine, draft, model, apply, save }) {
|
|||
return r.outcome;
|
||||
}
|
||||
|
||||
// The form loosened by the rule an aberration broke, for a new archive.
|
||||
function loosen(broke) {
|
||||
if (broke.kind === 'gates' && broke.gate) gates.value = [...new Set([...gates.value.split(/[\s,]+/).filter(Boolean), broke.gate])].join(', ');
|
||||
else if (broke.kind === 'connectivity') conn.value = 'all';
|
||||
form.open = true;
|
||||
stats.textContent = `The specification now ${broke.kind === 'gates' ? `allows ${broke.gate}` : 'lets a gate join any pair'}. This archive was built under the old rule; start a new one to search under the new.`;
|
||||
startBtn.focus();
|
||||
}
|
||||
|
||||
// What is new in a kept entry, said and flagged when it is worth a second look.
|
||||
async function novelty(id, who, by) {
|
||||
const v = await engine.discoverNovelty?.(spec, archive, id).catch(() => null);
|
||||
const nv = noveltyLabel(v, { verified: true });
|
||||
if (!nv) return;
|
||||
say(`${id}: ${nv.text}.`, nv.flag ? 'find' : '');
|
||||
if (nv.flag) found?.flag({ kind: nv.flag, title: `${id} (${who}): ${nv.text}`, why: nv.why, where: 'Workbench', source: entryById(id)?.source, by });
|
||||
}
|
||||
|
||||
// What a judged candidate adds to its outcome: an aberration's invitation to
|
||||
// revise a rule, and what is new in it once kept.
|
||||
async function after(o, who, source, by) {
|
||||
const ab = o.evaluation?.aberration;
|
||||
if (ab) {
|
||||
const broke = ab.broke?.kind === 'gates' ? `it uses ${ab.broke.gate}` : ab.broke?.kind === 'connectivity' ? 'it joins qubits that are not neighbours' : 'it breaks a rule';
|
||||
const why = `It breaks only one rule (${ab.rule}); without it, it would be verified (${ab.strength}) at cost ${num(ab.cost)}, ${MEASURES[spec.axes[0]]} ${ab.axes[0]}, ${MEASURES[spec.axes[1]]} ${ab.axes[1]}.`;
|
||||
const tryBtn = ab.broke ? el('button', { class: 'qs-btn qs-small qs-wb-try', type: 'button', text: 'Try without that rule', onclick: () => loosen(ab.broke) }) : null;
|
||||
say(`${who}: an aberration. ${why} The rule may be worth revising. `, 'find', tryBtn);
|
||||
found?.flag({ kind: 'aberration', title: `${who}: ${broke}`, why, where: 'Workbench', source, by });
|
||||
}
|
||||
if (o.kept) await novelty(o.kept, who, by);
|
||||
}
|
||||
|
||||
async function searchMore() {
|
||||
if (busy || !spec) return;
|
||||
setBusy(true);
|
||||
const total = 100, chunk = 25;
|
||||
const before = summary.stats.kept;
|
||||
const kept = [];
|
||||
try {
|
||||
for (let done = 0; done < total; done += chunk) {
|
||||
stats.textContent = `The blind search: ${done} of ${total} edits judged…`;
|
||||
// A fresh seed for every step, so a search taken up again does not replay.
|
||||
const r = await engine.discoverSearch(spec, archive, archive.stats.evaluations + archive.stats.duplicates + 1, chunk);
|
||||
({ archive, summary } = r);
|
||||
kept.push(...(r.kept ?? []));
|
||||
}
|
||||
say(`The blind search: ${total} edits judged, ${summary.stats.kept - before} kept.`);
|
||||
persist();
|
||||
// Only what is worth a second look is said of the search's finds.
|
||||
for (const id of kept) {
|
||||
const nv = noveltyLabel(await engine.discoverNovelty?.(spec, archive, id).catch(() => null), { verified: true });
|
||||
if (nv?.flag) { say(`${id} (the blind search): ${nv.text}.`, 'find'); found?.flag({ kind: nv.flag, title: `${id} (the blind search): ${nv.text}`, why: nv.why, where: 'Workbench', source: entryById(id)?.source, by: byOf('mutate') }); }
|
||||
}
|
||||
} catch (e) { say(`The search stopped: ${e.message}`, 'bad'); }
|
||||
finally { setBusy(false); render(); }
|
||||
}
|
||||
|
|
@ -262,8 +318,10 @@ export function workbenchPane({ engine, draft, model, apply, save }) {
|
|||
const ideas = await m.propose(spec, shown, 3);
|
||||
if (!ideas.length) say('The model wrote no generator in a ```qasm block.', 'bad');
|
||||
for (const p of ideas) {
|
||||
const o = await submit(p.qasm, shown[0]?.id ?? null, `model: ${m.model?.name || 'the model'}`, p.idea || null);
|
||||
const proposer = `model: ${m.model?.name || 'the model'}`;
|
||||
const o = await submit(p.qasm, shown[0]?.id ?? null, proposer, p.idea || null);
|
||||
say(outcomeText(o, p.idea || 'an idea'), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad');
|
||||
await after(o, p.idea || 'an idea', p.qasm, byOf(proposer));
|
||||
}
|
||||
persist();
|
||||
} catch (e) { say(`The model did not answer: ${e.message}`, 'bad'); }
|
||||
|
|
@ -278,6 +336,7 @@ export function workbenchPane({ engine, draft, model, apply, save }) {
|
|||
const parent = openedFrom && entryById(openedFrom)?.source !== src ? openedFrom : null;
|
||||
const o = await submit(src, parent, 'person', null);
|
||||
say(outcomeText(o, 'Your program'), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad');
|
||||
await after(o, 'Your program', src, byOf('person'));
|
||||
persist();
|
||||
} catch (e) { say(`It could not be judged: ${e.message}`, 'bad'); }
|
||||
finally { setBusy(false); render(); }
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ self.onmessage = async (ev) => {
|
|||
case 'discover_search': text = e().discover_search(JSON.stringify(ev.data.spec), JSON.stringify(ev.data.archive), ev.data.from >>> 0, ev.data.evaluations >>> 0); break;
|
||||
case 'history': text = e().history(ev.data.name, JSON.stringify(ev.data.args ?? {})); break;
|
||||
case 'locks': text = mod.engine_locks(ev.data.name, JSON.stringify(ev.data.args ?? {})); break;
|
||||
case 'novelty': text = mod.engine_novelty(JSON.stringify(ev.data.args ?? {})); break;
|
||||
case 'discover_novelty': text = mod.engine_discover_novelty(JSON.stringify(ev.data.spec), JSON.stringify(ev.data.archive), ev.data.entry); break;
|
||||
default: throw new Error(`no operation ${op}`);
|
||||
}
|
||||
const value = JSON.parse(text);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@
|
|||
// 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;
|
||||
// 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;
|
||||
// - 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;
|
||||
|
|
@ -38,9 +40,12 @@
|
|||
// - 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; with the model, its generators are judged at
|
||||
// every size, a wrong one and one with a gate the specification forbids left out, and
|
||||
// it is told the specification and shown the cheapest found;
|
||||
// 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; 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;
|
||||
// - 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';
|
||||
|
|
@ -224,6 +229,24 @@ const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textConten
|
|||
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 nv = await page.textContent('.qs-cand .qs-novelty');
|
||||
check(nv === 'a different route to GHZ, by a chain of CNOTs' && await page.$('.qs-cand.find'), `a rewrite that makes the GHZ state, built unlike the chain, is labelled a different route (${nv})`);
|
||||
check(/0\.\d\d apart/.test(await page.getAttribute('.qs-cand .qs-novelty', 'title')), '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');
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
@ -437,15 +460,21 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
|
|||
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');
|
||||
await page.waitForFunction(() => /^Your program: /.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||||
// 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())[0];
|
||||
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})`);
|
||||
await setProgram(CHAIN);
|
||||
await page.click('#qs-wb-open');
|
||||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||||
|
|
@ -469,6 +498,24 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
|
|||
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(/^\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
|
||||
|
|
@ -563,6 +610,8 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
|
|||
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');
|
||||
|
|
@ -579,6 +628,8 @@ const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-
|
|||
&& 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');
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ A cheap route proves a design classically easy. None being cheap proves nothing
|
|||
- `draw_circuit`
|
||||
- `verify_quantum_result`
|
||||
- `discover_evaluate`, `discover_submit` and `discover_archive` (the workbench, below)
|
||||
- `novelty_qasm` and `discover_novelty` (what is new, below)
|
||||
- `history_commit`, `history_changes`, `history_blame` and `history_log` (a program's history, below)
|
||||
- `list_methods`
|
||||
|
||||
|
|
@ -235,6 +236,25 @@ What the person types is theirs to change; the rules bind proposals. Only what a
|
|||
- **The history:** `history changes` names each change's breaches. `history propose` refuses to accept a change that breaks a rule, `--accept all` takes only the changes the rules allow, and the version records the rest as blocked.
|
||||
- **For agents:** through `wai-quantum mcp`, `history_commit` refuses an agent's edit that breaks a rule, with the rule, and `history_changes` shows each change's breaches and the rules as sentences.
|
||||
|
||||
## What is new
|
||||
|
||||
`wai-quantum novelty` says what is new in a program, and what is a known construction found again:
|
||||
|
||||
```sh
|
||||
wai-quantum novelty mine.qasm # its nearest known construction, and what it is found to be
|
||||
wai-quantum novelty mine.qasm --against draft.qasm # compared with your own programs too
|
||||
wai-quantum novelty gen.qasm --input n=6 --reference ref.qasm --weigh depth,two_qubit
|
||||
wai-quantum discover novelty ghz-all.json e12 # one entry of an archive, against the others
|
||||
```
|
||||
|
||||
- **The known constructions** are GHZ by a chain of CNOTs, by doubling and by fan-out from one qubit, the uniform superposition, and the quantum Fourier transform, each built at the program's width (to 12 qubits). `--against` adds programs to compare with; `discover novelty` compares an entry with the archive's other elites.
|
||||
- **The distance** is structural: each gate a token (its kind, and its qubits relative to its first), two programs compared as multisets of those tokens and of consecutive pairs of them. It is 0 when they are built alike and 1 when they share nothing. It says nothing about what a program does.
|
||||
- **Found again:** when the engine proves a program does what a known construction does (the same state from |0…0⟩ for a preparation, the same circuit otherwise), it names it. Built unlike it (a distance of 0.5 or more), it is **a different route to a known result**, the case worth a second look. A program that is itself one known construction is not a different route to another.
|
||||
- **Strengths not asked for:** with `--reference`, the measures it beats the reference on that `--weigh` does not name.
|
||||
- **An aberration:** in the workbench, a candidate that breaks only the specification's rules (its gates, its connectivity) and would be verified without them. It is never kept. `discover eval` and `discover submit` say what it would cost and which rule it breaks, as a reason to revise the rule.
|
||||
|
||||
A high distance is a reason to look, not a claim that nobody has built it: the engine knows the constructions above and the programs it is given, not the literature.
|
||||
|
||||
## The research tools
|
||||
|
||||
`wai-quantum lab …` holds the research tools:
|
||||
|
|
|
|||
|
|
@ -109,6 +109,15 @@ pub enum Cmd {
|
|||
#[command(flatten)]
|
||||
files: Files,
|
||||
},
|
||||
/// What is new in one entry: its nearest known construction or elite, each known construction it is found to be, and the strengths the cost did not ask for
|
||||
Novelty {
|
||||
#[command(flatten)]
|
||||
files: Files,
|
||||
/// The entry's id
|
||||
id: String,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// A specification read from `path`: a reference given as `"@file"` is read
|
||||
|
|
@ -153,7 +162,15 @@ fn evaluation_text(spec: &Spec, e: &Evaluation) -> String {
|
|||
let sizes: Vec<String> = e.sizes.iter().map(|s| s["inputs"].as_object().map(|o| o.iter().map(|(k, v)| format!("{k} = {v}")).collect::<Vec<_>>().join(", ")).unwrap_or_default()).collect();
|
||||
match (&e.failure, e.cost, e.axes) {
|
||||
(None, Some(cost), Some(axes)) => format!("verified ({}) at {}: cost {cost}, {}", e.strength.as_deref().unwrap_or(""), sizes.join("; "), axes_text(spec, &json!(axes))),
|
||||
(Some(why), _, _) => format!("not verified: {why}"),
|
||||
(Some(why), _, _) => match &e.aberration {
|
||||
Some(ab) => format!(
|
||||
"not verified: {why}\nan aberration: it breaks only that rule, and without it would be verified ({}) at cost {}, {}; the rule may be worth revising",
|
||||
ab["strength"].as_str().unwrap_or(""),
|
||||
ab["cost"],
|
||||
axes_text(spec, &ab["axes"])
|
||||
),
|
||||
None => format!("not verified: {why}"),
|
||||
},
|
||||
_ => "not verified".into(),
|
||||
}
|
||||
}
|
||||
|
|
@ -193,6 +210,17 @@ pub fn main(cmd: Cmd) {
|
|||
Cmd::Submit { files, candidate, parent, proposer, note, key: k, json } => submit(&files, &candidate, parent.as_deref(), &proposer, note.as_deref(), &k, json),
|
||||
Cmd::Show { files, source, json } => show(&files, source.as_deref(), json),
|
||||
Cmd::Verify { files } => verify(&files),
|
||||
Cmd::Novelty { files, id, json } => (|| {
|
||||
let ev = Evaluator::new(read_spec(&files.spec)?)?;
|
||||
let a = load(&ev, &archive_path(&files.spec, files.archive.as_deref()))?;
|
||||
let v = wai_quantum::engine::discover::novelty(&ev, &a, &id)?;
|
||||
if json {
|
||||
println!("{v}");
|
||||
} else {
|
||||
print!("{}", super::novelty_text(&v));
|
||||
}
|
||||
Ok(())
|
||||
})(),
|
||||
};
|
||||
r.unwrap_or_else(|e| super::die(&e));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
//! wai-quantum discover search ghz.json # cheaper generators, each checked at every size
|
||||
//! wai-quantum history blame prog.qasm # whose hand each line and gate is in
|
||||
//! wai-quantum locks check prog.qasm theirs.qasm # the rules each proposed change breaks
|
||||
//! wai-quantum novelty prog.qasm # its nearest known construction, and what it rediscovers
|
||||
//! wai-quantum run prog.qasm --history prog.history.json # the receipt binds who wrote it
|
||||
//! wai-quantum submit prog.qasm # on the job service
|
||||
//! wai-quantum verify result.json # a result's receipt, or a lab receipt
|
||||
|
|
@ -194,6 +195,22 @@ enum Command {
|
|||
#[command(subcommand)]
|
||||
cmd: history::Cmd,
|
||||
},
|
||||
/// What is new in a program: its structural distance to the known constructions (and to programs given), each known construction the engine proves it to be, and the strengths a cost did not ask for
|
||||
Novelty {
|
||||
/// The program; `-` or nothing reads standard input
|
||||
file: Option<PathBuf>,
|
||||
/// More of the inspiring set: programs to compare with (repeatable)
|
||||
#[arg(long = "against", value_name = "FILE")]
|
||||
against: Vec<PathBuf>,
|
||||
/// The program it is meant to improve on: the measures it beats it on that --weigh does not name
|
||||
#[arg(long, value_name = "FILE")]
|
||||
reference: Option<PathBuf>,
|
||||
/// The measures the cost weighs, comma-separated (as depth,two_qubit)
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
weigh: Vec<String>,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// The person's rules for what a proposal may change: held blocks, gates, qubits, what to avoid, a region (`wai-quantum locks --help`)
|
||||
Locks {
|
||||
#[command(subcommand)]
|
||||
|
|
@ -372,6 +389,27 @@ pub fn run_here(req: &Value, memory_gb: u64, key: &wai_quantum::ed25519_dalek::S
|
|||
job::run(req, memory_gb << 30, key, json!({ "unmetered": UNMETERED }))
|
||||
}
|
||||
|
||||
/// What is new in a circuit as a reader takes it in.
|
||||
pub(crate) fn novelty_text(v: &Value) -> String {
|
||||
let mut out = String::new();
|
||||
if let Some(n) = v["nearest"].as_object() {
|
||||
out.push_str(&format!("nearest {} (distance {}, of {} compared)\n", n["name"].as_str().unwrap_or(""), n["distance"], v["compared"]));
|
||||
}
|
||||
let found = v["rediscovers"].as_array().cloned().unwrap_or_default();
|
||||
if found.is_empty() {
|
||||
out.push_str("rediscovers no known construction the engine checks\n");
|
||||
}
|
||||
for r in &found {
|
||||
let what = if r["prepares"] == true { "the same state as" } else { "the same circuit as" };
|
||||
let flag = if r["route"] == "a different route" { " ← a different route to a known result" } else { "" };
|
||||
out.push_str(&format!("rediscovers {what} {} (by {}; distance {}){flag}\n", r["name"].as_str().unwrap_or(""), r["how"].as_str().unwrap_or(""), r["distance"]));
|
||||
}
|
||||
for s in v["unasked"].as_array().into_iter().flatten() {
|
||||
out.push_str(&format!("unasked {}: {} against the reference's {}\n", s["measure"].as_str().unwrap_or(""), s["candidate"], s["reference"]));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A critique as a reader takes it in.
|
||||
fn print_critique(name: &str, c: &Value) {
|
||||
let n = |k: &str| c[k].as_u64().unwrap_or(0);
|
||||
|
|
@ -681,6 +719,21 @@ fn main() {
|
|||
Command::Discover { cmd } => discover::main(cmd),
|
||||
Command::History { cmd } => history::main(cmd),
|
||||
Command::Locks { cmd } => locks::main(cmd),
|
||||
Command::Novelty { file, against, reference, weigh, json } => {
|
||||
let inputs = INPUTS.get().cloned().unwrap_or_default();
|
||||
let text = read_text(file.as_deref()).unwrap_or_else(|e| die(&e));
|
||||
let others: Vec<(String, String)> = against.iter().map(|p| (p.display().to_string(), read_text(Some(p)).unwrap_or_else(|e| die(&e)))).collect();
|
||||
let reference = reference.map(|p| read_text(Some(&p)).unwrap_or_else(|e| die(&e)));
|
||||
let v = wai_quantum::engine::novelty::assess(&text, &inputs, &others, reference.as_deref(), &weigh);
|
||||
if v["ok"] != true {
|
||||
die(v["error"].as_str().unwrap_or("the program does not read"));
|
||||
}
|
||||
if json {
|
||||
println!("{v}");
|
||||
} else {
|
||||
print!("{}", novelty_text(&v));
|
||||
}
|
||||
}
|
||||
Command::Lab { args } => lab::main(args),
|
||||
Command::Completions { shell } => {
|
||||
clap_complete::generate(shell, &mut Cli::command(), "wai-quantum", &mut std::io::stdout());
|
||||
|
|
|
|||
|
|
@ -120,6 +120,18 @@ fn tools() -> Value {
|
|||
"description": "An archive's summary: what the searches spent (evaluations, seconds, joules) and found (verified, kept, improvements), the reference, the best, the front of the two axes, and the best of every cell, with its proposer, parent and note. With sources, each cell's generator too, to propose from.",
|
||||
"inputSchema": { "type": "object", "required": ["spec"], "properties": { "spec": { "type": "string", "description": "The specification's path." }, "archive": { "type": "string", "description": "The archive's path, if not beside the specification." }, "sources": { "type": "boolean", "description": "Include every cell's generator." } } },
|
||||
},
|
||||
{
|
||||
"name": "novelty_qasm",
|
||||
"title": "What is new in a program",
|
||||
"description": "A program's structural distance to the known constructions (GHZ by a chain, by doubling and by fan-out; the uniform superposition; the quantum Fourier transform) and to programs you give, the nearest named; each known construction the engine proves it to be (the same state, or the same circuit), and whether by a different route, which is worth a second look; and, with a reference, the measures it beats the reference on that the cost (weighed) does not count. A high distance is a reason to look, not a claim that nobody has built it.",
|
||||
"inputSchema": { "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string", "description": "The program, or a generator with its values in inputs." }, "inputs": inputs_schema(), "against": { "type": "array", "items": { "type": "object", "required": ["name", "qasm"], "properties": { "name": { "type": "string" }, "qasm": { "type": "string" } } }, "description": "More programs to compare with." }, "reference": { "type": "string", "description": "The program it is meant to improve on." }, "weighed": { "type": "array", "items": { "type": "string" }, "description": "The measures the cost weighs." } } },
|
||||
},
|
||||
{
|
||||
"name": "discover_novelty",
|
||||
"title": "What is new in a workbench entry",
|
||||
"description": "For one entry of a workbench archive, read at the specification's largest size: its nearest known construction or elite and the distance, each known construction it is found to be (and whether by a different route), and the measures it beats the reference on that the specification's cost does not weigh.",
|
||||
"inputSchema": { "type": "object", "required": ["spec", "id"], "properties": { "spec": { "type": "string", "description": "The specification's path." }, "archive": { "type": "string", "description": "The archive's path, if not beside the specification." }, "id": { "type": "string", "description": "The entry's id." } } },
|
||||
},
|
||||
{
|
||||
"name": "history_commit",
|
||||
"title": "Record your edit in a program's history",
|
||||
|
|
@ -168,6 +180,8 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli
|
|||
if name.starts_with("history_") {
|
||||
return history(name, args, key, client);
|
||||
}
|
||||
// A tool that reads a generator at its inputs itself takes them as given.
|
||||
let raw = args;
|
||||
let args = &at_inputs(args)?;
|
||||
let qasm = || args.get("qasm").and_then(Value::as_str).ok_or("give \"qasm\": the program");
|
||||
match name {
|
||||
|
|
@ -198,6 +212,7 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli
|
|||
Ok(json!({ "valid": valid, "key": key }))
|
||||
}
|
||||
"list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })),
|
||||
"novelty_qasm" => wai_quantum::engine::novelty::call(raw),
|
||||
_ => Err(format!("no tool {name}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -249,6 +264,13 @@ fn discover(name: &str, args: &Value, key: &SigningKey) -> Result<Value, String>
|
|||
crate::discover::save(&a, &archive)?;
|
||||
Ok(json!({ "outcome": out, "best": a.best().map(|e| json!({ "id": e.id, "cost": e.cost, "axes": e.axes })) }))
|
||||
}
|
||||
"discover_novelty" => {
|
||||
let path = spec_path()?;
|
||||
let ev = Evaluator::new(crate::discover::read_spec(path)?)?;
|
||||
let archive = crate::discover::archive_path(path, text("archive").map(Path::new));
|
||||
let a = crate::discover::load(&ev, &archive)?;
|
||||
wai_quantum::engine::discover::novelty(&ev, &a, text("id").ok_or("give \"id\": the entry's id")?)
|
||||
}
|
||||
"discover_archive" => {
|
||||
let path = spec_path()?;
|
||||
let ev = Evaluator::new(crate::discover::read_spec(path)?)?;
|
||||
|
|
@ -397,7 +419,7 @@ mod tests {
|
|||
assert!(handle(&json!({ "jsonrpc": "2.0", "method": "notifications/initialized" }), 1, &key(), &mut Client::default()).is_none());
|
||||
let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key(), &mut Client::default()).unwrap();
|
||||
let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(names, ["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "draw_circuit", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "history_commit", "history_changes", "history_blame", "history_log", "list_methods"]);
|
||||
assert_eq!(names, ["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "critique_qasm", "check_equivalent", "improve_qasm", "classical_shortcuts", "draw_circuit", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "history_commit", "history_changes", "history_blame", "history_log", "list_methods"]);
|
||||
let run = handle(
|
||||
&json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "run_quantum_circuit", "arguments": { "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "observables": ["Z0 Z1"] } } }),
|
||||
1,
|
||||
|
|
@ -457,6 +479,17 @@ mod tests {
|
|||
assert!(call("lint_qasm", &json!({ "qasm": star }), 1, &key(), &Client::default()).unwrap_err().contains("give n a value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_asks_what_is_new_and_what_is_found_again() {
|
||||
let ladder = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[n - 1];\nfor uint i in [1:n-1] {\n cx q[n - i], q[n - i - 1];\n}\n";
|
||||
let v = call("novelty_qasm", &json!({ "qasm": ladder, "inputs": { "n": 5 } }), 1, &key(), &Client::default()).unwrap();
|
||||
let routes: Vec<&str> = v["rediscovers"].as_array().unwrap().iter().map(|r| r["route"].as_str().unwrap()).collect();
|
||||
assert!(routes.contains(&"a different route"), "{v}");
|
||||
let mine = call("novelty_qasm", &json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "against": [{ "name": "my draft", "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n" }] }), 1, &key(), &Client::default()).unwrap();
|
||||
assert_eq!(mine["nearest"]["name"], "my draft", "{mine}");
|
||||
assert!(call("novelty_qasm", &json!({ "qasm": "qubit[1] q; foo q[0];" }), 1, &key(), &Client::default()).unwrap_err().contains("does not read"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_s_edit_is_recorded_as_its_own_under_its_name() {
|
||||
let dir = std::env::temp_dir().join(format!("wai-quantum-mcp-history-{}", std::process::id()));
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ tauri = { version = "2", features = [] }
|
|||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||
# The assistant's model server, called by the app (a local one by default).
|
||||
ureq = { version = "3", features = ["json"] }
|
||||
wai-quantum = { version = "0.6.15", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt"] }
|
||||
wai-quantum = { version = "0.6.15", path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_toolchain"] }
|
||||
wai-quantum-cli = { version = "0.1.0", path = "../wai-quantum-cli", default-features = false }
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wai_quantum::ed25519_dalek::SigningKey;
|
||||
use wai_quantum::engine::discover::{Archive, Budget, Evaluator, SearchOptions, Spec, search};
|
||||
use wai_quantum::engine::{catalog, critique, draw, energy, equiv, job, locks, shortcuts, versions};
|
||||
use wai_quantum::engine::discover::{self, Archive, Budget, Evaluator, SearchOptions, Spec, search};
|
||||
use wai_quantum::engine::{catalog, critique, draw, energy, equiv, job, locks, novelty, shortcuts, versions};
|
||||
|
||||
/// What a result says of its energy here.
|
||||
const UNMETERED: &str = "the desktop app does not meter a run's energy";
|
||||
|
|
@ -212,6 +212,26 @@ fn locks(name: String, args: String) -> String {
|
|||
.unwrap_or_else(error_json)
|
||||
}
|
||||
|
||||
/// What is new in a program ([`novelty::call`]), its arguments as JSON: the
|
||||
/// answer, or `{error}`.
|
||||
#[tauri::command]
|
||||
fn novelty(args: String) -> String {
|
||||
serde_json::from_str::<Value>(&args)
|
||||
.map_err(|e| format!("the arguments are not JSON: {e}"))
|
||||
.and_then(|a| novelty::call(&a))
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(error_json)
|
||||
}
|
||||
|
||||
/// What is new in one entry of an archive ([`discover::novelty`]), or `{error}`.
|
||||
#[tauri::command]
|
||||
fn discover_novelty(spec: String, archive: String, id: String) -> String {
|
||||
evaluator(&spec)
|
||||
.and_then(|ev| read_archive(&ev, &archive).and_then(|a| discover::novelty(&ev, &a, &id)))
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(error_json)
|
||||
}
|
||||
|
||||
fn history_in(key: &SigningKey, name: &str, args: &str) -> String {
|
||||
serde_json::from_str::<Value>(args)
|
||||
.map_err(|e| format!("{name}: the arguments are not JSON: {e}"))
|
||||
|
|
@ -412,7 +432,7 @@ fn main() {
|
|||
};
|
||||
tauri::Builder::default()
|
||||
.manage(Studio { key, job_bytes: job_bytes(), running: Arc::new(Mutex::new(None)), cancelled: Arc::new(Mutex::new(false)) })
|
||||
.invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, critique, equivalent, variants, shortcuts, draw, layout, verify, signer, save_job, save_program, chat, models, discover_start, discover_submit, discover_search, discover_evaluate, instantiate, history, locks])
|
||||
.invoke_handler(tauri::generate_handler![run_job, cancel, plan_job, methods, lint, critique, equivalent, variants, shortcuts, draw, layout, verify, signer, save_job, save_program, chat, models, discover_start, discover_submit, discover_search, discover_evaluate, discover_novelty, instantiate, history, locks, novelty])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("the window opens");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ never creates information.
|
|||
| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` |
|
||||
| `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis |
|
||||
| `quantum_qasm` | **the OpenQASM reader the job service runs**, for every surface that takes a program: OpenQASM 3 and the OpenQASM 2 forms in common use, every register, a gate on a whole register, gate definitions with parameters, `ctrl`/`negctrl` modifiers, angle expressions, comments, and measurement in every spelling. A program that measures and then acts, resets after a gate or uses `if` is kept as dynamic steps. Generators: integer `input`s, `const`s and `for` loops (ranges, steps, sets, nested), with integer arithmetic in indices and sizes and `if` on integers decided on reading, so one program describes a family of circuits; `qasm::parse_with` reads it at given inputs and `qasm::instantiate` writes that circuit out as plain OpenQASM. `while`, `inv`, `pow` and anything else not listed are refused with their line. Lowered to each method's input exactly up to a global phase. `qasm::dyadic_circuit` gives the exact state vector's circuit (dyadic angles) for a program whose measurements all come at the end, and refuses any other with the reason |
|
||||
| `quantum_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`); the specification-and-evaluator workbench (`engine::discover`: a specification, the engine's verdict on a generator at every size, and an archive of the cheapest verified generator in every cell of two measures, sealed); a program's history, attributed (`engine::versions`: versions in a sealed chain, each with who made it and how, proposals reviewed change by change, and each line and gate in the hand that wrote it, which a job's receipt can bind); the person's rules for proposals (`engine::locks`: held blocks, the gates allowed, the qubit declarations, what to avoid and a region, checked change by change); and the CPU energy the kernel counts for one process (`engine::energy`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time |
|
||||
| `quantum_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`); the specification-and-evaluator workbench (`engine::discover`: a specification, the engine's verdict on a generator at every size, and an archive of the cheapest verified generator in every cell of two measures, sealed); a program's history, attributed (`engine::versions`: versions in a sealed chain, each with who made it and how, proposals reviewed change by change, and each line and gate in the hand that wrote it, which a job's receipt can bind); the person's rules for proposals (`engine::locks`: held blocks, the gates allowed, the qubit declarations, what to avoid and a region, checked change by change); what is new in a program (`engine::novelty`: its structural distance to the known constructions and to programs given, each known construction it is proved to be and whether by a different route, and the strengths a cost did not ask for; a workbench candidate that breaks only a rule is an aberration, with the cost it would have); and the CPU energy the kernel counts for one process (`engine::energy`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time |
|
||||
| `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector |
|
||||
| `quantum_mps` | tensor-network (matrix-product-state) backend |
|
||||
| `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) |
|
||||
|
|
|
|||
|
|
@ -213,11 +213,19 @@ pub struct Evaluation {
|
|||
/// The two axes at the largest size, when every size read.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub axes: Option<[u64; 2]>,
|
||||
/// An aberration (Wiggins's term): a candidate that breaks only the
|
||||
/// specification's rules (its gates, its connectivity) and would be
|
||||
/// verified without them: why (`rule`), which rule (`broke`: `{"kind":
|
||||
/// "gates", "gate"}` or `{"kind": "connectivity"}`), and the cost, axes
|
||||
/// and strength it would have. An invitation to revise the rules, never
|
||||
/// kept.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aberration: Option<Value>,
|
||||
}
|
||||
|
||||
impl Evaluation {
|
||||
fn failed(why: String, sizes: Vec<Value>) -> Evaluation {
|
||||
Evaluation { verified: false, strength: None, failure: Some(why), sizes, cost: None, axes: None }
|
||||
Evaluation { verified: false, strength: None, failure: Some(why), sizes, cost: None, axes: None, aberration: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -247,13 +255,15 @@ impl Evaluator {
|
|||
Ok(Evaluator { spec, sizes, allowed })
|
||||
}
|
||||
|
||||
/// The rules a candidate breaks at one size, if any.
|
||||
fn rules(&self, p: &Program) -> Option<String> {
|
||||
/// The rule a candidate breaks at one size, if any: which (`{"kind":
|
||||
/// "gates", "gate"}` or `{"kind": "connectivity"}`) and why.
|
||||
fn rules(&self, p: &Program) -> Option<(Value, String)> {
|
||||
if let Some(allowed) = &self.allowed {
|
||||
for op in &p.ops {
|
||||
let name = critique::kind_name(op.kind);
|
||||
if !allowed.contains(name) {
|
||||
return Some(format!("it uses `{name}`, which the specification does not allow (allowed: {})", allowed.iter().cloned().collect::<Vec<_>>().join(", ")));
|
||||
let why = format!("it uses `{name}`, which the specification does not allow (allowed: {})", allowed.iter().cloned().collect::<Vec<_>>().join(", "));
|
||||
return Some((json!({ "kind": "gates", "gate": name }), why));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -262,7 +272,8 @@ impl Evaluator {
|
|||
if op.qubits.len() >= 2 {
|
||||
let (lo, hi) = (op.qubits.iter().min().unwrap(), op.qubits.iter().max().unwrap());
|
||||
if (hi - lo) as usize != op.qubits.len() - 1 {
|
||||
return Some(format!("`{}` acts on qubits {:?}, which are not neighbours on the line", critique::kind_name(op.kind), op.qubits));
|
||||
let why = format!("`{}` acts on qubits {:?}, which are not neighbours on the line", critique::kind_name(op.kind), op.qubits);
|
||||
return Some((json!({ "kind": "connectivity" }), why));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -270,15 +281,28 @@ impl Evaluator {
|
|||
None
|
||||
}
|
||||
|
||||
/// The engine's verdict on `candidate`.
|
||||
/// The engine's verdict on `candidate`. One that breaks only the rules is
|
||||
/// judged again without them, and its verdict there is its aberration.
|
||||
pub fn evaluate(&self, candidate: &str) -> Evaluation {
|
||||
let (mut e, broke) = self.judge(candidate, true);
|
||||
if let Some(broke) = broke {
|
||||
let (free, _) = self.judge(candidate, false);
|
||||
if let (true, Some(cost), Some(axes)) = (free.verified, free.cost, free.axes) {
|
||||
e.aberration = Some(json!({ "rule": e.failure, "broke": broke, "cost": cost, "axes": axes, "strength": free.strength }));
|
||||
}
|
||||
}
|
||||
e
|
||||
}
|
||||
|
||||
/// The verdict, with the rule broken when a rule decided it.
|
||||
fn judge(&self, candidate: &str, rules: bool) -> (Evaluation, Option<Value>) {
|
||||
let declared: BTreeSet<String> = match qasm::inputs(candidate) {
|
||||
Ok(v) => v.into_iter().collect(),
|
||||
Err(e) => return Evaluation::failed(format!("it does not read: {e}"), Vec::new()),
|
||||
Err(e) => return (Evaluation::failed(format!("it does not read: {e}"), Vec::new()), None),
|
||||
};
|
||||
let given: BTreeSet<String> = self.spec.inputs.keys().cloned().collect();
|
||||
if declared != given {
|
||||
return Evaluation::failed(format!("it takes inputs {declared:?}, and the specification gives values for {given:?}"), Vec::new());
|
||||
return (Evaluation::failed(format!("it takes inputs {declared:?}, and the specification gives values for {given:?}"), Vec::new()), None);
|
||||
}
|
||||
let mut sizes = Vec::new();
|
||||
let mut measures = Vec::new();
|
||||
|
|
@ -286,10 +310,10 @@ impl Evaluator {
|
|||
for (ins, reference) in &self.sizes {
|
||||
let p = match qasm::parse_with(candidate, ins) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Evaluation::failed(format!("at {}: it does not read: {e}", describe(ins)), sizes),
|
||||
Err(e) => return (Evaluation::failed(format!("at {}: it does not read: {e}", describe(ins)), sizes), None),
|
||||
};
|
||||
if let Some(why) = self.rules(&p) {
|
||||
return Evaluation::failed(format!("at {}: {why}", describe(ins)), sizes);
|
||||
if let Some((broke, why)) = self.rules(&p).filter(|_| rules) {
|
||||
return (Evaluation::failed(format!("at {}: {why}", describe(ins)), sizes), Some(broke));
|
||||
}
|
||||
let verdict = match self.spec.task {
|
||||
Task::Equivalent => equiv::equivalent_programs(&p, reference),
|
||||
|
|
@ -301,7 +325,7 @@ impl Evaluator {
|
|||
sizes.push(json!({ "inputs": ins, "verdict": v, "how": how, "measures": m.clone() }));
|
||||
// The first size that fails decides: the larger ones are not run.
|
||||
if v != "equivalent" {
|
||||
return Evaluation::failed(format!("at {}: {v} ({how}): {}", describe(ins), verdict["why"].as_str().unwrap_or("")), sizes);
|
||||
return (Evaluation::failed(format!("at {}: {v} ({how}): {}", describe(ins), verdict["why"].as_str().unwrap_or("")), sizes), None);
|
||||
}
|
||||
if weakest.as_deref().is_none_or(|w| rank(&how) < rank(w)) {
|
||||
weakest = Some(how);
|
||||
|
|
@ -311,10 +335,26 @@ impl Evaluator {
|
|||
let cost: f64 = measures.iter().map(|m| self.spec.cost.iter().map(|(k, w)| w * m[k.as_str()].as_f64().unwrap_or(0.0)).sum::<f64>()).sum();
|
||||
let last = measures.last().expect("a specification checks at least one size");
|
||||
let axes = [last[self.spec.axes[0].as_str()].as_u64().unwrap_or(0), last[self.spec.axes[1].as_str()].as_u64().unwrap_or(0)];
|
||||
Evaluation { verified: true, strength: weakest, failure: None, sizes, cost: Some(cost), axes: Some(axes) }
|
||||
(Evaluation { verified: true, strength: weakest, failure: None, sizes, cost: Some(cost), axes: Some(axes), aberration: None }, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// What is new in entry `id` of `archive` ([`super::novelty::assess`]), read at
|
||||
/// the specification's largest size: its distance to the known constructions,
|
||||
/// the archive's other elites and the reference; each known construction it is
|
||||
/// found to be; and the measures it beats the reference on that the cost did
|
||||
/// not weigh.
|
||||
pub fn novelty(ev: &Evaluator, archive: &Archive, id: &str) -> Result<Value, String> {
|
||||
let e = archive.entries.iter().find(|x| x.id == id).ok_or_else(|| format!("{id}: no such entry"))?;
|
||||
let (ins, _) = ev.sizes.last().ok_or("the specification checks no size")?;
|
||||
let others: Vec<(String, String)> = archive.elites().into_iter().filter(|x| x.id != id).map(|x| (format!("{} ({})", x.id, x.proposer), x.source.clone())).collect();
|
||||
let weighed: Vec<String> = ev.spec.cost.keys().cloned().collect();
|
||||
let mut v = super::novelty::assess(&e.source, ins, &others, Some(&ev.spec.reference), &weighed);
|
||||
v["entry"] = json!(id);
|
||||
v["inputs"] = json!(ins);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// One generator the archive keeps.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Entry {
|
||||
|
|
@ -754,6 +794,41 @@ mod tests {
|
|||
assert!(ev.evaluate("OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[n];\n").failure.unwrap().contains("does not read"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_candidate_that_breaks_only_a_rule_is_an_aberration_with_its_cost() {
|
||||
let mut s = spec(Task::Prepares);
|
||||
s.connectivity = Connectivity::Line;
|
||||
let ev = Evaluator::new(s).unwrap();
|
||||
// The doubling tree joins far qubits, which the line forbids: it would
|
||||
// be verified, and cheaper than the chain, without that rule.
|
||||
let tree = ev.evaluate(TREE);
|
||||
assert!(!tree.verified && !tree.failure.as_deref().unwrap().contains('\u{1}'), "{tree:?}");
|
||||
let ab = tree.aberration.expect("an aberration");
|
||||
assert!(ab["rule"].as_str().unwrap().contains("not neighbours"), "{ab}");
|
||||
assert_eq!(ab["broke"], json!({ "kind": "connectivity" }), "{ab}");
|
||||
let chain = ev.evaluate(CHAIN).cost.unwrap();
|
||||
assert!(ab["cost"].as_f64().unwrap() < chain, "{ab} against the chain's {chain}");
|
||||
// A wrong candidate is not an aberration, whatever rule it breaks.
|
||||
let wrong = TREE.replace("h q[0];\n", "");
|
||||
assert!(ev.evaluate(&wrong).aberration.is_none());
|
||||
// Nor is a verified one.
|
||||
assert!(ev.evaluate(CHAIN).aberration.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_entry_s_novelty_names_its_nearest_and_what_it_rediscovers() {
|
||||
let ev = Evaluator::new(spec(Task::Prepares)).unwrap();
|
||||
let mut a = Archive::new(&ev).unwrap();
|
||||
let out = a.submit(&ev, TREE, Some("e0"), "person", None);
|
||||
let id = out.kept.unwrap();
|
||||
let v = novelty(&ev, &a, &id).unwrap();
|
||||
let found: Vec<&str> = v["rediscovers"].as_array().unwrap().iter().map(|r| r["name"].as_str().unwrap()).collect();
|
||||
assert!(found.contains(&"GHZ, by doubling"), "{v}");
|
||||
assert_eq!(v["nearest"]["distance"], 0.0, "the tree is the doubling construction itself: {v}");
|
||||
assert!(v["unasked"].is_array(), "{v}");
|
||||
assert!(novelty(&ev, &a, "e9").unwrap_err().contains("no such entry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_specification_says_what_it_lacks() {
|
||||
let bad = |patch: Value| {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@
|
|||
//! proposal may change (blocks held between `// @lock` and `// @unlock`,
|
||||
//! the gates allowed, the qubit declarations, gates and text to avoid, the
|
||||
//! region asked for), checked change by change.
|
||||
//! - [`novelty`] (with `quantum_receipt`): what is new in a circuit. Its
|
||||
//! structural distance to an inspiring set (the known constructions and the
|
||||
//! caller's own circuits), each known construction the engine proves it to
|
||||
//! be found again (and whether by a different route), and the strengths a
|
||||
//! cost did not ask for.
|
||||
//! - [`versions`] (with `quantum_receipt`): a program's history, attributed.
|
||||
//! Every version names its parents, who made it (the person, a model, an
|
||||
//! outside agent, the engine) and how (an edit, a proposal reviewed change
|
||||
|
|
@ -62,6 +67,8 @@ pub mod job;
|
|||
#[cfg(feature = "quantum_receipt")]
|
||||
pub mod locks;
|
||||
#[cfg(feature = "quantum_receipt")]
|
||||
pub mod novelty;
|
||||
#[cfg(feature = "quantum_receipt")]
|
||||
pub mod receipt;
|
||||
pub mod run;
|
||||
pub mod shortcuts;
|
||||
|
|
|
|||
283
wai/wai-quantum/src/engine/novelty.rs
Normal file
283
wai/wai-quantum/src/engine/novelty.rs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
//! What is new in a circuit, and what is a known construction found again.
|
||||
//!
|
||||
//! - **The inspiring set** (Ritchie's term): the known constructions
|
||||
//! ([`known`]: the GHZ preparations by a chain, by doubling and by fan-out,
|
||||
//! the uniform superposition, and the toolchain's quantum Fourier
|
||||
//! transform), at the candidate's width, and whatever the caller adds (the
|
||||
//! person's own versions, an archive's entries).
|
||||
//! - **Distance** ([`distance`]) is structural. Each gate is a token, its kind
|
||||
//! and its qubits relative to its first, and two circuits are compared as
|
||||
//! multisets of those tokens and of the pairs of consecutive ones, by a
|
||||
//! weighted Jaccard distance: 0 when they are built alike, 1 when they share
|
||||
//! nothing. It says nothing about what a circuit does; the engine's
|
||||
//! equivalence check says that.
|
||||
//! - **Rediscovery**: a circuit the engine proves to do what a known
|
||||
//! construction does (the same state from `|0…0⟩` for a preparation, the
|
||||
//! same unitary otherwise) is that construction found again. Built unlike it
|
||||
//! (distance at least [`DIFFERENT_ROUTE`]), it is a different route to a
|
||||
//! known result: the case worth a second look. A circuit that is itself
|
||||
//! one known construction is no news for being the same as another: those
|
||||
//! matches are another known construction, not a different route.
|
||||
//! - **Novelty** is the distance to the nearest member of the inspiring set,
|
||||
//! which is named.
|
||||
//! - **Strengths not asked for**: the measures where a candidate beats its
|
||||
//! reference that the cost did not weigh.
|
||||
//!
|
||||
//! The distance and the labels are the engine's, and they are advisory: a
|
||||
//! high novelty is a reason to look, not a claim that nobody has built it.
|
||||
|
||||
use super::{critique, equiv};
|
||||
use crate::qasm::{self, Inputs, Program};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// The distance at which a rediscovery counts as a different route.
|
||||
pub const DIFFERENT_ROUTE: f64 = 0.5;
|
||||
|
||||
/// The widest circuit the known constructions are built for.
|
||||
pub const MAX_KNOWN_QUBITS: u32 = 12;
|
||||
|
||||
/// A known construction: its name, whether it is a state preparation (judged
|
||||
/// by the state it makes from `|0…0⟩`) or a unitary, and its program at one
|
||||
/// width.
|
||||
pub struct Known {
|
||||
pub name: &'static str,
|
||||
pub prepares: bool,
|
||||
pub program: Program,
|
||||
}
|
||||
|
||||
const GHZ_CHAIN: &str = "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 GHZ_DOUBLING: &str = "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 GHZ_FANOUT: &str = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [1:n-1] {\n cx q[0], q[i];\n}\n";
|
||||
const UNIFORM: &str = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nfor uint i in [0:n-1] {\n h q[i];\n}\n";
|
||||
|
||||
/// The known constructions at width `n`.
|
||||
pub fn known(n: u32) -> Vec<Known> {
|
||||
let mut out = Vec::new();
|
||||
if n == 0 || n > MAX_KNOWN_QUBITS {
|
||||
return out;
|
||||
}
|
||||
let at = Inputs::from([("n".to_string(), i64::from(n))]);
|
||||
let mut add = |name, prepares, src: &str| {
|
||||
if let Ok(program) = qasm::parse_with(src, &at) {
|
||||
out.push(Known { name, prepares, program });
|
||||
}
|
||||
};
|
||||
if n >= 2 {
|
||||
add("GHZ, by a chain of CNOTs", true, GHZ_CHAIN);
|
||||
if n >= 4 {
|
||||
add("GHZ, by doubling", true, GHZ_DOUBLING);
|
||||
add("GHZ, by fan-out from one qubit", true, GHZ_FANOUT);
|
||||
}
|
||||
}
|
||||
add("the uniform superposition", true, UNIFORM);
|
||||
#[cfg(feature = "quantum_toolchain")]
|
||||
if let Some(c) = crate::quantum_toolchain::build_algorithm("qft", n as u8, 0)
|
||||
&& let Ok(program) = qasm::parse(&crate::quantum_toolchain::to_qasm(&c))
|
||||
{
|
||||
out.push(Known { name: "the quantum Fourier transform", prepares: false, program });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A gate as a token: its kind and its qubits relative to its first.
|
||||
fn token(op: &qasm::Op) -> String {
|
||||
let first = op.qubits.first().copied().unwrap_or(0) as i64;
|
||||
let rel: Vec<String> = op.qubits.iter().map(|&q| (q as i64 - first).to_string()).collect();
|
||||
format!("{}:{}", critique::kind_name(op.kind), rel.join(","))
|
||||
}
|
||||
|
||||
fn bag(p: &Program) -> BTreeMap<String, u64> {
|
||||
let toks: Vec<String> = p.ops.iter().map(token).collect();
|
||||
let mut m = BTreeMap::new();
|
||||
for t in &toks {
|
||||
*m.entry(t.clone()).or_default() += 1;
|
||||
}
|
||||
for w in toks.windows(2) {
|
||||
*m.entry(format!("{} {}", w[0], w[1])).or_default() += 1;
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// The structural distance between two circuits, 0 to 1: the weighted
|
||||
/// Jaccard distance between their multisets of gate tokens and consecutive
|
||||
/// pairs of them.
|
||||
pub fn distance(a: &Program, b: &Program) -> f64 {
|
||||
let (x, y) = (bag(a), bag(b));
|
||||
let (mut both, mut either) = (0u64, 0u64);
|
||||
for k in x.keys().chain(y.keys().filter(|k| !x.contains_key(*k))) {
|
||||
let (p, q) = (x.get(k).copied().unwrap_or(0), y.get(k).copied().unwrap_or(0));
|
||||
both += p.min(q);
|
||||
either += p.max(q);
|
||||
}
|
||||
if either == 0 { 0.0 } else { 1.0 - both as f64 / either as f64 }
|
||||
}
|
||||
|
||||
fn rounded(x: f64) -> f64 {
|
||||
(x * 1000.0).round() / 1000.0
|
||||
}
|
||||
|
||||
/// What is new in `candidate` (read at `inputs`): its nearest known
|
||||
/// construction or other circuit and the distance to it, every known
|
||||
/// construction it is found to be (and whether by a different route), and,
|
||||
/// given a `reference`, the measures it beats the reference on that the cost
|
||||
/// (`weighed`) did not weigh. `others` are more of the inspiring set, as
|
||||
/// `(name, program text)`, each read at the same inputs.
|
||||
pub fn assess(candidate: &str, inputs: &Inputs, others: &[(String, String)], reference: Option<&str>, weighed: &[String]) -> Value {
|
||||
let p = match qasm::parse_with(candidate, inputs) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return json!({ "ok": false, "error": format!("the candidate does not read: {e}") }),
|
||||
};
|
||||
let mut nearest: Option<(String, f64)> = None;
|
||||
let mut consider = |name: &str, q: &Program| {
|
||||
let d = distance(&p, q);
|
||||
if nearest.as_ref().is_none_or(|(_, best)| d < *best) {
|
||||
nearest = Some((name.to_string(), d));
|
||||
}
|
||||
d
|
||||
};
|
||||
let mut rediscovers = Vec::new();
|
||||
let ks = known(p.n_qubits);
|
||||
let mut compared = ks.len();
|
||||
let mut same_as_one = false;
|
||||
for k in &ks {
|
||||
let d = consider(k.name, &k.program);
|
||||
if p.dynamic.is_some() || k.program.n_qubits != p.n_qubits {
|
||||
continue;
|
||||
}
|
||||
let v = if k.prepares { equiv::same_state(&p, &k.program) } else { equiv::equivalent_programs(&p, &k.program) };
|
||||
if v["verdict"] == "equivalent" && v["how"] != "random" {
|
||||
let route = if d >= DIFFERENT_ROUTE { "a different route" } else { "the same construction" };
|
||||
same_as_one |= route == "the same construction";
|
||||
rediscovers.push(json!({ "name": k.name, "route": route, "distance": rounded(d), "how": v["how"], "prepares": k.prepares }));
|
||||
}
|
||||
}
|
||||
if same_as_one {
|
||||
for r in rediscovers.iter_mut().filter(|r| r["route"] == "a different route") {
|
||||
r["route"] = json!("another known construction");
|
||||
}
|
||||
}
|
||||
for (name, src) in others {
|
||||
if let Ok(q) = qasm::parse_with(src, inputs) {
|
||||
consider(name, &q);
|
||||
compared += 1;
|
||||
}
|
||||
}
|
||||
let mut out = json!({
|
||||
"ok": true,
|
||||
"qubits": p.n_qubits,
|
||||
"compared": compared,
|
||||
"nearest": nearest.as_ref().map(|(n, d)| json!({ "name": n, "distance": rounded(*d) })),
|
||||
"novelty": nearest.as_ref().map(|(_, d)| rounded(*d)),
|
||||
"rediscovers": rediscovers,
|
||||
});
|
||||
if let Some(r) = reference.and_then(|r| qasm::parse_with(r, inputs).ok()) {
|
||||
let (mine, theirs) = (critique::measures(&p), critique::measures(&r));
|
||||
let strengths: Vec<Value> = super::discover::MEASURES
|
||||
.iter()
|
||||
.filter(|m| !weighed.iter().any(|w| w == *m))
|
||||
.filter_map(|m| {
|
||||
let (a, b) = (mine[*m].as_f64()?, theirs[*m].as_f64()?);
|
||||
(a < b).then(|| json!({ "measure": m, "candidate": a, "reference": b }))
|
||||
})
|
||||
.collect();
|
||||
out["unasked"] = json!(strengths);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// [`assess`] as a page, an app or an agent calls it, with JSON arguments:
|
||||
/// `qasm`, and `inputs`, `against` (`[{name, qasm}]`), `reference` and
|
||||
/// `weighed` as [`assess`] takes them.
|
||||
pub fn call(args: &Value) -> Result<Value, String> {
|
||||
let src = args.get("qasm").and_then(Value::as_str).ok_or("give \"qasm\": the program")?;
|
||||
let inputs: Inputs = match args.get("inputs") {
|
||||
None | Some(Value::Null) => Inputs::new(),
|
||||
Some(v) => serde_json::from_value(v.clone()).map_err(|e| format!("inputs: {e}"))?,
|
||||
};
|
||||
let strings = |k: &str| args.get(k).and_then(Value::as_array).into_iter().flatten();
|
||||
let others: Vec<(String, String)> = strings("against").filter_map(|o| Some((o.get("name")?.as_str()?.to_string(), o.get("qasm")?.as_str()?.to_string()))).collect();
|
||||
let weighed: Vec<String> = strings("weighed").filter_map(|w| w.as_str().map(str::to_string)).collect();
|
||||
let v = assess(src, &inputs, &others, args.get("reference").and_then(Value::as_str), &weighed);
|
||||
if v["ok"] == true { Ok(v) } else { Err(v["error"].as_str().unwrap_or("the program does not read").to_string()) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn at(n: i64) -> Inputs {
|
||||
Inputs::from([("n".to_string(), n)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_is_zero_for_the_same_build_and_grows_with_the_difference() {
|
||||
let chain = qasm::parse_with(GHZ_CHAIN, &at(6)).unwrap();
|
||||
let doubling = qasm::parse_with(GHZ_DOUBLING, &at(6)).unwrap();
|
||||
let fanout = qasm::parse_with(GHZ_FANOUT, &at(6)).unwrap();
|
||||
assert_eq!(distance(&chain, &chain), 0.0);
|
||||
let (cd, cf) = (distance(&chain, &doubling), distance(&chain, &fanout));
|
||||
assert!(cd > 0.3 && cf > 0.3 && cd <= 1.0, "chain to doubling {cd}, to fan-out {cf}");
|
||||
assert!((distance(&doubling, &chain) - cd).abs() < 1e-12, "the distance is symmetric");
|
||||
let empty = qasm::parse("OPENQASM 3.0;\nqubit[6] q;\n").unwrap();
|
||||
assert_eq!(distance(&empty, &empty), 0.0);
|
||||
assert_eq!(distance(&empty, &chain), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_known_construction_is_found_again_and_named() {
|
||||
let v = assess(GHZ_CHAIN, &at(5), &[], None, &[]);
|
||||
let r = &v["rediscovers"];
|
||||
assert!(r.as_array().unwrap().iter().any(|x| x["name"] == "GHZ, by a chain of CNOTs" && x["route"] == "the same construction"), "{v}");
|
||||
assert_eq!(v["nearest"]["distance"], 0.0, "{v}");
|
||||
assert_eq!(v["novelty"], 0.0);
|
||||
// Being the same state as the other GHZ builds is no news for the chain.
|
||||
assert!(r.as_array().unwrap().iter().all(|x| x["route"] != "a different route"), "{v}");
|
||||
assert!(r.as_array().unwrap().iter().any(|x| x["route"] == "another known construction"), "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_build_of_a_known_state_is_a_different_route() {
|
||||
// GHZ by a ladder from the far end: the same state, a different build.
|
||||
let ladder = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[n - 1];\nfor uint i in [1:n-1] {\n cx q[n - i], q[n - i - 1];\n}\n";
|
||||
let v = assess(ladder, &at(6), &[], None, &[]);
|
||||
let ghz: Vec<&Value> = v["rediscovers"].as_array().unwrap().iter().filter(|x| x["name"].as_str().unwrap().starts_with("GHZ")).collect();
|
||||
assert_eq!(ghz.len(), 3, "the same state as every GHZ construction: {v}");
|
||||
assert!(ghz.iter().all(|x| x["how"] == "state"), "{v}");
|
||||
assert!(ghz.iter().any(|x| x["route"] == "a different route"), "{v}");
|
||||
assert!(v["novelty"].as_f64().unwrap() > 0.3, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_circuit_that_is_none_of_them_rediscovers_nothing() {
|
||||
let other = "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\nt q[0];\ncx q[0], q[2];\nrz(0.3) q[1];\n";
|
||||
let v = assess(other, &Inputs::new(), &[], None, &[]);
|
||||
assert!(v["rediscovers"].as_array().unwrap().is_empty(), "{v}");
|
||||
assert!(v["nearest"]["name"].is_string() && v["novelty"].as_f64().unwrap() > 0.0, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_inspiring_set_takes_the_callers_own_circuits() {
|
||||
let mine = "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\nt q[0];\ncx q[0], q[2];\nrz(0.3) q[1];\n";
|
||||
let v = assess(mine, &Inputs::new(), &[("v4".into(), mine.into())], None, &[]);
|
||||
assert_eq!((v["nearest"]["name"].as_str(), v["novelty"].as_f64()), (Some("v4"), Some(0.0)), "{v}");
|
||||
assert_eq!(v["compared"], known(3).len() + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strengths_not_asked_for_are_those_the_cost_did_not_weigh() {
|
||||
let reference = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nt q[0];\nt q[0];\ncx q[0], q[1];\n";
|
||||
let candidate = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ns q[0];\ncx q[0], q[1];\n";
|
||||
let v = assess(candidate, &Inputs::new(), &[], Some(reference), &["depth".into()]);
|
||||
let unasked: Vec<&str> = v["unasked"].as_array().unwrap().iter().map(|s| s["measure"].as_str().unwrap()).collect();
|
||||
assert!(unasked.contains(&"t_count") && unasked.contains(&"gates") && !unasked.contains(&"depth"), "{v}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_toolchain")]
|
||||
#[test]
|
||||
fn the_toolchain_s_fourier_transform_is_known() {
|
||||
let c = crate::quantum_toolchain::build_algorithm("qft", 4, 0).unwrap();
|
||||
let v = assess(&crate::quantum_toolchain::to_qasm(&c), &Inputs::new(), &[], None, &[]);
|
||||
assert!(v["rediscovers"].as_array().unwrap().iter().any(|x| x["name"] == "the quantum Fourier transform"), "{v}");
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use serde_json::{Value, json};
|
||||
use wai::engine::discover::{Archive, Budget, Evaluator, SearchOptions, Spec, search};
|
||||
use wai::engine::{catalog, critique, draw, equiv, job, locks, shortcuts, versions};
|
||||
use wai::engine::discover::{self, Archive, Budget, Evaluator, SearchOptions, Spec, search};
|
||||
use wai::engine::{catalog, critique, draw, equiv, job, locks, novelty, shortcuts, versions};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// What a result run in a tab says of its energy.
|
||||
|
|
@ -266,6 +266,29 @@ pub fn engine_locks(op: &str, args: &str) -> String {
|
|||
.unwrap_or_else(error_json)
|
||||
}
|
||||
|
||||
/// What is new in a program ([`novelty::call`]), its arguments as JSON
|
||||
/// (`qasm`, and `inputs`, `against`, `reference`, `weighed`): its nearest
|
||||
/// known construction or other program, each known construction it is found
|
||||
/// to be, and whether by a different route; or `{error}`.
|
||||
#[wasm_bindgen]
|
||||
pub fn engine_novelty(args: &str) -> String {
|
||||
serde_json::from_str::<Value>(args)
|
||||
.map_err(|e| format!("the arguments are not JSON: {e}"))
|
||||
.and_then(|a| novelty::call(&a))
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(error_json)
|
||||
}
|
||||
|
||||
/// What is new in the entry `id` of an archive for `spec`, read at the
|
||||
/// specification's largest size ([`discover::novelty`]), or `{error}`.
|
||||
#[wasm_bindgen]
|
||||
pub fn engine_discover_novelty(spec: &str, archive_json: &str, id: &str) -> String {
|
||||
evaluator(spec)
|
||||
.and_then(|ev| archive(&ev, archive_json).and_then(|a| discover::novelty(&ev, &a, id)))
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(error_json)
|
||||
}
|
||||
|
||||
/// A generator written out at `inputs` (JSON, as `{"n": 5}`) as plain
|
||||
/// OpenQASM: `{text}`, or `{error}`.
|
||||
#[wasm_bindgen]
|
||||
|
|
@ -339,6 +362,27 @@ mod tests {
|
|||
assert!(engine_locks("check", "not json").contains("not JSON"));
|
||||
}
|
||||
|
||||
/// What is new in the page: a reversed chain is the GHZ state by a
|
||||
/// different route; an archive's entry is read against the others.
|
||||
#[test]
|
||||
fn novelty_is_read_in_the_page() {
|
||||
let ladder = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[n - 1];\nfor uint i in [1:n-1] { cx q[n - i], q[n - i - 1]; }\n";
|
||||
let v: Value = serde_json::from_str(&engine_novelty(&json!({ "qasm": ladder, "inputs": { "n": 5 } }).to_string())).unwrap();
|
||||
assert!(v["rediscovers"].as_array().unwrap().iter().any(|r| r["route"] == "a different route"), "{v}");
|
||||
assert!(engine_novelty("{\"qasm\": \"qubit[1] q; foo q[0];\"}").contains("does not read"));
|
||||
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
|
||||
let spec = json!({ "name": "ghz", "task": "prepares", "reference": CHAIN, "inputs": { "n": [3, 5] }, "cost": { "depth": 1.0 }, "axes": ["two_qubit", "depth"] }).to_string();
|
||||
let started: Value = serde_json::from_str(&e.discover_start(&spec)).unwrap();
|
||||
let doubling = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint k in [0:n] { for uint i in [0:2**k - 1] { if (2**k < n && i + 2**k < n) { cx q[i], q[i + 2**k]; } } }\n";
|
||||
let sub: Value = serde_json::from_str(&e.discover_submit(&spec, &started["archive"].to_string(), doubling, Some("e0".into()), "person", None)).unwrap();
|
||||
let id = sub["outcome"]["kept"].as_str().expect("the doubling is kept");
|
||||
let n: Value = serde_json::from_str(&engine_discover_novelty(&spec, &sub["archive"].to_string(), id)).unwrap();
|
||||
assert_eq!(n["entry"], id, "{n}");
|
||||
assert!(n["rediscovers"].as_array().unwrap().iter().any(|r| r["name"] == "GHZ, by doubling" && r["route"] == "the same construction"), "{n}");
|
||||
assert!(n["unasked"].is_array(), "{n}");
|
||||
assert!(engine_discover_novelty(&spec, &sub["archive"].to_string(), "e99").contains("no such entry"));
|
||||
}
|
||||
|
||||
fn bell(extra: &str) -> String {
|
||||
format!("{{\"qasm\": \"OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];\"{extra}}}")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue