Skip to main content

oxicuda_quantum/algorithm/
grover.rs

1//! Grover's unstructured search algorithm.
2//!
3//! Given a black-box *oracle* that marks a subset of computational-basis states
4//! `|w⟩` (the "solutions") out of `N = 2^n` items, Grover's algorithm finds a
5//! marked item with high probability using only `Θ(√(N/M))` oracle queries
6//! (`M` = number of marked items), a quadratic speed-up over the classical
7//! `Θ(N/M)`.
8//!
9//! The circuit prepares the uniform superposition `H^{⊗n}|0⟩`, then repeats the
10//! *Grover iterate* `G = D · O`:
11//!
12//! * **Oracle `O`** flips the sign of every marked basis amplitude:
13//!   `O|x⟩ = (-1)^{f(x)}|x⟩` where `f(x)=1` iff `x` is marked.
14//! * **Diffusion `D`** (inversion about the mean) reflects every amplitude about
15//!   the average amplitude: `D = 2|s⟩⟨s| - I` with `|s⟩ = H^{⊗n}|0⟩`. Acting on
16//!   an amplitude vector this is `a_x ↦ 2·⟨a⟩ - a_x`.
17//!
18//! The optimal number of iterations is `r = round(π/4 · √(N/M) - 1/2)`, which
19//! rotates the state vector to nearly coincide with the equal superposition of
20//! marked items. Reading out the register then returns a marked index with
21//! probability `≈ 1`.
22//!
23//! This implementation is **state-vector exact**: the oracle and diffusion are
24//! applied directly to the amplitude array (rather than synthesised from a
25//! multi-controlled-Z gate net), which keeps the routine fast and numerically
26//! clean while reproducing the textbook amplitude trajectory precisely.
27
28use crate::error::{QuantumError, QuantumResult};
29use crate::gates::hadamard::gate_h;
30use crate::statevec::apply_1q::apply_1q_inplace;
31use crate::statevec::state::StateVector;
32
33/// Outcome of [`grover_search`].
34#[derive(Debug, Clone)]
35pub struct GroverResult {
36    /// The basis index read out by the (deterministic) argmax measurement.
37    pub measured_index: usize,
38    /// Probability mass concentrated on `measured_index` after the iterations.
39    pub probability: f32,
40    /// Number of Grover iterations actually performed.
41    pub iterations: usize,
42}
43
44/// Compute the optimal Grover iteration count for `n_marked` solutions in a
45/// space of `dim = 2^n_qubits` items.
46///
47/// Returns `round(π/4 · √(dim / n_marked) - 1/2)`, clamped to be at least one
48/// when at least one item is marked and fewer than half the space is marked.
49/// When `n_marked == 0` (no solutions) the count is `0`. When `n_marked`
50/// approaches `dim` the analytic formula yields `0`, which is correct: the
51/// uniform state already has the marked mass.
52#[must_use]
53pub fn optimal_iterations(dim: usize, n_marked: usize) -> usize {
54    if n_marked == 0 || dim == 0 {
55        return 0;
56    }
57    let ratio = dim as f64 / n_marked as f64;
58    // θ defined by sin²θ = M/N; the state needs to rotate to π/2.
59    let theta = (n_marked as f64 / dim as f64).sqrt().asin();
60    if theta <= 0.0 {
61        return 0;
62    }
63    let raw = (std::f64::consts::FRAC_PI_2 / theta - 1.0) * 0.5;
64    let rounded = raw.round();
65    if rounded <= 0.0 {
66        // For ratio just above 1 (almost everything marked) zero is correct;
67        // otherwise guarantee at least one amplification step.
68        if ratio >= 2.0 { 1 } else { 0 }
69    } else {
70        rounded as usize
71    }
72}
73
74/// Validate the oracle and qubit count for a Grover run.
75fn validate(oracle_marks: &[usize], n_qubits: usize) -> QuantumResult<usize> {
76    if n_qubits == 0 || n_qubits > 30 {
77        return Err(QuantumError::InvalidQubitCount { n: n_qubits });
78    }
79    let dim = 1usize << n_qubits;
80    for &m in oracle_marks {
81        if m >= dim {
82            return Err(QuantumError::InvalidParameter {
83                name: format!("marked index {m} out of range for dim {dim}"),
84            });
85        }
86    }
87    Ok(dim)
88}
89
90/// Apply the phase oracle: negate the amplitude of every marked basis state.
91fn apply_oracle(sv: &mut StateVector, marks: &[bool]) {
92    for (a, &is_marked) in sv.amps.iter_mut().zip(marks.iter()) {
93        if is_marked {
94            *a = -*a;
95        }
96    }
97}
98
99/// Apply the diffusion operator (inversion about the mean) in place.
100fn apply_diffusion(sv: &mut StateVector) {
101    let dim = sv.amps.len();
102    if dim == 0 {
103        return;
104    }
105    let inv_dim = 1.0 / dim as f32;
106    let mut mean = num_complex::Complex::<f32>::new(0.0, 0.0);
107    for a in &sv.amps {
108        mean += *a;
109    }
110    mean *= inv_dim;
111    let two_mean = mean * 2.0;
112    for a in &mut sv.amps {
113        *a = two_mean - *a;
114    }
115}
116
117/// Run Grover's search and return the most probable basis index.
118///
119/// Builds the uniform superposition `H^{⊗n}|0⟩`, applies the optimal number of
120/// Grover iterates, then performs a deterministic argmax readout over the full
121/// register. `oracle_marks` is the (possibly empty, possibly multi-element) list
122/// of marked computational-basis indices.
123///
124/// When `oracle_marks` is empty no amplification occurs and the routine returns
125/// the argmax of the uniform state (index `0`) with probability `1/N`.
126///
127/// # Errors
128/// * [`QuantumError::InvalidQubitCount`] if `n_qubits` is `0` or `> 30`.
129/// * [`QuantumError::InvalidParameter`] if any marked index is `>= 2^n_qubits`.
130pub fn grover_search(oracle_marks: &[usize], n_qubits: usize) -> QuantumResult<GroverResult> {
131    let dim = validate(oracle_marks, n_qubits)?;
132
133    // Build a boolean mark-vector for O(dim) oracle application.
134    let mut marks = vec![false; dim];
135    for &m in oracle_marks {
136        marks[m] = true;
137    }
138    let n_marked = marks.iter().filter(|&&b| b).count();
139
140    // Prepare uniform superposition |s⟩ = H^{⊗n}|0⟩.
141    let mut sv = StateVector::new_zero_state(n_qubits)?;
142    for q in 0..n_qubits {
143        apply_1q_inplace(&mut sv, q, &gate_h())?;
144    }
145
146    let iterations = optimal_iterations(dim, n_marked);
147    for _ in 0..iterations {
148        apply_oracle(&mut sv, &marks);
149        apply_diffusion(&mut sv);
150    }
151
152    // Deterministic argmax readout (ties resolved by lowest index).
153    let mut measured_index = 0usize;
154    let mut best = f32::NEG_INFINITY;
155    for (i, a) in sv.amps.iter().enumerate() {
156        let p = a.norm_sqr();
157        if p > best {
158            best = p;
159            measured_index = i;
160        }
161    }
162
163    Ok(GroverResult {
164        measured_index,
165        probability: best,
166        iterations,
167    })
168}
169
170/// Total probability mass currently residing on the marked items.
171///
172/// Re-runs the amplification and sums `|a_x|²` over marked `x`; useful for
173/// asserting that amplitude has been amplified onto the solution subspace.
174///
175/// # Errors
176/// Same conditions as [`grover_search`].
177pub fn marked_probability(oracle_marks: &[usize], n_qubits: usize) -> QuantumResult<f32> {
178    let dim = validate(oracle_marks, n_qubits)?;
179    let mut marks = vec![false; dim];
180    for &m in oracle_marks {
181        marks[m] = true;
182    }
183    let n_marked = marks.iter().filter(|&&b| b).count();
184
185    let mut sv = StateVector::new_zero_state(n_qubits)?;
186    for q in 0..n_qubits {
187        apply_1q_inplace(&mut sv, q, &gate_h())?;
188    }
189    let iterations = optimal_iterations(dim, n_marked);
190    for _ in 0..iterations {
191        apply_oracle(&mut sv, &marks);
192        apply_diffusion(&mut sv);
193    }
194
195    let mass = sv
196        .amps
197        .iter()
198        .zip(marks.iter())
199        .filter(|&(_, &m)| m)
200        .map(|(a, _)| a.norm_sqr())
201        .sum::<f32>();
202    Ok(mass)
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn finds_marked_single_solution() {
211        // 3-qubit space, mark index 5; Grover must return it.
212        let res = grover_search(&[5], 3).expect("valid 3-qubit grover search for mark 5");
213        assert_eq!(res.measured_index, 5, "got {}", res.measured_index);
214        assert!(res.probability > 0.9, "prob={}", res.probability);
215    }
216
217    #[test]
218    fn amplitude_amplified_above_uniform() {
219        // After amplification, marked mass must far exceed the uniform baseline 1/N.
220        let n = 4;
221        let uniform = 1.0 / (1u32 << n) as f32;
222        let mass =
223            marked_probability(&[9], n).expect("valid marked probability for 4-qubit single mark");
224        assert!(
225            mass > uniform * 4.0,
226            "mass={mass} not amplified over uniform {uniform}"
227        );
228        assert!(mass > 0.9, "mass={mass}");
229    }
230
231    #[test]
232    fn optimal_iterations_matches_known_values() {
233        // n=2 (N=4), single mark: exactly 1 iteration gives certainty.
234        assert_eq!(optimal_iterations(4, 1), 1);
235        // n=3 (N=8), single mark: round(π/4·√8 - 1/2) = round(1.72) = 2.
236        assert_eq!(optimal_iterations(8, 1), 2);
237        // n=4 (N=16), single mark: round(π/4·√16 - 1/2) = round(2.64) = 3.
238        assert_eq!(optimal_iterations(16, 1), 3);
239    }
240
241    #[test]
242    fn multiple_marked_items_found() {
243        // Mark two of eight items; the returned index must be one of them and
244        // the marked subspace must hold most of the probability.
245        let marks = [2usize, 6];
246        let res = grover_search(&marks, 3).expect("valid 3-qubit grover search for marks [2, 6]");
247        assert!(
248            marks.contains(&res.measured_index),
249            "got {}",
250            res.measured_index
251        );
252        let mass = marked_probability(&marks, 3)
253            .expect("valid 3-qubit marked probability for marks [2, 6]");
254        assert!(mass > 0.9, "mass={mass}");
255    }
256
257    #[test]
258    fn no_marked_items_zero_iterations() {
259        // Empty oracle ⇒ no amplification, uniform state, argmax index 0.
260        let res = grover_search(&[], 3).expect("valid 3-qubit grover search with empty oracle");
261        assert_eq!(res.iterations, 0);
262        // Uniform state: every amplitude is 1/√8, probability 1/8.
263        assert!(
264            (res.probability - 1.0 / 8.0).abs() < 1e-5,
265            "p={}",
266            res.probability
267        );
268    }
269
270    #[test]
271    fn n_qubits_zero_errors() {
272        assert!(grover_search(&[0], 0).is_err());
273    }
274
275    #[test]
276    fn probability_concentrates_with_more_qubits() {
277        // Larger spaces still concentrate >0.9 on the single marked item.
278        for n in 2..=6 {
279            let target = (1usize << n) - 1; // last index
280            let mass = marked_probability(&[target], n)
281                .expect("valid marked probability for last-index target");
282            assert!(mass > 0.8, "n={n} mass={mass}");
283        }
284    }
285
286    #[test]
287    fn deterministic_runs() {
288        // Argmax readout has no randomness: identical runs agree exactly.
289        let a = grover_search(&[3], 4).expect("valid 4-qubit grover search for mark 3 (run a)");
290        let b = grover_search(&[3], 4).expect("valid 4-qubit grover search for mark 3 (run b)");
291        assert_eq!(a.measured_index, b.measured_index);
292        assert!((a.probability - b.probability).abs() < 1e-7);
293        assert_eq!(a.iterations, b.iterations);
294    }
295
296    #[test]
297    fn single_item_space() {
298        // Smallest space where Grover yields certainty: n=2 (N=4), single mark.
299        // Exactly one iteration rotates the state onto the marked item (prob 1).
300        // (n=1 / N=2 is the known degenerate case where amplification cannot
301        // exceed 1/2, so the meaningful "small space" check uses N=4.)
302        let res = grover_search(&[2], 2).expect("valid 2-qubit grover search for mark 2");
303        assert_eq!(res.measured_index, 2, "got {}", res.measured_index);
304        assert!(res.probability > 0.99, "prob={}", res.probability);
305    }
306
307    #[test]
308    fn n_eq_one_is_degenerate() {
309        // N=2, M=1: Grover provably cannot amplify beyond the uniform 1/2.
310        // The routine must still run cleanly and report a valid probability.
311        let mass =
312            marked_probability(&[1], 1).expect("valid 1-qubit marked probability for single mark");
313        assert!((mass - 0.5).abs() < 1e-4, "mass={mass}");
314    }
315
316    #[test]
317    fn out_of_range_mark_errors() {
318        // Index 8 is out of range for a 3-qubit (dim 8 ⇒ valid 0..=7) space.
319        assert!(grover_search(&[8], 3).is_err());
320    }
321
322    #[test]
323    fn preserves_norm_through_iterations() {
324        // Grover iterates are unitary; the marked + unmarked mass must sum to 1.
325        let n = 4;
326        let marks = [1usize, 7, 11];
327        let marked = marked_probability(&marks, n)
328            .expect("valid 4-qubit marked probability for marks [1, 7, 11]");
329        assert!(marked <= 1.0 + 1e-5, "marked mass {marked} exceeds 1");
330        // Re-derive total via a fresh full-state norm check.
331        let mut all = vec![false; 1 << n];
332        for &m in &marks {
333            all[m] = true;
334        }
335        let n_marked = marks.len();
336        let mut sv =
337            StateVector::new_zero_state(n).expect("valid 4-qubit zero state for norm check");
338        for q in 0..n {
339            apply_1q_inplace(&mut sv, q, &gate_h()).expect("valid qubit index for Hadamard gate");
340        }
341        for _ in 0..optimal_iterations(1 << n, n_marked) {
342            apply_oracle(&mut sv, &all);
343            apply_diffusion(&mut sv);
344        }
345        assert!((sv.norm_sq() - 1.0).abs() < 1e-4, "norm={}", sv.norm_sq());
346    }
347}