Skip to main content

wai_quantum/
quantum_toolchain.rs

1//! WAI Quantum Toolchain — the compute core of a classical-hardware quantum IDE
2//! (extensions/quantum-sim § Toolchain, `wai.quantum.toolchain`).
3//!
4//! Everyone teaches quantum circuits-first, inside one vendor's SDK, attached to a
5//! cloud QPU, behind a wall of linear algebra. This is the opposite: a
6//! framework-neutral toolchain over the byte-exact fixed-point simulator
7//! ([`crate::quantum`]) that runs entirely on the hardware you already have, with
8//! no cloud, no account, no vendor lock-in — and whose every run is reproducible
9//! to the bit and sealable into a signed receipt.
10//!
11//! Four layers sit on the simulator:
12//! - **Algorithm library** — the canonical algorithms (Bell, GHZ, QFT, Grover,
13//!   Bernstein–Vazirani, Deutsch–Jozsa, teleportation, superdense coding) as
14//!   circuit generators you can run and inspect, not read about.
15//! - **State analysis** — the readouts an IDE shows about a reconstructed state:
16//!   probabilities, Pauli expectations, per-qubit Bloch vectors, single-qubit
17//!   entanglement entropy and purity. (Display-layer `f64`; the byte-exact artifact
18//!   remains the statevector hash.)
19//! - **Backend selection** — detect whether a circuit is Clifford and recommend the
20//!   cheapest sufficient classical method (stabilizer tableau → dense statevector →
21//!   tensor network), the "classical referee" cascade the analysis calls for.
22//! - **OpenQASM interop** — import/export the lingua franca, so circuits move in and
23//!   out of any other toolchain without a rewrite.
24//!
25//! Honest boundary: the simulator's gate set is the **dyadic** family (Clifford +
26//! T + `P(k)=diag(1,e^{2πi/2^k})` with arbitrary controls) — every phase exactly
27//! representable, which is *why* the result is byte-identical on every machine.
28//! Continuous rotations (`RX(θ)` for irrational θ) are out of the exact set by
29//! construction, not omission.
30
31use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, StateVector, FRAC};
32
33// ===========================================================================
34// Algorithm library
35// ===========================================================================
36
37/// A multi-controlled-Z on all `n` qubits (controls `0..n-1`, target `n-1`) —
38/// the phase-flip primitive Grover's oracle and diffusion are built from.
39fn mcz(c: &mut Circuit, n: u8) {
40    if n == 1 {
41        c.z(0);
42        return;
43    }
44    c.ops.push(Gate { base: BaseGate::Z, controls: (0..n - 1).collect(), target: n - 1, param: 0 });
45}
46
47/// `|Φ⁺⟩ = (|00⟩+|11⟩)/√2` — the canonical entangled pair.
48pub fn bell() -> Circuit {
49    let mut c = Circuit::new(2);
50    c.h(0);
51    c.cx(0, 1);
52    c
53}
54
55/// The `n`-qubit GHZ state `(|0…0⟩+|1…1⟩)/√2` (a linear CX chain).
56pub fn ghz(n: u8) -> Circuit {
57    let mut c = Circuit::new(n.max(1));
58    c.h(0);
59    for i in 1..n {
60        c.cx(i - 1, i);
61    }
62    c
63}
64
65/// **Grover search** over `n` qubits for a single marked basis state, run for the
66/// optimal `⌊(π/4)√(2ⁿ)⌋` iterations — amplitude amplification made visible.
67pub fn grover(n: u8, marked: u32) -> Circuit {
68    let n = n.clamp(1, 12);
69    let mut c = Circuit::new(n);
70    for q in 0..n {
71        c.h(q);
72    }
73    let dim = 1u64 << n;
74    let iters = ((std::f64::consts::PI / 4.0) * (dim as f64).sqrt()).floor() as u32;
75    for _ in 0..iters.max(1) {
76        // oracle: phase-flip |marked>
77        for q in 0..n {
78            if (marked >> q) & 1 == 0 {
79                c.x(q);
80            }
81        }
82        mcz(&mut c, n);
83        for q in 0..n {
84            if (marked >> q) & 1 == 0 {
85                c.x(q);
86            }
87        }
88        // diffusion about the mean
89        for q in 0..n {
90            c.h(q);
91        }
92        for q in 0..n {
93            c.x(q);
94        }
95        mcz(&mut c, n);
96        for q in 0..n {
97            c.x(q);
98        }
99        for q in 0..n {
100            c.h(q);
101        }
102    }
103    c
104}
105
106/// **Bernstein–Vazirani** — recover the hidden `nbits`-bit `secret` in a *single*
107/// oracle query. Qubits `0..nbits` carry the answer; qubit `nbits` is the phase
108/// ancilla.
109pub fn bernstein_vazirani(secret: u32, nbits: u8) -> Circuit {
110    let nbits = nbits.clamp(1, 20);
111    let anc = nbits;
112    let mut c = Circuit::new(nbits + 1);
113    c.x(anc);
114    for q in 0..=nbits {
115        c.h(q);
116    }
117    for q in 0..nbits {
118        if (secret >> q) & 1 == 1 {
119            c.cx(q, anc);
120        }
121    }
122    for q in 0..nbits {
123        c.h(q);
124    }
125    c
126}
127
128/// **Deutsch–Jozsa** — one query decides whether a function is constant or
129/// balanced. A balanced oracle CXs every input into the ancilla; measuring the
130/// inputs gives all-zeros iff the function is constant.
131pub fn deutsch_jozsa(balanced: bool, nbits: u8) -> Circuit {
132    let nbits = nbits.clamp(1, 20);
133    let anc = nbits;
134    let mut c = Circuit::new(nbits + 1);
135    c.x(anc);
136    for q in 0..=nbits {
137        c.h(q);
138    }
139    if balanced {
140        for q in 0..nbits {
141            c.cx(q, anc);
142        }
143    }
144    for q in 0..nbits {
145        c.h(q);
146    }
147    c
148}
149
150/// **Quantum teleportation** (deferred-measurement form): qubit 0's prepared state
151/// is transported to qubit 2 through a shared Bell pair and unitary corrections.
152/// The final reduced state of qubit 2 equals qubit 0's initial state.
153pub fn teleportation() -> Circuit {
154    let mut c = Circuit::new(3);
155    // prepare a nontrivial state on qubit 0
156    c.h(0);
157    c.t(0);
158    // Bell pair on 1,2
159    c.h(1);
160    c.cx(1, 2);
161    // Bell-basis change on 0,1
162    c.cx(0, 1);
163    c.h(0);
164    // deferred corrections
165    c.cx(1, 2);
166    c.cz(0, 2);
167    c
168}
169
170/// **Superdense coding** — send two classical bits `bits ∈ 0..4` over one qubit of
171/// a Bell pair. Decoding and measuring recovers `bits`.
172pub fn superdense(bits: u8) -> Circuit {
173    let mut c = Circuit::new(2);
174    c.h(0);
175    c.cx(0, 1);
176    // encode so the decoded 2-bit index equals `bits`: Z shows up as q0 (value 1),
177    // X as q1 (value 2).
178    if bits & 1 != 0 {
179        c.z(0);
180    }
181    if bits & 2 != 0 {
182        c.x(0);
183    }
184    c.cx(0, 1);
185    c.h(0);
186    c
187}
188
189fn splitmix(s: &mut u64) -> u64 {
190    *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
191    let mut z = *s;
192    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
193    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
194    z ^ (z >> 31)
195}
196
197/// **Trotterized transverse-field Ising** dynamics — the many-body-dynamics circuit
198/// class the tensor-network and sparse-Pauli backends were built for. Each step
199/// applies a ZZ coupling on every line edge (`CX · P(k) · CX`) and a transverse X
200/// field (`H · P(k) · H`), all with dyadic (byte-exact) rotation angles.
201pub fn trotter_ising(n: u8, steps: u8) -> Circuit {
202    let n = n.clamp(2, 12);
203    let k = 3u16; // dyadic angle 2π/8 = π/4
204    let mut c = Circuit::new(n);
205    for _ in 0..steps.clamp(1, 20) {
206        for q in 0..n - 1 {
207            c.cx(q, q + 1);
208            c.p(k, q + 1);
209            c.cx(q, q + 1);
210        }
211        for q in 0..n {
212            c.h(q);
213            c.p(k, q);
214            c.h(q);
215        }
216    }
217    c
218}
219
220/// A random **Clifford** circuit (H / S / CX) — the stabilizer backend's home turf.
221pub fn random_clifford(n: u8, depth: u16, seed: u64) -> Circuit {
222    let n = n.clamp(1, 12);
223    let mut st = seed.wrapping_mul(0x2545_F491).wrapping_add(1);
224    let mut c = Circuit::new(n);
225    for _ in 0..depth {
226        match splitmix(&mut st) % 3 {
227            0 => {
228                c.h((splitmix(&mut st) % n as u64) as u8);
229            }
230            1 => {
231                c.s((splitmix(&mut st) % n as u64) as u8);
232            }
233            _ if n >= 2 => {
234                let a = (splitmix(&mut st) % n as u64) as u8;
235                let mut b = (splitmix(&mut st) % n as u64) as u8;
236                if b == a {
237                    b = (b + 1) % n;
238                }
239                c.cx(a, b);
240            }
241            _ => {
242                c.h(0);
243            }
244        }
245    }
246    c
247}
248
249/// Build a library algorithm by id. `param` carries the marked/secret/bits value.
250pub fn build_algorithm(id: &str, n: u8, param: u32) -> Option<Circuit> {
251    Some(match id {
252        "bell" => bell(),
253        "ghz" => ghz(n.clamp(2, 12)),
254        "qft" => Circuit::qft(n.clamp(1, 12)),
255        "grover" => grover(n.clamp(2, 10), param),
256        "bv" => bernstein_vazirani(param, n.clamp(1, 10)),
257        "dj" => deutsch_jozsa(param != 0, n.clamp(1, 10)),
258        "teleport" => teleportation(),
259        "superdense" => superdense((param & 3) as u8),
260        "trotter" => trotter_ising(n.clamp(2, 10), param.clamp(1, 12) as u8),
261        "rclifford" => random_clifford(n.clamp(2, 10), 40, param as u64),
262        _ => return None,
263    })
264}
265
266/// `(id, display name, one-line description)` for the whole library.
267pub fn catalog() -> Vec<(&'static str, &'static str, &'static str)> {
268    vec![
269        ("bell", "Bell pair", "The canonical entangled pair (|00⟩+|11⟩)/√2 — entanglement in two gates."),
270        ("ghz", "GHZ state", "n-qubit maximal entanglement (|0…0⟩+|1…1⟩)/√2."),
271        ("qft", "Quantum Fourier Transform", "The quantum FFT — Hadamard + controlled-phase ladder."),
272        ("grover", "Grover search", "Amplitude amplification finds a marked item in √N steps."),
273        ("bv", "Bernstein–Vazirani", "Recover a hidden bitstring in one query — interference at work."),
274        ("dj", "Deutsch–Jozsa", "One query decides constant vs balanced."),
275        ("teleport", "Teleportation", "Move a state across a Bell pair via corrections."),
276        ("superdense", "Superdense coding", "Two classical bits down one qubit."),
277        ("trotter", "Trotter Ising", "Transverse-field Ising dynamics — the tensor-network / sparse-Pauli home turf."),
278        ("rclifford", "Random Clifford", "A random Clifford circuit — the stabilizer backend's home turf."),
279    ]
280}
281
282// ===========================================================================
283// State analysis  (display-layer f64 — NOT the byte-exact artifact)
284// ===========================================================================
285
286const SCALE: f64 = (1u64 << FRAC) as f64;
287
288#[inline]
289fn re(a: Amp) -> f64 {
290    a.re as f64 / SCALE
291}
292#[inline]
293fn im(a: Amp) -> f64 {
294    a.im as f64 / SCALE
295}
296
297fn total_weight(sv: &StateVector) -> f64 {
298    sv.prob_weights().iter().map(|&w| w as f64).sum::<f64>()
299}
300
301/// Normalized per-basis-state probabilities.
302pub fn probabilities(sv: &StateVector) -> Vec<f64> {
303    let t = total_weight(sv);
304    if t <= 0.0 {
305        return vec![0.0; sv.amps.len()];
306    }
307    sv.prob_weights().iter().map(|&w| w as f64 / t).collect()
308}
309
310/// `⟨Z_q⟩` — the computational-basis magnetization of qubit `q`.
311pub fn expect_z(sv: &StateVector, q: u8) -> f64 {
312    let bit = 1usize << q;
313    let t = total_weight(sv);
314    if t <= 0.0 {
315        return 0.0;
316    }
317    let mut acc = 0.0;
318    for (i, w) in sv.prob_weights().iter().enumerate() {
319        let s = if i & bit == 0 { 1.0 } else { -1.0 };
320        acc += s * (*w as f64);
321    }
322    acc / t
323}
324
325/// `⟨X_q⟩` and `⟨Y_q⟩` — the off-diagonal (coherence) expectations.
326pub fn expect_xy(sv: &StateVector, q: u8) -> (f64, f64) {
327    let bit = 1usize << q;
328    let t = total_weight(sv);
329    if t <= 0.0 {
330        return (0.0, 0.0);
331    }
332    let mut ex = 0.0;
333    let mut ey = 0.0;
334    for i in 0..sv.amps.len() {
335        if i & bit == 0 {
336            let a = sv.amps[i];
337            let b = sv.amps[i | bit];
338            // conj(a)·b
339            ex += 2.0 * (re(a) * re(b) + im(a) * im(b));
340            ey += 2.0 * (re(a) * im(b) - im(a) * re(b));
341        }
342    }
343    (ex * SCALE * SCALE / t, ey * SCALE * SCALE / t)
344}
345
346/// The Bloch vector `(x, y, z)` of qubit `q` (its reduced single-qubit state).
347pub fn bloch(sv: &StateVector, q: u8) -> [f64; 3] {
348    let (x, y) = expect_xy(sv, q);
349    [x, y, expect_z(sv, q)]
350}
351
352/// Single-qubit **entanglement entropy** of qubit `q` with the rest, in bits:
353/// `0` when separable, `1` when maximally entangled. From the Bloch-vector length
354/// `r`: eigenvalues of the reduced density matrix are `(1±r)/2`.
355pub fn entanglement_entropy(sv: &StateVector, q: u8) -> f64 {
356    let b = bloch(sv, q);
357    let r = (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]).sqrt().min(1.0);
358    let lam = (1.0 + r) / 2.0;
359    binary_entropy(lam)
360}
361
362/// Purity `Tr(ρ_q²) = (1+r²)/2` of qubit `q` (`1` = pure, `0.5` = maximally mixed).
363pub fn purity(sv: &StateVector, q: u8) -> f64 {
364    let b = bloch(sv, q);
365    let r2 = (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]).min(1.0);
366    (1.0 + r2) / 2.0
367}
368
369fn binary_entropy(p: f64) -> f64 {
370    if p <= 0.0 || p >= 1.0 {
371        return 0.0;
372    }
373    -p * p.log2() - (1.0 - p) * (1.0 - p).log2()
374}
375
376/// The `top` most-probable basis states as `(index, probability)`, descending.
377pub fn top_outcomes(sv: &StateVector, top: usize) -> Vec<(usize, f64)> {
378    let mut v: Vec<(usize, f64)> = probabilities(sv).into_iter().enumerate().collect();
379    v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
380    v.truncate(top);
381    v
382}
383
384// ===========================================================================
385// Noise simulation — depolarizing channel via Monte-Carlo trajectories
386// ===========================================================================
387
388/// `⟨Z_q⟩` under a per-gate **depolarizing** channel, by averaging over
389/// `trajectories` random error insertions: after each gate, each qubit it touched
390/// suffers a uniformly random `X`/`Y`/`Z` with probability `depol_permille`/1000.
391/// Each trajectory reuses the byte-exact dense simulator; deterministic in `seed`.
392/// `⟨Z_q⟩` decays toward 0 as noise or depth grows — decoherence, made measurable.
393pub fn noisy_expect_z(
394    circuit: &Circuit,
395    q: u8,
396    depol_permille: u32,
397    trajectories: u32,
398    seed: u64,
399) -> f64 {
400    let mut st = seed.wrapping_mul(0x9E37_79B9).wrapping_add(1);
401    let mut acc = 0.0;
402    let traj = trajectories.max(1);
403    for _ in 0..traj {
404        let mut noisy = Circuit::new(circuit.n_qubits);
405        for g in &circuit.ops {
406            noisy.ops.push(g.clone());
407            for &qb in g.controls.iter().chain(std::iter::once(&g.target)) {
408                if (splitmix(&mut st) % 1000) < depol_permille as u64 {
409                    let base = match splitmix(&mut st) % 3 {
410                        0 => BaseGate::X,
411                        1 => BaseGate::Y,
412                        _ => BaseGate::Z,
413                    };
414                    noisy.ops.push(Gate { base, controls: vec![], target: qb, param: 0 });
415                }
416            }
417        }
418        if let Ok(sv) = noisy.simulate() {
419            acc += expect_z(&sv, q);
420        }
421    }
422    acc / traj as f64
423}
424
425// ===========================================================================
426// Backend selection — the cheapest-sufficient classical cascade
427// ===========================================================================
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430pub enum Backend {
431    /// Stabilizer tableau — Clifford circuits in O(n²), scales past statevector.
432    Stabilizer,
433    /// Exact dense statevector — 2ⁿ amplitudes, the workhorse.
434    Statevector,
435    /// Tensor-network contraction — the classical referee for large, low-
436    /// entanglement, non-Clifford circuits.
437    TensorNetwork,
438}
439
440/// Is every gate a Clifford operation? (Determines whether the polynomial-time
441/// stabilizer path applies.)
442pub fn is_clifford(c: &Circuit) -> bool {
443    c.ops.iter().all(|g| match g.controls.len() {
444        0 => matches!(
445            g.base,
446            BaseGate::I | BaseGate::X | BaseGate::Y | BaseGate::Z | BaseGate::H | BaseGate::S | BaseGate::Sdg
447        ) || (g.base == BaseGate::P && (g.param == 1 || g.param == 2)),
448        1 => matches!(g.base, BaseGate::X | BaseGate::Y | BaseGate::Z)
449            || (g.base == BaseGate::P && g.param == 1),
450        _ => false,
451    })
452}
453
454/// Recommend the cheapest sufficient classical backend for a circuit, with a
455/// one-line rationale.
456pub fn recommend_backend(c: &Circuit) -> (Backend, &'static str) {
457    if is_clifford(c) {
458        (Backend::Stabilizer, "all-Clifford → stabilizer tableau, O(n²); scales far past statevector")
459    } else if c.n_qubits <= 22 {
460        (Backend::Statevector, "non-Clifford, ≤22 qubits → exact dense statevector")
461    } else {
462        (Backend::TensorNetwork, "non-Clifford, >22 qubits → tensor-network contraction (the referee)")
463    }
464}
465
466/// Portable-exact classical work of a dense simulation: `n_ops · 2ⁿ` amplitude
467/// updates — recomputable from `(gates, qubits)` alone, exponential in qubits.
468pub fn statevector_work(c: &Circuit) -> u64 {
469    (c.ops.len() as u64).saturating_mul(1u64 << c.n_qubits.min(63))
470}
471
472// ===========================================================================
473// Transpiler — route onto nearest-neighbour (line) hardware, verified
474// ===========================================================================
475
476/// A circuit routed onto a linear nearest-neighbour coupling.
477pub struct Routed {
478    pub circuit: Circuit,
479    /// `final_map[logical] = physical`.
480    pub final_map: Vec<u8>,
481    pub swaps: u32,
482    pub source_2q: u32,
483    pub source_nonlocal: u32,
484}
485
486/// Route `src` onto a line topology `0-1-…-(n-1)`, inserting SWAPs (as 3·CX) so
487/// every one-control gate acts on adjacent physical qubits. Gates with ≥2 controls
488/// are placed on the mapped qubits directly (still correct, not made local).
489pub fn route_line(src: &Circuit) -> Routed {
490    let n = src.n_qubits;
491    let mut phys: Vec<u8> = (0..n).collect();
492    let mut logq: Vec<u8> = (0..n).collect();
493    let mut out = Circuit::new(n);
494    let mut swaps = 0u32;
495    let (mut s2q, mut nonlocal) = (0u32, 0u32);
496    let do_swap = |out: &mut Circuit, phys: &mut [u8], logq: &mut [u8], a: u8, b: u8| {
497        out.swap(a, b);
498        let (la, lb) = (logq[a as usize], logq[b as usize]);
499        logq.swap(a as usize, b as usize);
500        phys[la as usize] = b;
501        phys[lb as usize] = a;
502    };
503    for g in &src.ops {
504        match g.controls.len() {
505            0 => {
506                out.ops.push(Gate {
507                    base: g.base,
508                    controls: vec![],
509                    target: phys[g.target as usize],
510                    param: g.param,
511                });
512            }
513            1 => {
514                s2q += 1;
515                let (la, lb) = (g.controls[0], g.target);
516                let mut pa = phys[la as usize];
517                let pb = phys[lb as usize];
518                if (pa as i32 - pb as i32).abs() > 1 {
519                    nonlocal += 1;
520                    // shuffle the control toward the target until adjacent
521                    while (pa as i32 - pb as i32).abs() > 1 {
522                        let step = if pa < pb { pa + 1 } else { pa - 1 };
523                        do_swap(&mut out, &mut phys, &mut logq, pa, step);
524                        swaps += 1;
525                        pa = step;
526                    }
527                }
528                out.ops.push(Gate {
529                    base: g.base,
530                    controls: vec![phys[la as usize]],
531                    target: phys[lb as usize],
532                    param: g.param,
533                });
534            }
535            _ => {
536                s2q += 1;
537                out.ops.push(Gate {
538                    base: g.base,
539                    controls: g.controls.iter().map(|&c| phys[c as usize]).collect(),
540                    target: phys[g.target as usize],
541                    param: g.param,
542                });
543            }
544        }
545    }
546    Routed { circuit: out, final_map: phys, swaps, source_2q: s2q, source_nonlocal: nonlocal }
547}
548
549/// SWAP network restoring the identity mapping from `final_map`.
550fn restore_swaps(final_map: &[u8], n: u8) -> Vec<(u8, u8)> {
551    let mut logq = vec![0u8; n as usize];
552    for (l, &p) in final_map.iter().enumerate() {
553        logq[p as usize] = l as u8;
554    }
555    let mut phys: Vec<u8> = final_map.to_vec();
556    let mut out = Vec::new();
557    for p in 0..n as usize {
558        while logq[p] != p as u8 {
559            let cur = phys[p] as usize; // where logical p currently sits
560            out.push((p as u8, cur as u8));
561            let (lp, lc) = (logq[p], logq[cur]);
562            logq[p] = lc;
563            logq[cur] = lp;
564            phys[lp as usize] = cur as u8;
565            phys[lc as usize] = p as u8;
566        }
567    }
568    out
569}
570
571/// Is the routed circuit equivalent to the source? Checks that `routed ++
572/// (swaps restoring identity)` reconstructs the same byte-exact statevector.
573pub fn routing_equivalent(src: &Circuit, routed: &Routed) -> bool {
574    let mut full = routed.circuit.clone();
575    for (a, b) in restore_swaps(&routed.final_map, src.n_qubits) {
576        full.swap(a, b);
577    }
578    match (full.simulate(), src.simulate()) {
579        (Ok(a), Ok(b)) => a.statevector_hash() == b.statevector_hash(),
580        _ => false,
581    }
582}
583
584// ===========================================================================
585// OpenQASM interop — the framework-neutral lingua franca
586// ===========================================================================
587
588/// Emit OpenQASM 3.0 for a circuit (the dyadic gate set maps to standard gates;
589/// `P(k)` becomes `p(2*pi/2^k)`).
590pub fn to_qasm(c: &Circuit) -> String {
591    let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n");
592    s.push_str(&format!("qubit[{}] q;\n", c.n_qubits));
593    for g in &c.ops {
594        let t = g.target;
595        let line = match (g.base, g.controls.as_slice()) {
596            (BaseGate::I, []) => continue,
597            (BaseGate::X, []) => format!("x q[{t}];"),
598            (BaseGate::Y, []) => format!("y q[{t}];"),
599            (BaseGate::Z, []) => format!("z q[{t}];"),
600            (BaseGate::H, []) => format!("h q[{t}];"),
601            (BaseGate::S, []) => format!("s q[{t}];"),
602            (BaseGate::Sdg, []) => format!("sdg q[{t}];"),
603            (BaseGate::T, []) => format!("t q[{t}];"),
604            (BaseGate::Tdg, []) => format!("tdg q[{t}];"),
605            (BaseGate::P, []) => format!("p(2*pi/{}) q[{t}];", 1u64 << g.param),
606            (BaseGate::X, [c0]) => format!("cx q[{c0}],q[{t}];"),
607            (BaseGate::Y, [c0]) => format!("cy q[{c0}],q[{t}];"),
608            (BaseGate::Z, [c0]) => format!("cz q[{c0}],q[{t}];"),
609            (BaseGate::P, [c0]) => format!("cp(2*pi/{}) q[{c0}],q[{t}];", 1u64 << g.param),
610            (BaseGate::X, [a, b]) => format!("ccx q[{a}],q[{b}],q[{t}];"),
611            (BaseGate::Z, [a, b]) => format!("ccz q[{a}],q[{b}],q[{t}];"),
612            (base, ctrls) => format!("// unmapped: {base:?} controls={ctrls:?} target={t}"),
613        };
614        s.push_str(&line);
615        s.push('\n');
616    }
617    s
618}
619
620/// Parse a practical OpenQASM 2/3 subset into a circuit.
621///
622/// This targets what the mainstream toolchains actually export, not a tidy subset
623/// of the grammar: both register forms (`qubit[n] q;` and `qreg q[n];`), both
624/// spellings of measurement (`measure q[0] -> c[0];` and the OpenQASM 3 assignment
625/// `c[0] = measure q[0];`), the Clifford+T names, the rotations `rx`/`ry`/`rz`,
626/// `sx`/`sxdg`, the universal `u`/`u2`/`u3`, and `p`/`cp`/`crz`.
627/// `barrier`/`reset`/includes are ignored.
628///
629/// # Angles
630///
631/// Accepted as exact rational multiples of `π` — `pi`, `pi/4`, `3*pi/4`, `pi*0.5`,
632/// `0`, and negatives — provided the reduced denominator is a power of two. Such an
633/// angle needs no approximation: `m·2π/2^k` is emitted as one `P` gate per set bit
634/// of `m`, so it costs at most `DYADIC_MAX` gates and is byte-exact.
635///
636/// Anything else is **refused rather than rounded**. A simulator that silently
637/// approximated `π/3` would still return a hash, and that hash would be a lie.
638///
639/// `rz`, `rx`, `ry`, `sx` and `u*` are exact up to a global phase, which no
640/// measurement can observe; `crz` is exact outright, because its leftover factor
641/// lands on the control where it *would* be observable.
642pub fn from_qasm(src: &str) -> Result<Circuit, String> {
643    let mut n: Option<u8> = None;
644    // first pass: qubit count
645    for raw in src.lines() {
646        let line = strip(raw);
647        if let Some(rest) = line.strip_prefix("qubit[")
648            && let Some(end) = rest.find(']')
649        {
650            n = rest[..end].trim().parse().ok();
651        } else if let Some(rest) = line.strip_prefix("qreg ")
652            && let (Some(a), Some(b)) = (rest.find('['), rest.find(']'))
653        {
654            n = rest[a + 1..b].trim().parse().ok();
655        }
656    }
657    let n = n.ok_or("no qubit register declaration (qubit[n] q; or qreg q[n];)")?;
658    let mut c = Circuit::new(n);
659    for raw in src.lines() {
660        let line = strip(raw);
661        if line.is_empty() {
662            continue;
663        }
664        // Measurement is recorded, not simulated here, and the exporters disagree
665        // about how to spell it: OpenQASM 2 writes `measure q[0] -> c[0];` while
666        // Qiskit's OpenQASM 3 exporter writes `c[0] = measure q[0];`, whose first
667        // token is the *classical* register. Skip either shape before parsing a
668        // gate name out of the line.
669        if line.contains("measure") {
670            continue;
671        }
672        let head: String = line.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
673        let head = head.as_str();
674        match head {
675            "OPENQASM" | "include" | "qubit" | "qreg" | "creg" | "bit" | "measure" | "barrier"
676            | "reset" | "gate" => continue,
677            _ => {}
678        }
679        let qs = parse_qubits(&line);
680        let push = |c: &mut Circuit, base: BaseGate, controls: Vec<u8>, target: u8, param: u16| {
681            c.ops.push(Gate { base, controls, target, param });
682        };
683        match head {
684            "id" | "i" => {}
685            "x" => require1(&qs).map(|t| push(&mut c, BaseGate::X, vec![], t, 0))?,
686            "y" => require1(&qs).map(|t| push(&mut c, BaseGate::Y, vec![], t, 0))?,
687            "z" => require1(&qs).map(|t| push(&mut c, BaseGate::Z, vec![], t, 0))?,
688            "h" => require1(&qs).map(|t| push(&mut c, BaseGate::H, vec![], t, 0))?,
689            "s" => require1(&qs).map(|t| push(&mut c, BaseGate::S, vec![], t, 0))?,
690            "sdg" => require1(&qs).map(|t| push(&mut c, BaseGate::Sdg, vec![], t, 0))?,
691            "t" => require1(&qs).map(|t| push(&mut c, BaseGate::T, vec![], t, 0))?,
692            "tdg" => require1(&qs).map(|t| push(&mut c, BaseGate::Tdg, vec![], t, 0))?,
693            "cx" | "cnot" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::X, vec![a], t, 0))?,
694            "cy" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::Y, vec![a], t, 0))?,
695            "cz" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::Z, vec![a], t, 0))?,
696            "swap" => require2(&qs).map(|(a, b)| {
697                c.swap(a, b);
698            })?,
699            "ccx" | "toffoli" => require3(&qs).map(|(a, b, t)| push(&mut c, BaseGate::X, vec![a, b], t, 0))?,
700            "ccz" => require3(&qs).map(|(a, b, t)| push(&mut c, BaseGate::Z, vec![a, b], t, 0))?,
701            "p" | "u1" | "phase" => {
702                let t = parse_angle(&arg_n(&line, 0)?)?;
703                let q = require1(&qs)?;
704                push_turn(&mut c, &[], q, t);
705            }
706            "cp" | "cu1" | "cphase" => {
707                let t = parse_angle(&arg_n(&line, 0)?)?;
708                let (a, q) = require2(&qs)?;
709                push_turn(&mut c, &[a], q, t);
710            }
711            // Rotations. These are exact for dyadic angles but carry an
712            // unobservable global phase relative to the textbook `r*` matrices:
713            // `rz(θ)` is `P(θ)` up to `e^{-iθ/2}`. Measurement statistics are
714            // identical, which is what a circuit means.
715            "rz" => {
716                let t = parse_angle(&arg_n(&line, 0)?)?;
717                let q = require1(&qs)?;
718                push_turn(&mut c, &[], q, t);
719            }
720            "rx" => {
721                let t = parse_angle(&arg_n(&line, 0)?)?;
722                let q = require1(&qs)?;
723                push_rx(&mut c, q, t);
724            }
725            "ry" => {
726                let t = parse_angle(&arg_n(&line, 0)?)?;
727                let q = require1(&qs)?;
728                push_ry(&mut c, q, t);
729            }
730            // `sx` is `rx(π/2)` — a quarter turn, so `P(2)` between two `H`s.
731            "sx" | "sxdg" => {
732                let q = require1(&qs)?;
733                let t = if head == "sx" {
734                    Turn { m: 1, k: 2 }
735                } else {
736                    Turn { m: 3, k: 2 }
737                };
738                push_rx(&mut c, q, t);
739            }
740            // `u(θ,φ,λ) = rz(φ)·ry(θ)·rz(λ)`; the rightmost applies first.
741            "u" | "u3" | "u2" => {
742                let (theta, phi, lam) = if head == "u2" {
743                    // u2(φ,λ) = u3(π/2, φ, λ)
744                    (Turn { m: 1, k: 2 }, parse_angle(&arg_n(&line, 0)?)?, parse_angle(&arg_n(&line, 1)?)?)
745                } else {
746                    (
747                        parse_angle(&arg_n(&line, 0)?)?,
748                        parse_angle(&arg_n(&line, 1)?)?,
749                        parse_angle(&arg_n(&line, 2)?)?,
750                    )
751                };
752                let q = require1(&qs)?;
753                push_turn(&mut c, &[], q, lam);
754                push_ry(&mut c, q, theta);
755                push_turn(&mut c, &[], q, phi);
756            }
757            // Controlled-rz is exact with no phase caveat: `CP(θ)` preceded by
758            // `P(−θ/2)` on the control reproduces `diag(1,1,e^{−iθ/2},e^{iθ/2})`.
759            "crz" => {
760                // Halve the SIGNED angle, then wrap. Wrapping first would pick the
761                // wrong branch, because crz has period 4π and its half does not.
762                let (p, den) = angle_turns(&arg_n(&line, 0)?)?;
763                if den * 2 > (1i128 << crate::quantum::DYADIC_MAX) {
764                    return Err("crz angle is finer than the dyadic floor".into());
765                }
766                let (a, q) = require2(&qs)?;
767                push_turn(&mut c, &[], a, to_turn(-p, den * 2));
768                push_turn(&mut c, &[a], q, to_turn(p, den));
769            }
770            other => {
771                return Err(format!(
772                    "unsupported gate: '{other}' (supported: id x y z h s sdg t tdg sx sxdg \
773                     rx ry rz p/u1/phase u/u2/u3 cx cy cz cp/cu1 crz swap ccx ccz)"
774                ));
775            }
776        }
777    }
778    c.validate().map_err(|e| e.to_string())?;
779    Ok(c)
780}
781
782fn strip(line: &str) -> String {
783    let no_comment = line.split("//").next().unwrap_or("");
784    no_comment.trim().trim_end_matches(';').trim().to_string()
785}
786
787fn parse_qubits(line: &str) -> Vec<u8> {
788    let mut out = Vec::new();
789    let bytes = line.as_bytes();
790    let mut i = 0;
791    while i < bytes.len() {
792        if bytes[i] == b'[' {
793            let mut j = i + 1;
794            while j < bytes.len() && bytes[j] != b']' {
795                j += 1;
796            }
797            if let Ok(v) = line[i + 1..j].trim().parse::<u8>() {
798                out.push(v);
799            }
800            i = j + 1;
801        } else {
802            i += 1;
803        }
804    }
805    out
806}
807
808fn require1(qs: &[u8]) -> Result<u8, String> {
809    qs.first().copied().ok_or_else(|| "expected 1 qubit".into())
810}
811fn require2(qs: &[u8]) -> Result<(u8, u8), String> {
812    if qs.len() >= 2 {
813        Ok((qs[0], qs[1]))
814    } else {
815        Err("expected 2 qubits".into())
816    }
817}
818fn require3(qs: &[u8]) -> Result<(u8, u8, u8), String> {
819    if qs.len() >= 3 {
820        Ok((qs[0], qs[1], qs[2]))
821    } else {
822        Err("expected 3 qubits".into())
823    }
824}
825
826/// Parse a `p(...)`/`cp(...)` argument into a dyadic `k` where the phase is
827/// `2π/2^k`. Accepts `2*pi/2^k`, `2*pi/N` (N a power of two), `pi/N` (→ k=log2(N)+1),
828/// and bare `pi` (→ k=1).
829/// An angle held exactly, as `m / 2^k` of a full turn with `0 ≤ m < 2^k`.
830///
831/// The simulator's only parameterised gate is `P(k) = diag(1, e^{2πi/2^k})`, so
832/// a single gate reaches just the angles `2π/2^k`. Every *dyadic* angle is still
833/// exactly reachable, because the binary expansion of `m` splits it: each set bit
834/// `i` contributes `2^i/2^k` of a turn, which is `P(k − i)`. That bounds any angle
835/// at `DYADIC_MAX` gates rather than the `m` repetitions a naive reading suggests.
836#[derive(Clone, Copy, Debug, PartialEq, Eq)]
837struct Turn {
838    m: u64,
839    k: u32,
840}
841
842/// Parse a decimal literal as an exact rational. `"0.5"` is `1/2` — no float
843/// rounding enters, so `pi*0.5` stays exact.
844fn decimal_ratio(tok: &str) -> Option<(i128, i128)> {
845    if tok.is_empty() {
846        return None;
847    }
848    match tok.split_once('.') {
849        None => tok.parse::<i128>().ok().map(|v| (v, 1)),
850        Some((int, frac)) => {
851            if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
852                return None;
853            }
854            let scale = 10i128.checked_pow(frac.len() as u32)?;
855            let whole: i128 = if int.is_empty() { 0 } else { int.parse().ok()? };
856            let f: i128 = frac.parse().ok()?;
857            Some((whole * scale + f, scale))
858        }
859    }
860}
861
862/// Parse an OpenQASM angle expression into an exact fraction of a turn.
863///
864/// Handles what the mainstream exporters actually emit: `pi`, `pi/4`, `3*pi/4`,
865/// `2*pi/8`, `pi*0.5`, `0.5*pi`, `0`, and any of them negated. The angle must be a
866/// rational multiple of `π` whose denominator is a power of two once reduced —
867/// anything else is refused rather than approximated, because approximating it
868/// would quietly cost the byte-exactness the whole crate rests on.
869///
870/// A bare number with no `pi` is read as radians. That is the one place input is
871/// *interpreted* rather than read: it is accepted only when it lands within `1e-9`
872/// of an exact dyadic angle, which is the case when an exporter has printed a
873/// rounded decimal for an angle it meant exactly.
874/// Wrap a signed fraction of a turn into the `[0, 1)` representative the `P`
875/// ladder emits.
876fn to_turn(p: i128, q: i128) -> Turn {
877    let k = q.trailing_zeros();
878    let m = ((p % q) + q) % q;
879    Turn { m: m as u64, k }
880}
881
882/// As [`parse_angle`], but keeping the **signed** angle rather than its residue
883/// modulo a full turn.
884///
885/// The difference matters for half-angle gates. `crz` has period `4π`, not `2π`,
886/// so `θ` and `θ ± 2π` have halves that differ by `π`. Wrapping first and halving
887/// second silently picks the wrong branch. For `rz`/`rx`/`ry` the same leftover is
888/// a *global* phase and unobservable, which is why only `crz` needs this.
889fn angle_turns(arg: &str) -> Result<(i128, i128), String> {
890    let s = arg.replace(' ', "").to_lowercase();
891    if s.is_empty() {
892        return Err("empty angle".into());
893    }
894    let (sign, body) = match s.strip_prefix('-') {
895        Some(r) => (-1i128, r),
896        None => (1i128, s.strip_prefix('+').unwrap_or(&s)),
897    };
898
899    // Split into factors, remembering whether each is multiplied or divided in.
900    let mut parts: Vec<(char, String)> = Vec::new();
901    let (mut cur, mut op) = (String::new(), '*');
902    for ch in body.chars() {
903        if ch == '*' || ch == '/' {
904            parts.push((op, std::mem::take(&mut cur)));
905            op = ch;
906        } else {
907            cur.push(ch);
908        }
909    }
910    parts.push((op, cur));
911
912    let (mut num, mut den, mut pi_pow) = (sign, 1i128, 0i32);
913    for (op, tok) in &parts {
914        if tok == "pi" {
915            pi_pow += if *op == '*' { 1 } else { -1 };
916            continue;
917        }
918        let (a, b) = decimal_ratio(tok)
919            .ok_or_else(|| format!("cannot read '{tok}' in angle '{arg}'"))?;
920        if a == 0 && *op == '/' {
921            return Err("division by zero in angle".into());
922        }
923        if *op == '*' {
924            num *= a;
925            den *= b;
926        } else {
927            num *= b;
928            den *= a;
929        }
930    }
931
932    if num == 0 {
933        return Ok((0, 2));
934    }
935    if pi_pow == 0 {
936        // Bare radians. Accept only if it is a dyadic angle to within 1e-9.
937        let v = (num as f64 / den as f64) / std::f64::consts::TAU;
938        for k in 1..=crate::quantum::DYADIC_MAX as u32 {
939            let scaled = v * (1u64 << k) as f64;
940            let r = scaled.round();
941            if (scaled - r).abs() < 1e-9 {
942                return Ok((r as i128, 1i128 << k));
943            }
944        }
945        return Err(format!(
946            "angle '{arg}' is not a dyadic multiple of 2π; this simulator is exact, \
947             so it refuses angles it cannot represent rather than rounding them"
948        ));
949    }
950    if pi_pow != 1 {
951        return Err(format!("angle '{arg}' is not linear in pi"));
952    }
953
954    // angle = (num/den)·π, so in turns that is num/(2·den).
955    let mut p = num;
956    let mut q = den * 2;
957    if q < 0 {
958        p = -p;
959        q = -q;
960    }
961    let g = {
962        let (mut a, mut b) = (p.unsigned_abs(), q.unsigned_abs());
963        while b != 0 {
964            let t = a % b;
965            a = b;
966            b = t;
967        }
968        a.max(1) as i128
969    };
970    p /= g;
971    q /= g;
972    if q <= 0 || (q as u128).count_ones() != 1 {
973        return Err(format!(
974            "angle '{arg}' is not a dyadic multiple of 2π (reduced denominator {q}); \
975             this simulator is exact, so it refuses angles it cannot represent"
976        ));
977    }
978    let k = q.trailing_zeros();
979    if k > crate::quantum::DYADIC_MAX as u32 {
980        return Err(format!("angle '{arg}' is finer than the dyadic floor (k={k})"));
981    }
982    Ok((p, q))
983}
984
985/// Parse an OpenQASM angle expression into an exact fraction of a turn.
986///
987/// Handles what the mainstream exporters actually emit: `pi`, `pi/4`, `3*pi/4`,
988/// `2*pi/8`, `pi*0.5`, `0.5*pi`, `0`, and any of them negated. The angle must be a
989/// rational multiple of `π` whose denominator is a power of two once reduced —
990/// anything else is refused rather than approximated, because approximating it
991/// would quietly cost the byte-exactness the whole crate rests on.
992fn parse_angle(arg: &str) -> Result<Turn, String> {
993    let (p, q) = angle_turns(arg)?;
994    Ok(to_turn(p, q))
995}
996
997/// Read the `i`-th parenthesised argument of a gate line.
998fn arg_n(line: &str, i: usize) -> Result<String, String> {
999    let a = line.find('(').ok_or("gate needs an argument")?;
1000    let b = line[a..].find(')').ok_or("unterminated argument")? + a;
1001    line[a + 1..b]
1002        .split(',')
1003        .nth(i)
1004        .map(|s| s.trim().to_string())
1005        .ok_or_else(|| format!("expected at least {} argument(s)", i + 1))
1006}
1007
1008/// Emit an exact dyadic phase as `P` gates, one per set bit of `m`.
1009fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) {
1010    for i in 0..=t.k {
1011        if (t.m >> i) & 1 == 1 {
1012            let k = t.k - i;
1013            // k == 0 is a whole turn: the identity, so it contributes nothing.
1014            if k >= 1 {
1015                c.ops.push(Gate {
1016                    base: BaseGate::P,
1017                    controls: controls.to_vec(),
1018                    target,
1019                    param: k as u16,
1020                });
1021            }
1022        }
1023    }
1024}
1025
1026/// `rx(θ) = H · rz(θ) · H`, exactly, in the dyadic basis.
1027fn push_rx(c: &mut Circuit, target: u8, t: Turn) {
1028    c.h(target);
1029    push_turn(c, &[], target, t);
1030    c.h(target);
1031}
1032
1033/// `ry(θ) = S · rx(θ) · S†`. Circuit order applies the rightmost factor first.
1034fn push_ry(c: &mut Circuit, target: u8, t: Turn) {
1035    let one = |b: BaseGate| Gate { base: b, controls: vec![], target, param: 0 };
1036    c.ops.push(one(BaseGate::Sdg));
1037    push_rx(c, target, t);
1038    c.ops.push(one(BaseGate::S));
1039}
1040
1041/// The content hash of a circuit — the identity a run receipt binds.
1042pub fn circuit_id(c: &Circuit) -> [u8; 32] {
1043    circuit_hash(c)
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    #[test]
1049    #[ignore]
1050    fn probe_sv_perf() {
1051        use std::time::Instant;
1052        use crate::quantum::Circuit;
1053        println!("\n  statevector: time per gate vs width");
1054        for n in [8u8, 12, 16, 18, 20, 22] {
1055            let mut c = Circuit::new(n);
1056            for _ in 0..3 { for q in 0..n { c.h(q); } for q in 0..n - 1 { c.cx(q, q + 1); } }
1057            let gates = c.ops.len();
1058            let t0 = Instant::now();
1059            let sv = c.simulate().unwrap();
1060            let el = t0.elapsed();
1061            println!("   n={n:<3} dim={:<9} {gates:>4} gates  {:>9.2?} total  {:>8.1} ns/gate  {:>7.2} ns/amp-gate",
1062                1usize << n, el, el.as_nanos() as f64 / gates as f64,
1063                el.as_nanos() as f64 / (gates as f64 * (1usize << n) as f64));
1064            std::hint::black_box(sv.statevector_hash());
1065        }
1066        println!("\n  gate-kind cost at n=20 (200 gates each)");
1067        for kind in ["h", "t", "z", "cx", "cz"] {
1068            let n = 20u8;
1069            let mut c = Circuit::new(n);
1070            for i in 0..200 {
1071                let q = (i % n as usize) as u8;
1072                match kind {
1073                    "h" => { c.h(q); }
1074                    "t" => { c.t(q); }
1075                    "z" => { c.z(q); }
1076                    "cx" => { c.cx(q, (q + 1) % n); }
1077                    _ => { c.cz(q, (q + 1) % n); }
1078                }
1079            }
1080            let t0 = Instant::now();
1081            let sv = c.simulate().unwrap();
1082            let el = t0.elapsed();
1083            println!("   {kind:<3} {:>8.1} ns/gate", el.as_nanos() as f64 / 200.0);
1084            std::hint::black_box(sv.statevector_hash());
1085        }
1086    }
1087
1088    use super::*;
1089
1090    fn approx(a: f64, b: f64, tol: f64) -> bool {
1091        (a - b).abs() < tol
1092    }
1093
1094    #[test]
1095    fn bell_is_maximally_entangled() {
1096        let sv = bell().simulate().unwrap();
1097        let p = probabilities(&sv);
1098        assert!(approx(p[0b00], 0.5, 1e-4) && approx(p[0b11], 0.5, 1e-4));
1099        assert!(approx(p[0b01], 0.0, 1e-4) && approx(p[0b10], 0.0, 1e-4));
1100        // each qubit is maximally entangled with the other
1101        assert!(approx(entanglement_entropy(&sv, 0), 1.0, 1e-3), "S={}", entanglement_entropy(&sv, 0));
1102        assert!(approx(purity(&sv, 0), 0.5, 1e-3));
1103    }
1104
1105    #[test]
1106    fn ghz_all_or_nothing() {
1107        let sv = ghz(4).simulate().unwrap();
1108        let p = probabilities(&sv);
1109        assert!(approx(p[0], 0.5, 1e-4) && approx(p[15], 0.5, 1e-4));
1110        assert!(approx(entanglement_entropy(&sv, 2), 1.0, 1e-3));
1111    }
1112
1113    #[test]
1114    fn plus_state_bloch() {
1115        // H|0> = |+>  → Bloch (1,0,0), zero entanglement (product state)
1116        let mut c = Circuit::new(1);
1117        c.h(0);
1118        let sv = c.simulate().unwrap();
1119        let b = bloch(&sv, 0);
1120        assert!(approx(b[0], 1.0, 1e-3) && approx(b[1], 0.0, 1e-3) && approx(b[2], 0.0, 1e-3), "bloch={b:?}");
1121        assert!(approx(entanglement_entropy(&sv, 0), 0.0, 1e-3));
1122    }
1123
1124    #[test]
1125    fn grover_amplifies_the_marked_state() {
1126        for &(n, marked) in &[(3u8, 5u32), (4, 11), (5, 20)] {
1127            let sv = grover(n, marked).simulate().unwrap();
1128            let top = top_outcomes(&sv, 1)[0];
1129            assert_eq!(top.0, marked as usize, "grover n={n} should peak at {marked}");
1130            assert!(top.1 > 0.6, "marked prob {} too low", top.1);
1131        }
1132    }
1133
1134    #[test]
1135    fn bernstein_vazirani_recovers_secret() {
1136        let secret = 0b10110u32;
1137        let sv = bernstein_vazirani(secret, 5).simulate().unwrap();
1138        // marginal over the 5 input qubits should concentrate on `secret`
1139        let top = top_outcomes(&sv, 1)[0];
1140        assert_eq!(top.0 & 0b11111, secret as usize, "BV should reveal the secret");
1141    }
1142
1143    #[test]
1144    fn deutsch_jozsa_distinguishes() {
1145        let sv_c = deutsch_jozsa(false, 4).simulate().unwrap();
1146        let sv_b = deutsch_jozsa(true, 4).simulate().unwrap();
1147        // constant → inputs all-zero; balanced → inputs nonzero
1148        let inputs = |i: usize| i & 0b1111;
1149        assert_eq!(inputs(top_outcomes(&sv_c, 1)[0].0), 0, "constant → all-zero inputs");
1150        assert_ne!(inputs(top_outcomes(&sv_b, 1)[0].0), 0, "balanced → nonzero inputs");
1151    }
1152
1153    #[test]
1154    fn superdense_roundtrips_all_four() {
1155        for bits in 0u8..4 {
1156            let sv = superdense(bits).simulate().unwrap();
1157            let top = top_outcomes(&sv, 1)[0];
1158            assert_eq!(top.0, bits as usize, "superdense {bits} decoded wrong");
1159            assert!(top.1 > 0.99);
1160        }
1161    }
1162
1163    #[test]
1164    fn clifford_detection() {
1165        assert!(is_clifford(&bell()));
1166        assert!(is_clifford(&ghz(5)));
1167        assert!(!is_clifford(&teleportation())); // has T
1168        assert!(!is_clifford(&Circuit::qft(4))); // has P(k>2)
1169        assert_eq!(recommend_backend(&bell()).0, Backend::Stabilizer);
1170        assert_eq!(recommend_backend(&Circuit::qft(6)).0, Backend::Statevector);
1171    }
1172
1173    #[test]
1174    fn qasm_round_trip() {
1175        for c in [bell(), ghz(3), Circuit::qft(4), grover(3, 5)] {
1176            let qasm = to_qasm(&c);
1177            let back = from_qasm(&qasm).expect("reparse");
1178            // same reconstructed state ⇒ same circuit semantics
1179            assert_eq!(
1180                c.simulate().unwrap().statevector_hash(),
1181                back.simulate().unwrap().statevector_hash(),
1182                "QASM round-trip changed the state"
1183            );
1184        }
1185    }
1186
1187    #[test]
1188    fn qasm_imports_foreign_circuit() {
1189        let src = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n";
1190        let c = from_qasm(src).unwrap();
1191        assert_eq!(c.simulate().unwrap().statevector_hash(), bell().simulate().unwrap().statevector_hash());
1192    }
1193
1194    /// Equality of circuits *as physics*: the same state up to the global phase
1195    /// that no measurement can see. `rz` and `P` differ by exactly that factor, so
1196    /// hash equality would be the wrong test here.
1197    fn same_state(a: &str, b: &Circuit) {
1198        let got = from_qasm(a).unwrap_or_else(|e| panic!("parse {a:?}: {e}"));
1199        let f = got.simulate().unwrap().fidelity_fx(&b.simulate().unwrap());
1200        assert!(
1201            (f - crate::quantum::ONE).abs() < 1 << 12,
1202            "{a:?} gave fidelity {f} against the reference, want 1"
1203        );
1204    }
1205
1206    /// Qiskit's OpenQASM 3 exporter writes measurement as an assignment, so the
1207    /// first token of the line is the classical register rather than a gate. This
1208    /// is its default output shape, so failing it means failing the format the
1209    /// most widely used toolchain emits.
1210    #[test]
1211    fn qasm3_assignment_measure_parses() {
1212        let src = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\n\
1213                   h q[0];\ncx q[0], q[1];\nc[0] = measure q[0];\nc[1] = measure q[1];\n";
1214        same_state(src, &bell());
1215    }
1216
1217    /// The rotations the mainstream exporters emit, each checked against the gate
1218    /// it must equal rather than merely accepted by the parser.
1219    #[test]
1220    fn rotations_decompose_to_the_dyadic_basis() {
1221        let mut x = Circuit::new(1);
1222        x.x(0);
1223        let mut y = Circuit::new(1);
1224        y.y(0);
1225        let mut z = Circuit::new(1);
1226        z.z(0);
1227        let mut t = Circuit::new(1);
1228        t.t(0);
1229
1230        same_state("qreg q[1];\nrx(pi) q[0];", &x);
1231        same_state("qreg q[1];\nry(pi) q[0];", &y);
1232        same_state("qreg q[1];\nrz(pi) q[0];", &z);
1233        same_state("qreg q[1];\np(pi/4) q[0];", &t);
1234        // u3(π,0,π) is X — the canonical single-qubit-gate spelling
1235        same_state("qreg q[1];\nu3(pi,0,pi) q[0];", &x);
1236        same_state("qreg q[1];\nu(pi,0,pi) q[0];", &x);
1237        // two √X make an X
1238        same_state("qreg q[1];\nsx q[0];\nsx q[0];", &x);
1239    }
1240
1241    /// A general dyadic angle is not one `P` gate — `3π/4` is `P(3)` then `P(2)`,
1242    /// found from the binary expansion of the numerator. Check it against the
1243    /// explicit pair, and check the bit-splitting really happened.
1244    #[test]
1245    fn a_general_dyadic_angle_splits_across_gates() {
1246        let mut want = Circuit::new(1);
1247        want.h(0).p(3, 0).p(2, 0);
1248        let got = from_qasm("qreg q[1];\nh q[0];\np(3*pi/4) q[0];").unwrap();
1249        assert_eq!(
1250            got.simulate().unwrap().statevector_hash(),
1251            want.simulate().unwrap().statevector_hash()
1252        );
1253        assert_eq!(got.ops.len(), 3, "3π/4 should split into two P gates after the H");
1254    }
1255
1256    /// Negative and zero angles, which exporters emit constantly and the old
1257    /// parser refused outright. A rotation and its inverse must cancel exactly.
1258    #[test]
1259    fn negative_and_zero_angles_are_exact() {
1260        let plain = Circuit::new(1);
1261        same_state("qreg q[1];\nrz(0) q[0];", &plain);
1262        same_state("qreg q[1];\np(0) q[0];", &plain);
1263        same_state("qreg q[1];\nrz(pi/2) q[0];\nrz(-pi/2) q[0];", &plain);
1264        same_state("qreg q[1];\nsx q[0];\nsxdg q[0];", &plain);
1265        // `pi*0.5` is the decimal spelling; 0.5 is an exact binary fraction
1266        let mut s = Circuit::new(1);
1267        s.s(0);
1268        same_state("qreg q[1];\np(pi*0.5) q[0];", &s);
1269        same_state("qreg q[1];\np(0.5*pi) q[0];", &s);
1270    }
1271
1272    /// Controlled-`rz` is exact with no phase caveat at all, because the leftover
1273    /// factor lands on the control where it is observable — and representable.
1274    #[test]
1275    fn controlled_rz_is_exact() {
1276        // A rotation and its inverse must cancel to nothing — checked on a
1277        // superposed control, where crz and cp are observably different, so this
1278        // cannot pass by accidentally implementing the wrong one.
1279        let mut hh = Circuit::new(2);
1280        hh.h(0).h(1);
1281        same_state(
1282            "qreg q[2];\nh q[0];\nh q[1];\ncrz(pi/2) q[0],q[1];\ncrz(-pi/2) q[0],q[1];",
1283            &hh,
1284        );
1285        let crz = from_qasm("qreg q[2];\nh q[0];\nh q[1];\ncrz(pi/2) q[0],q[1];").unwrap();
1286        let mut cp = Circuit::new(2);
1287        cp.h(0).h(1).cp(2, 0, 1);
1288        assert_ne!(
1289            crz.simulate().unwrap().statevector_hash(),
1290            cp.simulate().unwrap().statevector_hash(),
1291            "crz must not silently be cp — they differ by a phase on the control"
1292        );
1293    }
1294
1295    /// The refusal matters as much as the acceptance: an angle this simulator
1296    /// cannot hold exactly is rejected rather than rounded, because rounding it
1297    /// would quietly cost the byte-exactness everything else depends on.
1298    #[test]
1299    fn non_dyadic_angles_are_refused_not_rounded() {
1300        for src in ["qreg q[1];\nrz(pi/3) q[0];", "qreg q[1];\np(pi/5) q[0];"] {
1301            let e = from_qasm(src).expect_err("a non-dyadic angle must not be accepted");
1302            assert!(e.contains("dyadic"), "error should say why: {e}");
1303        }
1304    }
1305
1306    #[test]
1307    fn catalog_all_build_and_simulate() {
1308        for (id, _, _) in catalog() {
1309            let c = build_algorithm(id, 4, 5).expect(id);
1310            assert!(c.simulate().is_ok(), "{id} failed to simulate");
1311        }
1312    }
1313
1314    #[test]
1315    fn routing_preserves_the_state() {
1316        // route several circuits onto a line and prove equivalence via statevector.
1317        for c in [bell(), ghz(5), Circuit::qft(5), trotter_ising(6, 3)] {
1318            let r = route_line(&c);
1319            assert!(routing_equivalent(&c, &r), "routed circuit must reproduce the state");
1320            // every routed one-control gate is now nearest-neighbour on the line
1321            for g in &r.circuit.ops {
1322                if g.controls.len() == 1 {
1323                    let d = (g.controls[0] as i32 - g.target as i32).abs();
1324                    assert_eq!(d, 1, "routed 2q gate must be local: {} {}", g.controls[0], g.target);
1325                }
1326            }
1327        }
1328    }
1329
1330    #[test]
1331    fn noise_decoheres_the_signal() {
1332        // a qubit prepared in |1> reads ⟨Z⟩ = -1 with no noise; depolarizing noise
1333        // pulls it toward 0.
1334        let mut c = Circuit::new(2);
1335        c.x(0);
1336        for _ in 0..6 {
1337            c.cx(0, 1);
1338            c.cx(0, 1); // identity pair, but each gate is a noise opportunity
1339        }
1340        let e0 = noisy_expect_z(&c, 0, 0, 300, 1); // no noise → exact
1341        let e1 = noisy_expect_z(&c, 0, 40, 500, 1); // light noise
1342        let e2 = noisy_expect_z(&c, 0, 150, 500, 1); // heavier noise
1343        assert!(approx(e0, -1.0, 1e-9), "no noise ⇒ exact: {e0}");
1344        // ⟨Z⟩ decoheres monotonically from -1 toward 0 as noise grows
1345        assert!(e1 > e0 && e1 < 0.0, "light noise decoheres: {e1}");
1346        assert!(e2 > e1, "more noise ⇒ more decoherence: {e2} > {e1}");
1347    }
1348
1349    #[test]
1350    fn showcase_circuits_target_the_right_backends() {
1351        // Trotter Ising is non-Clifford (has P(3)=T-family) → routed to statevector/
1352        // tensor-network, its home turf.
1353        let ti = trotter_ising(6, 4);
1354        assert!(!is_clifford(&ti), "Trotter Ising should be non-Clifford");
1355        assert!(ti.simulate().is_ok());
1356        // Random Clifford → stabilizer tier.
1357        let rc = random_clifford(6, 60, 7);
1358        assert!(is_clifford(&rc), "random Clifford must be Clifford");
1359        assert_eq!(random_clifford(6, 60, 7), random_clifford(6, 60, 7), "deterministic in seed");
1360    }
1361}