Mesh Drafting
Reveal the topological shape (clusters, loops, voids) hiding inside poetry drafting so writers read structure at a glance.
QTDA· shape of data
Section · Quantum
full primer →The hook.
Poetry drafting gets fed into a quantum shape-finder; writers 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 poetry drafting reduces to a shape of data problem; the kernel returns a result you can drop straight into the UI.
Kernel
a Quantum Topological Data Analysis pipeline returning Betti numbers (b0, b1, b2) from a Vietoris-Rips filtration
Drives the UI as
a 'shape signature' visualization with connected-component, loop, and void counts
Appendix A
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 →
~22.1 KB410 lines1 msg · ~5 credits
Build a one-page web app called "Mesh Drafting" for writers, poets, screenwriters, narrative designers.
Headline: Reveal the topological shape (clusters, loops, voids) hiding inside poetry drafting so writers 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 (poetry drafting); 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 Writing, Poetry & Narrative 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>" }
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.
- 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.
- If you install deps with `pip install --target .pydeps ...`, add
`.pydeps/` to .gitignore FIRST. An unignored vendored dep tree makes
the build time out and roll back, wasting credits.
- 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)
- A big Guppy/Selene turn can fail with "An internal error occurred". That is
a task-transaction rollback: every file write from that turn is discarded.
Do not debug the app — nothing broke. Re-do the work in smaller gates.
- 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]).
- 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.
[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.
TAM
$2.0B
the writing tools market (~$1.5B) and >100M working writers.
SAM
$440M
the 22% of that market actively buying poetry drafting-adjacent software.
SOM
$31M
a realistic 7% capture of the serviceable slice in years 1–3 via the hackathon launch and creator-led distribution.
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
plot outlining
Field Outlining
Reveal the topological shape (clusters, loops, voids) hiding inside plot outlining so writers read structure at a glance.
poetry draftingTwin Drafting
Compare poetry drafting candidates by quantum fidelity so writers pick the closest match in one tap.
poetry draftingEncoded Drafting
Encode poetry drafting as an amplitude vector and plot the embedding so writers navigate possibilities visually.
poetry draftingOracle Drafting
Search the combinatorial space of poetry drafting with a grover oracle that surfaces the right configuration in √n tries.