Contour Gallery
Reveal the topological shape (clusters, loops, voids) hiding inside client galleries so photographers read structure at a glance.
The hook.
Client galleries gets fed into a quantum shape-finder; photographers see clusters, loops, and gaps drawn on top of their work instead of having to spot structure by eye.
Why this primitiveQTDA is the right primitive here because client galleries reduces to a shape of data problem; the kernel returns a result you can drop straight into the UI.
The mega-prompt.
This prompt is engineered to ship in a single Lovable build. Real Quantinuum Guppy/Selene circuit runs in the Linux sandbox at build time and the results are baked in as JSON. read the build strategy โ
Build a one-page web app called "Contour Gallery" for photographers, photo editors, photojournalists.
Headline: Reveal the topological shape (clusters, loops, voids) hiding inside client galleries so photographers read structure at a glance.
CONSTRAINTS (read first):
- I am on Lovable's FREE plan with ~5 credits. Build this in ONE message. Do NOT add features beyond what is listed.
- No accounts, no Lovable Cloud, no database, no auth. Static app only.
- One page: a workspace. Plus a small "About the quantum" strip at the bottom.
REAL QUANTUM (Quantinuum Guppy + Selene) โ run AT BUILD TIME in the Lovable Linux sandbox, not at runtime:
1. In the sandbox, run: pip install "guppylang>=1.0"
2. Create quantum/kernel.py containing a Quantum Topological Data Analysis pipeline returning Betti numbers (b0, b1, b2) from a Vietoris-Rips filtration. It MUST be a real .py file on disk (Guppy reads source via inspect.getsource โ REPL/exec strings fail).
3. Create quantum/run.py that imports the @guppy kernel, compiles it, and runs it on the Selene emulator (โค8 qubits, 256 shots) over a single dataset of 32 points across 8 filtration radii = 8 Betti triples. Write the real results to src/data/quantum-results.json with shape: an array of { input, output } records where output contains Betti numbers (b0, b1, b2).
4. Execute quantum/run.py ONCE during the build so src/data/quantum-results.json is committed real Selene output. Do not call Python at runtime โ Cloudflare Workers cannot run Python.
FRONTEND (TanStack Start, single route /):
- Import src/data/quantum-results.json statically.
- Workspace UI: left = input picker tied to the precomputed grid (client galleries); center = live visual built from the selected record; right = a 'shape signature' visualization with connected-component, loop, and void counts driven by the JSON.
- "Quantum trace" disclosure: show the raw Betti numbers (b0, b1, b2) for the active record and a "View Guppy source" toggle that prints the contents of quantum/kernel.py inline, so judges see this is genuine QTDA on Selene, not a fake.
- Design: dark workspace, ONE accent color matched to the Photography mood, ONE typeface family, tactile micro-interaction when the user picks a new record.
Deliverable: a single deploy where every number on screen traces back to a real Selene shot from quantum/kernel.py. Ship in one message.
--- QUANTINUUM APPENDIX (self-contained reference โ paste verbatim) ---
[1] INSTALL (Lovable Linux sandbox, Python >= 3.12):
python -m pip install --no-cache-dir "guppylang>=1.0" numpy
# Guppy v1 is a BREAKING release: the Selene emulator now ships INSIDE guppylang.
# Do NOT pip-install the old standalone emulator package, and do NOT use pre-v1 syntax.
[2] IMPORTS (Guppy v1 โ use exactly these, no other quantum libs):
# quantum/kernel.py and quantum/run.py top
from guppylang import guppy, OptimizationLevel
from guppylang.std.builtins import array, output # `output`, NOT `result`
from guppylang.std.quantum import qubit, h, cx, rx, ry, rz, measure, measure_array, discard, t as tgate, tdg
from guppylang.std.angles import angle, pi
from selene_sim import Quest # error models still live here
from selene_sim.error_models import (
IdealErrorModel, DepolarizingErrorModel, SimpleLeakageErrorModel,
)
import math, json, sys, tempfile, importlib.util, uuid
from pathlib import Path
[3] HARD RULES (violating any breaks the build):
- GUPPY v1: use output("tag", v) โ `result()` no longer exists. measure(q) returns a
Measurement object; call .read() before using it in output(...) or an `if`.
Run via my_kernel.emulator(...).with_shots(S).run() โ the pre-v1 build() helper is gone.
- @guppy reads source via inspect.getsource โ kernels MUST live in a real .py file on disk. No exec(), no REPL strings, no inline templates.
- Allowed gate set ONLY: h, rx, ry, rz, cx, tgate, tdg. There is NO native ccx/toffoli, cswap, cphase, or crz โ decompose using the snippets in [7].
- Qubit ownership: a qubit passed to a function is moved. You MUST measure() or discard() every qubit exactly once; never reuse after measure.
- Angle hygiene before baking a float into generated source:
theta = ((theta + math.pi) % (2.0 * math.pi)) - math.pi
and write it with repr: f"... {theta!r} ..." (str(float) can truncate).
[4] SELENE SHOT LOOP (Guppy v1 emulator builder โ the ONLY correct form):
res = (
my_kernel # the @guppy program itself, NOT .compile()
.emulator(n_qubits=N)
.with_shots(S)
.with_seed(7)
.with_simulator(Quest())
.with_error_model(IdealErrorModel())
.run()
)
shots = []
for shot in res: # EmulatorResult is iterable
shots.append({str(tag): int(v) for tag, v in shot.entries})
# N = MAX number of qubits simultaneously LIVE in the kernel.
# measure(q) releases the slot, so one ancilla reused across k windows still counts as 1.
# The pre-v1 build(prog.compile()) + shot-loop helper NO LONGER EXISTS.
# Other builder methods: with_n_processes, with_timeout, with_verbose, with_progress_bar.
# ALWAYS pin .with_seed(<int>) โ an unseeded run is not reproducible evidence.
# If you report GATE COUNTS, also pin program.with_opt_level(OptimizationLevel.Classical);
# v1 optimises on compile and will otherwise flatter your numbers.
[5] DRIVER PATTERN โ sweep a kernel over many inputs (closures do NOT work):
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT))
def run_one(params: dict, shots: int = 256):
# Bake params as literals into a fresh .py file that imports your kernel helpers.
src = (
"from quantum.kernel import guppy, my_helper\n"
"@guppy\n"
"def program() -> None:\n"
f" my_helper({params['a']!r}, {params['b']!r})\n"
)
tmp = Path(tempfile.gettempdir()) / "qprogs"; tmp.mkdir(exist_ok=True)
name = f"prog_{uuid.uuid4().hex[:8]}"
path = tmp / f"{name}.py"; path.write_text(src)
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod # register BEFORE exec_module
spec.loader.exec_module(mod)
res = (
mod.program
.emulator(n_qubits=5)
.with_shots(shots)
.with_simulator(Quest())
.run()
)
out = []
for shot in res:
out.append({str(l): int(v) for l, v in shot.entries})
return out
[6] PER-QUBIT INTEGER DECODE (host-side):
# kernel emits: for j in range(n): output(f"x{j}", measure(q[j]).read())
# measure() returns a Measurement in v1 โ .read() gives the bool/bit.
# Compiler error if you forget: "Values of type 'Measurement' cannot be passed to 'output' directly".
def decode(rec, n):
x = 0
for j in range(n): x |= (rec.get(f"x{j}", 0) & 1) << j
return x
[7] DECOMPOSITION LIBRARY (copy verbatim into quantum/kernel.py):
# ---- Toffoli (CCX) from H, CX, T, Tdg โ 6-T standard decomposition ----
@guppy
def toffoli(c1: qubit, c2: qubit, tgt: qubit) -> None:
h(tgt)
cx(c2, tgt); tdg(tgt)
cx(c1, tgt); tgate(tgt)
cx(c2, tgt); tdg(tgt)
cx(c1, tgt); tgate(c2); tgate(tgt)
h(tgt)
cx(c1, c2); tgate(c1); tdg(c2)
cx(c1, c2)
# ---- CSWAP (Fredkin) from CX + Toffoli ----
@guppy
def cswap(c: qubit, a: qubit, b: qubit) -> None:
cx(b, a)
toffoli(c, a, b)
cx(b, a)
# ---- Controlled phase exp(i*theta) on |11> from rz + cx ----
@guppy
def cphase(c: qubit, d: qubit, theta: float) -> None:
rz(d, angle(theta / 2.0))
cx(c, d)
rz(d, angle(-theta / 2.0))
cx(c, d)
# ---- Amplitude-encoded feature state (3 floats in [0,1] โ 2-qubit state) ----
@guppy
def prep_features(q0: qubit, q1: qubit, a: float, b: float, c: float) -> None:
ry(q0, angle(a))
ry(q1, angle(b))
cx(q0, q1)
rz(q1, angle(c))
# ---- SWAP test kernel; HOST inverts: F = clamp(2*P(anc=0) - 1, 0, 1) ----
@guppy
def swap_test(ai: float, bi: float, ci: float,
aj: float, bj: float, cj: float) -> None:
anc = qubit()
pi0 = qubit(); pi1 = qubit()
pj0 = qubit(); pj1 = qubit()
prep_features(pi0, pi1, ai, bi, ci)
prep_features(pj0, pj1, aj, bj, cj)
h(anc)
cswap(anc, pi0, pj0)
cswap(anc, pi1, pj1)
h(anc)
output("anc", measure(anc).read())
discard(pi0); discard(pi1); discard(pj0); discard(pj1)
# n_qubits = 5 for swap_test above.
[8] CLASSICAL CROSS-CHECK (NumPy reference โ commit alongside quantum result):
import numpy as np
I = np.eye(2); X = np.array([[0,1],[1,0]])
def RY(t): c,s = math.cos(t/2), math.sin(t/2); return np.array([[c,-s],[s,c]])
def RZ(t): return np.array([[np.exp(-1j*t/2),0],[0,np.exp(1j*t/2)]])
CX = np.array([[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]])
def prep_psi(a,b,c):
s = np.kron(RY(a)@np.array([1,0]), RY(b)@np.array([1,0]))
s = CX @ s
s = np.kron(I, RZ(c)) @ s
return s
def classical_fidelity(p, q):
return float(abs(np.vdot(prep_psi(*p), prep_psi(*q)))**2)
# Commit BOTH quantum and classical values per record:
# {"input": [...], "quantum": 0.873, "classical": 0.881, "shots": 256}
[9] FRONTEND HANDOFF (TanStack Start):
# quantum/run.py last step:
Path("src/data/quantum-results.json").write_text(json.dumps({
"records": records, # list of {input, quantum, classical, ...}
"circuit": {"n_qubits": 5, "shots": 256},
"kernel_source": Path("quantum/kernel.py").read_text(), # show on /code tab
}, indent=2))
# In React: import data from "@/data/quantum-results.json" (resolveJsonModule:true).
# Render data.records; render data.kernel_source in a <pre> inside the Quantum trace tab.
[10] BUILD ORDER (do not deviate โ keeps you under 5 credits):
1. Write quantum/kernel.py with the helpers from [7] you actually need.
2. Write quantum/run.py: driver pattern from [5], Selene loop from [4], cross-check from [8], JSON write from [9].
3. Execute once: python quantum/run.py (commit src/data/quantum-results.json).
4. Build the React route: static JSON import, shadcn Card + Tabs + Slider + Badge wiring.
5. Deploy. No Python at runtime; every on-screen number traces to committed Selene output.
[11] OUTPUT CONTRACT โ selene_run v1 (write this exact shape to src/data/quantum-results.json):
{
"schemaVersion": 1,
"experiment": "<idea-slug>",
"title": "<idea title>",
"description": "<one plain-English sentence โ what a non-quantum dev sees>",
"kernel": { "snippet": "<first 40 lines of quantum/kernel.py>", "qubits": <int>, "shotsPerRow": <int> },
"verdict": { "text": "<one-sentence headline of the result>", "good": true },
"metrics": [ { "name": "<label>", "value": <number>, "unit": "<%|bits|Hz|...>", "good": true } ],
"series": [ { "id": "<id>", "kind": "histogram"|"bar"|"line", "title": "...",
"xLabel": "...", "yLabel": "...", "yKeys": ["count"],
"points": [ { "label": "<x>", "values": { "count": <num> } } ] } ]
}
Also include provenance so the result is checkable:
"extras": { "shots": <int>, "seed": <int>, "nQubits": <int>,
"errorModel": "IdealErrorModel"|"DepolarizingErrorModel(...)",
"guppy": ">=1.0", "generatedAt": "<ISO date>",
"bell_anticorrelated": <int> }
Plus a top-level "receipt" object exactly as specified in [18], carrying
receipt.grade from [21]. Ship src/data/experiment-spec.json ([20]) and
src/data/circuit-audit.json ([21]) alongside this file and render both.
Also ship src/data/dataset-triage.json ([22]) and src/data/manifest.json ([23]),
and carry top-level "mechanism" and "performance" objects exactly as in [23].
NO ESCAPE HATCH: extras is for non-rendered metadata ONLY. Anything the user is
meant to SEE belongs in metrics[] or series[] โ if you need extras to render, the
mapping is wrong. Render the provenance line as plain text under the chart.
Reason: one schema = one renderer = a polished demo without bespoke React per idea.
All metric / series VALUES must come from real Selene shot data committed in step 3 โ no mocks.
[12] RENDERER โ create src/components/SeleneRunView.tsx with EXACTLY this body (no deps beyond react + tailwind):
import * as React from "react";
type Point = { label: string; values: Record<string, number> };
type Series = { id: string; kind: "histogram"|"bar"|"line"; title: string;
xLabel?: string; yLabel?: string; yKeys: string[]; points: Point[] };
type Metric = { name: string; value: number; unit?: string; good?: boolean };
export type SeleneRun = {
schemaVersion: 1; experiment: string; title: string; description: string;
kernel: { snippet: string; qubits: number; shotsPerRow: number };
verdict: { text: string; good: boolean };
metrics: Metric[]; series: Series[]; notes?: string;
};
const fmt = (n: number) => Math.abs(n) >= 100 ? n.toFixed(0) : Math.abs(n) >= 1 ? n.toFixed(2) : n.toFixed(3);
function Bars({ s }: { s: Series }) {
const max = Math.max(1, ...s.points.flatMap(p => s.yKeys.map(k => p.values[k] ?? 0)));
return (
<div className="space-y-1">
{s.points.map((p, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
<div className="w-20 truncate text-muted-foreground">{p.label}</div>
<div className="flex-1 h-3 bg-muted rounded-sm overflow-hidden">
<div className="h-full bg-primary" style={{ width: `${(100*(p.values[s.yKeys[0]]??0))/max}%` }} />
</div>
<div className="w-12 text-right tabular-nums">{fmt(p.values[s.yKeys[0]]??0)}</div>
</div>
))}
</div>
);
}
function Line({ s }: { s: Series }) {
const W=320, H=120, P=20;
const ys = s.points.map(p => p.values[s.yKeys[0]] ?? 0);
const min = Math.min(...ys), max = Math.max(...ys), span = max - min || 1;
const pts = ys.map((y, i) => {
const x = P + (i*(W-2*P))/Math.max(1, ys.length-1);
const yy = H - P - ((y - min)/span)*(H - 2*P);
return `${x},${yy}`;
}).join(" ");
return (
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-32">
<polyline fill="none" stroke="currentColor" strokeWidth="2" points={pts} className="text-primary" />
</svg>
);
}
export function SeleneRunView({ run }: { run: SeleneRun }) {
return (
<div className="space-y-6">
<header>
<div className="text-xs uppercase tracking-wider text-muted-foreground">{run.experiment}</div>
<h2 className="text-2xl font-semibold">{run.title}</h2>
<p className="text-sm text-muted-foreground">{run.description}</p>
<div className={`mt-2 inline-block px-3 py-1 rounded-full text-xs ${run.verdict.good?"bg-emerald-500/15 text-emerald-400":"bg-amber-500/15 text-amber-400"}`}>
{run.verdict.text}
</div>
</header>
<section className="grid grid-cols-2 md:grid-cols-4 gap-3">
{run.metrics.map((m, i) => (
<div key={i} className="rounded-lg border border-border p-3">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">{m.name}</div>
<div className="text-xl font-semibold tabular-nums">{fmt(m.value)}<span className="text-xs text-muted-foreground ml-1">{m.unit}</span></div>
</div>
))}
</section>
<section className="space-y-6">
{run.series.map(s => (
<div key={s.id} className="rounded-lg border border-border p-4">
<div className="flex items-baseline justify-between mb-3">
<div className="text-sm font-medium">{s.title}</div>
<div className="text-[10px] text-muted-foreground">{s.xLabel} / {s.yLabel}</div>
</div>
{s.kind === "line" ? <Line s={s} /> : <Bars s={s} />}
</div>
))}
</section>
<footer className="text-[11px] text-muted-foreground">
kernel: {run.kernel.qubits} qubits ยท {run.kernel.shotsPerRow} shots/row
</footer>
</div>
);
}
Then in the route: import data from "@/data/quantum-results.json"; <SeleneRunView run={data as any} />.
Quantum trace tab: <pre>{data.kernel.snippet}</pre>.
[13] SKILL v2 GOTCHAS (must-follow)
- angle(x) is HALFTURNS, not radians. angle(0.5) == ฯ/2 (S gate).
For a radian ฮธ, write angle(ฮธ / math.pi). Never pass raw radians.
- If a source formula contains an explicit ฯ (e.g. t = ฯ/(16ยทh)),
divide the ฯ out BEFORE angle(): angle(1/16), NOT angle(math.pi/16).
- Only three Selene noise models exist:
from selene_sim.error_models import (
IdealErrorModel, DepolarizingErrorModel, SimpleLeakageErrorModel,
)
There is NO coherent / T1-T2 model โ do not import one.
- Optional "noisy vs ideal" second pass (adds one extra series):
run the same kernel twice, once with IdealErrorModel() and once with
DepolarizingErrorModel(p_1q=1e-4, p_2q=1.29e-3, p_meas=0.9e-3),
emit both under series[].points so the chart shows the gap.
- Run the CLASSICAL baseline BEFORE writing any quantum code and keep its number.
An encoding bug that scores a perfect 1.000 on every input is caught by the
baseline, never by the circuit.
- Shot-noise pass bar is 4*sqrt(0.5/shots) โ see [16]. Do NOT use 3*sqrt(p(1-p)/n);
it produces false FAILs whenever a probability sits near 0 or 1.
- NEVER show a number without its engine and shot count in the same view, and
never call an emulator run "hardware" or "the QPU" (see [18]).
- If a result fails to replicate, WITHDRAW it. Do not reword it vaguer and keep
the chart.
- AN ENVELOPE PASS IS NOT A WIN. It says the circuit behaved; it does not say
the method beat the baseline. Report mechanism and performance separately ([23]).
- CONSISTENCY IS NOT CORRECTNESS: a wrong observable measured perfectly passes
the envelope, the Bell control and the verdict flag. Write the observable and
its falsification criterion down BEFORE the run ([20]) and reread them.
- Hardware footer line (render below the chart, one <div> only):
"Runs today on Helios (98 qubits). Roadmap: Sol โ Apollo โ Lumos."
[14] BUILD SAFETY (Lovable sandbox + production runtime)
- Results ship as a COMMITTED JSON file only. Do NOT create a
createServerFn, API route, or handler that shells out to Python or
reads a cache directory: production stubs child_process and blocks
arbitrary filesystem reads, so it builds and then fails live.
- BEFORE the first pip install: add `.pydeps/` (and `_cache_run/`) to
.gitignore. Installing with `pip install --target .pydeps ...` writes tens
of thousands of files; if they are not ignored, the end-of-turn SAVE times
out and the completed work is ROLLED BACK. The build did not fail - the
save did. Ignored deps stay local to the sandbox and are reinstalled if the
environment is recreated; that is expected and costs nothing.
- Make the sweep resumable: before moving to the next row, write it to
_cache_run/<row-tag>.json and skip rows whose cache file exists.
Assemble src/data/quantum-results.json from the cache at the end.
Keep the grid small (<= 100 rows, <= 256 shots/row) so one pass fits.
[15] IF THE TURN ROLLS BACK (Lovable orchestration)
- Symptom: the work finished, then "An internal error occurred" and every file
from that turn is gone. Cause, in order of likelihood:
1. An unignored vendored dep tree (.pydeps/, _cache_run/) made the save
exceed its time limit. Fix: gitignore it, reinstall, retry.
2. The turn was simply too large. Fix: split into atomic gates below.
It is NOT a sandbox restart and NOT an app bug โ do not debug the app.
- Recovery sequence, in this order: (1) add .pydeps/ to .gitignore, (2) reinstall
deps into .pydeps/, (3) re-run the smoke test, (4) confirm BOTH the code and
the smoke-test result persisted before scaling to the full sweep.
- Author in ATOMIC GATES, one per message, each ending in a saved artifact:
(a) quantum/kernel.py + a smoke run of a handful of shots
(b) quantum/run.py resumable driver (per-row cache)
(c) the cached sweep -> src/data/quantum-results.json
(d) the React route reading the committed JSON
A rollback then costs one gate, not the whole build.
- Persistence canary: on a fresh session, make one trivial edit and end the
turn. If it persists, larger gates are safe.
[16] EVIDENCE & VERIFICATION (do this, it is what makes the demo credible)
- STEP 0, before any quantum code: compute the CLASSICAL baseline for your task
(cosine similarity, brute-force search, direct FFT โ whatever the honest
non-quantum answer is). Print it, keep it, and show it next to the quantum
number in the UI. If the quantum result is identical to a trivial baseline on
every input, your encoding is degenerate โ fix the encoding, not the chart.
- PASS BAR for any probability comparison (quantum shots vs NumPy reference):
import math
tol = 4.0 * math.sqrt(0.5 / shots) # shots = shots PER CELL
ok = abs(p_measured - p_expected) <= tol
At 256 shots tol ~= 0.088. Use this exact form. The textbook
3*sqrt(p(1-p)/n) form collapses to ~0 near p=0 or p=1 and reports false FAILs.
- The verdict.good flag in [11] MUST be computed from that comparison
(e.g. good = all(cells_ok)), never hand-written as true.
- ANGLE SANITY CELL (30 seconds, saves the whole sweep): before the full run,
do one 256-shot single-qubit check โ prepare h(q); rz(q, angle(0.5)); h(q),
measure, and assert P(1) ~= 0.5 within tol. If it comes out 0 or 1 you passed
radians into angle() somewhere. Halfturns, always (see [13]).
- Store that tol as receipt.envelope ([18]) and compute verdict.good from the
STORED value, so a reader can re-derive your verdict without re-running.
- verdict.text must answer the falsification criterion you wrote in [20] โ
in its own words, say whether the run could have failed and did not.
- Report shots + seed + tolerance in the UI next to the verdict. A number
without its shot count and seed is a rumour, not a result.
[17] OPTIONAL โ LIVE SHOT STREAM (stretch goal, ONLY if you still have credits)
Skip this unless the committed JSON demo from [1]-[12] is already working. It is
polish, not proof.
- Selene/Python cannot run in production. For a live "watch the shots land" panel,
port ONE small kernel (<= 4 qubits, gates H / RX / RZ / CX / measure) to a pure
TypeScript statevector mini-sim in src/lib/mini-sim.ts: complex amplitudes as two
Float64Array's, one closed-form unitary per gate, projective measure with collapse
(compute P(0), draw vs rng(), zero the dead branch, renormalise).
- Mirror the Guppy gate ORDER exactly and return the same labelled keys your
Python driver passes to output(...). Same kernel, two runtimes.
- Stream it from src/routes/api/public/shots.ts as Server-Sent Events
(`data: ${JSON.stringify(shot)}\n\n`), seeded from a URL param so a session is
reproducible. The client shows a "Live-verified" badge once >= 500 streamed shots
land within the [16] tolerance of the analytic probability, "Drift" otherwise.
- HARD LIMIT: <= 4 qubits and no noise model. Anything bigger, anything needing real
compilation or a noise model, and anything that IS the claim stays in the committed
Selene JSON. The stream is a UX layer on top of an already-verified experiment.
[18] HONEST LABELLING & RECEIPTS (how a judge decides you are trustworthy)
- Never display a number without its ENGINE and SHOT COUNT in the same view.
"F = 0.94" is a rumour. "F = 0.94 - Selene/Quest emulator, 256 shots, seed 7"
is a result.
- Attach a receipt to src/data/quantum-results.json (top level, alongside
metrics/series), and render engine + shots under the headline number:
"receipt": {
"schema": "qas/envelope/0.1",
"claims": ["<a sentence that could be FALSE, e.g. 'the SWAP test ranks
pair 3-7 closest'>"],
"engine": "Selene/Quest (Guppy >=1.0)",
"backend_qualifier": "emulator - NOT QPU hardware",
"shots": <int>, "seed": <int>,
"envelope": <4*sqrt(0.5/shots)>,
"verdict": "PASS" | "GAP" | "FAIL"
}
Store the computed envelope so a reader can re-derive your verdict.
claims[] holds SENTENCES that could be false, not topics.
- FORBIDDEN phrasings, no exceptions: "ran on the QPU", "quantum hardware",
"cryptographically verified", "quantum advantage" (unless [19] passed).
You ran an emulator. Say emulator. It is still real quantum computation and
judges respect the honesty far more than the overclaim.
- Verdict grades: PASS = envelope present and satisfied. GAP = envelope present,
a field missing (fill it, do not downgrade the claim). FAIL = envelope present
and contradicted -> fix it or withdraw. Missing information is not a lie;
a contradicted claim is.
- If a leg cannot run (out of memory, sandbox timeout, too many qubits), record it
as {"status":"assessed-blocked","limit":"<what stopped it>","error":"<exact
error string>"} and show it. A blocked leg reported honestly reads as rigour;
omitted, it reads as a gap in the work.
- WITHDRAW, DON'T SOFTEN: if a result fails to replicate at a larger size or
different seed, remove the claim and say it was withdrawn. Never reword it into
something vaguer and keep the chart.
[19] DEQUANTIZATION GATE + BELL CONTROL (two cheap checks, huge credibility)
- DEQUANTIZATION GATE - run BEFORE you write the word "quantum" in any comparison:
implement the classical surrogate for your task ([16] STEP 0) and report it
either way. If the classical method reproduces your distribution within the
[18] envelope, the advantage claim dissolves - say so in the UI ("classically
equivalent at this size"). A demo that names its own limit wins the room.
- SHOT LADDER: run the headline cell at 128, 512 and 2048 shots and plot the
metric with its envelope at each. Metric moves and envelopes shrink together
=> shot-noise-limited (real signal). Metric flat with overlapping envelopes
and the classical number inside them => classically equivalent. State which.
- BELL CONTROL - one extra kernel per batch, near-free, and the ONLY check that
catches a corrupted run rather than a wrong circuit:
@guppy
def bell_control() -> None:
a, b = qubit(), qubit()
h(a); cx(a, b)
output("a", measure(a).read()); output("b", measure(b).read())
Run it in the SAME driver pass as your sweep, with the same shots and seed.
anti = sum(1 for s in shots if s["a"] != s["b"])
ok = anti / shots_n <= 4.0 * math.sqrt(0.5 / shots_n)
If ok is False the WHOLE batch is bad - do not report it as one bad row among
twenty. Re-run. Record "bell_anticorrelated": anti next to your target value so
the check lives in the committed artefact, not in a console log you threw away.
[20] EXPERIMENT SPEC โ WRITE IT BEFORE YOU RUN (30 lines, saves the demo)
Every automated check you have โ envelope, Bell control, verdict flag โ tests
CONSISTENCY, not CORRECTNESS. A wrong observable measured perfectly passes all
of them. The observable is a scientific judgement wearing the clothes of a
config field sitting next to shots. So write it down first, in plain English,
and read it back before you spend a single shot.
- Write src/data/experiment-spec.json BEFORE the sweep runs:
{
"observable": "<one sentence: what the number physically MEANS,
e.g. 'P(ancilla=0) of a SWAP test between two
amplitude-encoded 3-feature phrases'>",
"baseline": "<the classical answer it is compared to, named>",
"falsification":"<what result would prove this hook WRONG,
e.g. 'F = 1.000 for every pair => degenerate encoding'>",
"shots": <int>, "seed": <int>, "n_qubits": <int>
}
- The driver READS shots/seed/n_qubits from this file. Never hardcode them in
run.py as well โ spec and run must not be able to drift apart.
- Render it on the page as a small "What we measured" panel (three lines:
observable / baseline / falsification). No credentials, no job ids, and never
the word "verified" โ a spec is a contract, not evidence.
- Before running: reread falsification. If your run CANNOT produce that outcome,
the criterion is decorative โ rewrite it until it can.
[21] CIRCUIT AUDIT + RECEIPT GRADE (two files, both cheap)
- CIRCUIT AUDIT โ compute from the COMPILED program, never type it into copy:
gate_count, two_qubit_fraction, depth, and per-qubit idle windows
Write src/data/circuit-audit.json and have the UI read that file. A gate count
typed into a paragraph is the single easiest thing for a judge to falsify.
The idle windows are the interesting part: a long ancilla idle band is where
a real machine would lose coherence โ say so in one line.
- RECEIPT GRADE โ grade the artefact FOUR ways and show the grade beside the
headline number:
PASS envelope present and satisfied
GAP envelope present, a field missing -> fill it, do NOT downgrade
the claim; missing information is not a lie
STRUCTURAL no envelope at all -> it is a picture, not a result
FAIL envelope present and CONTRADICTED -> fix or withdraw
Put it in receipt.grade next to receipt.verdict.
- SEEDED DETERMINISM: any RNG-driven visual (animated particles, generated
layouts, simulated agents) records its seed AND tick index in the JSON. An
unseeded simulation is not reproducible, so it grades STRUCTURAL and gets no
receipt โ label it "illustrative, not measured".
- SUBSTRATE + CLOCK: any count of simulated things must name its execution
substrate and clock in the SAME sentence โ "10,000 browser-side agents,
advancing only while this page is open" โ never a bare population number at
the same visual rank as a measured quantum figure.
[22] REAL DATA, TRIAGED (a corpus beats a generator)
- Prefer ONE public corpus over your own synthetic generator. A synthetic lane
proves the code runs; someone else's published data proves the method
TRANSFERS. Name the source and link it in the UI.
- Write src/data/dataset-triage.json BEFORE running anything:
{ "source": "<name + url>",
"verdict": "ADOPT" | "PROBE" | "PARK",
"consumer": "<which kernel eats it, e.g. swap_test via prep_features>",
"fixture_sha256": "<sha256 of the file you ACTUALLY parsed>",
"rows": <int>, "columns": [...],
"parse_rule": "<column order, units, angle convention>",
"notes": "..." }
ADOPT = an existing entrypoint consumes it as-is. PROBE = needs a small
adapter, say which. PARK = no consumer exists โ PARK IS A VERDICT, not a
to-do; never attach "future work" to it.
- Hash the fixture YOU parsed, never the upstream archive. If you only read a
slice, say so in notes.
- A download that timed out or was too large is a row, not a silence:
{ "status": "assessed-blocked", "size": "<MB>",
"reason": "<timeout|quota|auth>", "schema_read": false }
and one plain sentence saying its schema was never read.
- WRITE THE PARSE RULE DOWN. A misread column order or a degrees-vs-radians
slip compiles fine, runs fine, and answers a different question with a
perfectly plausible histogram.
- Render a one-line "Data:" credit under the headline number.
[23] MECHANISM vs PERFORMANCE, AND WITHDRAWAL (two verdicts, never one)
- The 4*sqrt(0.5/shots) envelope grades MECHANISM โ did the circuit do what the
maths says. It says NOTHING about whether your quantum method beat the
classical baseline. Keep them in separate fields and separate UI lines:
"mechanism": { "verdict": "PASS"|"FAIL", "envelope": <float>, "delta": <float> }
"performance": { "verdict": "win"|"tie"|"loss",
"quantum": <score>, "classical": <score>, "metric": "<name>" }
A PASS on mechanism must NEVER be displayed as a win. "Our method lost to the
classical baseline" belongs in receipt.claims[] at the SAME rank as "it ran".
Judges reward that sentence; they punish its absence.
- Best-of-N sampling is a shot-budget artefact, not an optimiser. If you take
the best of N shots, label it so and report the median too.
- ARTEFACT MANIFEST โ src/data/manifest.json maps each claim to its CURRENT file:
{ "<claim-id>": { "file": "quantum-results.json", "state": "current" },
"<older-id>": { "file": "run-v1.json", "state": "superseded",
"superseded_by": "<claim-id>" } }
The UI resolves artefacts THROUGH the manifest and THROWS on a missing entry.
Never fall back to "the newest readable file" โ that is how a withdrawn result
keeps rendering.
- A result that fails to replicate at a larger size or a different seed is marked
"superseded" and disappears from the page. Withdraw it; do not reword it.
- A leg stopped by a platform limit is "assessed-blocked" with the limit and the
exact error string โ assessed, not untried.
[HOOK] QTDA โ Betti numbers from a point cloud.
Per point: amplitude-encode with prep_features() from [7] on โค 8 qubits, measure all โ empirical distribution.
Host-side post-processing:
- Pairwise distance d_ij = 1 - quantum_fidelity (use swap_test() from [7] across all pairs).
- For each filtration radius r โ {r0..r7}: build graph with edge iff d_ij โค r.
- b0 = #connected components (BFS/union-find).
- b1 = E - V + b0 (independent cycles for a 1-complex).
- b2 = 0 unless you fill triangles (optional).
selene_run mapping:
metrics: [ {"name":"b0","value":b0_at_r_mid}, {"name":"b1","value":b1_at_r_mid}, {"name":"b2","value":0} ]
series: [ {"id":"betti-curve","kind":"line","title":"Betti numbers vs radius",
"xLabel":"r","yLabel":"count","yKeys":["b0"],
"points":[{"label":f"{r:.2f}","values":{"b0":b0,"b1":b1}} for r,b0,b1,_ in scan]} ]Market sizing.
Indicative figures for hackathon pitches โ refine with your own research before raising.