oxicuda_quantum/algorithms/
quantum_walk.rs1use crate::error::{QuantumError, QuantumResult};
51use crate::gates::hadamard::gate_h;
52use crate::statevec::apply_1q::apply_1q_inplace;
53use crate::statevec::state::StateVector;
54use num_complex::Complex;
55
56type Complex32 = Complex<f32>;
57
58#[derive(Debug, Clone)]
60pub struct CoinedWalk {
61 n_positions: usize,
63 position_qubits: usize,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum CoinInit {
70 Zero,
72 One,
74 SymmetricI,
77}
78
79impl CoinedWalk {
80 pub fn new(n_positions: usize) -> QuantumResult<Self> {
87 if n_positions < 2 || !n_positions.is_power_of_two() {
88 return Err(QuantumError::InvalidParameter {
89 name: format!("n_positions={n_positions} must be a power of two ≥ 2"),
90 });
91 }
92 let position_qubits = n_positions.trailing_zeros() as usize;
93 if position_qubits + 1 > 29 {
94 return Err(QuantumError::InvalidParameter {
95 name: format!("cycle of {n_positions} sites needs too many qubits"),
96 });
97 }
98 Ok(Self {
99 n_positions,
100 position_qubits,
101 })
102 }
103
104 #[must_use]
106 pub fn n_positions(&self) -> usize {
107 self.n_positions
108 }
109
110 #[must_use]
112 pub fn n_qubits(&self) -> usize {
113 self.position_qubits + 1
114 }
115
116 pub fn initial_state(&self, x0: usize, coin: CoinInit) -> QuantumResult<StateVector> {
122 if x0 >= self.n_positions {
123 return Err(QuantumError::QubitIndexOutOfRange {
124 index: x0,
125 n_qubits: self.n_positions,
126 });
127 }
128 let n_qubits = self.n_qubits();
129 let dim = 1usize << n_qubits;
130 let mut amps = vec![Complex32::new(0.0, 0.0); dim];
131 let base = x0 << 1; let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
133 match coin {
134 CoinInit::Zero => amps[base] = Complex32::new(1.0, 0.0),
135 CoinInit::One => amps[base | 1] = Complex32::new(1.0, 0.0),
136 CoinInit::SymmetricI => {
137 amps[base] = Complex32::new(inv_sqrt2, 0.0);
138 amps[base | 1] = Complex32::new(0.0, inv_sqrt2);
139 }
140 }
141 Ok(StateVector { amps, n_qubits })
142 }
143
144 fn apply_shift(&self, sv: &mut StateVector) -> QuantumResult<()> {
149 let expected = self.n_qubits();
150 if sv.n_qubits != expected {
151 return Err(QuantumError::DimensionMismatch {
152 expected: 1usize << expected,
153 got: sv.amps.len(),
154 });
155 }
156 let n = self.n_positions;
157 let dim = sv.amps.len();
158 let mut out = vec![Complex32::new(0.0, 0.0); dim];
159 for (i, &a) in sv.amps.iter().enumerate() {
160 let coin = i & 1;
161 let pos = i >> 1;
162 let new_pos = if coin == 0 {
163 (pos + 1) % n
164 } else {
165 (pos + n - 1) % n
166 };
167 out[(new_pos << 1) | coin] = a;
168 }
169 sv.amps = out;
170 Ok(())
171 }
172
173 pub fn step(&self, sv: &mut StateVector) -> QuantumResult<()> {
179 apply_1q_inplace(sv, 0, &gate_h())?;
180 self.apply_shift(sv)
181 }
182
183 pub fn position_distribution(&self, sv: &StateVector) -> QuantumResult<Vec<f32>> {
191 let expected = self.n_qubits();
192 if sv.n_qubits != expected {
193 return Err(QuantumError::DimensionMismatch {
194 expected: 1usize << expected,
195 got: sv.amps.len(),
196 });
197 }
198 let mut dist = vec![0.0_f32; self.n_positions];
199 for (i, a) in sv.amps.iter().enumerate() {
200 dist[i >> 1] += a.norm_sqr();
201 }
202 Ok(dist)
203 }
204
205 pub fn evolve(&self, x0: usize, coin: CoinInit, t_steps: usize) -> QuantumResult<Vec<f32>> {
211 let mut sv = self.initial_state(x0, coin)?;
212 for _ in 0..t_steps {
213 self.step(&mut sv)?;
214 }
215 self.position_distribution(&sv)
216 }
217}
218
219#[must_use]
226pub fn position_std_about(dist: &[f32], x0: usize) -> f64 {
227 let n = dist.len() as i64;
228 let mut mean = 0.0_f64;
229 let mut total = 0.0_f64;
230 for (x, &p) in dist.iter().enumerate() {
231 let mut off = x as i64 - x0 as i64;
232 if off >= n / 2 {
234 off -= n;
235 } else if off < -n / 2 {
236 off += n;
237 }
238 mean += off as f64 * p as f64;
239 total += p as f64;
240 }
241 if total > 0.0 {
242 mean /= total;
243 }
244 let mut var = 0.0_f64;
245 for (x, &p) in dist.iter().enumerate() {
246 let mut off = x as i64 - x0 as i64;
247 if off >= n / 2 {
248 off -= n;
249 } else if off < -n / 2 {
250 off += n;
251 }
252 let d = off as f64 - mean;
253 var += d * d * p as f64;
254 }
255 if total > 0.0 {
256 var /= total;
257 }
258 var.max(0.0).sqrt()
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 const TOL: f32 = 1e-5;
266 const INV_SQRT2: f32 = std::f32::consts::FRAC_1_SQRT_2;
267
268 #[test]
271 fn single_step_two_amplitude_split() {
272 let walk = CoinedWalk::new(8).expect("valid CoinedWalk with 8 positions");
273 let x0 = 3usize;
274 let mut sv = walk
275 .initial_state(x0, CoinInit::Zero)
276 .expect("valid initial state at x0");
277 walk.step(&mut sv)
278 .expect("step should succeed on valid state");
279
280 let n = 8usize;
282 let idx_right = ((x0 + 1) % n) << 1; let idx_left = (((x0 + n - 1) % n) << 1) | 1; for (i, a) in sv.amps.iter().enumerate() {
285 if i == idx_right {
286 assert!((a.re - INV_SQRT2).abs() < TOL, "right re={}", a.re);
287 assert!(a.im.abs() < TOL, "right im={}", a.im);
288 } else if i == idx_left {
289 assert!((a.re - INV_SQRT2).abs() < TOL, "left re={}", a.re);
290 assert!(a.im.abs() < TOL, "left im={}", a.im);
291 } else {
292 assert!(a.norm() < TOL, "spurious amp[{i}]={a:?}");
293 }
294 }
295 }
296
297 #[test]
300 fn ballistic_spreading_linear_std() {
301 let walk = CoinedWalk::new(128).expect("valid CoinedWalk with 128 positions");
302 let x0 = 64usize;
303 let t_values: Vec<usize> = (4..=24).collect();
304 let sigmas: Vec<f64> = t_values
305 .iter()
306 .map(|&t| {
307 let dist = walk
308 .evolve(x0, CoinInit::SymmetricI, t)
309 .expect("evolve should succeed");
310 position_std_about(&dist, x0)
311 })
312 .collect();
313
314 let n = t_values.len() as f64;
316 let sx: f64 = t_values.iter().map(|&t| t as f64).sum();
317 let sy: f64 = sigmas.iter().sum();
318 let sxx: f64 = t_values.iter().map(|&t| (t as f64) * (t as f64)).sum();
319 let sxy: f64 = t_values
320 .iter()
321 .zip(&sigmas)
322 .map(|(&t, &s)| (t as f64) * s)
323 .sum();
324 let slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);
325 let intercept = (sy - slope * sx) / n;
326
327 let mean_y = sy / n;
329 let ss_tot: f64 = sigmas.iter().map(|&s| (s - mean_y).powi(2)).sum();
330 let ss_res: f64 = t_values
331 .iter()
332 .zip(&sigmas)
333 .map(|(&t, &s)| {
334 let pred = slope * t as f64 + intercept;
335 (s - pred).powi(2)
336 })
337 .sum();
338 let r2 = 1.0 - ss_res / ss_tot;
339
340 assert!(slope > 0.3, "slope={slope} should be positive & ~0.54");
341 assert!(r2 > 0.97, "R²={r2} should be ≈ 1 (linear growth)");
342
343 let t_max = *t_values.last().expect("t_values is non-empty");
345 let sigma_max = *sigmas.last().expect("sigmas is non-empty");
346 let classical = (t_max as f64).sqrt();
347 assert!(
348 sigma_max > 1.8 * classical,
349 "quantum σ={sigma_max} not ≫ classical √T={classical}"
350 );
351 }
352
353 #[test]
355 fn probability_conserved_each_step() {
356 let walk = CoinedWalk::new(32).expect("valid CoinedWalk with 32 positions");
357 let mut sv = walk
358 .initial_state(10, CoinInit::Zero)
359 .expect("valid initial state at position 10");
360 for t in 0..30 {
361 let dist = walk
362 .position_distribution(&sv)
363 .expect("position distribution should succeed on valid state");
364 let total: f32 = dist.iter().sum();
365 assert!((total - 1.0).abs() < 1e-4, "step {t}: Σp={total}");
366 assert!((sv.norm_sq() - 1.0).abs() < 1e-4, "step {t}: norm");
367 walk.step(&mut sv)
368 .expect("step should succeed on valid state");
369 }
370 }
371
372 #[test]
377 fn step_operator_is_unitary() {
378 let walk = CoinedWalk::new(16).expect("valid CoinedWalk with 16 positions");
379 let mut a = walk
381 .initial_state(2, CoinInit::Zero)
382 .expect("valid initial state at position 2");
383 let mut b = walk
384 .initial_state(5, CoinInit::One)
385 .expect("valid initial state at position 5");
386 let before = a
387 .inner_product(&b)
388 .expect("inner product between same-dimension states");
389 walk.step(&mut a)
390 .expect("step should succeed on valid state a");
391 walk.step(&mut b)
392 .expect("step should succeed on valid state b");
393 let after = a
394 .inner_product(&b)
395 .expect("inner product between same-dimension states after step");
396 assert!((before.norm()) < 1e-6, "states should start orthogonal");
397 assert!(after.norm() < 1e-5, "U broke orthogonality: {after:?}");
398 assert!((a.norm_sq() - 1.0).abs() < 1e-5);
399 assert!((b.norm_sq() - 1.0).abs() < 1e-5);
400
401 let mut rng = crate::handle::LcgRng::new(2026);
404 let dim = 1usize << walk.n_qubits();
405 let mut amps = Vec::with_capacity(dim);
406 for _ in 0..dim {
407 amps.push(Complex32::new(rng.next_normal(), rng.next_normal()));
408 }
409 let mut sv = StateVector {
410 amps,
411 n_qubits: walk.n_qubits(),
412 };
413 sv.normalize_inplace();
414 for _ in 0..20 {
415 walk.step(&mut sv)
416 .expect("step should succeed on valid state");
417 assert!((sv.norm_sq() - 1.0).abs() < 1e-4, "norm drift");
418 }
419 }
420
421 #[test]
424 fn symmetric_coin_symmetric_distribution() {
425 let walk = CoinedWalk::new(64).expect("valid CoinedWalk with 64 positions");
426 let x0 = 32usize;
427 let dist = walk
428 .evolve(x0, CoinInit::SymmetricI, 18)
429 .expect("evolve should succeed");
430 let n = 64usize;
431 for j in 1..20 {
432 let right = dist[(x0 + j) % n];
433 let left = dist[(x0 + n - j) % n];
434 assert!(
435 (right - left).abs() < 1e-5,
436 "asymmetry at j={j}: right={right}, left={left}"
437 );
438 }
439 }
440
441 #[test]
444 fn zero_coin_is_biased() {
445 let walk = CoinedWalk::new(64).expect("valid CoinedWalk with 64 positions");
446 let x0 = 32usize;
447 let dist = walk
448 .evolve(x0, CoinInit::Zero, 18)
449 .expect("evolve should succeed");
450 let n = 64usize;
451 let mut right_mass = 0.0_f32;
453 let mut left_mass = 0.0_f32;
454 for j in 1..30 {
455 right_mass += dist[(x0 + j) % n];
456 left_mass += dist[(x0 + n - j) % n];
457 }
458 assert!(
459 (right_mass - left_mass).abs() > 0.1,
460 "expected bias: right={right_mass}, left={left_mass}"
461 );
462 }
463
464 #[test]
468 fn small_cycle_periodic() {
469 let walk = CoinedWalk::new(2).expect("valid CoinedWalk with 2 positions");
470 let initial = walk
471 .initial_state(0, CoinInit::Zero)
472 .expect("valid initial state at position 0");
473 let mut sv = initial.clone();
474 walk.step(&mut sv).expect("first step should succeed");
475 walk.step(&mut sv).expect("second step should succeed");
476 for (a, b) in sv.amps.iter().zip(initial.amps.iter()) {
478 assert!((a.re - b.re).abs() < 1e-5, "re {a:?} vs {b:?}");
479 assert!((a.im - b.im).abs() < 1e-5, "im {a:?} vs {b:?}");
480 }
481
482 let d0 = walk
484 .position_distribution(&initial)
485 .expect("position distribution of initial state should succeed");
486 let d2 = walk
487 .position_distribution(&sv)
488 .expect("position distribution after 2 steps should succeed");
489 for (p0, p2) in d0.iter().zip(d2.iter()) {
490 assert!((p0 - p2).abs() < 1e-5, "dist not periodic: {p0} vs {p2}");
491 }
492 }
493
494 #[test]
498 fn cyclic_translation_covariance() {
499 let walk = CoinedWalk::new(32).expect("valid CoinedWalk with 32 positions");
500 let t = 10usize;
501 let from_zero = walk
502 .evolve(0, CoinInit::SymmetricI, t)
503 .expect("evolve from position 0 should succeed");
504 let x0 = 7usize;
505 let from_x0 = walk
506 .evolve(x0, CoinInit::SymmetricI, t)
507 .expect("evolve from position x0 should succeed");
508 let n = 32usize;
509 for x in 0..n {
510 let shifted = from_zero[(x + n - x0) % n];
511 assert!(
512 (from_x0[x] - shifted).abs() < 1e-5,
513 "covariance broken at x={x}: {} vs {}",
514 from_x0[x],
515 shifted
516 );
517 }
518 }
519
520 #[test]
522 fn rejects_non_power_of_two() {
523 assert!(CoinedWalk::new(6).is_err());
524 assert!(CoinedWalk::new(0).is_err());
525 assert!(CoinedWalk::new(1).is_err());
526 assert!(CoinedWalk::new(8).is_ok());
527 }
528
529 #[test]
530 fn rejects_out_of_range_start() {
531 let walk = CoinedWalk::new(8).expect("valid CoinedWalk with 8 positions");
532 assert!(walk.initial_state(8, CoinInit::Zero).is_err());
533 assert!(walk.initial_state(0, CoinInit::Zero).is_ok());
534 }
535}