Skip to main content

oxicuda_quantum/algorithm/
simon.rs

1//! Simon's algorithm.
2//!
3//! Given a two-to-one (or one-to-one) Boolean function `f : {0,1}^n → {0,1}^n`
4//! with the promise `f(x) = f(y) ⇔ y = x ⊕ s` for some hidden period
5//! `s ∈ {0,1}^n`, Simon's algorithm recovers `s` with `O(n)` quantum queries —
6//! an *exponential* speed-up over the `Θ(2^{n/2})` classical lower bound. It is
7//! the historical precursor to Shor's algorithm: a single run of the quantum
8//! circuit returns a uniformly random bit-string `y` satisfying `y · s = 0
9//! (mod 2)`, and after collecting `n − 1` linearly independent such constraints
10//! the hidden string is fixed (up to the trivial all-zero solution when
11//! `s = 0`).
12//!
13//! ## Circuit (on `2n` qubits)
14//!
15//! The first `n` qubits form the *query* register, the second `n` the *output*
16//! register (this crate is **little-endian**, so query bit `i` ↦ qubit `i` and
17//! output bit `j` ↦ qubit `n + j`):
18//!
19//! 1. Prepare `H^{⊗n}` on the query register.
20//! 2. Apply the oracle `U_f|x⟩|0⟩ = |x⟩|f(x)⟩`.
21//! 3. Apply `H^{⊗n}` on the query register again.
22//! 4. Measure the query register — the outcome `y` is orthogonal to `s`.
23//!
24//! ## Classical post-processing
25//!
26//! The sampled constraints are assembled into a binary matrix and reduced to
27//! row-echelon form over `GF(2)`; the unique non-trivial vector in the kernel is
28//! the period `s`. [`recover_period_from_constraints`] exposes this solver
29//! independently of the quantum sampler.
30
31use crate::error::{QuantumError, QuantumResult};
32use crate::gates::hadamard::gate_h;
33use crate::handle::LcgRng;
34use crate::statevec::apply_1q::apply_1q_inplace;
35use crate::statevec::state::StateVector;
36
37/// Outcome of [`simon`].
38#[derive(Debug, Clone)]
39pub struct SimonResult {
40    /// The recovered hidden period `s` (little-endian: bit `i` ↦ query qubit `i`).
41    ///
42    /// Equals `0` when the oracle is one-to-one (the trivial period).
43    pub period: usize,
44    /// The linearly independent constraint vectors `y` sampled from the circuit
45    /// (each satisfies `y · s = 0 (mod 2)`).
46    pub constraints: Vec<usize>,
47    /// Number of circuit executions (shots) consumed to gather the constraints.
48    pub shots_used: usize,
49}
50
51/// Run one Simon circuit and return a single sampled constraint `y`.
52///
53/// The output register is *not* measured explicitly; instead the query register
54/// is sampled after the final Hadamard layer. Because `f` is realised exactly on
55/// the amplitude array, the marginal distribution of the query register matches
56/// the textbook `y · s = 0` support.
57fn sample_constraint<F>(oracle: F, n: usize, rng: &mut LcgRng) -> QuantumResult<usize>
58where
59    F: Fn(usize) -> usize,
60{
61    let total_qubits = 2 * n;
62    let mut sv = StateVector::new_zero_state(total_qubits)?;
63
64    // Step 1: Hadamard the query register (qubits 0..n).
65    for q in 0..n {
66        apply_1q_inplace(&mut sv, q, &gate_h())?;
67    }
68
69    // Step 2: oracle U_f|x⟩|0⟩ = |x⟩|f(x)⟩. The input output register is |0⟩, so
70    // amplitude on basis index `x` (query) maps to `x | (f(x) << n)`.
71    let dim = 1usize << total_qubits;
72    let mut new_amps = vec![num_complex::Complex::<f32>::new(0.0, 0.0); dim];
73    let query_mask = (1usize << n) - 1;
74    let out_mask = ((1usize << n) - 1) << n;
75    for (idx, a) in sv.amps.iter().enumerate() {
76        if a.norm_sqr() == 0.0 {
77            continue;
78        }
79        let x = idx & query_mask;
80        let y_in = (idx & out_mask) >> n;
81        let fx = oracle(x) & ((1usize << n) - 1);
82        let y_out = y_in ^ fx;
83        let target = x | (y_out << n);
84        new_amps[target] += *a;
85    }
86    sv.amps = new_amps;
87
88    // Step 3: re-apply Hadamards to the query register.
89    for q in 0..n {
90        apply_1q_inplace(&mut sv, q, &gate_h())?;
91    }
92
93    // Step 4: sample the query register bit-by-bit, collapsing as we go.
94    let mut y = 0usize;
95    for q in 0..n {
96        let (bit, collapsed) = sv.sample_measure(q, rng)?;
97        sv = collapsed;
98        if bit {
99            y |= 1 << q;
100        }
101    }
102    Ok(y)
103}
104
105/// Insert constraint `y` into a `GF(2)` row-echelon basis, returning `true` when
106/// it was linearly independent of the existing rows (and hence added).
107fn gf2_insert(basis: &mut Vec<usize>, mut y: usize) -> bool {
108    if y == 0 {
109        return false;
110    }
111    for &row in basis.iter() {
112        // Reduce `y` by the pivot (lowest set bit) of `row`.
113        let pivot = row & row.wrapping_neg();
114        if (y & pivot) != 0 {
115            y ^= row;
116        }
117        if y == 0 {
118            return false;
119        }
120    }
121    basis.push(y);
122    true
123}
124
125/// Recover the hidden period `s` from a set of constraint vectors `y` (each
126/// satisfying `y · s = 0 (mod 2)`) over `n` bits.
127///
128/// Solves the homogeneous system `Y s = 0` over `GF(2)` by Gaussian elimination
129/// and returns the unique non-trivial kernel vector. If the constraints do not
130/// pin down a single non-zero solution (under-determined), the all-zero period
131/// `0` is returned, signalling either a one-to-one oracle or insufficient rank.
132///
133/// # Errors
134/// * [`QuantumError::InvalidQubitCount`] if `n` is `0` or `> 30`.
135pub fn recover_period_from_constraints(constraints: &[usize], n: usize) -> QuantumResult<usize> {
136    if n == 0 || n > 30 {
137        return Err(QuantumError::InvalidQubitCount { n });
138    }
139    // Build a reduced row-echelon basis over GF(2).
140    let mut basis: Vec<usize> = Vec::new();
141    for &c in constraints {
142        let masked = c & ((1usize << n) - 1);
143        gf2_insert(&mut basis, masked);
144    }
145    let rank = basis.len();
146    // The solution space has dimension n - rank. A unique non-trivial period
147    // exists exactly when rank == n - 1.
148    if rank != n - 1 {
149        return Ok(0);
150    }
151
152    // Full reduce to RREF so each pivot column is isolated.
153    let mut rref = basis.clone();
154    for i in 0..rref.len() {
155        let pivot_i = rref[i] & rref[i].wrapping_neg();
156        for j in 0..rref.len() {
157            if i != j && (rref[j] & pivot_i) != 0 {
158                rref[j] ^= rref[i];
159            }
160        }
161    }
162    let pivot_cols: Vec<usize> = rref
163        .iter()
164        .map(|&r| (r & r.wrapping_neg()).trailing_zeros() as usize)
165        .collect();
166
167    // The single free column is the one not used as a pivot.
168    let mut free_col = None;
169    for col in 0..n {
170        if !pivot_cols.contains(&col) {
171            free_col = Some(col);
172            break;
173        }
174    }
175    let free_col = match free_col {
176        Some(c) => c,
177        None => return Ok(0),
178    };
179
180    // Set the free variable to 1; each pivot variable equals the coefficient of
181    // the free column in its row.
182    let mut s = 1usize << free_col;
183    for (row_idx, &row) in rref.iter().enumerate() {
184        if (row >> free_col) & 1 == 1 {
185            s |= 1 << pivot_cols[row_idx];
186        }
187    }
188    Ok(s & ((1usize << n) - 1))
189}
190
191/// Run Simon's algorithm to recover the hidden period of a two-to-one oracle.
192///
193/// `oracle(x)` returns `f(x)` for query index `x ∈ [0, 2^n)`, where `f` obeys
194/// the Simon promise `f(x) = f(y) ⇔ y = x ⊕ s`. The routine repeatedly runs the
195/// quantum circuit (seeded by `rng`) until `n − 1` linearly independent
196/// constraints have been collected (or `max_shots` is exhausted), then solves
197/// for `s` classically.
198///
199/// # Errors
200/// * [`QuantumError::InvalidQubitCount`] if `n` is `0` or `> 15` (the circuit
201///   uses `2n` qubits; `2n ≤ 30`).
202/// * [`QuantumError::MeasurementFailed`] is propagated from the sampler.
203pub fn simon<F>(
204    oracle: F,
205    n: usize,
206    max_shots: usize,
207    rng: &mut LcgRng,
208) -> QuantumResult<SimonResult>
209where
210    F: Fn(usize) -> usize,
211{
212    if n == 0 || n > 15 {
213        return Err(QuantumError::InvalidQubitCount { n });
214    }
215    let mut basis: Vec<usize> = Vec::new();
216    let mut shots_used = 0usize;
217    while basis.len() < n - 1 && shots_used < max_shots {
218        let y = sample_constraint(&oracle, n, rng)?;
219        shots_used += 1;
220        gf2_insert(&mut basis, y);
221    }
222    let period = recover_period_from_constraints(&basis, n)?;
223    Ok(SimonResult {
224        period,
225        constraints: basis,
226        shots_used,
227    })
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    /// Build a two-to-one oracle with period `s` over `n` bits.
235    ///
236    /// `f(x) = min(x, x ⊕ s)` is constant exactly on each coset `{x, x ⊕ s}`,
237    /// so it satisfies the Simon promise for any non-zero `s`.
238    fn two_to_one(s: usize) -> impl Fn(usize) -> usize {
239        move |x: usize| x.min(x ^ s)
240    }
241
242    #[test]
243    fn gf2_insert_rejects_zero() {
244        let mut basis = Vec::new();
245        assert!(!gf2_insert(&mut basis, 0));
246        assert!(basis.is_empty());
247    }
248
249    #[test]
250    fn gf2_insert_detects_dependence() {
251        let mut basis = Vec::new();
252        assert!(gf2_insert(&mut basis, 0b011));
253        assert!(gf2_insert(&mut basis, 0b101));
254        // 0b110 = 0b011 ⊕ 0b101 is dependent.
255        assert!(!gf2_insert(&mut basis, 0b110));
256        assert_eq!(basis.len(), 2);
257    }
258
259    #[test]
260    fn recover_period_simple_2bit() {
261        // s = 0b01; constraints orthogonal to it: y · s = 0 ⇒ y0 = 0 ⇒ y = 0b10.
262        let s = recover_period_from_constraints(&[0b10], 2)
263            .expect("single valid 2-bit constraint for n=2");
264        assert_eq!(s, 0b01);
265    }
266
267    #[test]
268    fn recover_period_3bit() {
269        // s = 0b101. Two independent constraints orthogonal to s.
270        // y·s = y0 ⊕ y2 = 0. Pick y = 0b010 (y0=0,y2=0) and y = 0b101 (1⊕1=0).
271        let s = recover_period_from_constraints(&[0b010, 0b101], 3)
272            .expect("two valid 3-bit constraints for n=3");
273        assert_eq!(s, 0b101);
274    }
275
276    #[test]
277    fn recover_period_underdetermined_returns_zero() {
278        // Only one constraint for n=3 ⇒ rank 1 ≠ n-1=2 ⇒ trivial.
279        let s = recover_period_from_constraints(&[0b010], 3)
280            .expect("single constraint for n=3 is valid, returns 0 for underdetermined");
281        assert_eq!(s, 0);
282    }
283
284    #[test]
285    fn recover_period_invalid_n() {
286        assert!(recover_period_from_constraints(&[1], 0).is_err());
287    }
288
289    #[test]
290    fn sample_constraint_is_orthogonal() {
291        let s = 0b11usize;
292        let mut rng = LcgRng::new(7);
293        for _ in 0..20 {
294            let y = sample_constraint(two_to_one(s), 2, &mut rng)
295                .expect("2-qubit Simon circuit with valid two-to-one oracle");
296            assert_eq!((y & s).count_ones() & 1, 0, "y={y:b} not orthogonal to s");
297        }
298    }
299
300    #[test]
301    fn simon_recovers_period_2bit() {
302        let s = 0b01usize;
303        let mut rng = LcgRng::new(123);
304        let res = simon(two_to_one(s), 2, 200, &mut rng)
305            .expect("simon with valid 2-qubit oracle and 200 shots");
306        assert_eq!(res.period, s, "constraints={:?}", res.constraints);
307    }
308
309    #[test]
310    fn simon_recovers_period_3bit() {
311        let s = 0b110usize;
312        let mut rng = LcgRng::new(456);
313        let res = simon(two_to_one(s), 3, 500, &mut rng)
314            .expect("simon with valid 3-qubit oracle and 500 shots");
315        assert_eq!(res.period, s, "constraints={:?}", res.constraints);
316    }
317
318    #[test]
319    fn simon_constraints_all_orthogonal() {
320        let s = 0b101usize;
321        let mut rng = LcgRng::new(999);
322        let res = simon(two_to_one(s), 3, 500, &mut rng)
323            .expect("simon with valid 3-qubit oracle and 500 shots");
324        for &y in &res.constraints {
325            assert_eq!((y & s).count_ones() & 1, 0, "y={y:b} not orthogonal");
326        }
327    }
328
329    #[test]
330    fn simon_collects_n_minus_one_constraints() {
331        let s = 0b110usize;
332        let mut rng = LcgRng::new(2024);
333        let res = simon(two_to_one(s), 3, 500, &mut rng)
334            .expect("simon with valid 3-qubit oracle and 500 shots");
335        assert_eq!(
336            res.constraints.len(),
337            2,
338            "should reach n-1 independent rows"
339        );
340        assert!(res.shots_used >= 2);
341    }
342
343    #[test]
344    fn simon_invalid_qubit_count() {
345        let mut rng = LcgRng::new(1);
346        assert!(simon(|x| x, 0, 10, &mut rng).is_err());
347        assert!(simon(|x| x, 16, 10, &mut rng).is_err());
348    }
349
350    #[test]
351    fn simon_deterministic_with_same_seed() {
352        let s = 0b11usize;
353        let mut rng_a = LcgRng::new(55);
354        let mut rng_b = LcgRng::new(55);
355        let a = simon(two_to_one(s), 2, 200, &mut rng_a)
356            .expect("simon with valid 2-qubit oracle, rng_a");
357        let b = simon(two_to_one(s), 2, 200, &mut rng_b)
358            .expect("simon with valid 2-qubit oracle, rng_b");
359        assert_eq!(a.period, b.period);
360        assert_eq!(a.constraints, b.constraints);
361    }
362}