Skip to main content

oxicuda_quantum/algorithm/
swap_test.rs

1//! Ancilla-interference primitives: the **SWAP test** and the **Hadamard test**.
2//!
3//! Both extract a number from the interference of an ancilla qubit prepared in
4//! `|+⟩`, conditionally coupled to a work register, and measured in the `X`
5//! basis. They are the canonical building blocks for overlap estimation and for
6//! measuring expectation values of (controlled) unitaries.
7//!
8//! * [`swap_test`] estimates the squared state overlap `|⟨ψ|φ⟩|²` from the
9//!   ancilla statistics `P(0) = (1 + |⟨ψ|φ⟩|²)/2` (Buhrman et al., 2001).
10//! * [`hadamard_test`] estimates `Re⟨ψ|U|ψ⟩` (or `Im⟨ψ|U|ψ⟩` with the imaginary
11//!   variant) from `P(0) = (1 ± value)/2`.
12//!
13//! Because this crate stores the full state vector, the ancilla measurement
14//! probability is read off *exactly* (`measure_prob`) rather than sampled, so
15//! the returned values are the analytic results, not shot estimates.
16
17use num_complex::Complex;
18
19use crate::error::{QuantumError, QuantumResult};
20use crate::gates::hadamard::{gate_h, gate_sdg};
21use crate::statevec::apply_1q::{apply_1q_controlled, apply_1q_inplace};
22use crate::statevec::state::StateVector;
23
24type Complex32 = Complex<f32>;
25
26/// Controlled-SWAP (Fredkin): swap qubits `q1` and `q2` when `ctrl` is `|1⟩`.
27///
28/// Implemented directly on the amplitude array — for every basis state with
29/// `ctrl = 1` and differing `q1`/`q2` bits, the amplitude is exchanged with its
30/// bit-swapped partner. This is exact and phase-free (unlike a `T`-decomposed
31/// Toffoli network), which matters for entangled inputs.
32fn apply_cswap(sv: &mut StateVector, ctrl: usize, q1: usize, q2: usize) -> QuantumResult<()> {
33    let n = sv.n_qubits;
34    for &qq in &[ctrl, q1, q2] {
35        if qq >= n {
36            return Err(QuantumError::QubitIndexOutOfRange {
37                index: qq,
38                n_qubits: n,
39            });
40        }
41    }
42    if ctrl == q1 || ctrl == q2 || q1 == q2 {
43        return Err(QuantumError::InvalidParameter {
44            name: "ctrl, q1, q2 must be distinct".into(),
45        });
46    }
47    let cm = 1usize << ctrl;
48    let m1 = 1usize << q1;
49    let m2 = 1usize << q2;
50    let dim = sv.amps.len();
51    for i in 0..dim {
52        // Touch each (q1=1,q2=0) representative once and swap with its partner.
53        if (i & cm) != 0 && (i & m1) != 0 && (i & m2) == 0 {
54            let j = (i & !m1) | m2;
55            sv.amps.swap(i, j);
56        }
57    }
58    Ok(())
59}
60
61/// Build the product state `|0⟩_anc ⊗ |ψ1⟩ ⊗ |ψ2⟩` on `1 + 2k` qubits with the
62/// ancilla as qubit `0`, register 1 on qubits `1..=k`, register 2 on
63/// `k+1..=2k` (little-endian throughout).
64fn tensor_with_ancilla(psi1: &StateVector, psi2: &StateVector) -> StateVector {
65    let k = psi1.n_qubits;
66    let n = 1 + 2 * k;
67    let dim = 1usize << n;
68    let mut amps = vec![Complex32::new(0.0, 0.0); dim];
69    for (i1, &a1) in psi1.amps.iter().enumerate() {
70        for (i2, &a2) in psi2.amps.iter().enumerate() {
71            let global = (i1 << 1) | (i2 << (1 + k));
72            amps[global] = a1 * a2;
73        }
74    }
75    StateVector { amps, n_qubits: n }
76}
77
78/// Estimate the squared overlap `|⟨ψ1|ψ2⟩|²` of two `k`-qubit states via the
79/// SWAP test.
80///
81/// The result is read from `P(ancilla = 0) = (1 + |⟨ψ1|ψ2⟩|²)/2` and clamped
82/// to `[0, 1]` to absorb floating-point round-off.
83///
84/// # Errors
85/// * [`QuantumError::DimensionMismatch`] if the two states act on different
86///   numbers of qubits.
87/// * [`QuantumError::InvalidQubitCount`] if the states are empty or the
88///   combined register would exceed the simulator's qubit limit.
89pub fn swap_test(psi1: &StateVector, psi2: &StateVector) -> QuantumResult<f32> {
90    if psi1.n_qubits != psi2.n_qubits {
91        return Err(QuantumError::DimensionMismatch {
92            expected: psi1.n_qubits,
93            got: psi2.n_qubits,
94        });
95    }
96    let k = psi1.n_qubits;
97    if k == 0 {
98        return Err(QuantumError::InvalidQubitCount { n: 0 });
99    }
100    let n = 1 + 2 * k;
101    if n > 30 {
102        return Err(QuantumError::InvalidQubitCount { n });
103    }
104
105    let mut sv = tensor_with_ancilla(psi1, psi2);
106    apply_1q_inplace(&mut sv, 0, &gate_h())?;
107    for j in 0..k {
108        apply_cswap(&mut sv, 0, 1 + j, 1 + k + j)?;
109    }
110    apply_1q_inplace(&mut sv, 0, &gate_h())?;
111
112    let p0 = sv.measure_prob(0, false)?;
113    Ok((2.0 * p0 - 1.0).clamp(0.0, 1.0))
114}
115
116/// Estimate `⟨ψ|U|ψ⟩` for a single-qubit state `ψ` and single-qubit gate `U`
117/// via the Hadamard test.
118///
119/// With `imaginary = false` the real part `Re⟨ψ|U|ψ⟩` is returned (from
120/// `P(0) = (1 + Re⟨ψ|U|ψ⟩)/2`); with `imaginary = true` the imaginary part
121/// `Im⟨ψ|U|ψ⟩` is returned, obtained by inserting `S†` on the ancilla after the
122/// first Hadamard.
123///
124/// # Errors
125/// [`QuantumError::InvalidQubitCount`] if `psi` is not a single-qubit state.
126pub fn hadamard_test(
127    psi: &StateVector,
128    gate: &[[Complex32; 2]; 2],
129    imaginary: bool,
130) -> QuantumResult<f32> {
131    if psi.n_qubits != 1 {
132        return Err(QuantumError::InvalidQubitCount { n: psi.n_qubits });
133    }
134    // |0⟩_anc ⊗ |ψ⟩ on 2 qubits: ancilla = qubit 0, system = qubit 1.
135    let mut amps = vec![Complex32::new(0.0, 0.0); 4];
136    amps[0] = psi.amps[0]; // |anc=0, sys=0⟩
137    amps[2] = psi.amps[1]; // |anc=0, sys=1⟩
138    let mut sv = StateVector { amps, n_qubits: 2 };
139
140    apply_1q_inplace(&mut sv, 0, &gate_h())?;
141    if imaginary {
142        apply_1q_inplace(&mut sv, 0, &gate_sdg())?;
143    }
144    apply_1q_controlled(&mut sv, 0, 1, gate)?;
145    apply_1q_inplace(&mut sv, 0, &gate_h())?;
146
147    let p0 = sv.measure_prob(0, false)?;
148    Ok(2.0 * p0 - 1.0)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::gates::controlled::apply_cnot;
155    use crate::gates::hadamard::gate_s;
156    use crate::gates::pauli::{gate_x, gate_z};
157
158    fn ket_zero(n: usize) -> StateVector {
159        StateVector::new_zero_state(n).expect("valid n-qubit zero state")
160    }
161
162    fn ket_one() -> StateVector {
163        let mut sv = StateVector::new_zero_state(1).expect("valid 1-qubit zero state");
164        apply_1q_inplace(&mut sv, 0, &gate_x()).expect("X gate applied to qubit 0");
165        sv
166    }
167
168    fn ket_plus() -> StateVector {
169        let mut sv = StateVector::new_zero_state(1).expect("valid 1-qubit zero state");
170        apply_1q_inplace(&mut sv, 0, &gate_h()).expect("H gate applied to qubit 0");
171        sv
172    }
173
174    #[test]
175    fn swap_test_identical_states_overlap_one() {
176        let a = ket_zero(1);
177        let b = ket_zero(1);
178        let ov = swap_test(&a, &b).expect("swap test on identical 1-qubit states");
179        assert!((ov - 1.0).abs() < 1e-5, "overlap={ov}");
180    }
181
182    #[test]
183    fn swap_test_orthogonal_states_overlap_zero() {
184        let a = ket_zero(1);
185        let b = ket_one();
186        let ov = swap_test(&a, &b).expect("swap test on orthogonal 1-qubit states");
187        assert!(ov.abs() < 1e-5, "overlap={ov}");
188    }
189
190    #[test]
191    fn swap_test_zero_and_plus_overlap_half() {
192        // |⟨0|+⟩|² = 1/2.
193        let a = ket_zero(1);
194        let b = ket_plus();
195        let ov = swap_test(&a, &b).expect("swap test on |0⟩ and |+⟩ states");
196        assert!((ov - 0.5).abs() < 1e-5, "overlap={ov}");
197    }
198
199    #[test]
200    fn swap_test_two_qubit_identical_overlap_one() {
201        // Bell-like 2-qubit state vs. itself.
202        let mut a = StateVector::new_zero_state(2).expect("valid 2-qubit zero state");
203        apply_1q_inplace(&mut a, 0, &gate_h()).expect("H gate applied to qubit 0");
204        apply_cnot(&mut a, 0, 1).expect("CNOT with control 0 target 1");
205        let b = a.clone();
206        let ov = swap_test(&a, &b).expect("swap test on identical 2-qubit Bell states");
207        assert!((ov - 1.0).abs() < 1e-5, "overlap={ov}");
208    }
209
210    #[test]
211    fn swap_test_two_qubit_orthogonal_overlap_zero() {
212        let a = ket_zero(2); // |00⟩
213        let mut b = StateVector::new_zero_state(2).expect("valid 2-qubit zero state");
214        apply_1q_inplace(&mut b, 1, &gate_x()).expect("X gate applied to qubit 1"); // |10⟩ (qubit 1 set)
215        let ov = swap_test(&a, &b).expect("swap test on orthogonal 2-qubit states");
216        assert!(ov.abs() < 1e-5, "overlap={ov}");
217    }
218
219    #[test]
220    fn swap_test_mismatched_sizes_error() {
221        let a = ket_zero(1);
222        let b = ket_zero(2);
223        assert!(swap_test(&a, &b).is_err());
224    }
225
226    #[test]
227    fn hadamard_test_z_on_zero_is_plus_one() {
228        let psi = ket_zero(1);
229        let re = hadamard_test(&psi, &gate_z(), false)
230            .expect("Hadamard test with Z gate on |0⟩, real part");
231        assert!((re - 1.0).abs() < 1e-5, "re={re}");
232    }
233
234    #[test]
235    fn hadamard_test_z_on_plus_is_zero() {
236        let psi = ket_plus();
237        let re = hadamard_test(&psi, &gate_z(), false)
238            .expect("Hadamard test with Z gate on |+⟩, real part");
239        assert!(re.abs() < 1e-5, "re={re}");
240    }
241
242    #[test]
243    fn hadamard_test_x_on_zero_is_zero() {
244        // ⟨0|X|0⟩ = 0.
245        let psi = ket_zero(1);
246        let re = hadamard_test(&psi, &gate_x(), false)
247            .expect("Hadamard test with X gate on |0⟩, real part");
248        assert!(re.abs() < 1e-5, "re={re}");
249    }
250
251    #[test]
252    fn hadamard_test_s_on_plus_real_and_imag() {
253        // ⟨+|S|+⟩ = (1 + i)/2.
254        let psi = ket_plus();
255        let re = hadamard_test(&psi, &gate_s(), false)
256            .expect("Hadamard test with S gate on |+⟩, real part");
257        let im = hadamard_test(&psi, &gate_s(), true)
258            .expect("Hadamard test with S gate on |+⟩, imaginary part");
259        assert!((re - 0.5).abs() < 1e-5, "re={re}");
260        assert!((im - 0.5).abs() < 1e-5, "im={im}");
261    }
262
263    #[test]
264    fn hadamard_test_requires_single_qubit() {
265        let psi = ket_zero(2);
266        assert!(hadamard_test(&psi, &gate_z(), false).is_err());
267    }
268}