Pendulum Feedback
Estimate the dominant resonance phase of beta reader feedback so writers lock onto what is really driving the piece.
Phase estimation· resonance
Section · Quantum
full primer →The hook.
Beta reader feedback is matched against a target resonance using quantum phase estimation; writers see a single dial that snaps when alignment is strongest.
Why this primitivePhase estimation is the right primitive here because beta reader feedback reduces to a resonance problem; the kernel returns a result you can drop straight into the UI.
Kernel
a Quantum Phase Estimation kernel returning the dominant eigenphase of a problem operator
Drives the UI as
a phase dial (0 to 2π) tied to a resonance/match indicator
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 →
~21.9 KB408 lines1 msg · ~5 credits
Build a one-page web app called "Pendulum Feedback" for writers, poets, screenwriters, narrative designers.
Headline: Estimate the dominant resonance phase of beta reader feedback so writers lock onto what is really driving the piece.
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 Phase Estimation kernel returning the dominant eigenphase of a problem operator. 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 16 unitaries = 16 estimated phases. Write the real results to src/data/quantum-results.json with shape: an array of { input, output } records where output contains phase angle (0-2π).
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 (beta reader feedback); center = live visual built from the selected record; right = a phase dial (0 to 2π) tied to a resonance/match indicator driven by the JSON.
- "Quantum trace" disclosure: show the raw phase angle (0-2π) 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 Phase estimation 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] PHASE ESTIMATION — eigenphase readout.
k control qubits + eigenstate register. Apply h on each control, then controlled-U^{2^j}
(decompose controlled-U with cphase from [7] when U is a Z-rotation).
Inverse QFT on controls (run the QFT primitive from the qft hook with negated angles), then measure.
Decoded integer y → phase φ = y / (2**k).
selene_run mapping:
metrics: [ {"name":"phase","value":phi,"unit":"rad"},
{"name":"control qubits","value":k,"unit":""} ]
series: [ {"id":"phase-bins","kind":"bar","title":"Phase-bin counts",
"xLabel":"y/2^k","yLabel":"shots","yKeys":["count"],
"points":[{"label":f"{y/(2**k):.2f}","values":{"count":c}} for y,c in sorted(hist.items())]} ]Market sizing.
TAM
$15.0B
the writing tools market (~$1.5B) and >100M working writers.
SAM
$2.9B
the 19% of that market actively buying beta reader feedback-adjacent software.
SOM
$228M
a realistic 8% 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
Tuner Outlining
Estimate the dominant resonance phase of plot outlining so writers lock onto what is really driving the piece.
poetry draftingLock Drafting
Estimate the dominant resonance phase of poetry drafting so writers lock onto what is really driving the piece.
screenplay beatsAnchor Beat
Estimate the dominant resonance phase of screenplay beats so writers lock onto what is really driving the piece.
worldbuilding biblesPivot Bible
Estimate the dominant resonance phase of worldbuilding bibles so writers lock onto what is really driving the piece.