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