Skip to main content

scirs2_core/quantum/
circuit.rs

1//! Quantum circuit construction, execution, and analysis.
2//!
3//! # Overview
4//!
5//! A [`QuantumCircuit`] is an ordered sequence of gate operations applied to
6//! a fixed-width quantum register.  Each operation records the gate and the
7//! qubit indices it targets.
8//!
9//! ```rust
10//! use scirs2_core::quantum::circuit::QuantumCircuit;
11//! use scirs2_core::quantum::qubits::QubitRegister;
12//! use rand::SeedableRng;
13//! use rand_chacha::ChaCha20Rng;
14//!
15//! // Build and run a Bell-pair circuit.
16//! let mut circ = QuantumCircuit::new(2);
17//! circ.h(0).expect("should succeed");
18//! circ.cx(0, 1).expect("should succeed");
19//!
20//! let initial = QubitRegister::new_zero_state(2).expect("should succeed");
21//! let final_state = circ.run(initial).expect("should succeed");
22//!
23//! // Measure several times (seeded for reproducibility).
24//! let mut rng = ChaCha20Rng::seed_from_u64(42);
25//! let bits = circ.measure_all(&final_state, &mut rng).expect("should succeed");
26//! assert!(bits == vec![0, 0] || bits == vec![1, 1]);
27//! ```
28
29use rand::{Rng, RngExt};
30
31use super::error::{QuantumError, QuantumResult};
32use super::gates::{
33    apply_gate, Fredkin, Hadamard, Identity, PauliX, PauliY, PauliZ, PhaseS, PhaseSdg, PhaseShift,
34    PhaseT, PhaseTdg, QuantumGate, RotX, RotY, RotZ, Toffoli, Unitary1Q, CNOT, CU, CZ, SWAP,
35};
36use super::qubits::QubitRegister;
37
38// ─────────────────────────────────────────────────────────────────────────────
39// GateOp — a single gate applied to specific qubits
40// ─────────────────────────────────────────────────────────────────────────────
41
42/// A single gate application: the gate and the target qubit indices.
43pub struct GateOp {
44    /// Boxed gate (heap-allocated so we can store heterogeneous gate types).
45    gate: Box<dyn QuantumGate>,
46    /// Target qubit indices (length == gate.n_qubits()).
47    qubits: Vec<usize>,
48}
49
50impl GateOp {
51    fn new(gate: impl QuantumGate + 'static, qubits: Vec<usize>) -> Self {
52        Self {
53            gate: Box::new(gate),
54            qubits,
55        }
56    }
57}
58
59impl std::fmt::Debug for GateOp {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "GateOp({}, qubits={:?})", self.gate.name(), self.qubits)
62    }
63}
64
65// ─────────────────────────────────────────────────────────────────────────────
66// QuantumCircuit
67// ─────────────────────────────────────────────────────────────────────────────
68
69/// A quantum circuit: an ordered list of gate operations acting on an n-qubit
70/// register.
71///
72/// Gates are appended via convenience methods (`h`, `cx`, `rx`, …) or via the
73/// generic [`QuantumCircuit::add_gate`] method.  The circuit is executed by
74/// calling [`QuantumCircuit::run`] which returns the final statevector.
75pub struct QuantumCircuit {
76    /// Number of qubits the circuit acts on.
77    n_qubits: usize,
78    /// Ordered gate operations.
79    ops: Vec<GateOp>,
80}
81
82impl QuantumCircuit {
83    // ── Constructors ─────────────────────────────────────────────────────────
84
85    /// Create an empty circuit acting on `n_qubits` qubits.
86    pub fn new(n_qubits: usize) -> Self {
87        Self {
88            n_qubits,
89            ops: Vec::new(),
90        }
91    }
92
93    // ── Properties ───────────────────────────────────────────────────────────
94
95    /// Number of qubits in this circuit.
96    pub fn n_qubits(&self) -> usize {
97        self.n_qubits
98    }
99
100    /// Number of gate operations in the circuit.
101    pub fn n_ops(&self) -> usize {
102        self.ops.len()
103    }
104
105    /// Circuit depth: the number of *sequential* layers when gates on disjoint
106    /// qubits are parallelised.
107    ///
108    /// Uses a greedy layer-assignment algorithm: each gate is placed in the
109    /// earliest layer that has no qubit overlap with it.
110    pub fn circuit_depth(&self) -> usize {
111        // layer_finish[q] = the layer in which qubit q was last used.
112        let mut layer_finish = vec![0usize; self.n_qubits];
113        let mut depth = 0usize;
114
115        for op in &self.ops {
116            // The gate must be placed after all layers that touch its qubits.
117            let earliest = op
118                .qubits
119                .iter()
120                .map(|&q| layer_finish[q])
121                .max()
122                .unwrap_or(0);
123            let layer = earliest + 1;
124            for &q in &op.qubits {
125                layer_finish[q] = layer;
126            }
127            if layer > depth {
128                depth = layer;
129            }
130        }
131        depth
132    }
133
134    // ── Gate append ──────────────────────────────────────────────────────────
135
136    /// Append an arbitrary gate acting on `qubits`.
137    ///
138    /// Validates that the qubit indices are in range before storing the gate.
139    pub fn add_gate(
140        &mut self,
141        gate: impl QuantumGate + 'static,
142        qubits: &[usize],
143    ) -> QuantumResult<()> {
144        if qubits.len() != gate.n_qubits() {
145            return Err(QuantumError::GateArityMismatch {
146                gate_qubits: gate.n_qubits(),
147                supplied: qubits.len(),
148            });
149        }
150        for &q in qubits {
151            if q >= self.n_qubits {
152                return Err(QuantumError::QubitIndexOutOfRange {
153                    index: q,
154                    n_qubits: self.n_qubits,
155                });
156            }
157        }
158        // Duplicate check.
159        for i in 0..qubits.len() {
160            for j in (i + 1)..qubits.len() {
161                if qubits[i] == qubits[j] {
162                    return Err(QuantumError::DuplicateQubitIndex { index: qubits[i] });
163                }
164            }
165        }
166        self.ops.push(GateOp::new(gate, qubits.to_vec()));
167        Ok(())
168    }
169
170    // ── Execution ─────────────────────────────────────────────────────────────
171
172    /// Execute the circuit on `initial_state` and return the final statevector.
173    ///
174    /// The register must have exactly `self.n_qubits` qubits.
175    pub fn run(&self, initial_state: QubitRegister) -> QuantumResult<QubitRegister> {
176        if initial_state.n_qubits() != self.n_qubits {
177            return Err(QuantumError::CircuitRegisterMismatch {
178                circuit_qubits: self.n_qubits,
179                register_qubits: initial_state.n_qubits(),
180            });
181        }
182        let mut state = initial_state;
183        for op in &self.ops {
184            apply_gate(&mut state, op.gate.as_ref(), &op.qubits)?;
185        }
186        Ok(state)
187    }
188
189    /// Measure all qubits of `state` once, returning a bit-string (qubit 0 first).
190    ///
191    /// Does *not* collapse the state; call this method on the result of [`Self::run`].
192    pub fn measure_all<R: Rng>(
193        &self,
194        state: &QubitRegister,
195        rng: &mut R,
196    ) -> QuantumResult<Vec<u8>> {
197        if state.n_qubits() != self.n_qubits {
198            return Err(QuantumError::CircuitRegisterMismatch {
199                circuit_qubits: self.n_qubits,
200                register_qubits: state.n_qubits(),
201            });
202        }
203        Ok(state.measure_all(rng))
204    }
205
206    /// Run the circuit `shots` times from `initial_state` and collect all
207    /// measurement outcomes.
208    ///
209    /// Each shot independently executes the circuit and performs a single
210    /// full-register measurement.
211    pub fn sample<R: Rng>(
212        &self,
213        initial_state: &QubitRegister,
214        shots: usize,
215        rng: &mut R,
216    ) -> QuantumResult<Vec<Vec<u8>>> {
217        if initial_state.n_qubits() != self.n_qubits {
218            return Err(QuantumError::CircuitRegisterMismatch {
219                circuit_qubits: self.n_qubits,
220                register_qubits: initial_state.n_qubits(),
221            });
222        }
223        let final_state = self.run(initial_state.clone())?;
224        let results = (0..shots).map(|_| final_state.measure_all(rng)).collect();
225        Ok(results)
226    }
227
228    // ── Convenience single-qubit gates ────────────────────────────────────────
229
230    /// Append Identity on qubit `q`.
231    pub fn id(&mut self, q: usize) -> QuantumResult<()> {
232        self.add_gate(Identity, &[q])
233    }
234
235    /// Append Pauli-X (NOT) on qubit `q`.
236    pub fn x(&mut self, q: usize) -> QuantumResult<()> {
237        self.add_gate(PauliX, &[q])
238    }
239
240    /// Append Pauli-Y on qubit `q`.
241    pub fn y(&mut self, q: usize) -> QuantumResult<()> {
242        self.add_gate(PauliY, &[q])
243    }
244
245    /// Append Pauli-Z on qubit `q`.
246    pub fn z(&mut self, q: usize) -> QuantumResult<()> {
247        self.add_gate(PauliZ, &[q])
248    }
249
250    /// Append Hadamard on qubit `q`.
251    pub fn h(&mut self, q: usize) -> QuantumResult<()> {
252        self.add_gate(Hadamard, &[q])
253    }
254
255    /// Append S gate on qubit `q`.
256    pub fn s(&mut self, q: usize) -> QuantumResult<()> {
257        self.add_gate(PhaseS, &[q])
258    }
259
260    /// Append S† gate on qubit `q`.
261    pub fn sdg(&mut self, q: usize) -> QuantumResult<()> {
262        self.add_gate(PhaseSdg, &[q])
263    }
264
265    /// Append T gate on qubit `q`.
266    pub fn t(&mut self, q: usize) -> QuantumResult<()> {
267        self.add_gate(PhaseT, &[q])
268    }
269
270    /// Append T† gate on qubit `q`.
271    pub fn tdg(&mut self, q: usize) -> QuantumResult<()> {
272        self.add_gate(PhaseTdg, &[q])
273    }
274
275    /// Append Rx(θ) on qubit `q`.
276    pub fn rx(&mut self, theta: f64, q: usize) -> QuantumResult<()> {
277        self.add_gate(RotX { theta }, &[q])
278    }
279
280    /// Append Ry(θ) on qubit `q`.
281    pub fn ry(&mut self, theta: f64, q: usize) -> QuantumResult<()> {
282        self.add_gate(RotY { theta }, &[q])
283    }
284
285    /// Append Rz(θ) on qubit `q`.
286    pub fn rz(&mut self, theta: f64, q: usize) -> QuantumResult<()> {
287        self.add_gate(RotZ { theta }, &[q])
288    }
289
290    /// Append P(λ) phase-shift on qubit `q`.
291    pub fn p(&mut self, lambda: f64, q: usize) -> QuantumResult<()> {
292        self.add_gate(PhaseShift { lambda }, &[q])
293    }
294
295    /// Append U(θ, φ, λ) on qubit `q`.
296    pub fn u(&mut self, theta: f64, phi: f64, lambda: f64, q: usize) -> QuantumResult<()> {
297        self.add_gate(Unitary1Q { theta, phi, lambda }, &[q])
298    }
299
300    // ── Convenience two-qubit gates ───────────────────────────────────────────
301
302    /// Append CNOT with `control` controlling `target`.
303    pub fn cx(&mut self, control: usize, target: usize) -> QuantumResult<()> {
304        self.add_gate(CNOT, &[control, target])
305    }
306
307    /// Append CZ with `control` and `target`.
308    pub fn cz(&mut self, control: usize, target: usize) -> QuantumResult<()> {
309        self.add_gate(CZ, &[control, target])
310    }
311
312    /// Append SWAP of `q0` and `q1`.
313    pub fn swap(&mut self, q0: usize, q1: usize) -> QuantumResult<()> {
314        self.add_gate(SWAP, &[q0, q1])
315    }
316
317    /// Append Controlled-U where `control` triggers `gate` on `target`.
318    pub fn cu(
319        &mut self,
320        gate: impl QuantumGate + 'static,
321        control: usize,
322        target: usize,
323    ) -> QuantumResult<()> {
324        let cu_gate = CU::new(gate)?;
325        self.add_gate(cu_gate, &[control, target])
326    }
327
328    // ── Convenience three-qubit gates ─────────────────────────────────────────
329
330    /// Append Toffoli (CCX) with two controls and one target.
331    pub fn ccx(&mut self, c0: usize, c1: usize, target: usize) -> QuantumResult<()> {
332        self.add_gate(Toffoli, &[c0, c1, target])
333    }
334
335    /// Append Fredkin (CSWAP) with one control and two targets.
336    pub fn cswap(&mut self, control: usize, t0: usize, t1: usize) -> QuantumResult<()> {
337        self.add_gate(Fredkin, &[control, t0, t1])
338    }
339
340    // ── Barrier (no-op annotation) ────────────────────────────────────────────
341
342    /// Add a barrier (Identity gates) across all specified qubits.
343    ///
344    /// Barriers have no physical effect; they exist to delimit logical sections
345    /// of a circuit and prevent gate optimisers from merging across the boundary.
346    pub fn barrier(&mut self, qubits: &[usize]) -> QuantumResult<()> {
347        for &q in qubits {
348            self.add_gate(Identity, &[q])?;
349        }
350        Ok(())
351    }
352}
353
354impl std::fmt::Debug for QuantumCircuit {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        write!(
357            f,
358            "QuantumCircuit({} qubits, {} ops)",
359            self.n_qubits,
360            self.ops.len()
361        )?;
362        for (i, op) in self.ops.iter().enumerate() {
363            write!(f, "\n  [{i}] {} on {:?}", op.gate.name(), op.qubits)?;
364        }
365        Ok(())
366    }
367}
368
369// ─────────────────────────────────────────────────────────────────────────────
370// Built-in circuit factories
371// ─────────────────────────────────────────────────────────────────────────────
372
373/// Build the standard 2-qubit Bell pair circuit: H⊗I then CNOT(0→1).
374///
375/// When run on |00⟩ this produces the maximally-entangled state
376/// |Φ+⟩ = (|00⟩ + |11⟩) / √2.
377pub fn bell_pair_circuit() -> QuantumCircuit {
378    let mut c = QuantumCircuit::new(2);
379    c.h(0)
380        .expect("h gate on qubit 0 is always valid for 2-qubit circuit");
381    c.cx(0, 1)
382        .expect("cx gate on qubits 0,1 is always valid for 2-qubit circuit");
383    c
384}
385
386/// Build a GHZ-state circuit for `n_qubits` qubits.
387///
388/// Produces |GHZ⟩ = (|0…0⟩ + |1…1⟩) / √2 from |0…0⟩.
389///
390/// `n_qubits` must be ≥ 2.
391pub fn ghz_circuit(n_qubits: usize) -> QuantumResult<QuantumCircuit> {
392    if n_qubits < 2 {
393        return Err(QuantumError::InvalidQubitCount(n_qubits));
394    }
395    let mut c = QuantumCircuit::new(n_qubits);
396    c.h(0)?;
397    for q in 1..n_qubits {
398        c.cx(0, q)?;
399    }
400    Ok(c)
401}
402
403/// Build the n-qubit Quantum Fourier Transform (QFT) circuit.
404///
405/// Implements the standard decomposition:
406/// ```text
407/// QFT|j⟩ = (1/√N) Σ_{k=0}^{N-1} e^{2πijk/N} |k⟩
408/// ```
409/// using H gates followed by controlled phase rotations.
410///
411/// The circuit **does not** include the final SWAP reversal pass — if you need
412/// the bit-reversed output convention used by most QFT literature, call
413/// [`qft_circuit_with_swap`] instead.
414pub fn qft_circuit(n_qubits: usize) -> QuantumResult<QuantumCircuit> {
415    if n_qubits == 0 {
416        return Err(QuantumError::InvalidQubitCount(n_qubits));
417    }
418    let mut c = QuantumCircuit::new(n_qubits);
419
420    for target in 0..n_qubits {
421        // Hadamard on target qubit.
422        c.h(target)?;
423        // Controlled phase rotations from all subsequent qubits.
424        for control in (target + 1)..n_qubits {
425            let k = (control - target + 1) as f64;
426            let lambda = std::f64::consts::PI / (2.0_f64.powi((k - 1.0) as i32));
427            c.cu(PhaseShift { lambda }, control, target)?;
428        }
429    }
430    Ok(c)
431}
432
433/// Build the n-qubit QFT circuit with the output-reversal SWAP pass included.
434///
435/// This gives the conventional QFT output ordering where qubit 0 holds the
436/// most-significant frequency component.
437pub fn qft_circuit_with_swap(n_qubits: usize) -> QuantumResult<QuantumCircuit> {
438    if n_qubits == 0 {
439        return Err(QuantumError::InvalidQubitCount(n_qubits));
440    }
441    let mut c = qft_circuit(n_qubits)?;
442    // Reverse qubit order with SWAP gates.
443    for i in 0..(n_qubits / 2) {
444        c.swap(i, n_qubits - 1 - i)?;
445    }
446    Ok(c)
447}
448
449/// Build the n-qubit inverse QFT circuit.
450pub fn iqft_circuit(n_qubits: usize) -> QuantumResult<QuantumCircuit> {
451    if n_qubits == 0 {
452        return Err(QuantumError::InvalidQubitCount(n_qubits));
453    }
454    let mut c = QuantumCircuit::new(n_qubits);
455
456    // IQFT is the QFT with conjugated phases applied in reverse order.
457    for target in (0..n_qubits).rev() {
458        // Controlled phase rotations (reversed, negated phase).
459        for control in (target + 1..n_qubits).rev() {
460            let k = (control - target + 1) as f64;
461            let lambda = -std::f64::consts::PI / (2.0_f64.powi((k - 1.0) as i32));
462            c.cu(PhaseShift { lambda }, control, target)?;
463        }
464        // Hadamard on target qubit.
465        c.h(target)?;
466    }
467    Ok(c)
468}
469
470/// Build the quantum phase-estimation (QPE) circuit skeleton.
471///
472/// This creates `n_counting` counting qubits and `n_target` target qubits.
473/// The returned circuit prepares the counting qubits in superposition and
474/// leaves the eigenstate preparation as the caller's responsibility
475/// (apply gates to the target register before calling this or extend the
476/// circuit afterwards).
477///
478/// The last step is the IQFT on the counting register.
479pub fn phase_estimation_circuit(
480    n_counting: usize,
481    n_target: usize,
482) -> QuantumResult<QuantumCircuit> {
483    if n_counting == 0 || n_target == 0 {
484        return Err(QuantumError::InvalidQubitCount(n_counting + n_target));
485    }
486    let n_total = n_counting + n_target;
487    let mut c = QuantumCircuit::new(n_total);
488
489    // Hadamard on all counting qubits.
490    for q in 0..n_counting {
491        c.h(q)?;
492    }
493    // IQFT on counting register.
494    let iqft = iqft_circuit(n_counting)?;
495    for op in &iqft.ops {
496        c.add_gate_raw(op.gate.as_ref(), &op.qubits)?;
497    }
498    Ok(c)
499}
500
501impl QuantumCircuit {
502    /// Internal helper: append a gate operation by cloning the gate matrix into a
503    /// `MatrixGate` wrapper.  Used to import sub-circuits.
504    fn add_gate_raw(&mut self, gate: &dyn QuantumGate, qubits: &[usize]) -> QuantumResult<()> {
505        // Bounds-check qubits (relative to this circuit's qubit count).
506        for &q in qubits {
507            if q >= self.n_qubits {
508                return Err(QuantumError::QubitIndexOutOfRange {
509                    index: q,
510                    n_qubits: self.n_qubits,
511                });
512            }
513        }
514        let mat_gate = MatrixGate {
515            matrix: gate.matrix(),
516            n_qubits: gate.n_qubits(),
517            name: gate.name().to_string(),
518        };
519        self.ops.push(GateOp::new(mat_gate, qubits.to_vec()));
520        Ok(())
521    }
522}
523
524/// A gate defined by an explicit matrix (used internally for sub-circuit import).
525struct MatrixGate {
526    matrix: ndarray::Array2<num_complex::Complex<f64>>,
527    n_qubits: usize,
528    name: String,
529}
530
531impl QuantumGate for MatrixGate {
532    fn matrix(&self) -> ndarray::Array2<num_complex::Complex<f64>> {
533        self.matrix.clone()
534    }
535    fn n_qubits(&self) -> usize {
536        self.n_qubits
537    }
538    fn name(&self) -> &str {
539        &self.name
540    }
541}
542
543// ─────────────────────────────────────────────────────────────────────────────
544// Tests
545// ─────────────────────────────────────────────────────────────────────────────
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use num_complex::Complex;
551    use rand::SeedableRng;
552    use rand_chacha::ChaCha20Rng;
553
554    const TOL: f64 = 1e-10;
555
556    #[test]
557    fn test_bell_pair_probabilities() {
558        let circ = bell_pair_circuit();
559        let init = QubitRegister::new_zero_state(2).expect("valid");
560        let state = circ.run(init).expect("run ok");
561
562        let p00 = state.probability(0).expect("ok");
563        let p11 = state.probability(3).expect("ok");
564        let p01 = state.probability(1).expect("ok");
565        let p10 = state.probability(2).expect("ok");
566
567        assert!((p00 - 0.5).abs() < TOL, "p00={}", p00);
568        assert!((p11 - 0.5).abs() < TOL, "p11={}", p11);
569        assert!(p01.abs() < TOL, "p01={}", p01);
570        assert!(p10.abs() < TOL, "p10={}", p10);
571    }
572
573    #[test]
574    fn test_bell_pair_measurement_outcomes() {
575        let circ = bell_pair_circuit();
576        let init = QubitRegister::new_zero_state(2).expect("valid");
577        let state = circ.run(init).expect("run ok");
578        let mut rng = ChaCha20Rng::seed_from_u64(7);
579        for _ in 0..20 {
580            let bits = circ.measure_all(&state, &mut rng).expect("ok");
581            assert!(
582                bits == vec![0, 0] || bits == vec![1, 1],
583                "Bell pair must measure |00⟩ or |11⟩, got {:?}",
584                bits
585            );
586        }
587    }
588
589    #[test]
590    fn test_ghz_3qubits() {
591        let circ = ghz_circuit(3).expect("valid");
592        let init = QubitRegister::new_zero_state(3).expect("valid");
593        let state = circ.run(init).expect("run ok");
594
595        let p000 = state.probability(0).expect("ok"); // |000⟩
596        let p111 = state.probability(7).expect("ok"); // |111⟩
597
598        assert!((p000 - 0.5).abs() < TOL, "p000={}", p000);
599        assert!((p111 - 0.5).abs() < TOL, "p111={}", p111);
600        // All other probabilities should be ~0.
601        for k in [1usize, 2, 3, 4, 5, 6] {
602            let p = state.probability(k).expect("ok");
603            assert!(p.abs() < TOL, "p[{}]={}", k, p);
604        }
605    }
606
607    #[test]
608    fn test_circuit_depth_sequential() {
609        // Two X gates on the same qubit → depth 2.
610        let mut c = QuantumCircuit::new(1);
611        c.x(0).expect("ok");
612        c.x(0).expect("ok");
613        assert_eq!(c.circuit_depth(), 2);
614    }
615
616    #[test]
617    fn test_circuit_depth_parallel() {
618        // H on qubit 0 and H on qubit 1 can run in parallel → depth 1.
619        let mut c = QuantumCircuit::new(2);
620        c.h(0).expect("ok");
621        c.h(1).expect("ok");
622        assert_eq!(c.circuit_depth(), 1);
623    }
624
625    #[test]
626    fn test_circuit_depth_mixed() {
627        // H(0), H(1) [parallel] → CNOT(0,1) [serial] → depth 2.
628        let mut c = QuantumCircuit::new(2);
629        c.h(0).expect("ok");
630        c.h(1).expect("ok");
631        c.cx(0, 1).expect("ok");
632        assert_eq!(c.circuit_depth(), 2);
633    }
634
635    #[test]
636    fn test_circuit_register_mismatch() {
637        let circ = bell_pair_circuit();
638        let wrong = QubitRegister::new_zero_state(3).expect("valid");
639        let err = circ.run(wrong);
640        assert!(matches!(
641            err,
642            Err(QuantumError::CircuitRegisterMismatch { .. })
643        ));
644    }
645
646    #[test]
647    fn test_qft_two_qubits_normalised() {
648        let circ = qft_circuit(2).expect("valid");
649        let init = QubitRegister::new_zero_state(2).expect("valid");
650        let state = circ.run(init).expect("run ok");
651        assert!(
652            state.is_normalised(1e-10),
653            "QFT output should be normalised"
654        );
655    }
656
657    #[test]
658    fn test_qft_iqft_roundtrip() {
659        // QFT then IQFT should return to the original state (up to global phase).
660        let n = 3;
661        let qft = qft_circuit(n).expect("valid");
662        let iqft = iqft_circuit(n).expect("valid");
663
664        let init = QubitRegister::new_basis_state(n, 3).expect("valid");
665        let after_qft = qft.run(init.clone()).expect("qft ok");
666        let after_iqft = iqft.run(after_qft).expect("iqft ok");
667
668        let fidelity = init.fidelity(&after_iqft).expect("ok");
669        assert!(
670            (fidelity - 1.0).abs() < 1e-9,
671            "QFT·IQFT fidelity should be 1, got {}",
672            fidelity
673        );
674    }
675
676    #[test]
677    fn test_sample_returns_correct_shots() {
678        let circ = bell_pair_circuit();
679        let init = QubitRegister::new_zero_state(2).expect("valid");
680        let mut rng = ChaCha20Rng::seed_from_u64(99);
681        let shots = circ.sample(&init, 50, &mut rng).expect("ok");
682        assert_eq!(shots.len(), 50);
683    }
684
685    #[test]
686    fn test_toffoli_circuit() {
687        // CCX(control0=0, control1=1, target=2) with big-endian gate mapping:
688        //   control0=qubit0, control1=qubit1, target=qubit2
689        // Start: index 3 (qubit0=1, qubit1=1, qubit2=0) - both controls set.
690        // Toffoli flips qubit2: index 3 -> index 7.
691        let mut c = QuantumCircuit::new(3);
692        c.ccx(0, 1, 2).expect("ok");
693        let init = QubitRegister::new_basis_state(3, 3).expect("valid");
694        let state = c.run(init).expect("run ok");
695        let p7 = state.probability(7).expect("ok");
696        assert!((p7 - 1.0).abs() < TOL, "Toffoli p7={}", p7);
697    }
698
699    #[test]
700    fn test_x_x_identity() {
701        // X·X = I
702        let mut c = QuantumCircuit::new(1);
703        c.x(0).expect("ok");
704        c.x(0).expect("ok");
705        let init = QubitRegister::new_zero_state(1).expect("valid");
706        let state = c.run(init.clone()).expect("run ok");
707        let fidelity = init.fidelity(&state).expect("ok");
708        assert!((fidelity - 1.0).abs() < TOL);
709    }
710}