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