use crate::error::{QuantumError, QuantumResult};
use crate::gates::hadamard::gate_h;
use crate::statevec::apply_1q::apply_1q_inplace;
use crate::statevec::state::StateVector;
use num_complex::Complex;
type Complex32 = Complex<f32>;
#[derive(Debug, Clone)]
pub struct CoinedWalk {
n_positions: usize,
position_qubits: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoinInit {
Zero,
One,
SymmetricI,
}
impl CoinedWalk {
pub fn new(n_positions: usize) -> QuantumResult<Self> {
if n_positions < 2 || !n_positions.is_power_of_two() {
return Err(QuantumError::InvalidParameter {
name: format!("n_positions={n_positions} must be a power of two ≥ 2"),
});
}
let position_qubits = n_positions.trailing_zeros() as usize;
if position_qubits + 1 > 29 {
return Err(QuantumError::InvalidParameter {
name: format!("cycle of {n_positions} sites needs too many qubits"),
});
}
Ok(Self {
n_positions,
position_qubits,
})
}
#[must_use]
pub fn n_positions(&self) -> usize {
self.n_positions
}
#[must_use]
pub fn n_qubits(&self) -> usize {
self.position_qubits + 1
}
pub fn initial_state(&self, x0: usize, coin: CoinInit) -> QuantumResult<StateVector> {
if x0 >= self.n_positions {
return Err(QuantumError::QubitIndexOutOfRange {
index: x0,
n_qubits: self.n_positions,
});
}
let n_qubits = self.n_qubits();
let dim = 1usize << n_qubits;
let mut amps = vec![Complex32::new(0.0, 0.0); dim];
let base = x0 << 1; let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
match coin {
CoinInit::Zero => amps[base] = Complex32::new(1.0, 0.0),
CoinInit::One => amps[base | 1] = Complex32::new(1.0, 0.0),
CoinInit::SymmetricI => {
amps[base] = Complex32::new(inv_sqrt2, 0.0);
amps[base | 1] = Complex32::new(0.0, inv_sqrt2);
}
}
Ok(StateVector { amps, n_qubits })
}
fn apply_shift(&self, sv: &mut StateVector) -> QuantumResult<()> {
let expected = self.n_qubits();
if sv.n_qubits != expected {
return Err(QuantumError::DimensionMismatch {
expected: 1usize << expected,
got: sv.amps.len(),
});
}
let n = self.n_positions;
let dim = sv.amps.len();
let mut out = vec![Complex32::new(0.0, 0.0); dim];
for (i, &a) in sv.amps.iter().enumerate() {
let coin = i & 1;
let pos = i >> 1;
let new_pos = if coin == 0 {
(pos + 1) % n
} else {
(pos + n - 1) % n
};
out[(new_pos << 1) | coin] = a;
}
sv.amps = out;
Ok(())
}
pub fn step(&self, sv: &mut StateVector) -> QuantumResult<()> {
apply_1q_inplace(sv, 0, &gate_h())?;
self.apply_shift(sv)
}
pub fn position_distribution(&self, sv: &StateVector) -> QuantumResult<Vec<f32>> {
let expected = self.n_qubits();
if sv.n_qubits != expected {
return Err(QuantumError::DimensionMismatch {
expected: 1usize << expected,
got: sv.amps.len(),
});
}
let mut dist = vec![0.0_f32; self.n_positions];
for (i, a) in sv.amps.iter().enumerate() {
dist[i >> 1] += a.norm_sqr();
}
Ok(dist)
}
pub fn evolve(&self, x0: usize, coin: CoinInit, t_steps: usize) -> QuantumResult<Vec<f32>> {
let mut sv = self.initial_state(x0, coin)?;
for _ in 0..t_steps {
self.step(&mut sv)?;
}
self.position_distribution(&sv)
}
}
#[must_use]
pub fn position_std_about(dist: &[f32], x0: usize) -> f64 {
let n = dist.len() as i64;
let mut mean = 0.0_f64;
let mut total = 0.0_f64;
for (x, &p) in dist.iter().enumerate() {
let mut off = x as i64 - x0 as i64;
if off >= n / 2 {
off -= n;
} else if off < -n / 2 {
off += n;
}
mean += off as f64 * p as f64;
total += p as f64;
}
if total > 0.0 {
mean /= total;
}
let mut var = 0.0_f64;
for (x, &p) in dist.iter().enumerate() {
let mut off = x as i64 - x0 as i64;
if off >= n / 2 {
off -= n;
} else if off < -n / 2 {
off += n;
}
let d = off as f64 - mean;
var += d * d * p as f64;
}
if total > 0.0 {
var /= total;
}
var.max(0.0).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f32 = 1e-5;
const INV_SQRT2: f32 = std::f32::consts::FRAC_1_SQRT_2;
#[test]
fn single_step_two_amplitude_split() {
let walk = CoinedWalk::new(8).expect("valid CoinedWalk with 8 positions");
let x0 = 3usize;
let mut sv = walk
.initial_state(x0, CoinInit::Zero)
.expect("valid initial state at x0");
walk.step(&mut sv)
.expect("step should succeed on valid state");
let n = 8usize;
let idx_right = ((x0 + 1) % n) << 1; let idx_left = (((x0 + n - 1) % n) << 1) | 1; for (i, a) in sv.amps.iter().enumerate() {
if i == idx_right {
assert!((a.re - INV_SQRT2).abs() < TOL, "right re={}", a.re);
assert!(a.im.abs() < TOL, "right im={}", a.im);
} else if i == idx_left {
assert!((a.re - INV_SQRT2).abs() < TOL, "left re={}", a.re);
assert!(a.im.abs() < TOL, "left im={}", a.im);
} else {
assert!(a.norm() < TOL, "spurious amp[{i}]={a:?}");
}
}
}
#[test]
fn ballistic_spreading_linear_std() {
let walk = CoinedWalk::new(128).expect("valid CoinedWalk with 128 positions");
let x0 = 64usize;
let t_values: Vec<usize> = (4..=24).collect();
let sigmas: Vec<f64> = t_values
.iter()
.map(|&t| {
let dist = walk
.evolve(x0, CoinInit::SymmetricI, t)
.expect("evolve should succeed");
position_std_about(&dist, x0)
})
.collect();
let n = t_values.len() as f64;
let sx: f64 = t_values.iter().map(|&t| t as f64).sum();
let sy: f64 = sigmas.iter().sum();
let sxx: f64 = t_values.iter().map(|&t| (t as f64) * (t as f64)).sum();
let sxy: f64 = t_values
.iter()
.zip(&sigmas)
.map(|(&t, &s)| (t as f64) * s)
.sum();
let slope = (n * sxy - sx * sy) / (n * sxx - sx * sx);
let intercept = (sy - slope * sx) / n;
let mean_y = sy / n;
let ss_tot: f64 = sigmas.iter().map(|&s| (s - mean_y).powi(2)).sum();
let ss_res: f64 = t_values
.iter()
.zip(&sigmas)
.map(|(&t, &s)| {
let pred = slope * t as f64 + intercept;
(s - pred).powi(2)
})
.sum();
let r2 = 1.0 - ss_res / ss_tot;
assert!(slope > 0.3, "slope={slope} should be positive & ~0.54");
assert!(r2 > 0.97, "R²={r2} should be ≈ 1 (linear growth)");
let t_max = *t_values.last().expect("t_values is non-empty");
let sigma_max = *sigmas.last().expect("sigmas is non-empty");
let classical = (t_max as f64).sqrt();
assert!(
sigma_max > 1.8 * classical,
"quantum σ={sigma_max} not ≫ classical √T={classical}"
);
}
#[test]
fn probability_conserved_each_step() {
let walk = CoinedWalk::new(32).expect("valid CoinedWalk with 32 positions");
let mut sv = walk
.initial_state(10, CoinInit::Zero)
.expect("valid initial state at position 10");
for t in 0..30 {
let dist = walk
.position_distribution(&sv)
.expect("position distribution should succeed on valid state");
let total: f32 = dist.iter().sum();
assert!((total - 1.0).abs() < 1e-4, "step {t}: Σp={total}");
assert!((sv.norm_sq() - 1.0).abs() < 1e-4, "step {t}: norm");
walk.step(&mut sv)
.expect("step should succeed on valid state");
}
}
#[test]
fn step_operator_is_unitary() {
let walk = CoinedWalk::new(16).expect("valid CoinedWalk with 16 positions");
let mut a = walk
.initial_state(2, CoinInit::Zero)
.expect("valid initial state at position 2");
let mut b = walk
.initial_state(5, CoinInit::One)
.expect("valid initial state at position 5");
let before = a
.inner_product(&b)
.expect("inner product between same-dimension states");
walk.step(&mut a)
.expect("step should succeed on valid state a");
walk.step(&mut b)
.expect("step should succeed on valid state b");
let after = a
.inner_product(&b)
.expect("inner product between same-dimension states after step");
assert!((before.norm()) < 1e-6, "states should start orthogonal");
assert!(after.norm() < 1e-5, "U broke orthogonality: {after:?}");
assert!((a.norm_sq() - 1.0).abs() < 1e-5);
assert!((b.norm_sq() - 1.0).abs() < 1e-5);
let mut rng = crate::handle::LcgRng::new(2026);
let dim = 1usize << walk.n_qubits();
let mut amps = Vec::with_capacity(dim);
for _ in 0..dim {
amps.push(Complex32::new(rng.next_normal(), rng.next_normal()));
}
let mut sv = StateVector {
amps,
n_qubits: walk.n_qubits(),
};
sv.normalize_inplace();
for _ in 0..20 {
walk.step(&mut sv)
.expect("step should succeed on valid state");
assert!((sv.norm_sq() - 1.0).abs() < 1e-4, "norm drift");
}
}
#[test]
fn symmetric_coin_symmetric_distribution() {
let walk = CoinedWalk::new(64).expect("valid CoinedWalk with 64 positions");
let x0 = 32usize;
let dist = walk
.evolve(x0, CoinInit::SymmetricI, 18)
.expect("evolve should succeed");
let n = 64usize;
for j in 1..20 {
let right = dist[(x0 + j) % n];
let left = dist[(x0 + n - j) % n];
assert!(
(right - left).abs() < 1e-5,
"asymmetry at j={j}: right={right}, left={left}"
);
}
}
#[test]
fn zero_coin_is_biased() {
let walk = CoinedWalk::new(64).expect("valid CoinedWalk with 64 positions");
let x0 = 32usize;
let dist = walk
.evolve(x0, CoinInit::Zero, 18)
.expect("evolve should succeed");
let n = 64usize;
let mut right_mass = 0.0_f32;
let mut left_mass = 0.0_f32;
for j in 1..30 {
right_mass += dist[(x0 + j) % n];
left_mass += dist[(x0 + n - j) % n];
}
assert!(
(right_mass - left_mass).abs() > 0.1,
"expected bias: right={right_mass}, left={left_mass}"
);
}
#[test]
fn small_cycle_periodic() {
let walk = CoinedWalk::new(2).expect("valid CoinedWalk with 2 positions");
let initial = walk
.initial_state(0, CoinInit::Zero)
.expect("valid initial state at position 0");
let mut sv = initial.clone();
walk.step(&mut sv).expect("first step should succeed");
walk.step(&mut sv).expect("second step should succeed");
for (a, b) in sv.amps.iter().zip(initial.amps.iter()) {
assert!((a.re - b.re).abs() < 1e-5, "re {a:?} vs {b:?}");
assert!((a.im - b.im).abs() < 1e-5, "im {a:?} vs {b:?}");
}
let d0 = walk
.position_distribution(&initial)
.expect("position distribution of initial state should succeed");
let d2 = walk
.position_distribution(&sv)
.expect("position distribution after 2 steps should succeed");
for (p0, p2) in d0.iter().zip(d2.iter()) {
assert!((p0 - p2).abs() < 1e-5, "dist not periodic: {p0} vs {p2}");
}
}
#[test]
fn cyclic_translation_covariance() {
let walk = CoinedWalk::new(32).expect("valid CoinedWalk with 32 positions");
let t = 10usize;
let from_zero = walk
.evolve(0, CoinInit::SymmetricI, t)
.expect("evolve from position 0 should succeed");
let x0 = 7usize;
let from_x0 = walk
.evolve(x0, CoinInit::SymmetricI, t)
.expect("evolve from position x0 should succeed");
let n = 32usize;
for x in 0..n {
let shifted = from_zero[(x + n - x0) % n];
assert!(
(from_x0[x] - shifted).abs() < 1e-5,
"covariance broken at x={x}: {} vs {}",
from_x0[x],
shifted
);
}
}
#[test]
fn rejects_non_power_of_two() {
assert!(CoinedWalk::new(6).is_err());
assert!(CoinedWalk::new(0).is_err());
assert!(CoinedWalk::new(1).is_err());
assert!(CoinedWalk::new(8).is_ok());
}
#[test]
fn rejects_out_of_range_start() {
let walk = CoinedWalk::new(8).expect("valid CoinedWalk with 8 positions");
assert!(walk.initial_state(8, CoinInit::Zero).is_err());
assert!(walk.initial_state(0, CoinInit::Zero).is_ok());
}
}