diff --git a/brainsnn-r3f-app/README.md b/brainsnn-r3f-app/README.md index 2d58cd7..4c0b651 100644 --- a/brainsnn-r3f-app/README.md +++ b/brainsnn-r3f-app/README.md @@ -71,6 +71,46 @@ All optional. Copy [.env.example](.env.example) to `.env` and fill in only what | 33 | Multimodal RAG Router | [MultimodalRagPanel.jsx](src/components/MultimodalRagPanel.jsx) + [utils/multimodalRag.js](src/utils/multimodalRag.js) | | 34 | Vector-Graph Fusion | [VectorGraphFusionPanel.jsx](src/components/VectorGraphFusionPanel.jsx) | | 35 | Direct Content Insertion (JSON) | [DirectInsertPanel.jsx](src/components/DirectInsertPanel.jsx) | +| 101 | Quantum Coherence Lab | [QuantumCoherencePanel.jsx](src/components/QuantumCoherencePanel.jsx) + [utils/quantumCoherence.js](src/utils/quantumCoherence.js) | +| 102 | Bell Pair Lab | [BellPairPanel.jsx](src/components/BellPairPanel.jsx) + [utils/bellPair.js](src/utils/bellPair.js) | +| 103 | Quantum Sweep | [QuantumSweepPanel.jsx](src/components/QuantumSweepPanel.jsx) + [utils/quantumSweep.js](src/utils/quantumSweep.js) | +| 104 | Quantum Glossary | [QuantumGlossaryPanel.jsx](src/components/QuantumGlossaryPanel.jsx) | + +## Quantum Coherence Lab + +**Layer 101 — Quantum Coherence Lab.** A pure-JavaScript, in-browser simulation +of a single qubit running through `|0⟩ → H → RZ(θ) → H → M`. Slide the phase +θ to watch interference move probability between |0⟩ and |1⟩. Add noise to +damp the fringe. Toggle a mid-circuit observation to collapse superposition. +Stack X·X pairs (algebraically identity) to watch decoherence eat depth. + +**What this is.** A teaching sandbox for the *mechanism* behind the word +"alignment": phase coherence steers outcomes; noise and observation kill it. +A **Scientific / Metaphor** mode toggle reframes the same numbers in +plain English alongside the math. + +**What this is not.** This does **not** prove literal multiverse theory, +consciousness collapse, Planck foam, or spiritual portals. Those are framing +metaphors when the toggle is on, not physics claims. + +**Future backend.** The function surface (`runPhaseExperiment`, +`runDecoherenceExperiment`, etc.) is intentionally compatible with the +hardware-grade Qiskit suite at [`quantum_alignment/`](../quantum_alignment/), +which runs the same three experiments on ideal Aer, noisy Aer, or real IBM +Quantum hardware (and is shaped to swap in OriginQ later). **No vendor API +keys are added to the frontend** — IBM tokens stay in the Python suite, +read from `IBM_QUANTUM_TOKEN` at runtime only. + +**Cluster siblings.** Three follow-on layers extend L101 into a coherent quantum module: +- **Layer 102 — Bell Pair Lab.** Two qubits run through `H ⊗ I → CNOT` to build the Bell state `|Φ+⟩ = (|00⟩ + |11⟩) / √2`. RY(θ) on qubit 0 lets you watch correlation slide from +1 (mirrored) to 0 (decohered) to −1 (anti-mirrored). Important framing: this is statistical correlation, *not* information transfer. +- **Layer 103 — Quantum Sweep.** Auto-sweeps θ / noise / X·X-depth, plots P(0) and P(1) against the closed-form ideal, and exports a CSV with the same column shape as `quantum_alignment/results/results.csv` so browser-sim curves can be compared directly with the Qiskit ideal/noisy/real curves. +- **Layer 104 — Quantum Glossary.** Searchable reference card for every term used in L101–L103 — plain language, the math, and a metaphor column explicitly framed as a teaching aid. + +Run the unit tests directly with Node (no extra dev deps): + +```bash +npm run test:quantum +``` ## Keyboard shortcuts diff --git a/brainsnn-r3f-app/package.json b/brainsnn-r3f-app/package.json index 00c8c03..dc9a3e3 100644 --- a/brainsnn-r3f-app/package.json +++ b/brainsnn-r3f-app/package.json @@ -11,7 +11,8 @@ "build": "vite build", "preview": "vite preview", "start": "node server.js", - "start:dev": "npm run build && node server.js" + "start:dev": "npm run build && node server.js", + "test:quantum": "node --test src/utils/quantumCoherence.test.mjs src/utils/bellPair.test.mjs src/utils/quantumSweep.test.mjs" }, "dependencies": { "@ffmpeg/ffmpeg": "^0.12.15", diff --git a/brainsnn-r3f-app/src/App.jsx b/brainsnn-r3f-app/src/App.jsx index afb3b3f..4b2f636 100644 --- a/brainsnn-r3f-app/src/App.jsx +++ b/brainsnn-r3f-app/src/App.jsx @@ -81,6 +81,10 @@ import HotkeyMap from './components/HotkeyMap'; import ThemePanel from './components/ThemePanel'; import CommunityPackPanel from './components/CommunityPackPanel'; import MilestonePanel from './components/MilestonePanel'; +import QuantumCoherencePanel from './components/QuantumCoherencePanel'; +import BellPairPanel from './components/BellPairPanel'; +import QuantumSweepPanel from './components/QuantumSweepPanel'; +import QuantumGlossaryPanel from './components/QuantumGlossaryPanel'; import { registerServiceWorker } from './utils/pwa'; import { registerTheme } from './utils/theme'; import DreamModePanel from './components/DreamModePanel'; @@ -779,6 +783,58 @@ export default function App() { + + { + markActivity(); + setState((prev) => { + const regions = { ...prev.regions }; + for (const [region, delta] of Object.entries(deltas)) { + if (regions[region] === undefined) continue; + regions[region] = Math.max(0.04, Math.min(0.95, regions[region] + delta * 0.3)); + } + return { + ...prev, + regions, + tick: (prev.tick ?? 0) + 1, + scenario: `Quantum Coherence (${score}/100)`, + }; + }); + toastSuccess(`Quantum coherence ${score}/100 mapped to brain · ${result.kind}`); + }} + /> + + + + { + markActivity(); + setState((prev) => { + const regions = { ...prev.regions }; + for (const [region, delta] of Object.entries(deltas)) { + if (regions[region] === undefined) continue; + regions[region] = Math.max(0.04, Math.min(0.95, regions[region] + delta * 0.3)); + } + return { + ...prev, + regions, + tick: (prev.tick ?? 0) + 1, + scenario: `Bell Pair (corr ${result.correlation.toFixed(2)})`, + }; + }); + toastSuccess(`Bell correlation ${result.correlation.toFixed(2)} mapped to brain`); + }} + /> + + + + + + + + + + diff --git a/brainsnn-r3f-app/src/components/BellPairPanel.jsx b/brainsnn-r3f-app/src/components/BellPairPanel.jsx new file mode 100644 index 0000000..8f325e0 --- /dev/null +++ b/brainsnn-r3f-app/src/components/BellPairPanel.jsx @@ -0,0 +1,199 @@ +import React, { useMemo, useState } from 'react'; +import { runBellPairExperiment, mapBellToBrainState } from '../utils/bellPair'; + +/** + * Layer 102 — Bell Pair Lab. + * + * Two-qubit entanglement, in-browser. Builds |Φ+⟩ = (|00⟩ + |11⟩)/√2 by + * running |00⟩ → H ⊗ I → CNOT, then rotates qubit 0 with RY(θ) so the user + * can watch correlation break smoothly. Shows joint outcomes |00⟩ |01⟩ |10⟩ + * |11⟩ as four bars, plus a signed correlation strength in [-1, 1]. + * + * Important framing: correlation here is a quantum statistical fact about + * a many-shot distribution, not "spooky action" at a distance in any + * mystical sense. No ER=EPR, no consciousness telepathy, no portals. + */ + +const SHOTS_OPTIONS = [256, 1024, 4096]; + +const BASIS_LABELS = ['|00⟩', '|01⟩', '|10⟩', '|11⟩']; +const BASIS_COLORS = ['#5ad4ff', '#fdab43', '#dd6974', '#a86fdf']; + +function fmtPercent(p) { + return `${(p * 100).toFixed(1)}%`; +} + +function explain({ correlation, rotationTheta, noise, mode }) { + const corr = correlation.toFixed(2); + if (mode === 'metaphor') { + if (correlation > 0.85) { + return `Two coins, perfectly mirrored (correlation ${corr}). Either both heads or both tails — never mixed. Metaphor: a pair of friends who finish each other's sentences.`; + } + if (correlation < -0.85) { + return `Two coins, perfectly anti-mirrored (correlation ${corr}). Always one head and one tail. Metaphor: opposites locked into balance.`; + } + if (Math.abs(correlation) < 0.15) { + return `The link snapped. Each coin lands independent of the other (correlation ${corr}). Metaphor: rotated the question hard enough that the shared answer dissolved.`; + } + return `Partial agreement (correlation ${corr}). Metaphor: the friendship is real but fading.`; + } + // scientific + if (rotationTheta === 0 && noise < 0.05) { + return `|Φ+⟩ measured in the computational basis. Outcomes are 50/50 split between |00⟩ and |11⟩, never |01⟩ or |10⟩. Correlation ${corr}: maximal classical-impossible correlation across two non-interacting qubits.`; + } + if (Math.abs(correlation) < 0.15) { + return `Rotation moved qubit 0 to a basis where the Bell state's correlations average out. Correlation ${corr}. Reading qubit 0 no longer predicts qubit 1. (No information was sent — this is the basis-mismatch lesson, not "spooky action".)`; + } + if (correlation < 0) { + return `RY(${rotationTheta.toFixed(2)}) flipped the in-basis correlation. Anti-correlated outcomes (|01⟩, |10⟩) now dominate. Correlation ${corr}.`; + } + return `Bell state with rotation ${rotationTheta.toFixed(2)} and noise ${noise.toFixed(2)}. Correlation ${corr}; noise ${noise > 0 ? 'is mixing in uniform randomness' : 'is zero'}.`; +} + +function JointBars({ distribution, counts }) { + return ( +
+ {distribution.map((p, i) => ( +
+
+ {BASIS_LABELS[i]} + {fmtPercent(p)} · {counts[i]} shots +
+
+
+
+
+ ))} +
+ ); +} + +function CircuitDiagram({ rotationTheta }) { + return ( +
+
q0 |0⟩ ──[H]──●──{rotationTheta !== 0 ? `[RY(${rotationTheta.toFixed(2)})]──` : '──────────────'}M
+
q1 |0⟩ ─────────⊕────────────────M
+
+ ); +} + +export default function BellPairPanel({ onApplyToBrain } = {}) { + const [rotationTheta, setRotationTheta] = useState(0); + const [shots, setShots] = useState(1024); + const [noise, setNoise] = useState(0); + const [mode, setMode] = useState('scientific'); + const [runToken, setRunToken] = useState(0); + + const result = useMemo(() => { + void runToken; + return runBellPairExperiment({ rotationTheta, shots, noise }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rotationTheta, shots, noise, runToken]); + + const explanation = useMemo( + () => explain({ correlation: result.correlation, rotationTheta, noise, mode }), + [result.correlation, rotationTheta, noise, mode], + ); + + const brainDeltas = useMemo(() => mapBellToBrainState(result), [result]); + const corrColor = result.correlation > 0.5 ? '#5ee69a' : result.correlation < -0.5 ? '#a86fdf' : '#fdab43'; + + return ( +
+
Layer 102 · bell pair lab
+

Two qubits, one shared answer

+

+ Build the Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2 with{' '} + H ⊗ I → CNOT, then rotate qubit 0 with RY(θ) and measure + both qubits jointly. Correlation runs from +1 (perfect mirror) to −1 + (perfect opposites) to 0 (noise dissolved the link). +

+

+ This is statistical correlation across many shots, not + information transfer. No consciousness, no telepathy, no portals. +

+ +
+ + + + {onApplyToBrain && ( + + )} +
+ +
+
+
+ RY rotation θ on qubit 0 (0 → π) + {rotationTheta.toFixed(3)} +
+ setRotationTheta(parseFloat(e.target.value))} style={{ width: '100%' }} /> +
+
+
+ Depolarizing noise (0 = pure, 1 = uniform) + {noise.toFixed(2)} +
+ setNoise(parseFloat(e.target.value))} style={{ width: '100%' }} /> +
+
+ Shots: + {SHOTS_OPTIONS.map((s) => ( + + ))} +
+
+ + + + + +
+ + Correlation strength {result.correlation.toFixed(3)} + + + {result.correlation > 0.5 ? 'mirrored' : result.correlation < -0.5 ? 'anti-mirrored' : 'decohered'} + +
+ +

{explanation}

+ +
+ Region deltas (preview) +
+ {Object.entries(brainDeltas).map(([region, delta]) => ( +
+
{region}
+
0 ? '#5ee69a' : '#94a3b8' }}>{delta >= 0 ? '+' : ''}{delta.toFixed(2)}
+
+ ))} +
+
+
+ ); +} diff --git a/brainsnn-r3f-app/src/components/QuantumCoherencePanel.jsx b/brainsnn-r3f-app/src/components/QuantumCoherencePanel.jsx new file mode 100644 index 0000000..6d92fd0 --- /dev/null +++ b/brainsnn-r3f-app/src/components/QuantumCoherencePanel.jsx @@ -0,0 +1,394 @@ +import React, { useMemo, useState } from 'react'; +import { + runPhaseExperiment, + runObservationExperiment, + runDecoherenceExperiment, + coherenceScore, + mapQuantumToBrainState, +} from '../utils/quantumCoherence'; + +/** + * Layer 101 — Quantum Coherence Lab panel. + * + * Local, in-browser simulation of the simplest possible quantum circuit: + * |0⟩ → H → RZ(θ) → H → M + * + * Teaches superposition, phase, interference, observation, noise, and + * decoherence. The "Metaphor" toggle re-frames the same numbers in + * everyday language; nothing in this panel claims literal multiverse + * theory, consciousness collapse, Planck foam, or spiritual portals — + * those are framing aids, not physics claims. + * + * For an offline / hardware-grade version of the same three experiments, + * see /quantum_alignment/ (Qiskit + ideal/noisy/IBM-real backends). + */ + +const SHOTS_OPTIONS = [256, 1024, 4096]; + +const SHOTS_HINT = { + 256: 'Quick — noisy bars', + 1024: 'Default — balanced', + 4096: 'Slow — smooth bars', +}; + +function fmtPercent(p) { + return `${(p * 100).toFixed(1)}%`; +} + +function ProbabilityBars({ distribution, counts }) { + const [p0, p1] = distribution; + return ( +
+ {[ + { label: '|0⟩', p: p0, count: counts[0], color: '#5ad4ff' }, + { label: '|1⟩', p: p1, count: counts[1], color: '#a86fdf' }, + ].map((b) => ( +
+
+ {b.label} + + {fmtPercent(b.p)} · {b.count} shots + +
+
+
+
+
+ ))} +
+ ); +} + +function CircuitRow({ theta, observeMidway, depth }) { + const gates = [ + { label: 'H', desc: 'Hadamard — make superposition' }, + { label: `RZ(${theta.toFixed(2)})`, desc: 'Rotate the relative phase' }, + ]; + if (observeMidway) { + gates.push({ label: '👁 M', desc: 'Mid-circuit measurement' }); + } + if (depth > 0) { + gates.push({ label: `(X·X)×${depth}`, desc: 'Identity on paper, decoherence in practice' }); + } + gates.push({ label: 'H', desc: 'Hadamard — recombine for interference' }); + gates.push({ label: 'M', desc: 'Final measurement — read out 0 or 1' }); + + return ( +
+ |0⟩ + {gates.map((g, i) => ( + + + + {g.label} + + + ))} +
+ ); +} + +function buildExplanation({ result, score, mode, theta, observeMidway, depth, noise }) { + const [p0, p1] = result.distribution; + const dominant = p0 >= p1 ? '|0⟩' : '|1⟩'; + const dominantPct = fmtPercent(Math.max(p0, p1)); + const balanced = Math.abs(p0 - p1) < 0.1; + + if (mode === 'metaphor') { + if (observeMidway) { + return `Watching the qubit mid-flight collapsed it. Like checking a Schrödinger box too early — the second H spreads the now-classical bit back into ~50/50, score ${score}/100. Metaphor: peek at a held thought and you lose the held thought.`; + } + if (balanced) { + return `Phase tuned to a place where both outcomes interfere equally — ${dominantPct} either way. Metaphor: two stories with equal pull. Coherence ${score}/100.`; + } + if (noise > 0.5) { + return `Noise ate the interference. The qubit drifted toward ${dominant} (${dominantPct}) but without the clean fringe. Metaphor: signal in a loud room. Coherence ${score}/100.`; + } + if (depth > 0) { + return `Stacked ${depth} X·X pairs — algebraically a no-op, but each gate leaks. Metaphor: a long whispered chain stays the same on paper, drifts in real life. Coherence ${score}/100.`; + } + return `Phase steered the wave to ${dominant} (${dominantPct}). Metaphor: aligned attention picks one outcome out of two. Coherence ${score}/100.`; + } + + // Scientific mode + if (observeMidway) { + return `Mid-circuit measurement collapsed |+⟩ to a basis state, killing interference at the second H. Result is ~50/50 (${dominantPct} ${dominant}). Coherence ${score}/100. This is the "watched-path kills fringe" lesson.`; + } + if (depth > 0) { + return `${depth} X·X pairs. Ideal: identity (P(0)=1). With noise=${noise.toFixed(2)} and dephasing per gate, you got P(${dominant})=${dominantPct}. Coherence drops with depth × noise. Score ${score}/100.`; + } + if (balanced) { + return `θ=${theta.toFixed(2)} sits near π/2 — H·RZ(π/2)·H gives ~50/50. P(${dominant})=${dominantPct}. Interference fringe present at low noise. Score ${score}/100.`; + } + return `H → RZ(${theta.toFixed(2)}) → H → M. Phase rotation interferes at the second H, biasing the readout to ${dominant} (${dominantPct}). Noise=${noise.toFixed(2)}, coherence ${score}/100.`; +} + +export default function QuantumCoherencePanel({ onApplyToBrain } = {}) { + const [theta, setTheta] = useState(0); + const [shots, setShots] = useState(1024); + const [noise, setNoise] = useState(0); + const [depth, setDepth] = useState(0); // X-X pair count + const [observeMidway, setObserveMidway] = useState(false); + const [mode, setMode] = useState('scientific'); // 'scientific' | 'metaphor' + const [runToken, setRunToken] = useState(0); // bumps every Re-run click + + const result = useMemo(() => { + void runToken; + if (observeMidway) { + return runObservationExperiment({ shots, observeMidway: true, noise }); + } + if (depth > 0) { + return runDecoherenceExperiment({ xxPairs: depth, shots, noise }); + } + return runPhaseExperiment({ theta, shots, noise }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [theta, shots, noise, depth, observeMidway, runToken]); + + const score = useMemo( + () => coherenceScore({ + noise, + depth: Math.max(1, depth + 1), + observedMidway: observeMidway, + }), + [noise, depth, observeMidway], + ); + + const explanation = useMemo( + () => buildExplanation({ result, score, mode, theta, observeMidway, depth, noise }), + [result, score, mode, theta, observeMidway, depth, noise], + ); + + const brainDeltas = useMemo(() => mapQuantumToBrainState(result), [result]); + + return ( +
+
Layer 101 · quantum coherence lab
+

Phase, interference, decoherence — in your browser

+

+ A single qubit running |0⟩ → H → RZ(θ) → H → M, simulated + locally in JavaScript. Slide θ to see interference move probability + between |0⟩ and |1⟩. Add noise. Toggle a mid-circuit observation. + Stack X·X pairs to watch identity-on-paper fall apart in practice. +

+

+ This teaches the mechanism behind the word "alignment" — phase + coherence steers outcomes; noise and observation kill it. It does + not prove multiverse theory, consciousness collapse, Planck foam, + or spiritual portals. Those are metaphors when the toggle is on. + For an offline Qiskit run on ideal / noisy / real IBM hardware, see + the quantum_alignment/ suite at the repo root. +

+ +
+ + + + {onApplyToBrain && ( + + )} +
+ + {/* Sliders */} +
+
+
+ Phase θ (0 → π) + {theta.toFixed(3)} +
+ setTheta(parseFloat(e.target.value))} + disabled={observeMidway || depth > 0} + style={{ width: '100%' }} + /> +
+ +
+
+ Noise (0 = ideal, 1 = thermal mush) + {noise.toFixed(2)} +
+ setNoise(parseFloat(e.target.value))} + style={{ width: '100%' }} + /> +
+ +
+
+ Depth — extra X·X pairs (algebraically zero work) + {depth} +
+ setDepth(parseInt(e.target.value, 10))} + style={{ width: '100%' }} + /> +
+ +
+ Shots: + {SHOTS_OPTIONS.map((s) => ( + + ))} + +
+
+ + {/* Circuit visualization */} + + + {/* Probability bars */} + + + {/* Coherence score */} +
= 70 ? '#5ee69a' : score >= 40 ? '#fdab43' : '#dd6974'}`, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }} + > + + Coherence score{' '} + {score}/100 + + + {score >= 70 ? 'phase intact' : score >= 40 ? 'fading' : 'decohered'} + +
+ + {/* Explanation */} +

+ {explanation} +

+ + {/* Brain deltas preview */} +
+ + Region deltas (preview) + +
+ {Object.entries(brainDeltas).map(([region, delta]) => ( +
+
{region}
+
0 ? '#5ee69a' : '#94a3b8' }}> + {delta >= 0 ? '+' : ''}{delta.toFixed(2)} +
+
+ ))} +
+
+
+ ); +} diff --git a/brainsnn-r3f-app/src/components/QuantumGlossaryPanel.jsx b/brainsnn-r3f-app/src/components/QuantumGlossaryPanel.jsx new file mode 100644 index 0000000..8ec18d1 --- /dev/null +++ b/brainsnn-r3f-app/src/components/QuantumGlossaryPanel.jsx @@ -0,0 +1,284 @@ +import React, { useMemo, useState } from 'react'; + +/** + * Layer 104 — Quantum Glossary panel. + * + * A searchable reference card for every quantum term used in the L101 – + * L103 cluster. Plain language + the math, side by side. Complements the + * Metaphor toggle in the other panels: lets a user look up an unfamiliar + * symbol without leaving the page. + * + * Nothing in here claims literal multiverse / consciousness / portal + * physics. The "metaphor" column is explicitly framed as a teaching aid. + */ + +const TERMS = [ + { + term: '|0⟩, |1⟩', + category: 'state', + plain: 'The two definite answers a single qubit can give when measured.', + math: 'Computational basis states. State vector [1, 0] and [0, 1].', + metaphor: 'Two doors. After you measure, the qubit is behind one of them.', + }, + { + term: '|+⟩, |-⟩', + category: 'state', + plain: 'Equal-strength mixtures of |0⟩ and |1⟩, with relative phase + or -.', + math: '(|0⟩ ± |1⟩) / √2.', + metaphor: 'A coin spinning. + = clockwise, - = counter-clockwise; both still 50/50 when it lands.', + }, + { + term: 'Superposition', + category: 'state', + plain: 'A qubit holding both 0 and 1 at the same time, with relative weights and a phase.', + math: 'α|0⟩ + β|1⟩ with |α|² + |β|² = 1.', + metaphor: 'A coin in flight — neither heads nor tails until something lands the question.', + }, + { + term: 'Phase (relative)', + category: 'state', + plain: 'The angle between the |0⟩ and |1⟩ amplitudes — invisible until you interfere them.', + math: 'arg(β / α). Global phase doesn’t affect probabilities.', + metaphor: 'How the coin spins. Two spins that are out of step cancel each other.', + }, + { + term: 'Amplitude', + category: 'state', + plain: 'Complex number whose squared magnitude is the probability of an outcome.', + math: 'P(state) = |amplitude|².', + metaphor: 'The strength and direction of one side of a wave.', + }, + { + term: 'Hadamard (H)', + category: 'gate', + plain: 'Turns |0⟩ into an equal superposition |+⟩ — and back.', + math: 'H = (1/√2) [[1, 1], [1, -1]]. H · H = I.', + metaphor: 'Spin the coin. Spin it again with the same flick — it lands the way it started.', + }, + { + term: 'Pauli-X', + category: 'gate', + plain: 'Bit flip — swap |0⟩ and |1⟩.', + math: 'X = [[0, 1], [1, 0]].', + metaphor: 'Pull the switch from "off" to "on".', + }, + { + term: 'Pauli-Z', + category: 'gate', + plain: 'Phase flip — multiplies |1⟩ by -1, leaves |0⟩ alone.', + math: 'Z = [[1, 0], [0, -1]].', + metaphor: 'Reverse the spin direction without changing whether it’s heads or tails.', + }, + { + term: 'RZ(θ)', + category: 'gate', + plain: 'Rotates the relative phase by an angle θ.', + math: 'RZ(θ) = diag(e^{-iθ/2}, e^{+iθ/2}).', + metaphor: 'Twist the coin’s spin axis by θ before it lands.', + }, + { + term: 'RY(θ)', + category: 'gate', + plain: 'Rotates around the Y axis — moves probability between |0⟩ and |1⟩ smoothly.', + math: 'RY(θ) = [[cos(θ/2), -sin(θ/2)], [sin(θ/2), cos(θ/2)]].', + metaphor: 'Tilt the coin: more heads or more tails depending on how far you tilt.', + }, + { + term: 'CNOT', + category: 'gate', + plain: 'Two-qubit gate: flips the target qubit only when the control is |1⟩.', + math: 'CNOT|c, t⟩ = |c, t ⊕ c⟩.', + metaphor: 'A pair of coins glued together: flipping coin A flips coin B if A landed heads.', + }, + { + term: 'Bell state |Φ+⟩', + category: 'state', + plain: 'Maximally entangled two-qubit state where both qubits always agree when measured.', + math: '(|00⟩ + |11⟩) / √2. Built by H ⊗ I → CNOT.', + metaphor: 'Two coins that always land the same way — but only because their flips were prepared together, not because one signals the other.', + }, + { + term: 'Entanglement', + category: 'state', + plain: 'A correlation between two qubits stronger than any classical pair can have.', + math: 'A pure 2-qubit state that is *not* expressible as |a⟩ ⊗ |b⟩.', + metaphor: '"Linked dice." Not telepathy — the link comes from how they were built, and it carries no information by itself.', + }, + { + term: 'Measurement', + category: 'process', + plain: 'Sample one of the basis states with probability |amplitude|². The state collapses.', + math: 'For α|0⟩ + β|1⟩: P(0) = |α|², P(1) = |β|². Post-measurement state = the basis ket you got.', + metaphor: 'The coin lands. After that, it’s heads-or-tails, not spinning.', + }, + { + term: 'Mid-circuit measurement', + category: 'process', + plain: 'Measuring partway through a circuit. Destroys interference for any later gates.', + math: 'Projects the state onto the measurement basis; the rest of the circuit acts on a classical bit.', + metaphor: 'Peeking at the spinning coin before it lands stops the spin.', + }, + { + term: 'Interference', + category: 'process', + plain: 'When two paths through a circuit reinforce or cancel based on their relative phase.', + math: 'Constructive: amplitudes add. Destructive: they cancel.', + metaphor: 'Two ripples in a pond meeting peak-to-peak (taller) or peak-to-trough (flat).', + }, + { + term: 'Decoherence', + category: 'noise', + plain: 'Loss of phase information through unwanted coupling to the environment.', + math: 'T2 dephasing damps off-diagonal density-matrix elements.', + metaphor: 'A whisper getting drowned out as a room fills up.', + }, + { + term: 'Dephasing', + category: 'noise', + plain: 'A specific decoherence channel: the relative phase randomizes while populations stay.', + math: 'Damps imaginary parts of off-diagonal amplitudes / matrix elements.', + metaphor: 'The coin keeps spinning at the same speed but its axis wobbles randomly.', + }, + { + term: 'Bit-flip channel', + category: 'noise', + plain: 'A gate misfire: with probability p, the qubit’s state is X-flipped.', + math: 'ρ → (1-p) ρ + p X ρ X.', + metaphor: 'A 1-in-N chance the switch jiggled to the wrong position.', + }, + { + term: 'Depolarizing noise', + category: 'noise', + plain: 'With probability p, replace the qubit’s state with a uniform random one.', + math: 'ρ → (1-p) ρ + p · I/2.', + metaphor: 'A small chance the coin gets snatched and replaced with a freshly-tossed one.', + }, + { + term: 'Coherence score (this app)', + category: 'metric', + plain: '0–100 summary of how much quantum-ness survived a run. Drops with noise and depth.', + math: 'See utils/quantumCoherence.js → coherenceScore().', + metaphor: 'Battery left in the qubit at the end of the experiment.', + }, + { + term: 'Correlation strength (this app)', + category: 'metric', + plain: 'Bell-pair signed correlation: +1 mirrored, 0 independent, -1 anti-mirrored.', + math: '(P(00) + P(11)) - (P(01) + P(10)).', + metaphor: 'How much the two coins agree across many tosses.', + }, + { + term: 'Shots', + category: 'metric', + plain: 'How many times you rerun the circuit and measure. More shots = smoother bars.', + math: 'Shot noise scales as 1 / √N.', + metaphor: 'How many coin tosses you average over.', + }, +]; + +const CATEGORY_LABEL = { + state: 'States', + gate: 'Gates', + process: 'Processes', + noise: 'Noise', + metric: 'Metrics', +}; + +const CATEGORY_COLOR = { + state: '#5ad4ff', + gate: '#a86fdf', + process: '#fdab43', + noise: '#dd6974', + metric: '#5ee69a', +}; + +export default function QuantumGlossaryPanel() { + const [q, setQ] = useState(''); + const [activeCategory, setActiveCategory] = useState('all'); + + const filtered = useMemo(() => { + const needle = q.trim().toLowerCase(); + return TERMS.filter((t) => { + if (activeCategory !== 'all' && t.category !== activeCategory) return false; + if (!needle) return true; + return ( + t.term.toLowerCase().includes(needle) + || t.plain.toLowerCase().includes(needle) + || t.math.toLowerCase().includes(needle) + || t.metaphor.toLowerCase().includes(needle) + ); + }); + }, [q, activeCategory]); + + const counts = useMemo(() => { + const c = { all: TERMS.length }; + for (const t of TERMS) c[t.category] = (c[t.category] || 0) + 1; + return c; + }, []); + + return ( +
+
Layer 104 · quantum glossary
+

{TERMS.length} terms used in the quantum cluster

+

+ Plain language plus the math, side by side. Includes a metaphor + column — these are explicitly framed as teaching aids, not + physics claims about consciousness, multiverses, or anything beyond + what the math says. +

+ +
+ setQ(e.target.value)} + style={{ flex: 1, minWidth: 220 }} + /> + +
+ +

+ Showing {filtered.length} of {TERMS.length} +

+ +
+ {filtered.map((t) => ( +
+
+ {t.term} +
+ {CATEGORY_LABEL[t.category]} +
+
+
+
{t.plain}
+
{t.math}
+
+ metaphor: {t.metaphor} +
+
+
+ ))} + {filtered.length === 0 &&

No matches.

} +
+
+ ); +} diff --git a/brainsnn-r3f-app/src/components/QuantumSweepPanel.jsx b/brainsnn-r3f-app/src/components/QuantumSweepPanel.jsx new file mode 100644 index 0000000..6f40b21 --- /dev/null +++ b/brainsnn-r3f-app/src/components/QuantumSweepPanel.jsx @@ -0,0 +1,193 @@ +import React, { useMemo, useState } from 'react'; +import { runSweep, rowsToCsv, sweepSummary } from '../utils/quantumSweep'; + +/** + * Layer 103 — Quantum Sweep panel. + * + * Auto-sweeps a parameter through the L101 single-qubit experiment family + * and renders a small SVG line chart of P(0) (and the ideal curve where + * one is well-defined). Lets the user download a CSV with the same column + * shape as the offline Qiskit suite at /quantum_alignment/results/. + */ + +const KINDS = [ + { id: 'phase', label: 'Phase θ ∈ [0, π]', help: 'Sweeps θ; ideal P(0) = cos²(θ/2).' }, + { id: 'noise', label: 'Noise ∈ [0, 1]', help: 'Sweeps depolarizing-style noise at fixed θ.' }, + { id: 'depth', label: 'X·X depth', help: 'Sweeps logical-identity depth; ideal P(0)=1.' }, +]; + +const SHOTS_OPTIONS = [256, 1024, 4096]; + +function csvDownload(rows, kind) { + const csv = rowsToCsv(rows); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + a.href = url; + a.download = `quantum_sweep_${kind}_${ts}.csv`; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 0); +} + +function SweepChart({ rows }) { + if (!rows.length) return null; + const W = 360; + const H = 140; + const padL = 32; + const padR = 8; + const padT = 8; + const padB = 22; + const innerW = W - padL - padR; + const innerH = H - padT - padB; + const xs = rows.map((r) => r.parameter); + const xMin = Math.min(...xs); + const xMax = Math.max(...xs); + const xSpan = xMax - xMin || 1; + const xCoord = (v) => padL + ((v - xMin) / xSpan) * innerW; + const yCoord = (p) => padT + (1 - p) * innerH; + const linePath = (key) => + rows.map((r, i) => `${i === 0 ? 'M' : 'L'} ${xCoord(r.parameter).toFixed(2)} ${yCoord(r[key]).toFixed(2)}`).join(' '); + + return ( + + {/* gridlines + axis labels */} + {[0, 0.25, 0.5, 0.75, 1].map((y) => ( + + + {y.toFixed(2)} + + ))} + {/* ideal curve, dashed */} + + {/* measured P(0) */} + + {/* measured P(1) */} + + {/* x ticks */} + {rows[0].parameterLabel} + {rows[rows.length - 1].parameterLabel} + {/* legend */} + + + P(0) measured + + P(1) + + P(0) ideal + + + ); +} + +export default function QuantumSweepPanel() { + const [kind, setKind] = useState('phase'); + const [shots, setShots] = useState(1024); + const [steps, setSteps] = useState(9); + const [noise, setNoise] = useState(0.05); + const [theta, setTheta] = useState(Math.PI / 2); + const [maxDepth, setMaxDepth] = useState(12); + const [runToken, setRunToken] = useState(0); + + const rows = useMemo(() => { + void runToken; + return runSweep({ kind, shots, steps, noise, theta, maxDepth }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [kind, shots, steps, noise, theta, maxDepth, runToken]); + + const summary = useMemo(() => sweepSummary(rows), [rows]); + const kindMeta = KINDS.find((k) => k.id === kind) || KINDS[0]; + + return ( +
+
Layer 103 · quantum sweep
+

Auto-sweep a parameter, download a CSV

+

+ Runs the L101 experiment across a range of parameters, plots P(0) + and P(1) against the closed-form ideal, and exports the same column + shape as the offline Qiskit suite at quantum_alignment/results/. + Compare browser-sim curves with hardware curves directly. +

+ +
+ {KINDS.map((k) => ( + + ))} + + +
+ +
+
+
+ Steps + {steps} +
+ setSteps(parseInt(e.target.value, 10))} style={{ width: '100%' }} /> +
+ {kind !== 'noise' && ( +
+
+ Noise (fixed) + {noise.toFixed(2)} +
+ setNoise(parseFloat(e.target.value))} style={{ width: '100%' }} /> +
+ )} + {kind === 'noise' && ( +
+
+ θ (fixed) — phase circuit angle + {theta.toFixed(2)} +
+ setTheta(parseFloat(e.target.value))} style={{ width: '100%' }} /> +
+ )} + {kind === 'depth' && ( +
+
+ Max X·X depth + {maxDepth} +
+ setMaxDepth(parseInt(e.target.value, 10))} style={{ width: '100%' }} /> +
+ )} +
+ Shots: + {SHOTS_OPTIONS.map((s) => ( + + ))} +
+
+ + + + {summary && ( +
+
{summary.n} rows
+
avg error {summary.avgError.toFixed(3)}
+
max error {summary.maxError.toFixed(3)}
+
P(0) range {summary.rangeP0.toFixed(3)}
+
avg coherence {summary.avgCoherence.toFixed(0)}/100
+
+ )} + +

+ {kindMeta.help} CSV columns mirror the Qiskit suite where they overlap. +

+
+ ); +} diff --git a/brainsnn-r3f-app/src/utils/bellPair.js b/brainsnn-r3f-app/src/utils/bellPair.js new file mode 100644 index 0000000..4157985 --- /dev/null +++ b/brainsnn-r3f-app/src/utils/bellPair.js @@ -0,0 +1,237 @@ +/** + * Layer 102 — Bell Pair Lab utilities + * + * Two-qubit entanglement, browser-native. Builds the Bell state + * + * |Φ+⟩ = (|00⟩ + |11⟩) / √2 + * + * by running |00⟩ → H ⊗ I → CNOT(0,1), and lets the user rotate one of the + * qubits before measurement to see correlations break / persist. + * + * Convention: a 2-qubit state is a length-4 array of complex amplitudes, + * indexed as state[2*q1 + q0] for basis ket |q1 q0⟩. Qubit 0 is the + * "right" / least-significant bit. + * + * Reuses the complex helpers from quantumCoherence.js. Single-qubit gates + * are lifted into the 2-qubit space pair-by-pair to keep allocations tiny. + */ + +import { complex, mulComplex, abs2 } from './quantumCoherence.js'; + +const SQRT1_2 = 1 / Math.SQRT2; + +// ---------- 2-qubit state helpers ------------------------------------------ + +export function zeroPair() { + return [complex(1, 0), complex(0, 0), complex(0, 0), complex(0, 0)]; +} + +function cloneState(state) { + return state.map((z) => ({ re: z.re, im: z.im })); +} + +/** + * Apply a 2x2 matrix `[[m00, m01], [m10, m11]]` (each entry complex) to one + * qubit of a 2-qubit state. `qubit` is 0 or 1; we pair indices that differ + * only in that bit and apply the matrix to each pair. + */ +function applySingleQubit(state, qubit, m00, m01, m10, m11) { + const out = cloneState(state); + const stride = 1 << qubit; + for (let i = 0; i < 4; i += 1) { + if ((i & stride) !== 0) continue; + const j = i | stride; + const a = state[i]; + const b = state[j]; + // out[i] = m00*a + m01*b + out[i] = { + re: m00.re * a.re - m00.im * a.im + m01.re * b.re - m01.im * b.im, + im: m00.re * a.im + m00.im * a.re + m01.re * b.im + m01.im * b.re, + }; + // out[j] = m10*a + m11*b + out[j] = { + re: m10.re * a.re - m10.im * a.im + m11.re * b.re - m11.im * b.im, + im: m10.re * a.im + m10.im * a.re + m11.re * b.im + m11.im * b.re, + }; + } + return out; +} + +export function applyHadamard(state, qubit) { + const h = complex(SQRT1_2, 0); + const negH = complex(-SQRT1_2, 0); + return applySingleQubit(state, qubit, h, h, h, negH); +} + +export function applyPauliX(state, qubit) { + return applySingleQubit(state, qubit, complex(0, 0), complex(1, 0), complex(1, 0), complex(0, 0)); +} + +/** + * RY(θ) = [[cos(θ/2), -sin(θ/2)], [sin(θ/2), cos(θ/2)]] — real-valued, + * which is convenient because it produces measurement-axis rotations + * that the user can see in the joint distribution. + */ +export function applyRY(state, qubit, theta) { + const c = complex(Math.cos(theta / 2), 0); + const s = complex(Math.sin(theta / 2), 0); + const negS = complex(-s.re, 0); + return applySingleQubit(state, qubit, c, negS, s, c); +} + +/** RZ on a 2-qubit state (single qubit). */ +export function applyRZQubit(state, qubit, theta) { + const half = theta / 2; + const m00 = complex(Math.cos(-half), Math.sin(-half)); + const m11 = complex(Math.cos(half), Math.sin(half)); + return applySingleQubit(state, qubit, m00, complex(0, 0), complex(0, 0), m11); +} + +/** + * CNOT — controlled-X. Flips `target` iff `control` is |1⟩. + * Implemented as a permutation of amplitudes. + */ +export function applyCNOT(state, control, target) { + if (control === target) return cloneState(state); + const out = cloneState(state); + for (let i = 0; i < 4; i += 1) { + const cBit = (i >> control) & 1; + if (cBit === 1) { + const j = i ^ (1 << target); + if (j > i) { + const tmp = out[i]; + out[i] = out[j]; + out[j] = tmp; + } + } + } + return out; +} + +// ---------- joint measurement ---------------------------------------------- + +/** + * Returns the 4-element probability vector for outcomes + * |00⟩, |01⟩, |10⟩, |11⟩ (normalized). + */ +export function jointDistribution(state) { + const probs = state.map(abs2); + const total = probs.reduce((a, v) => a + v, 0) || 1; + return probs.map((p) => p / total); +} + +/** + * Sample `shots` joint measurements; returns counts [c00, c01, c10, c11]. + */ +export function sampleJointShots(distribution, shots) { + const total = Math.max(0, Math.floor(shots) || 0); + if (!total) return [0, 0, 0, 0]; + const counts = [0, 0, 0, 0]; + // Build cumulative thresholds. + const cum = []; + let acc = 0; + for (let i = 0; i < 4; i += 1) { + acc += distribution[i] ?? 0; + cum.push(acc); + } + for (let s = 0; s < total; s += 1) { + const r = Math.random(); + for (let i = 0; i < 4; i += 1) { + if (r < cum[i]) { + counts[i] += 1; + break; + } + } + } + return counts; +} + +/** + * Probability that the two qubits agree (both 0 or both 1). + * For a pure Bell state |Φ+⟩ this is 1; rotation breaks the correlation + * smoothly, which is the lesson of the panel. + */ +export function correlationStrength(distribution) { + const agree = (distribution[0] ?? 0) + (distribution[3] ?? 0); + const disagree = (distribution[1] ?? 0) + (distribution[2] ?? 0); + return agree - disagree; // -1 (anti-correlated) … 1 (correlated) +} + +// ---------- Bell pair experiment ------------------------------------------- + +/** + * Build the Bell state, optionally rotate qubit 0 by `rotationTheta` (RY) to + * preview correlation breakdown, and measure. + * + * |00⟩ → H ⊗ I → CNOT(0, 1) → RY(theta) on qubit 0 → joint measurement. + * + * Noise model: depolarizing-style. We mix the pure-state distribution with + * the uniform distribution by weight `noise` — i.e. with probability `noise` + * we pretend the qubits got randomized. This cleanly takes correlation from + * +1 (noise=0) toward 0 (noise=1), unlike a bit-flip channel that just + * toggles between perfectly correlated and perfectly anti-correlated. + */ +export function runBellPairExperiment({ + rotationTheta = 0, + shots = 1024, + noise = 0, +} = {}) { + let state = zeroPair(); + state = applyHadamard(state, 0); + state = applyCNOT(state, 0, 1); + if (rotationTheta !== 0) { + state = applyRY(state, 0, rotationTheta); + } + + let distribution = jointDistribution(state); + const n = Math.max(0, Math.min(1, Number(noise) || 0)); + if (n > 0) { + distribution = distribution.map((p) => (1 - n) * p + n * 0.25); + } + const counts = sampleJointShots(distribution, shots); + const correlation = correlationStrength(distribution); + return { + kind: 'bell', + rotationTheta, + shots, + noise: n, + distribution, + counts, + correlation, + finalState: state, + }; +} + +// ---------- brain mapping --------------------------------------------------- + +/** + * Bell-pair brain mapping — entanglement = strong inter-region binding. + * - HPC ↑ correlation (binding two threads = associative memory) + * - PFC ↑ |correlation| (executive lock-on to a coherent answer) + * - CTX ↑ shots / 4096 (more data = more cortex) + * - AMY ↑ noise + * - THL ↑ when |rotationTheta| > 0 (relay traffic between qubits) + * - BG ↑ dominance of the lead outcome + * - CBL ↑ noise * (1 - |correlation|) (correction work when binding fails) + */ +export function mapBellToBrainState(result) { + if (!result || !result.distribution) { + return { CTX: 0, HPC: 0, THL: 0, AMY: 0, BG: 0, PFC: 0, CBL: 0 }; + } + const corr = Math.max(-1, Math.min(1, result.correlation || 0)); + const noise = Math.max(0, Math.min(1, Number(result.noise) || 0)); + const rot = Math.abs(result.rotationTheta || 0); + const shots = Math.max(1, Number(result.shots) || 1); + const lead = Math.max(...result.distribution); + const dominance = Math.max(0, lead - 0.25); // 0 = uniform, 0.75 = pure + const clamp = (v) => Math.max(-1, Math.min(1, v)); + return { + HPC: clamp(corr * 0.6), + PFC: clamp(Math.abs(corr) * 0.55), + CTX: clamp(Math.min(1, Math.log10(shots) / 4) * 0.4), + AMY: clamp(noise * 0.5), + THL: clamp(Math.min(1, rot / Math.PI) * 0.45), + BG: clamp(dominance * 0.55), + CBL: clamp(noise * (1 - Math.abs(corr)) * 0.6), + }; +} diff --git a/brainsnn-r3f-app/src/utils/bellPair.test.mjs b/brainsnn-r3f-app/src/utils/bellPair.test.mjs new file mode 100644 index 0000000..1c934db --- /dev/null +++ b/brainsnn-r3f-app/src/utils/bellPair.test.mjs @@ -0,0 +1,117 @@ +/** + * Layer 102 — Bell Pair Lab tests. + * + * Run from brainsnn-r3f-app/ with: + * node --test src/utils/bellPair.test.mjs + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + zeroPair, + applyHadamard, + applyCNOT, + applyPauliX, + applyRY, + jointDistribution, + correlationStrength, + runBellPairExperiment, + mapBellToBrainState, +} from './bellPair.js'; + +test('zeroPair starts at |00>', () => { + const dist = jointDistribution(zeroPair()); + assert.ok(dist[0] > 0.999); + assert.ok(dist[1] < 0.001); + assert.ok(dist[2] < 0.001); + assert.ok(dist[3] < 0.001); +}); + +test('Hadamard on qubit 0 of |00> gives (|00>+|01>)/sqrt(2)', () => { + const state = applyHadamard(zeroPair(), 0); + const dist = jointDistribution(state); + assert.ok(Math.abs(dist[0] - 0.5) < 1e-9); + assert.ok(Math.abs(dist[1] - 0.5) < 1e-9); + assert.ok(Math.abs(dist[2]) < 1e-9); + assert.ok(Math.abs(dist[3]) < 1e-9); +}); + +test('CNOT(0,1) flips qubit 1 only when qubit 0 is 1', () => { + // |01> (qubit 0 = 1, qubit 1 = 0) -> |11> + let state = applyPauliX(zeroPair(), 0); + state = applyCNOT(state, 0, 1); + const dist = jointDistribution(state); + assert.ok(dist[3] > 0.999, `expected |11>, got ${JSON.stringify(dist)}`); +}); + +test('Bell state |Φ+> = H ⊗ I then CNOT gives 50/50 on |00> and |11>', () => { + let state = applyHadamard(zeroPair(), 0); + state = applyCNOT(state, 0, 1); + const dist = jointDistribution(state); + assert.ok(Math.abs(dist[0] - 0.5) < 1e-9); + assert.ok(Math.abs(dist[3] - 0.5) < 1e-9); + assert.ok(dist[1] < 1e-9); + assert.ok(dist[2] < 1e-9); +}); + +test('correlationStrength of Bell state is +1', () => { + let state = applyHadamard(zeroPair(), 0); + state = applyCNOT(state, 0, 1); + const dist = jointDistribution(state); + assert.ok(correlationStrength(dist) > 0.999); +}); + +test('rotation by pi/2 on Bell state breaks correlation toward 0', () => { + let state = applyHadamard(zeroPair(), 0); + state = applyCNOT(state, 0, 1); + state = applyRY(state, 0, Math.PI / 2); + const dist = jointDistribution(state); + const corr = correlationStrength(dist); + assert.ok(Math.abs(corr) < 0.05, `expected ~0 correlation after pi/2 rotation; got ${corr}`); +}); + +test('rotation by pi flips correlation to anti-correlated', () => { + let state = applyHadamard(zeroPair(), 0); + state = applyCNOT(state, 0, 1); + state = applyRY(state, 0, Math.PI); + const dist = jointDistribution(state); + // After RY(pi) on qubit 0 of (|00>+|11>)/sqrt(2): qubit 0 flips amplitude + // sign per branch — measurement now favors |10> and |01> (anti-correlated). + assert.ok(correlationStrength(dist) < -0.99, + `expected fully anti-correlated, got ${correlationStrength(dist)}`); +}); + +test('runBellPairExperiment with rotationTheta=0 noise=0 reports correlation=1', () => { + const res = runBellPairExperiment({ rotationTheta: 0, shots: 4096, noise: 0 }); + assert.equal(res.kind, 'bell'); + assert.ok(res.correlation > 0.999, `got ${res.correlation}`); + // counts should be split between [0] and [3], with [1] and [2] tiny + assert.ok(res.counts[0] + res.counts[3] === 4096); +}); + +test('runBellPairExperiment with high noise lowers correlation', () => { + const clean = runBellPairExperiment({ rotationTheta: 0, shots: 4096, noise: 0 }); + const noisy = runBellPairExperiment({ rotationTheta: 0, shots: 4096, noise: 0.9 }); + assert.ok(noisy.correlation < clean.correlation, + `noisy ${noisy.correlation} >= clean ${clean.correlation}`); +}); + +test('mapBellToBrainState: HPC rises with correlation', () => { + const correlated = mapBellToBrainState(runBellPairExperiment({ rotationTheta: 0, shots: 256, noise: 0 })); + const decorrelated = mapBellToBrainState(runBellPairExperiment({ rotationTheta: Math.PI / 2, shots: 256, noise: 0 })); + assert.ok(correlated.HPC > decorrelated.HPC, + `HPC: correlated ${correlated.HPC} should exceed decorrelated ${decorrelated.HPC}`); +}); + +test('mapBellToBrainState: AMY rises with noise', () => { + const clean = mapBellToBrainState(runBellPairExperiment({ rotationTheta: 0, shots: 256, noise: 0 })); + const noisy = mapBellToBrainState(runBellPairExperiment({ rotationTheta: 0, shots: 256, noise: 0.85 })); + assert.ok(noisy.AMY > clean.AMY); +}); + +test('all brain deltas are bounded to [-1, 1]', () => { + const res = runBellPairExperiment({ rotationTheta: Math.PI / 3, shots: 1024, noise: 0.4 }); + const deltas = mapBellToBrainState(res); + for (const [region, v] of Object.entries(deltas)) { + assert.ok(v >= -1 && v <= 1, `${region}=${v} out of range`); + } +}); diff --git a/brainsnn-r3f-app/src/utils/layerCatalog.js b/brainsnn-r3f-app/src/utils/layerCatalog.js index 110bd86..fd23169 100644 --- a/brainsnn-r3f-app/src/utils/layerCatalog.js +++ b/brainsnn-r3f-app/src/utils/layerCatalog.js @@ -107,6 +107,10 @@ export const LAYER_CATALOG = [ { id: 98, name: 'Theme + A11y', group: 'view', blurb: 'Dark/light, high-contrast, reduced-motion, font scale.' }, { id: 99, name: 'Federated Community Firewall', group: 'firewall', blurb: 'Weekly-rotated community rule pack.' }, { id: 100, name: 'Milestone Dashboard', group: 'view', blurb: '100 layers shipped — synthesis + personal stats.' }, + { id: 101, name: 'Quantum Coherence Lab', group: 'experimental', blurb: 'In-browser single-qubit phase / interference / decoherence sandbox.' }, + { id: 102, name: 'Bell Pair Lab', group: 'experimental', blurb: 'Two-qubit |Φ+⟩ entanglement + RY rotation + correlation strength.' }, + { id: 103, name: 'Quantum Sweep', group: 'experimental', blurb: 'Auto-sweep θ / noise / depth, plot vs ideal, download CSV.' }, + { id: 104, name: 'Quantum Glossary', group: 'experimental', blurb: 'Searchable glossary of every quantum term used in L101–L103.' }, ]; export const LAYER_GROUPS = { @@ -116,6 +120,7 @@ export const LAYER_GROUPS = { data: { label: 'Data & State', color: '#5ee69a' }, backend: { label: 'Backend & Agents', color: '#77dbe4' }, progression: { label: 'Progression', color: '#e57b40' }, + experimental: { label: 'Experimental Simulation', color: '#5ad4ff' }, }; export function searchLayers(query = '') { diff --git a/brainsnn-r3f-app/src/utils/quantumCoherence.js b/brainsnn-r3f-app/src/utils/quantumCoherence.js new file mode 100644 index 0000000..1ad192e --- /dev/null +++ b/brainsnn-r3f-app/src/utils/quantumCoherence.js @@ -0,0 +1,347 @@ +/** + * Layer 101 — Quantum Coherence Lab + * + * Browser-native, JavaScript-only simulation of a single qubit going through + * a tiny circuit: + * + * |0⟩ → H → RZ(θ) → H → M + * + * Goal: teach the physical mechanism behind "alignment" — superposition, + * phase, interference, observation, noise, and decoherence — without + * claiming any of the metaphors are literal physics. + * + * No backend, no IBM/OriginQ keys. The function shapes here are intentionally + * compatible with the Qiskit suite at /quantum_alignment/, so a future + * version can swap the local sampler for hardware (Aer noisy / IBM Quantum / + * OriginQ) without changing the panel. + * + * NOTHING in this file proves multiverse theory, consciousness collapse, + * Planck foam, or spiritual portals. The metaphor framing lives only in + * the panel UI, not in the math. + */ + +// ---------- complex helpers ------------------------------------------------- + +export function complex(re, im = 0) { + return { re, im }; +} + +export function addComplex(a, b) { + return { re: a.re + b.re, im: a.im + b.im }; +} + +export function mulComplex(a, b) { + return { + re: a.re * b.re - a.im * b.im, + im: a.re * b.im + a.im * b.re, + }; +} + +/** |z|^2 — squared magnitude. */ +export function abs2(z) { + return z.re * z.re + z.im * z.im; +} + +// ---------- single-qubit state --------------------------------------------- + +/** + * State is a 2-vector of complex amplitudes [α, β] for |0⟩ and |1⟩. + * normalizeState scales so |α|^2 + |β|^2 = 1 (or returns |0⟩ if zero). + */ +export function normalizeState(state) { + const total = abs2(state[0]) + abs2(state[1]); + if (!Number.isFinite(total) || total <= 1e-12) { + return [complex(1, 0), complex(0, 0)]; + } + const k = 1 / Math.sqrt(total); + return [ + complex(state[0].re * k, state[0].im * k), + complex(state[1].re * k, state[1].im * k), + ]; +} + +const SQRT1_2 = 1 / Math.SQRT2; + +/** Hadamard — creates / collapses superposition. */ +export function applyH(state) { + const a = state[0]; + const b = state[1]; + return [ + complex((a.re + b.re) * SQRT1_2, (a.im + b.im) * SQRT1_2), + complex((a.re - b.re) * SQRT1_2, (a.im - b.im) * SQRT1_2), + ]; +} + +/** Pauli-X — bit flip. */ +export function applyX(state) { + return [state[1], state[0]]; +} + +/** Pauli-Z — phase flip on |1⟩. */ +export function applyZ(state) { + return [state[0], complex(-state[1].re, -state[1].im)]; +} + +/** + * RZ(θ) — rotate phase. Standard form: diag(e^{-iθ/2}, e^{+iθ/2}). + * Global phase on |0⟩ is harmless for measurement; the *relative* phase + * between the two basis amplitudes is what matters for interference. + */ +export function applyRZ(state, theta) { + const half = theta / 2; + const c0 = complex(Math.cos(-half), Math.sin(-half)); + const c1 = complex(Math.cos(half), Math.sin(half)); + return [mulComplex(c0, state[0]), mulComplex(c1, state[1])]; +} + +// ---------- measurement ----------------------------------------------------- + +/** Returns [P(0), P(1)] for a normalized (or near-normalized) state. */ +export function measureDistribution(state) { + const norm = normalizeState(state); + const p0 = abs2(norm[0]); + const p1 = abs2(norm[1]); + const total = p0 + p1 || 1; + return [p0 / total, p1 / total]; +} + +/** + * Multinomial sample of `shots` measurements given a [P(0), P(1)] distribution. + * Uses Math.random; tests should compare proportions with tolerances rather + * than exact counts. + */ +export function sampleShots(distribution, shots) { + const total = Math.max(0, Math.floor(shots) || 0); + if (!total) return [0, 0]; + const p0 = Math.max(0, Math.min(1, distribution[0] ?? 0)); + let zeros = 0; + for (let i = 0; i < total; i += 1) { + if (Math.random() < p0) zeros += 1; + } + return [zeros, total - zeros]; +} + +// ---------- simple noise model --------------------------------------------- + +/** + * Dephasing damps the off-diagonal coherence. We model the state as a + * length-2 amplitude vector but simulate the qualitative effect of T2 + * dephasing by squeezing the imaginary part of β toward zero. This + * reproduces "lose interference fringe with noise" without a full + * density-matrix sim. + */ +function dephase(state, factor) { + const k = Math.max(0, Math.min(1, factor)); + if (k >= 0.999) return state; + return [ + state[0], + complex(state[1].re, state[1].im * k), + ]; +} + +/** + * Mix the ideal [P(0), P(1)] distribution with the uniform [0.5, 0.5] by + * weight `noise`. This is a depolarizing-style channel applied at the + * distribution level, which keeps tests deterministic in expectation — + * unlike a sampled bit-flip, which produces a single ±1 trajectory. + */ +function depolarizeDistribution(distribution, noise) { + const n = Math.max(0, Math.min(1, noise)); + if (n <= 0) return distribution; + const [p0, p1] = distribution; + return [(1 - n) * p0 + n * 0.5, (1 - n) * p1 + n * 0.5]; +} + +// ---------- experiments ----------------------------------------------------- + +/** + * H → RZ(θ) → H → M. + * + * Pure (noise = 0): + * θ = 0 → P(0) = 1 + * θ = π → P(1) = 1 + * θ = π/2 → P(0) = P(1) = 0.5 + * + * Higher noise damps the interference fringe, pushing both outcomes toward + * 0.5 regardless of θ. This is the "alignment" picture: phase coherence is + * the resource; noise is the enemy. + * + * Mirrors `phase_circuit(theta)` in /quantum_alignment/quantum_alignment_tests.py. + */ +export function runPhaseExperiment({ theta = 0, shots = 1024, noise = 0 } = {}) { + let state = [complex(1, 0), complex(0, 0)]; + state = applyH(state); + state = applyRZ(state, theta); + // Dephasing between RZ and final H damps the interference fringe. + state = dephase(state, Math.max(0, 1 - noise)); + state = applyH(state); + // Mix in uniform at the distribution level so noise=1 → 50/50 deterministically. + const distribution = depolarizeDistribution(measureDistribution(state), noise); + const counts = sampleShots(distribution, shots); + return { + kind: 'phase', + theta, + shots, + noise, + distribution, + counts, + finalState: state, + }; +} + +/** + * H → (optional middle measurement) → H → M. + * + * Without the middle measurement: H ∘ H = I, so |0⟩ stays |0⟩ → P(0) ≈ 1. + * With the middle measurement: superposition collapses, the second H + * spreads it back out → P(0) ≈ P(1) ≈ 0.5. + * + * Demonstrates "observation kills interference" — the quantum-Zeno / + * which-path lesson, without invoking consciousness. + * + * Mirrors `observation_circuit_a/b` in /quantum_alignment/. + */ +export function runObservationExperiment({ + shots = 1024, + observeMidway = false, + noise = 0, +} = {}) { + let state = [complex(1, 0), complex(0, 0)]; + state = applyH(state); + + if (observeMidway) { + const [p0] = measureDistribution(state); + const collapsed = Math.random() < p0 + ? [complex(1, 0), complex(0, 0)] + : [complex(0, 0), complex(1, 0)]; + state = collapsed; + } else { + // Apply mild dephasing instead — a "soft watch" that costs coherence. + state = dephase(state, Math.max(0, 1 - noise)); + } + + state = applyH(state); + + const distribution = depolarizeDistribution(measureDistribution(state), noise); + const counts = sampleShots(distribution, shots); + return { + kind: 'observation', + observeMidway, + shots, + noise, + distribution, + counts, + finalState: state, + }; +} + +/** + * Stack `xxPairs` X-X pairs in a row. Algebraically X·X = I, so a perfect + * machine returns P(0) = 1 regardless of depth. With noise, each gate is + * a chance to misfire, so deeper circuits decohere — which is the point. + * + * Mirrors `noise_depth_circuit(num_xx_pairs)` in /quantum_alignment/. + */ +export function runDecoherenceExperiment({ + xxPairs = 1, + shots = 1024, + noise = 0, +} = {}) { + const pairs = Math.max(0, Math.floor(xxPairs) || 0); + // Pure-state algebra: X·X = I, so the ideal distribution is always [1, 0]. + // Effective depolarization compounds per pair: 1 - (1 - noise)^pairs. + // This keeps the result deterministic in expectation while still showing + // depth-driven decoherence growth. + const ideal = [1, 0]; + const perPair = Math.max(0, Math.min(1, noise)); + const effective = pairs === 0 ? perPair : 1 - Math.pow(1 - perPair, pairs); + const distribution = depolarizeDistribution(ideal, effective); + const counts = sampleShots(distribution, shots); + return { + kind: 'decoherence', + xxPairs: pairs, + shots, + noise, + distribution, + counts, + }; +} + +// ---------- coherence score ------------------------------------------------- + +/** + * 0 – 100 score capturing how much "quantum-ness" survived the run. + * - More noise lowers the score. + * - Deeper circuits lower the score. + * - A midway measurement nukes the score (you broke the wavefunction). + * + * Pure determinism (no Math.random) so the UI is stable. + */ +export function coherenceScore({ + noise = 0, + depth = 1, + observedMidway = false, +} = {}) { + const n = Math.max(0, Math.min(1, Number(noise) || 0)); + const d = Math.max(1, Math.floor(Number(depth) || 1)); + const observationPenalty = observedMidway ? 0.5 : 0; + const depthFactor = Math.exp(-0.18 * (d - 1)); + const noiseFactor = 1 - n * 0.85; + const raw = depthFactor * noiseFactor - observationPenalty; + const clamped = Math.max(0, Math.min(1, raw)); + return Math.round(clamped * 100); +} + +// ---------- brain mapping --------------------------------------------------- + +/** + * Convert a quantum experiment result into per-region brain deltas. + * Returns a simple { REGION: delta } object — caller decides how strongly + * to nudge state.regions (typical pattern is `delta * 0.3`). + * + * - PFC ↑ with coherenceScore (alignment / executive coherence) + * - AMY ↑ with noise (the system is rattled) + * - THL ↑ with signal routing (more shots = more relay traffic) + * - HPC ↑ when prior state survives (low depth + low noise = memory holds) + * - BG ↑ when one outcome dominates (decisive gating) + * - CBL ↑ when error / noise correction is high (cerebellum tunes timing) + * - CTX ↑ with experiment complexity (more gates = more cortical work) + * + * Deltas are bounded to [-1, 1]; consumers should scale further. + */ +export function mapQuantumToBrainState(result) { + if (!result || !result.distribution) { + return { CTX: 0, HPC: 0, THL: 0, AMY: 0, BG: 0, PFC: 0, CBL: 0 }; + } + const noise = Math.max(0, Math.min(1, Number(result.noise) || 0)); + const shots = Math.max(1, Number(result.shots) || 1); + const depth = Math.max(1, Number(result.xxPairs ?? 1)); + const observedMidway = !!result.observeMidway; + + const score = coherenceScore({ noise, depth, observedMidway }) / 100; + + const [p0, p1] = result.distribution; + const dominance = Math.abs((p0 ?? 0) - (p1 ?? 0)); + const survival = (p0 ?? 0); + + const routing = Math.min(1, Math.log10(shots) / 4); + const complexity = Math.min( + 1, + (result.kind === 'decoherence' ? depth / 10 : 0.35) + + (result.kind === 'observation' && observedMidway ? 0.15 : 0) + + (result.kind === 'phase' ? 0.4 : 0), + ); + const correction = noise * (1 - score); + + const clamp = (v) => Math.max(-1, Math.min(1, v)); + + return { + PFC: clamp(score * 0.6), + AMY: clamp(noise * 0.5), + THL: clamp(routing * 0.45), + HPC: clamp(survival * 0.5 * (1 - noise * 0.5)), + BG: clamp(dominance * 0.5), + CBL: clamp(correction * 0.6), + CTX: clamp(complexity * 0.5), + }; +} diff --git a/brainsnn-r3f-app/src/utils/quantumCoherence.test.mjs b/brainsnn-r3f-app/src/utils/quantumCoherence.test.mjs new file mode 100644 index 0000000..cf226a4 --- /dev/null +++ b/brainsnn-r3f-app/src/utils/quantumCoherence.test.mjs @@ -0,0 +1,171 @@ +/** + * Layer 101 — Quantum Coherence Lab tests. + * + * Run from brainsnn-r3f-app/ with: + * node --test src/utils/quantumCoherence.test.mjs + * + * Uses Node's built-in test runner (Node 18+). No extra dev deps. + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + runPhaseExperiment, + runObservationExperiment, + runDecoherenceExperiment, + coherenceScore, + measureDistribution, + applyH, + applyX, + applyZ, + applyRZ, + complex, + abs2, + normalizeState, + mapQuantumToBrainState, +} from './quantumCoherence.js'; + +const SHOTS = 4096; + +test('phase experiment with theta=0 returns high P(0)', () => { + const res = runPhaseExperiment({ theta: 0, shots: SHOTS, noise: 0 }); + assert.equal(res.kind, 'phase'); + assert.ok( + res.distribution[0] > 0.95, + `expected P(0) > 0.95, got ${res.distribution[0]}`, + ); +}); + +test('phase experiment with theta=pi returns high P(1)', () => { + const res = runPhaseExperiment({ theta: Math.PI, shots: SHOTS, noise: 0 }); + assert.ok( + res.distribution[1] > 0.95, + `expected P(1) > 0.95, got ${res.distribution[1]}`, + ); +}); + +test('phase experiment at theta=pi/2 splits ~50/50', () => { + const res = runPhaseExperiment({ theta: Math.PI / 2, shots: SHOTS, noise: 0 }); + assert.ok( + Math.abs(res.distribution[0] - 0.5) < 0.05, + `expected P(0) ~ 0.5, got ${res.distribution[0]}`, + ); +}); + +test('observation experiment with observeMidway=false returns high P(0)', () => { + // H ∘ H = I, so |0⟩ stays |0⟩. + const res = runObservationExperiment({ shots: SHOTS, observeMidway: false, noise: 0 }); + assert.ok( + res.distribution[0] > 0.95, + `expected P(0) > 0.95 with no midway observation, got ${res.distribution[0]}`, + ); +}); + +test('observation experiment with observeMidway=true is more random', () => { + // Average over runs since the midway measurement is stochastic. + let p0Sum = 0; + const trials = 40; + for (let i = 0; i < trials; i += 1) { + const r = runObservationExperiment({ shots: 256, observeMidway: true, noise: 0 }); + p0Sum += r.distribution[0]; + } + const avgP0 = p0Sum / trials; + assert.ok( + avgP0 > 0.35 && avgP0 < 0.65, + `expected avg P(0) ~ 0.5 after midway measurement, got ${avgP0.toFixed(3)}`, + ); +}); + +test('increasing noise lowers coherenceScore', () => { + const low = coherenceScore({ noise: 0, depth: 1, observedMidway: false }); + const mid = coherenceScore({ noise: 0.5, depth: 1, observedMidway: false }); + const high = coherenceScore({ noise: 1, depth: 1, observedMidway: false }); + assert.ok(low > mid, `expected ${low} > ${mid}`); + assert.ok(mid > high, `expected ${mid} > ${high}`); +}); + +test('increasing depth lowers coherenceScore', () => { + const shallow = coherenceScore({ noise: 0.1, depth: 1, observedMidway: false }); + const deeper = coherenceScore({ noise: 0.1, depth: 5, observedMidway: false }); + const deepest = coherenceScore({ noise: 0.1, depth: 12, observedMidway: false }); + assert.ok(shallow > deeper, `expected ${shallow} > ${deeper}`); + assert.ok(deeper > deepest, `expected ${deeper} > ${deepest}`); +}); + +test('observing midway dings coherenceScore', () => { + const watched = coherenceScore({ noise: 0, depth: 1, observedMidway: true }); + const unwatched = coherenceScore({ noise: 0, depth: 1, observedMidway: false }); + assert.ok(unwatched > watched, `expected ${unwatched} > ${watched}`); +}); + +test('decoherence experiment ideal (noise=0) returns ~P(0)=1', () => { + const res = runDecoherenceExperiment({ xxPairs: 8, shots: SHOTS, noise: 0 }); + assert.ok( + res.distribution[0] > 0.99, + `X·X pairs are identity at noise=0; got P(0)=${res.distribution[0]}`, + ); +}); + +test('decoherence experiment with noise lowers P(0) vs ideal', () => { + const ideal = runDecoherenceExperiment({ xxPairs: 6, shots: SHOTS, noise: 0 }); + const noisy = runDecoherenceExperiment({ xxPairs: 6, shots: SHOTS, noise: 0.7 }); + assert.ok( + noisy.distribution[0] < ideal.distribution[0], + `expected noisy P(0) < ideal P(0); got ${noisy.distribution[0]} vs ${ideal.distribution[0]}`, + ); +}); + +test('Hadamard on |0> gives equal probabilities', () => { + const state = applyH([complex(1, 0), complex(0, 0)]); + const [p0, p1] = measureDistribution(state); + assert.ok(Math.abs(p0 - 0.5) < 1e-9); + assert.ok(Math.abs(p1 - 0.5) < 1e-9); +}); + +test('Pauli-X flips |0> to |1>', () => { + const state = applyX([complex(1, 0), complex(0, 0)]); + const [p0, p1] = measureDistribution(state); + assert.ok(p1 > 0.999); + assert.ok(p0 < 0.001); +}); + +test('Pauli-Z preserves probabilities (phase only)', () => { + const start = applyH([complex(1, 0), complex(0, 0)]); + const after = applyZ(start); + const before = measureDistribution(start); + const post = measureDistribution(after); + assert.ok(Math.abs(before[0] - post[0]) < 1e-9); +}); + +test('RZ(2π) acts as identity on probabilities', () => { + const start = applyH([complex(1, 0), complex(0, 0)]); + const after = applyRZ(start, Math.PI * 2); + assert.ok(Math.abs(abs2(start[0]) - abs2(after[0])) < 1e-9); + assert.ok(Math.abs(abs2(start[1]) - abs2(after[1])) < 1e-9); +}); + +test('normalizeState normalizes near-zero state to |0>', () => { + const norm = normalizeState([complex(0, 0), complex(0, 0)]); + assert.equal(norm[0].re, 1); + assert.equal(norm[1].re, 0); +}); + +test('mapQuantumToBrainState returns 7 region deltas in [-1, 1]', () => { + const res = runPhaseExperiment({ theta: 0, shots: 1024, noise: 0 }); + const deltas = mapQuantumToBrainState(res); + for (const region of ['CTX', 'HPC', 'THL', 'AMY', 'BG', 'PFC', 'CBL']) { + assert.ok(region in deltas, `missing region ${region}`); + assert.ok(deltas[region] >= -1 && deltas[region] <= 1, `${region} out of range: ${deltas[region]}`); + } +}); + +test('mapQuantumToBrainState: AMY rises with noise', () => { + const clean = mapQuantumToBrainState(runPhaseExperiment({ theta: 0, shots: 256, noise: 0 })); + const noisy = mapQuantumToBrainState(runPhaseExperiment({ theta: 0, shots: 256, noise: 0.9 })); + assert.ok(noisy.AMY > clean.AMY, `expected noisy AMY > clean AMY; got ${noisy.AMY} vs ${clean.AMY}`); +}); + +test('mapQuantumToBrainState: PFC rises with coherence (clean run > noisy run)', () => { + const clean = mapQuantumToBrainState(runPhaseExperiment({ theta: 0, shots: 256, noise: 0 })); + const noisy = mapQuantumToBrainState(runPhaseExperiment({ theta: 0, shots: 256, noise: 0.95 })); + assert.ok(clean.PFC > noisy.PFC, `expected clean PFC > noisy PFC; got ${clean.PFC} vs ${noisy.PFC}`); +}); diff --git a/brainsnn-r3f-app/src/utils/quantumSweep.js b/brainsnn-r3f-app/src/utils/quantumSweep.js new file mode 100644 index 0000000..acdb3f2 --- /dev/null +++ b/brainsnn-r3f-app/src/utils/quantumSweep.js @@ -0,0 +1,202 @@ +/** + * Layer 103 — Quantum Sweep + * + * Automates parameter sweeps over the L101 single-qubit experiment family + * and emits a CSV with the same column shape as the offline Qiskit suite at + * /quantum_alignment/results/results.csv. The point: a user can run a + * sweep in the browser, download the CSV, and compare it directly against + * the Qiskit ideal/noisy/real CSV for the same parameters. + * + * Three sweep kinds: + * - 'phase' — sweeps θ ∈ [0, π] for fixed noise / shots + * - 'noise' — sweeps noise ∈ [0, 1] for fixed θ / shots + * - 'depth' — sweeps X·X pair count ∈ [0, maxDepth] for fixed + * noise / shots (decoherence experiment) + * + * Pure JS, no DOM, deterministic-ish (uses Math.random for shot sampling). + */ + +import { + runPhaseExperiment, + runDecoherenceExperiment, + coherenceScore, +} from './quantumCoherence.js'; + +// ---------- sweep ----------------------------------------------------------- + +function linspace(start, stop, count) { + if (count <= 1) return [start]; + const step = (stop - start) / (count - 1); + const out = []; + for (let i = 0; i < count; i += 1) out.push(start + step * i); + return out; +} + +function rangeInts(start, stop, count) { + // inclusive, integer-snapped, evenly spaced. + if (count <= 1) return [Math.round(start)]; + const out = new Set(); + const step = (stop - start) / (count - 1); + for (let i = 0; i < count; i += 1) { + out.add(Math.max(0, Math.round(start + step * i))); + } + return Array.from(out).sort((a, b) => a - b); +} + +/** + * Run a sweep. `kind` is 'phase' | 'noise' | 'depth'. Returns an array of + * row objects. Each row carries enough context to reproduce: parameter, + * shots, noise, distribution, counts, expected probability for the ideal + * outcome where one is well-defined. + */ +export function runSweep({ + kind = 'phase', + shots = 1024, + steps = 9, + noise = 0, + theta = 0, + maxDepth = 12, +} = {}) { + const rows = []; + if (kind === 'phase') { + for (const t of linspace(0, Math.PI, steps)) { + const res = runPhaseExperiment({ theta: t, shots, noise }); + const expectedP0 = Math.cos(t / 2) ** 2; + rows.push({ + kind, + parameter: t, + parameterLabel: `θ=${t.toFixed(3)}`, + shots, + noise, + p0: res.distribution[0], + p1: res.distribution[1], + counts0: res.counts[0], + counts1: res.counts[1], + expectedP0, + error: Math.abs(res.distribution[0] - expectedP0), + coherence: coherenceScore({ noise, depth: 1, observedMidway: false }), + }); + } + return rows; + } + if (kind === 'noise') { + for (const n of linspace(0, 1, steps)) { + const res = runPhaseExperiment({ theta, shots, noise: n }); + const expectedP0 = Math.cos(theta / 2) ** 2; + rows.push({ + kind, + parameter: n, + parameterLabel: `noise=${n.toFixed(2)}`, + shots, + noise: n, + p0: res.distribution[0], + p1: res.distribution[1], + counts0: res.counts[0], + counts1: res.counts[1], + expectedP0, + error: Math.abs(res.distribution[0] - expectedP0), + coherence: coherenceScore({ noise: n, depth: 1, observedMidway: false }), + }); + } + return rows; + } + if (kind === 'depth') { + for (const d of rangeInts(0, maxDepth, steps)) { + const res = runDecoherenceExperiment({ xxPairs: d, shots, noise }); + rows.push({ + kind, + parameter: d, + parameterLabel: `xx=${d}`, + shots, + noise, + p0: res.distribution[0], + p1: res.distribution[1], + counts0: res.counts[0], + counts1: res.counts[1], + expectedP0: 1.0, + error: Math.abs(res.distribution[0] - 1.0), + coherence: coherenceScore({ noise, depth: d + 1, observedMidway: false }), + }); + } + return rows; + } + throw new Error(`Unknown sweep kind: ${kind}`); +} + +// ---------- CSV serialization ---------------------------------------------- + +/** + * CSV columns mirror the Qiskit suite's results.csv where they overlap. + * Extra columns (counts0/counts1/coherence) are appended. + */ +export const SWEEP_CSV_COLUMNS = [ + 'kind', + 'parameter', + 'parameter_label', + 'shots', + 'noise', + 'p0', + 'p1', + 'counts0', + 'counts1', + 'expected_p0', + 'error', + 'coherence_score', +]; + +function csvEscape(value) { + if (value == null) return ''; + const str = typeof value === 'number' ? String(value) : String(value); + if (/[",\n]/.test(str)) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; +} + +export function rowsToCsv(rows) { + const lines = [SWEEP_CSV_COLUMNS.join(',')]; + for (const r of rows) { + lines.push([ + r.kind, + typeof r.parameter === 'number' ? r.parameter.toFixed(6) : r.parameter, + r.parameterLabel, + r.shots, + typeof r.noise === 'number' ? r.noise.toFixed(4) : r.noise, + r.p0?.toFixed(6) ?? '', + r.p1?.toFixed(6) ?? '', + r.counts0, + r.counts1, + r.expectedP0?.toFixed(6) ?? '', + r.error?.toFixed(6) ?? '', + r.coherence, + ].map(csvEscape).join(',')); + } + return lines.join('\n'); +} + +// ---------- summary stats --------------------------------------------------- + +export function sweepSummary(rows) { + if (!rows.length) return null; + let maxError = 0; + let avgError = 0; + let minP0 = Infinity; + let maxP0 = -Infinity; + let avgCoherence = 0; + for (const r of rows) { + if (r.error > maxError) maxError = r.error; + avgError += r.error; + if (r.p0 < minP0) minP0 = r.p0; + if (r.p0 > maxP0) maxP0 = r.p0; + avgCoherence += r.coherence; + } + return { + n: rows.length, + maxError: maxError, + avgError: avgError / rows.length, + minP0, + maxP0, + rangeP0: maxP0 - minP0, + avgCoherence: avgCoherence / rows.length, + }; +} diff --git a/brainsnn-r3f-app/src/utils/quantumSweep.test.mjs b/brainsnn-r3f-app/src/utils/quantumSweep.test.mjs new file mode 100644 index 0000000..9827fa9 --- /dev/null +++ b/brainsnn-r3f-app/src/utils/quantumSweep.test.mjs @@ -0,0 +1,86 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + runSweep, + rowsToCsv, + sweepSummary, + SWEEP_CSV_COLUMNS, +} from './quantumSweep.js'; + +test('phase sweep produces `steps` rows spanning 0..pi', () => { + const rows = runSweep({ kind: 'phase', shots: 1024, steps: 5, noise: 0 }); + assert.equal(rows.length, 5); + assert.ok(Math.abs(rows[0].parameter - 0) < 1e-9); + assert.ok(Math.abs(rows[rows.length - 1].parameter - Math.PI) < 1e-9); +}); + +test('phase sweep matches cos^2(theta/2) at theta=0 and theta=pi (noise=0)', () => { + const rows = runSweep({ kind: 'phase', shots: 4096, steps: 5, noise: 0 }); + const first = rows[0]; + const last = rows[rows.length - 1]; + assert.ok(first.error < 0.05, `theta=0 error too high: ${first.error}`); + assert.ok(last.error < 0.05, `theta=pi error too high: ${last.error}`); +}); + +test('noise sweep increasing degrades coherence monotonically', () => { + const rows = runSweep({ kind: 'noise', shots: 1024, steps: 6, theta: 0 }); + for (let i = 1; i < rows.length; i += 1) { + assert.ok( + rows[i].coherence <= rows[i - 1].coherence, + `coherence not monotone: rows[${i - 1}]=${rows[i - 1].coherence} rows[${i}]=${rows[i].coherence}`, + ); + } +}); + +test('depth sweep produces integer parameters in non-decreasing order', () => { + const rows = runSweep({ kind: 'depth', shots: 1024, steps: 5, noise: 0.1, maxDepth: 12 }); + for (let i = 1; i < rows.length; i += 1) { + assert.ok(rows[i].parameter >= rows[i - 1].parameter); + assert.ok(Number.isInteger(rows[i].parameter)); + } +}); + +test('rowsToCsv emits the expected header and one line per row', () => { + const rows = runSweep({ kind: 'phase', shots: 256, steps: 3, noise: 0 }); + const csv = rowsToCsv(rows); + const lines = csv.split('\n'); + assert.equal(lines[0], SWEEP_CSV_COLUMNS.join(',')); + assert.equal(lines.length, rows.length + 1); +}); + +test('CSV cells with commas are properly escaped', () => { + // forge a row with a comma in a string field + const rows = [{ + kind: 'phase', + parameter: 0.5, + parameterLabel: 'theta=0.5,foo', + shots: 256, + noise: 0, + p0: 0.5, + p1: 0.5, + counts0: 128, + counts1: 128, + expectedP0: 0.5, + error: 0, + coherence: 100, + }]; + const csv = rowsToCsv(rows); + assert.ok(csv.includes('"theta=0.5,foo"'), + `expected quoted comma; got: ${csv}`); +}); + +test('sweepSummary computes max / avg / range', () => { + const rows = runSweep({ kind: 'phase', shots: 1024, steps: 7, noise: 0 }); + const s = sweepSummary(rows); + assert.equal(s.n, 7); + assert.ok(s.rangeP0 > 0.5, `expected wide P(0) range across phase sweep, got ${s.rangeP0}`); + assert.ok(s.maxError >= 0); +}); + +test('sweepSummary returns null on empty input', () => { + assert.equal(sweepSummary([]), null); +}); + +test('unknown sweep kind throws', () => { + assert.throws(() => runSweep({ kind: 'mystery', steps: 3 })); +}); diff --git a/quantum_alignment/README.md b/quantum_alignment/README.md new file mode 100644 index 0000000..0304dc4 --- /dev/null +++ b/quantum_alignment/README.md @@ -0,0 +1,132 @@ +# Quantum Alignment Tests + +A small Qiskit suite that probes the *physical* meaning of "alignment" in +quantum computing — phase coherence, observation collapse, and decoherence — +across an ideal simulator, a noisy simulator, and (optionally) real IBM +Quantum hardware. + +> **Important framing.** "Multiverse alignment" is used here as a metaphor for +> *coherent quantum phase relationships and the interference patterns they +> produce*. This suite does **not** test, prove, or disprove literal parallel +> universes or the many-worlds interpretation. See [What this is **not**](#what-this-is-not). + +## What is being tested + +| # | Experiment | What it probes | +|---|---|---| +| 1 | Phase alignment / interference | `|0> → H → RZ(θ) → H → Measure`. Sweeps θ ∈ {0, π/8, π/4, π/2, 3π/4, π}. Ideal `p(0) = cos²(θ/2)`. | +| 2 | Observation collapse | Compares `H · H` (no mid-circuit measurement) against `H · Measure · H · Measure`. Mid-circuit measurement should destroy interference and push the final readout toward 50/50. | +| 3 | Decoherence vs. depth | `|0> → H → (X X)ⁿ → H → Measure` with n ∈ {0, 1, 2, 4, 8, 16, 32, 64}. Each `X X` is logical identity, so the ideal output is `p(0) = 1` for all n; any drift is gate noise + decoherence. | + +## Setup + +```bash +git clone +cd quantum_alignment + +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +`qiskit-ibm-runtime` is listed in `requirements.txt` for completeness, but the +ideal and noisy modes only need `qiskit`, `qiskit-aer`, `matplotlib`, and +`numpy`. The runtime package is imported lazily, only when you ask for +`--mode real`. + +## Running + +All three modes write the same artifacts to `./results/`: + +- `results.csv` — one row per (experiment, parameter) with raw probabilities and error +- `phase_alignment_plot.png` — Experiment 1 plot +- `noise_depth_plot.png` — Experiment 3 plot +- `report.md` — plain-English explanation of what the data shows + +### Ideal local simulator (no token required) + +```bash +python quantum_alignment_tests.py --mode ideal --shots 4096 +``` + +### Noisy simulator (no token required) + +Uses `qiskit_aer.AerSimulator.from_backend(GenericBackendV2(...))` so that a +realistic noise model is applied without contacting IBM: + +```bash +python quantum_alignment_tests.py --mode noisy --shots 4096 +``` + +### Real IBM Quantum hardware + +You need an IBM Quantum account and an API token. **Never hard-code the +token** and never commit it to git. The script reads it from the +`IBM_QUANTUM_TOKEN` environment variable. + +Recommended ways to set it safely: + +```bash +# Option A: prompt yourself, do not echo, do not store in shell history +read -s IBM_QUANTUM_TOKEN +export IBM_QUANTUM_TOKEN +python quantum_alignment_tests.py --mode real + +# Option B: a per-project .envrc loaded by direnv (add .envrc to .gitignore!) +echo 'export IBM_QUANTUM_TOKEN="$(security find-generic-password -s ibm-quantum -w)"' > .envrc +direnv allow + +# Option C: a system keyring / secret manager (1Password, macOS Keychain, etc.) +export IBM_QUANTUM_TOKEN="$(op read 'op://Personal/IBM Quantum/credential')" +python quantum_alignment_tests.py --mode real +``` + +To pin a specific backend (otherwise the script uses `least_busy`): + +```bash +python quantum_alignment_tests.py --mode real --backend ibm_brisbane +``` + +If your selected backend does not support mid-circuit measurement, pass +`--skip-observation` and run Experiment 2 separately on the simulator. The +script also gracefully records a "skipped" row in `results.csv` for any +mid-measure circuit it could not execute. + +## CLI reference + +``` +--mode {ideal,noisy,real} execution backend (default: ideal) +--backend NAME real-mode backend name (default: least_busy) +--shots N shots per circuit (default: 4096) +--seed N deterministic seed for noisy mode (default: 42) +--out PATH output directory (default: ./results) +--skip-observation skip Experiment 2 (for backends with no mid-circuit measure) +``` + +## What this is **not** + +- **Not a test of literal parallel universes.** Quantum mechanics is + interpretation-agnostic; no single-qubit experiment can settle the + many-worlds vs. Copenhagen vs. relational debate. +- **Not a hardware benchmark.** A handful of circuits at a few thousand shots + cannot replace published quantum-volume / EPLG / RB numbers. +- **Not a randomness or cryptographic source.** The bits returned here are + experimental data, not certified entropy. + +## What this **is** + +- A small, reproducible demo that quantum amplitudes have phase, that those + phases interfere, that observation destroys interference, and that real + hardware loses coherence as circuit depth grows. +- A scaffold you can extend with more circuits (entanglement, GHZ, Bell + inequality, randomized benchmarking, …) using the same backend / CSV / + report scaffolding. + +## Security notes + +- The token is **only** read from `IBM_QUANTUM_TOKEN` at runtime. The script + does not log it, does not write it to disk, and does not include it in any + output file or plot. +- `requirements.txt` pins minimum versions but does not freeze; in a + production setting you should `pip freeze > requirements.lock` and check the + lock file in. diff --git a/quantum_alignment/quantum_alignment_tests.py b/quantum_alignment/quantum_alignment_tests.py new file mode 100644 index 0000000..8061adb --- /dev/null +++ b/quantum_alignment/quantum_alignment_tests.py @@ -0,0 +1,708 @@ +"""Quantum alignment test suite. + +Runs three experiments that probe the *physical* meaning of "alignment" in a +quantum computer: + +1. Phase alignment / interference (single-qubit Mach-Zehnder via H-RZ-H) +2. Observation collapse (measurement in the middle of a circuit) +3. Decoherence / noise accumulation (deepening identity-equivalent X-X chains) + +This is *not* a test of literal parallel universes. "Multiverse alignment" is +treated here as a metaphor for coherent quantum phase relationships and the +interference patterns they produce. + +Three execution modes are supported: + + --mode ideal AerSimulator with no noise. + --mode noisy AerSimulator with a noise model derived from a synthetic + IBM-like fake backend (no IBM account or token required). + --mode real Real IBM Quantum hardware via qiskit-ibm-runtime. Requires + the IBM_QUANTUM_TOKEN environment variable. + +Outputs (under --out, default ./results): + + results.csv raw counts and probabilities for every shot batch + phase_alignment_plot.png p(0)/p(1)/error vs theta (Experiment 1) + noise_depth_plot.png p(0)/p(1)/error vs X-X depth (Experiment 3) + report.md plain-English summary of what the data shows +""" + +from __future__ import annotations + +import argparse +import csv +import math +import os +import sys +from dataclasses import dataclass, field +from typing import Callable, Iterable + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +from qiskit import QuantumCircuit, transpile +from qiskit_aer import AerSimulator + + +# --------------------------------------------------------------------------- +# Backend selection +# --------------------------------------------------------------------------- + + +@dataclass +class Backend: + """Bundle of (label, sampler) used by the rest of the script.""" + + label: str + mode: str + run: Callable[[QuantumCircuit, int], dict[str, int]] + supports_mid_measure: bool = True + notes: list[str] = field(default_factory=list) + + +def make_ideal_backend() -> Backend: + sim = AerSimulator() + + def run(circuit: QuantumCircuit, shots: int) -> dict[str, int]: + tqc = transpile(circuit, sim) + result = sim.run(tqc, shots=shots).result() + return result.get_counts() + + return Backend(label="aer-ideal", mode="ideal", run=run) + + +def make_noisy_backend(seed: int = 42) -> Backend: + """Noisy AerSimulator built from a synthetic IBM-like fake backend. + + We use ``qiskit.providers.fake_provider.GenericBackendV2`` because it ships + with qiskit core and does not require ``qiskit-ibm-runtime`` (which would + in turn require the IBM cloud SDK). ``AerSimulator.from_backend`` extracts + a noise model from the fake backend's calibration data. + """ + + from qiskit.providers.fake_provider import GenericBackendV2 + + fake = GenericBackendV2(num_qubits=5, seed=seed) + sim = AerSimulator.from_backend(fake) + + def run(circuit: QuantumCircuit, shots: int) -> dict[str, int]: + tqc = transpile(circuit, sim, optimization_level=0, seed_transpiler=seed) + result = sim.run(tqc, shots=shots).result() + return result.get_counts() + + return Backend( + label=f"aer-noisy({fake.name})", + mode="noisy", + run=run, + notes=[ + "Noise model derived from synthetic fake backend " + f"{fake.name!r} (qiskit GenericBackendV2).", + ], + ) + + +def make_real_backend(token: str, backend_name: str | None) -> Backend: + """Real IBM Quantum backend via qiskit-ibm-runtime. + + ``token`` is *only* used to instantiate the service; it is never logged or + written to disk by this script. + """ + + # Lazy import: keeps simulator-only runs working even if qiskit-ibm-runtime + # (and its IBM cloud SDK dependency tree) is not importable. + from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 + + service = QiskitRuntimeService(channel="ibm_quantum", token=token) + if backend_name: + backend = service.backend(backend_name) + else: + backend = service.least_busy(operational=True, simulator=False) + + sampler = SamplerV2(mode=backend) + + def run(circuit: QuantumCircuit, shots: int) -> dict[str, int]: + tqc = transpile(circuit, backend=backend, optimization_level=1) + job = sampler.run([tqc], shots=shots) + result = job.result() + # SamplerV2 result -> per-circuit PubResult; default classical register + # is named "meas" when QuantumCircuit.measure_all is used and "c" when + # we declared the register ourselves below. + pub = result[0] + data = pub.data + register_name = next(iter(data.__dict__)) if hasattr(data, "__dict__") else "c" + bit_array = getattr(data, register_name) + return bit_array.get_counts() + + return Backend( + label=f"ibm-real({backend.name})", + mode="real", + run=run, + # Most modern IBM backends support mid-circuit measurement, but + # individual devices vary. Caller can override via CLI if needed. + supports_mid_measure=True, + notes=[f"Running on IBM backend {backend.name!r}."], + ) + + +# --------------------------------------------------------------------------- +# Circuit builders +# --------------------------------------------------------------------------- + + +def phase_circuit(theta: float) -> QuantumCircuit: + """|0> -> H -> RZ(theta) -> H -> Measure.""" + qc = QuantumCircuit(1, 1, name=f"phase_{theta:.4f}") + qc.h(0) + qc.rz(theta, 0) + qc.h(0) + qc.measure(0, 0) + return qc + + +def observation_circuit_a() -> QuantumCircuit: + """A: |0> -> H -> H -> Measure (no mid-circuit observation).""" + qc = QuantumCircuit(1, 1, name="obs_A") + qc.h(0) + qc.h(0) + qc.measure(0, 0) + return qc + + +def observation_circuit_b() -> QuantumCircuit: + """B: |0> -> H -> Measure -> H -> Measure (mid-circuit collapse).""" + qc = QuantumCircuit(1, 2, name="obs_B") + qc.h(0) + qc.measure(0, 0) # collapse + qc.h(0) + qc.measure(0, 1) # final readout + return qc + + +def noise_depth_circuit(num_xx_pairs: int) -> QuantumCircuit: + """|0> -> H -> (X X) * n -> H -> Measure. + + Each X-X pair is the identity, so the *ideal* output is always |0>. Any + drift away from p(0)=1 on real or noisy hardware is gate noise + decoherence. + """ + qc = QuantumCircuit(1, 1, name=f"noise_{num_xx_pairs}") + qc.h(0) + for _ in range(num_xx_pairs): + qc.x(0) + qc.x(0) + qc.barrier() + qc.h(0) + qc.measure(0, 0) + return qc + + +# --------------------------------------------------------------------------- +# Counts analysis +# --------------------------------------------------------------------------- + + +def _last_bit_counts(counts: dict[str, int]) -> tuple[int, int]: + """Sum counts of the *final* classical bit (Qiskit prints little-endian). + + For circuit B (two classical bits) only the second measurement is the + "after re-prepared superposition" outcome -- that's the bit we care about. + """ + zeros = 0 + ones = 0 + for bitstring, count in counts.items(): + # strip any spaces inserted between registers + stripped = bitstring.replace(" ", "") + # leftmost char in qiskit's printed string is the highest-index bit; + # the *final* measurement of obs_B writes bit index 1, which is the + # leftmost char. For single-bit circuits both ends agree. + bit = stripped[0] + if bit == "0": + zeros += count + else: + ones += count + return zeros, ones + + +def _first_bit_counts(counts: dict[str, int]) -> tuple[int, int]: + zeros = 0 + ones = 0 + for bitstring, count in counts.items(): + stripped = bitstring.replace(" ", "") + bit = stripped[-1] + if bit == "0": + zeros += count + else: + ones += count + return zeros, ones + + +def probabilities(zeros: int, ones: int) -> tuple[float, float]: + total = zeros + ones + if total == 0: + return 0.0, 0.0 + return zeros / total, ones / total + + +# --------------------------------------------------------------------------- +# Experiments +# --------------------------------------------------------------------------- + + +THETAS: list[tuple[str, float]] = [ + ("0", 0.0), + ("pi/8", math.pi / 8), + ("pi/4", math.pi / 4), + ("pi/2", math.pi / 2), + ("3pi/4", 3 * math.pi / 4), + ("pi", math.pi), +] + +NOISE_DEPTHS: list[int] = [0, 1, 2, 4, 8, 16, 32, 64] + + +@dataclass +class Row: + experiment: str + backend_label: str + mode: str + label: str + parameter: float + shots: int + p0: float + p1: float + expected_p0: float + error: float + notes: str = "" + + +def run_phase_experiment(backend: Backend, shots: int) -> list[Row]: + rows: list[Row] = [] + for name, theta in THETAS: + # ideal probability of measuring 0 for H-RZ(theta)-H is cos(theta/2)^2 + expected_p0 = math.cos(theta / 2.0) ** 2 + counts = backend.run(phase_circuit(theta), shots) + zeros, ones = _first_bit_counts(counts) + p0, p1 = probabilities(zeros, ones) + rows.append( + Row( + experiment="phase_alignment", + backend_label=backend.label, + mode=backend.mode, + label=name, + parameter=theta, + shots=shots, + p0=p0, + p1=p1, + expected_p0=expected_p0, + error=abs(p0 - expected_p0), + ) + ) + return rows + + +def run_observation_experiment(backend: Backend, shots: int) -> list[Row]: + rows: list[Row] = [] + + counts_a = backend.run(observation_circuit_a(), shots) + zeros_a, ones_a = _first_bit_counts(counts_a) + p0_a, p1_a = probabilities(zeros_a, ones_a) + rows.append( + Row( + experiment="observation", + backend_label=backend.label, + mode=backend.mode, + label="A_no_mid_measure", + parameter=0, + shots=shots, + p0=p0_a, + p1=p1_a, + expected_p0=1.0, # H H = I + error=abs(p0_a - 1.0), + notes="H then H -> identity, expect p(0)=1", + ) + ) + + if not backend.supports_mid_measure: + rows.append( + Row( + experiment="observation", + backend_label=backend.label, + mode=backend.mode, + label="B_mid_measure", + parameter=0, + shots=shots, + p0=float("nan"), + p1=float("nan"), + expected_p0=0.5, + error=float("nan"), + notes=( + "Skipped: selected backend does not support mid-circuit " + "measurement. Run in --mode ideal or --mode noisy to see B." + ), + ) + ) + return rows + + counts_b = backend.run(observation_circuit_b(), shots) + zeros_b, ones_b = _last_bit_counts(counts_b) + p0_b, p1_b = probabilities(zeros_b, ones_b) + rows.append( + Row( + experiment="observation", + backend_label=backend.label, + mode=backend.mode, + label="B_mid_measure", + parameter=0, + shots=shots, + p0=p0_b, + p1=p1_b, + expected_p0=0.5, # collapse erases interference + error=abs(p0_b - 0.5), + notes="Mid-circuit measurement collapses the state; expect ~50/50", + ) + ) + return rows + + +def run_noise_depth_experiment(backend: Backend, shots: int) -> list[Row]: + rows: list[Row] = [] + for depth in NOISE_DEPTHS: + counts = backend.run(noise_depth_circuit(depth), shots) + zeros, ones = _first_bit_counts(counts) + p0, p1 = probabilities(zeros, ones) + # H (XX)^n H |0> = |0> ideally, regardless of n + rows.append( + Row( + experiment="noise_depth", + backend_label=backend.label, + mode=backend.mode, + label=f"xx_pairs={depth}", + parameter=depth, + shots=shots, + p0=p0, + p1=p1, + expected_p0=1.0, + error=abs(p0 - 1.0), + ) + ) + return rows + + +# --------------------------------------------------------------------------- +# Persistence + plots + report +# --------------------------------------------------------------------------- + + +CSV_FIELDS = [ + "experiment", + "backend_label", + "mode", + "label", + "parameter", + "shots", + "p0", + "p1", + "expected_p0", + "error", + "notes", +] + + +def write_csv(rows: Iterable[Row], path: str) -> None: + with open(path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=CSV_FIELDS) + writer.writeheader() + for row in rows: + writer.writerow({k: getattr(row, k) for k in CSV_FIELDS}) + + +def plot_phase(rows: list[Row], path: str, backend_label: str) -> None: + phase_rows = [r for r in rows if r.experiment == "phase_alignment"] + if not phase_rows: + return + thetas = [r.parameter for r in phase_rows] + p0 = [r.p0 for r in phase_rows] + p1 = [r.p1 for r in phase_rows] + err = [r.error for r in phase_rows] + expected = [r.expected_p0 for r in phase_rows] + + fig, ax = plt.subplots(figsize=(8, 5)) + ax.plot(thetas, p0, marker="o", label="p(0) measured") + ax.plot(thetas, p1, marker="o", label="p(1) measured") + ax.plot(thetas, expected, linestyle="--", label="p(0) ideal = cos^2(theta/2)") + ax.plot(thetas, err, marker="x", label="|p(0) - ideal|") + ax.set_xlabel("theta (radians)") + ax.set_ylabel("probability") + ax.set_title(f"Experiment 1: Phase alignment / interference ({backend_label})") + ax.set_xticks(thetas) + ax.set_xticklabels([r.label for r in phase_rows]) + ax.set_ylim(-0.05, 1.05) + ax.grid(True, alpha=0.3) + ax.legend(loc="center right") + fig.tight_layout() + fig.savefig(path, dpi=140) + plt.close(fig) + + +def plot_noise_depth(rows: list[Row], path: str, backend_label: str) -> None: + depth_rows = [r for r in rows if r.experiment == "noise_depth"] + if not depth_rows: + return + depths = [r.parameter for r in depth_rows] + p0 = [r.p0 for r in depth_rows] + p1 = [r.p1 for r in depth_rows] + err = [r.error for r in depth_rows] + + fig, ax = plt.subplots(figsize=(8, 5)) + ax.plot(depths, p0, marker="o", label="p(0) measured") + ax.plot(depths, p1, marker="o", label="p(1) measured") + ax.plot(depths, err, marker="x", label="error = 1 - p(0)") + ax.axhline(1.0, linestyle="--", alpha=0.5, label="ideal p(0)=1") + ax.set_xscale("symlog", linthresh=1) + ax.set_xlabel("number of X-X pairs (logical identity, real depth grows)") + ax.set_ylabel("probability") + ax.set_title(f"Experiment 3: Decoherence vs depth ({backend_label})") + ax.set_ylim(-0.05, 1.05) + ax.grid(True, alpha=0.3, which="both") + ax.legend(loc="center right") + fig.tight_layout() + fig.savefig(path, dpi=140) + plt.close(fig) + + +def write_report(rows: list[Row], path: str, backend: Backend, shots: int) -> None: + phase_rows = [r for r in rows if r.experiment == "phase_alignment"] + obs_rows = [r for r in rows if r.experiment == "observation"] + depth_rows = [r for r in rows if r.experiment == "noise_depth"] + + def fmt(p: float) -> str: + if p != p: # NaN + return "n/a" + return f"{p:.3f}" + + lines: list[str] = [] + lines.append("# Quantum Alignment Test Report") + lines.append("") + lines.append(f"- Backend: `{backend.label}`") + lines.append(f"- Mode: `{backend.mode}`") + lines.append(f"- Shots per circuit: {shots}") + if backend.notes: + lines.append("- Backend notes:") + for note in backend.notes: + lines.append(f" - {note}") + lines.append("") + lines.append("## What this report tests") + lines.append("") + lines.append( + "This suite probes three *physical* properties of quantum computation: " + "phase coherence and interference, the effect of measurement on a " + "superposition, and the accumulation of gate noise / decoherence as " + "circuit depth grows. \"Multiverse alignment\" is used here as a " + "metaphor for the coherent phase relationships that produce " + "interference -- it is **not** a literal claim about parallel " + "universes, and nothing here can prove or disprove the many-worlds " + "interpretation." + ) + lines.append("") + + # Experiment 1 + lines.append("## Experiment 1 -- Phase alignment / interference") + lines.append("") + lines.append( + "Circuit: `|0> - H - RZ(theta) - H - Measure`. The first H spreads " + "|0> into an equal superposition; RZ(theta) puts a relative phase " + "between the two branches; the second H lets those branches " + "interfere. The ideal outcome is `p(0) = cos^2(theta/2)`." + ) + lines.append("") + lines.append("| theta | p(0) measured | p(1) measured | p(0) ideal | |error| |") + lines.append("|---|---|---|---|---|") + for r in phase_rows: + lines.append( + f"| {r.label} | {fmt(r.p0)} | {fmt(r.p1)} | " + f"{fmt(r.expected_p0)} | {fmt(r.error)} |" + ) + lines.append("") + if phase_rows: + max_err = max(r.error for r in phase_rows) + lines.append( + f"Maximum deviation from the ideal interference pattern: " + f"**{max_err:.3f}**. " + f"On the ideal simulator this should be at the level of shot noise " + f"(~1/sqrt(shots) ~= {1.0 / math.sqrt(shots):.3f}). On a noisy or " + f"real backend it grows because gate errors and readout errors " + f"smear the interference fringes." + ) + lines.append("") + + # Experiment 2 + lines.append("## Experiment 2 -- Observation collapse") + lines.append("") + lines.append( + "Two circuits are compared. **A** applies H then H with no measurement " + "in between: H is its own inverse, so the qubit returns to |0> and we " + "expect `p(0) = 1`. **B** measures *between* the two H gates. That " + "mid-circuit measurement collapses the superposition to a definite " + "|0> or |1>; the second H then turns whichever basis state we have " + "into a fresh equal superposition, so the final readout is ~50/50. " + "If A is near 1.0 and B is near 0.5, observation has demonstrably " + "destroyed interference." + ) + lines.append("") + lines.append("| variant | p(0) | p(1) | expected p(0) | |error| | notes |") + lines.append("|---|---|---|---|---|---|") + for r in obs_rows: + lines.append( + f"| {r.label} | {fmt(r.p0)} | {fmt(r.p1)} | " + f"{fmt(r.expected_p0)} | {fmt(r.error)} | {r.notes} |" + ) + lines.append("") + + # Experiment 3 + lines.append("## Experiment 3 -- Decoherence / noise accumulation") + lines.append("") + lines.append( + "Circuit: `|0> - H - (X X)^n - H - Measure`. Each `X X` pair is the " + "identity, so the ideal outcome is always `p(0) = 1` regardless of n. " + "Any drift toward `p(0) = 0.5` as n grows is *purely* gate noise and " + "decoherence (T1/T2 relaxation, control errors, readout errors). On " + "the ideal simulator the line should stay flat at 1.0; on a noisy or " + "real backend it will sag toward 0.5." + ) + lines.append("") + lines.append("| X-X pairs | p(0) | p(1) | error = 1-p(0) |") + lines.append("|---|---|---|---|") + for r in depth_rows: + lines.append( + f"| {int(r.parameter)} | {fmt(r.p0)} | " + f"{fmt(r.p1)} | {fmt(r.error)} |" + ) + lines.append("") + if depth_rows: + deepest = depth_rows[-1] + lines.append( + f"At depth {int(deepest.parameter)} X-X pairs, p(0) is " + f"**{fmt(deepest.p0)}** vs the ideal **1.0** " + f"(error **{fmt(deepest.error)}**). " + "The further this number is from 1.0, the more decoherence the " + "device has accumulated over the run." + ) + lines.append("") + + # Scope / disclaimer + lines.append("## What this is NOT") + lines.append("") + lines.append( + "- Not a test of literal parallel universes or the many-worlds " + "interpretation. Quantum mechanics is interpretation-agnostic and " + "no single-qubit experiment can settle that debate." + ) + lines.append( + "- Not a benchmark of any specific IBM device. We use a tiny number " + "of circuits at modest shot counts; published quantum-volume / EPLG " + "numbers are far more rigorous." + ) + lines.append( + "- Not a security or randomness test. Do not use these counts as a " + "source of cryptographic entropy." + ) + lines.append("") + lines.append("## What this IS") + lines.append("") + lines.append( + "- A reproducible demonstration that quantum amplitudes have phase, " + "that phases interfere, that observation destroys interference, and " + "that real hardware loses coherence as depth grows." + ) + lines.append("") + + with open(path, "w") as f: + f.write("\n".join(lines)) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def select_backend(args: argparse.Namespace) -> Backend: + if args.mode == "ideal": + return make_ideal_backend() + if args.mode == "noisy": + return make_noisy_backend(seed=args.seed) + if args.mode == "real": + token = os.environ.get("IBM_QUANTUM_TOKEN") + if not token: + raise SystemExit( + "IBM_QUANTUM_TOKEN is not set. Export it (without quoting it " + "into shell history -- prefer `read -s` or a secret manager) " + "before running with --mode real, or pick --mode ideal/noisy." + ) + return make_real_backend(token=token, backend_name=args.backend) + raise SystemExit(f"Unknown mode: {args.mode!r}") + + +def parse_args(argv: list[str]) -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--mode", choices=["ideal", "noisy", "real"], default="ideal") + p.add_argument( + "--backend", + default=None, + help="(real mode only) IBM backend name. Defaults to least_busy.", + ) + p.add_argument("--shots", type=int, default=4096) + p.add_argument("--seed", type=int, default=42) + p.add_argument( + "--out", + default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "results"), + help="Output directory for CSV, plots, and report.", + ) + p.add_argument( + "--skip-observation", + action="store_true", + help="Skip Experiment 2 entirely (e.g., on a backend with no mid-circuit measurement support).", + ) + return p.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv if argv is not None else sys.argv[1:]) + os.makedirs(args.out, exist_ok=True) + + backend = select_backend(args) + np.random.seed(args.seed) + + print(f"[quantum_alignment] backend={backend.label} shots={args.shots}") + + rows: list[Row] = [] + print("[quantum_alignment] running Experiment 1: phase alignment...") + rows.extend(run_phase_experiment(backend, args.shots)) + if args.skip_observation: + print("[quantum_alignment] skipping Experiment 2 (per --skip-observation)") + else: + print("[quantum_alignment] running Experiment 2: observation collapse...") + rows.extend(run_observation_experiment(backend, args.shots)) + print("[quantum_alignment] running Experiment 3: noise vs depth...") + rows.extend(run_noise_depth_experiment(backend, args.shots)) + + csv_path = os.path.join(args.out, "results.csv") + phase_plot = os.path.join(args.out, "phase_alignment_plot.png") + noise_plot = os.path.join(args.out, "noise_depth_plot.png") + report_path = os.path.join(args.out, "report.md") + + write_csv(rows, csv_path) + plot_phase(rows, phase_plot, backend.label) + plot_noise_depth(rows, noise_plot, backend.label) + write_report(rows, report_path, backend, args.shots) + + print(f"[quantum_alignment] wrote {csv_path}") + print(f"[quantum_alignment] wrote {phase_plot}") + print(f"[quantum_alignment] wrote {noise_plot}") + print(f"[quantum_alignment] wrote {report_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/quantum_alignment/requirements.txt b/quantum_alignment/requirements.txt new file mode 100644 index 0000000..c05aaf1 --- /dev/null +++ b/quantum_alignment/requirements.txt @@ -0,0 +1,9 @@ +# Core +qiskit>=1.0 +qiskit-aer>=0.13 +matplotlib>=3.7 +numpy>=1.24 + +# Only needed for --mode real (real IBM hardware execution). +# Pulled in lazily; ideal/noisy modes work without it. +qiskit-ibm-runtime>=0.20 diff --git a/quantum_alignment/results/noise_depth_plot.png b/quantum_alignment/results/noise_depth_plot.png new file mode 100644 index 0000000..8a7fe04 Binary files /dev/null and b/quantum_alignment/results/noise_depth_plot.png differ diff --git a/quantum_alignment/results/phase_alignment_plot.png b/quantum_alignment/results/phase_alignment_plot.png new file mode 100644 index 0000000..93a6c6b Binary files /dev/null and b/quantum_alignment/results/phase_alignment_plot.png differ diff --git a/quantum_alignment/results/report.md b/quantum_alignment/results/report.md new file mode 100644 index 0000000..a571f9e --- /dev/null +++ b/quantum_alignment/results/report.md @@ -0,0 +1,62 @@ +# Quantum Alignment Test Report + +- Backend: `aer-noisy(generic_backend_5q)` +- Mode: `noisy` +- Shots per circuit: 4096 +- Backend notes: + - Noise model derived from synthetic fake backend 'generic_backend_5q' (qiskit GenericBackendV2). + +## What this report tests + +This suite probes three *physical* properties of quantum computation: phase coherence and interference, the effect of measurement on a superposition, and the accumulation of gate noise / decoherence as circuit depth grows. "Multiverse alignment" is used here as a metaphor for the coherent phase relationships that produce interference -- it is **not** a literal claim about parallel universes, and nothing here can prove or disprove the many-worlds interpretation. + +## Experiment 1 -- Phase alignment / interference + +Circuit: `|0> - H - RZ(theta) - H - Measure`. The first H spreads |0> into an equal superposition; RZ(theta) puts a relative phase between the two branches; the second H lets those branches interfere. The ideal outcome is `p(0) = cos^2(theta/2)`. + +| theta | p(0) measured | p(1) measured | p(0) ideal | |error| | +|---|---|---|---|---| +| 0 | 0.997 | 0.003 | 1.000 | 0.003 | +| pi/8 | 0.957 | 0.043 | 0.962 | 0.005 | +| pi/4 | 0.859 | 0.141 | 0.854 | 0.006 | +| pi/2 | 0.499 | 0.501 | 0.500 | 0.001 | +| 3pi/4 | 0.151 | 0.849 | 0.146 | 0.005 | +| pi | 0.004 | 0.996 | 0.000 | 0.004 | + +Maximum deviation from the ideal interference pattern: **0.006**. On the ideal simulator this should be at the level of shot noise (~1/sqrt(shots) ~= 0.016). On a noisy or real backend it grows because gate errors and readout errors smear the interference fringes. + +## Experiment 2 -- Observation collapse + +Two circuits are compared. **A** applies H then H with no measurement in between: H is its own inverse, so the qubit returns to |0> and we expect `p(0) = 1`. **B** measures *between* the two H gates. That mid-circuit measurement collapses the superposition to a definite |0> or |1>; the second H then turns whichever basis state we have into a fresh equal superposition, so the final readout is ~50/50. If A is near 1.0 and B is near 0.5, observation has demonstrably destroyed interference. + +| variant | p(0) | p(1) | expected p(0) | |error| | notes | +|---|---|---|---|---|---| +| A_no_mid_measure | 0.995 | 0.005 | 1.000 | 0.005 | H then H -> identity, expect p(0)=1 | +| B_mid_measure | 0.500 | 0.500 | 0.500 | 0.000 | Mid-circuit measurement collapses the state; expect ~50/50 | + +## Experiment 3 -- Decoherence / noise accumulation + +Circuit: `|0> - H - (X X)^n - H - Measure`. Each `X X` pair is the identity, so the ideal outcome is always `p(0) = 1` regardless of n. Any drift toward `p(0) = 0.5` as n grows is *purely* gate noise and decoherence (T1/T2 relaxation, control errors, readout errors). On the ideal simulator the line should stay flat at 1.0; on a noisy or real backend it will sag toward 0.5. + +| X-X pairs | p(0) | p(1) | error = 1-p(0) | +|---|---|---|---| +| 0 | 0.997 | 0.003 | 0.003 | +| 1 | 0.996 | 0.004 | 0.004 | +| 2 | 0.996 | 0.004 | 0.004 | +| 4 | 0.993 | 0.007 | 0.007 | +| 8 | 0.995 | 0.005 | 0.005 | +| 16 | 0.990 | 0.010 | 0.010 | +| 32 | 0.988 | 0.012 | 0.012 | +| 64 | 0.980 | 0.020 | 0.020 | + +At depth 64 X-X pairs, p(0) is **0.980** vs the ideal **1.0** (error **0.020**). The further this number is from 1.0, the more decoherence the device has accumulated over the run. + +## What this is NOT + +- Not a test of literal parallel universes or the many-worlds interpretation. Quantum mechanics is interpretation-agnostic and no single-qubit experiment can settle that debate. +- Not a benchmark of any specific IBM device. We use a tiny number of circuits at modest shot counts; published quantum-volume / EPLG numbers are far more rigorous. +- Not a security or randomness test. Do not use these counts as a source of cryptographic entropy. + +## What this IS + +- A reproducible demonstration that quantum amplitudes have phase, that phases interfere, that observation destroys interference, and that real hardware loses coherence as depth grows. diff --git a/quantum_alignment/results/results.csv b/quantum_alignment/results/results.csv new file mode 100644 index 0000000..ea77875 --- /dev/null +++ b/quantum_alignment/results/results.csv @@ -0,0 +1,17 @@ +experiment,backend_label,mode,label,parameter,shots,p0,p1,expected_p0,error,notes +phase_alignment,aer-noisy(generic_backend_5q),noisy,0,0.0,4096,0.99658203125,0.00341796875,1.0,0.00341796875, +phase_alignment,aer-noisy(generic_backend_5q),noisy,pi/8,0.39269908169872414,4096,0.95654296875,0.04345703125,0.9619397662556434,0.005396797505643369, +phase_alignment,aer-noisy(generic_backend_5q),noisy,pi/4,0.7853981633974483,4096,0.859130859375,0.140869140625,0.8535533905932737,0.005577468781726269, +phase_alignment,aer-noisy(generic_backend_5q),noisy,pi/2,1.5707963267948966,4096,0.4990234375,0.5009765625,0.5000000000000001,0.000976562500000111, +phase_alignment,aer-noisy(generic_backend_5q),noisy,3pi/4,2.356194490192345,4096,0.151123046875,0.848876953125,0.1464466094067263,0.004676437468273703, +phase_alignment,aer-noisy(generic_backend_5q),noisy,pi,3.141592653589793,4096,0.003662109375,0.996337890625,3.749399456654644e-33,0.003662109375, +observation,aer-noisy(generic_backend_5q),noisy,A_no_mid_measure,0,4096,0.995361328125,0.004638671875,1.0,0.004638671875,"H then H -> identity, expect p(0)=1" +observation,aer-noisy(generic_backend_5q),noisy,B_mid_measure,0,4096,0.499755859375,0.500244140625,0.5,0.000244140625,Mid-circuit measurement collapses the state; expect ~50/50 +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=0,0,4096,0.997314453125,0.002685546875,1.0,0.002685546875, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=1,1,4096,0.99560546875,0.00439453125,1.0,0.00439453125, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=2,2,4096,0.99609375,0.00390625,1.0,0.00390625, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=4,4,4096,0.993408203125,0.006591796875,1.0,0.006591796875, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=8,8,4096,0.9951171875,0.0048828125,1.0,0.0048828125, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=16,16,4096,0.989990234375,0.010009765625,1.0,0.010009765625, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=32,32,4096,0.98828125,0.01171875,1.0,0.01171875, +noise_depth,aer-noisy(generic_backend_5q),noisy,xx_pairs=64,64,4096,0.979736328125,0.020263671875,1.0,0.020263671875,