quantrs2_core/
quantum_walk.rs

1//! Quantum Walk Algorithms
2//!
3//! This module implements various quantum walk algorithms, including:
4//! - Discrete-time quantum walks on graphs
5//! - Continuous-time quantum walks
6//! - Szegedy quantum walks
7//!
8//! Quantum walks are the quantum analog of classical random walks and form
9//! the basis for many quantum algorithms.
10
11use crate::{
12    complex_ext::QuantumComplexExt,
13    error::{QuantRS2Error, QuantRS2Result},
14};
15use scirs2_core::ndarray::{Array1, Array2};
16use scirs2_core::Complex64;
17use std::collections::{HashMap, VecDeque};
18
19/// Types of graphs for quantum walks
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum GraphType {
22    /// Line graph (path graph)
23    Line,
24    /// Cycle graph
25    Cycle,
26    /// Complete graph
27    Complete,
28    /// Hypercube graph
29    Hypercube,
30    /// Grid graph (2D lattice)
31    Grid2D,
32    /// Custom graph
33    Custom,
34}
35
36/// Coin operators for discrete quantum walks
37#[derive(Debug, Clone)]
38pub enum CoinOperator {
39    /// Hadamard coin
40    Hadamard,
41    /// Grover coin
42    Grover,
43    /// DFT (Discrete Fourier Transform) coin
44    DFT,
45    /// Custom coin operator
46    Custom(Array2<Complex64>),
47}
48
49/// Search oracle for quantum walk search
50#[derive(Debug, Clone)]
51pub struct SearchOracle {
52    /// Marked vertices
53    pub marked: Vec<usize>,
54}
55
56impl SearchOracle {
57    /// Create a new search oracle with marked vertices
58    pub const fn new(marked: Vec<usize>) -> Self {
59        Self { marked }
60    }
61
62    /// Check if a vertex is marked
63    pub fn is_marked(&self, vertex: usize) -> bool {
64        self.marked.contains(&vertex)
65    }
66}
67
68/// Graph representation for quantum walks
69#[derive(Debug, Clone)]
70pub struct Graph {
71    /// Number of vertices
72    pub num_vertices: usize,
73    /// Adjacency list representation
74    pub edges: Vec<Vec<usize>>,
75    /// Optional edge weights
76    pub weights: Option<Vec<Vec<f64>>>,
77}
78
79impl Graph {
80    /// Create a new graph of a specific type
81    pub fn new(graph_type: GraphType, size: usize) -> Self {
82        let mut graph = Self {
83            num_vertices: match graph_type {
84                GraphType::Hypercube => 1 << size, // 2^size vertices
85                GraphType::Grid2D => size * size,  // size x size grid
86                _ => size,
87            },
88            edges: vec![],
89            weights: None,
90        };
91
92        // Initialize edges based on graph type
93        graph.edges = vec![Vec::new(); graph.num_vertices];
94
95        match graph_type {
96            GraphType::Line => {
97                for i in 0..size.saturating_sub(1) {
98                    graph.add_edge(i, i + 1);
99                }
100            }
101            GraphType::Cycle => {
102                for i in 0..size {
103                    graph.add_edge(i, (i + 1) % size);
104                }
105            }
106            GraphType::Complete => {
107                for i in 0..size {
108                    for j in i + 1..size {
109                        graph.add_edge(i, j);
110                    }
111                }
112            }
113            GraphType::Hypercube => {
114                let n = size; // dimension
115                for i in 0..(1 << n) {
116                    for j in 0..n {
117                        let neighbor = i ^ (1 << j);
118                        if neighbor > i {
119                            graph.add_edge(i, neighbor);
120                        }
121                    }
122                }
123            }
124            GraphType::Grid2D => {
125                for i in 0..size {
126                    for j in 0..size {
127                        let idx = i * size + j;
128                        // Right neighbor
129                        if j < size - 1 {
130                            graph.add_edge(idx, idx + 1);
131                        }
132                        // Bottom neighbor
133                        if i < size - 1 {
134                            graph.add_edge(idx, idx + size);
135                        }
136                    }
137                }
138            }
139            GraphType::Custom => {
140                // Empty graph, user will add edges manually
141            }
142        }
143
144        graph
145    }
146
147    /// Create an empty graph with given number of vertices
148    pub fn new_empty(num_vertices: usize) -> Self {
149        Self {
150            num_vertices,
151            edges: vec![Vec::new(); num_vertices],
152            weights: None,
153        }
154    }
155
156    /// Add an undirected edge
157    pub fn add_edge(&mut self, u: usize, v: usize) {
158        if u < self.num_vertices && v < self.num_vertices && u != v && !self.edges[u].contains(&v) {
159            self.edges[u].push(v);
160            self.edges[v].push(u);
161        }
162    }
163
164    /// Add a weighted edge
165    pub fn add_weighted_edge(&mut self, u: usize, v: usize, weight: f64) {
166        if self.weights.is_none() {
167            self.weights = Some(vec![vec![0.0; self.num_vertices]; self.num_vertices]);
168        }
169
170        self.add_edge(u, v);
171
172        if let Some(ref mut weights) = self.weights {
173            weights[u][v] = weight;
174            weights[v][u] = weight;
175        }
176    }
177
178    /// Get the degree of a vertex
179    pub fn degree(&self, vertex: usize) -> usize {
180        if vertex < self.num_vertices {
181            self.edges[vertex].len()
182        } else {
183            0
184        }
185    }
186
187    /// Get the adjacency matrix
188    pub fn adjacency_matrix(&self) -> Array2<f64> {
189        let mut matrix = Array2::zeros((self.num_vertices, self.num_vertices));
190
191        for (u, neighbors) in self.edges.iter().enumerate() {
192            for &v in neighbors {
193                if let Some(ref weights) = self.weights {
194                    matrix[[u, v]] = weights[u][v];
195                } else {
196                    matrix[[u, v]] = 1.0;
197                }
198            }
199        }
200
201        matrix
202    }
203
204    /// Get the Laplacian matrix
205    pub fn laplacian_matrix(&self) -> Array2<f64> {
206        let mut laplacian = Array2::zeros((self.num_vertices, self.num_vertices));
207
208        for v in 0..self.num_vertices {
209            let degree = self.degree(v) as f64;
210            laplacian[[v, v]] = degree;
211
212            for &neighbor in &self.edges[v] {
213                if let Some(ref weights) = self.weights {
214                    laplacian[[v, neighbor]] = -weights[v][neighbor];
215                } else {
216                    laplacian[[v, neighbor]] = -1.0;
217                }
218            }
219        }
220
221        laplacian
222    }
223
224    /// Get the normalized Laplacian matrix
225    pub fn normalized_laplacian_matrix(&self) -> Array2<f64> {
226        let mut norm_laplacian = Array2::zeros((self.num_vertices, self.num_vertices));
227
228        for v in 0..self.num_vertices {
229            let degree_v = self.degree(v) as f64;
230            if degree_v == 0.0 {
231                continue;
232            }
233
234            norm_laplacian[[v, v]] = 1.0;
235
236            for &neighbor in &self.edges[v] {
237                let degree_n = self.degree(neighbor) as f64;
238                if degree_n == 0.0 {
239                    continue;
240                }
241
242                let weight = if let Some(ref weights) = self.weights {
243                    weights[v][neighbor]
244                } else {
245                    1.0
246                };
247
248                norm_laplacian[[v, neighbor]] = -weight / (degree_v * degree_n).sqrt();
249            }
250        }
251
252        norm_laplacian
253    }
254
255    /// Get the transition matrix for random walks
256    pub fn transition_matrix(&self) -> Array2<f64> {
257        let mut transition = Array2::zeros((self.num_vertices, self.num_vertices));
258
259        for v in 0..self.num_vertices {
260            let degree = self.degree(v) as f64;
261            if degree == 0.0 {
262                continue;
263            }
264
265            for &neighbor in &self.edges[v] {
266                let weight = if let Some(ref weights) = self.weights {
267                    weights[v][neighbor]
268                } else {
269                    1.0
270                };
271
272                transition[[v, neighbor]] = weight / degree;
273            }
274        }
275
276        transition
277    }
278
279    /// Check if the graph is bipartite
280    pub fn is_bipartite(&self) -> bool {
281        let mut colors = vec![-1; self.num_vertices];
282
283        for start in 0..self.num_vertices {
284            if colors[start] != -1 {
285                continue;
286            }
287
288            let mut queue = VecDeque::new();
289            queue.push_back(start);
290            colors[start] = 0;
291
292            while let Some(vertex) = queue.pop_front() {
293                for &neighbor in &self.edges[vertex] {
294                    if colors[neighbor] == -1 {
295                        colors[neighbor] = 1 - colors[vertex];
296                        queue.push_back(neighbor);
297                    } else if colors[neighbor] == colors[vertex] {
298                        return false;
299                    }
300                }
301            }
302        }
303
304        true
305    }
306
307    /// Calculate the algebraic connectivity (second smallest eigenvalue of Laplacian)
308    pub fn algebraic_connectivity(&self) -> f64 {
309        let laplacian = self.laplacian_matrix();
310
311        // For small graphs, we can compute eigenvalues directly
312        // In practice, you'd use more sophisticated numerical methods
313        if self.num_vertices <= 10 {
314            self.compute_laplacian_eigenvalues(&laplacian)
315                .get(1)
316                .copied()
317                .unwrap_or(0.0)
318        } else {
319            // Approximate using power iteration for larger graphs
320            self.estimate_fiedler_value(&laplacian)
321        }
322    }
323
324    /// Compute eigenvalues of Laplacian (simplified implementation)
325    fn compute_laplacian_eigenvalues(&self, _laplacian: &Array2<f64>) -> Vec<f64> {
326        // This is a placeholder - in practice you'd use a proper eigenvalue solver
327        // For now, return approximate values based on graph structure
328        let mut eigenvalues = vec![0.0]; // Always has 0 eigenvalue
329
330        // Rough approximation based on degree sequence
331        for v in 0..self.num_vertices {
332            let degree = self.degree(v) as f64;
333            if degree > 0.0 {
334                eigenvalues.push(degree);
335            }
336        }
337
338        eigenvalues.sort_by(|a, b| {
339            a.partial_cmp(b)
340                .expect("Failed to compare eigenvalues in Graph::compute_laplacian_eigenvalues")
341        });
342        eigenvalues.truncate(self.num_vertices);
343        eigenvalues
344    }
345
346    /// Estimate Fiedler value using power iteration
347    fn estimate_fiedler_value(&self, _laplacian: &Array2<f64>) -> f64 {
348        // Simplified estimation - in practice use inverse power iteration
349        let max_degree = (0..self.num_vertices)
350            .map(|v| self.degree(v))
351            .max()
352            .unwrap_or(0);
353        2.0 / max_degree as f64 // Rough approximation
354    }
355
356    /// Get shortest path distances between all pairs of vertices
357    pub fn all_pairs_shortest_paths(&self) -> Array2<f64> {
358        let mut distances =
359            Array2::from_elem((self.num_vertices, self.num_vertices), f64::INFINITY);
360
361        // Initialize distances
362        for v in 0..self.num_vertices {
363            distances[[v, v]] = 0.0;
364            for &neighbor in &self.edges[v] {
365                let weight = if let Some(ref weights) = self.weights {
366                    weights[v][neighbor]
367                } else {
368                    1.0
369                };
370                distances[[v, neighbor]] = weight;
371            }
372        }
373
374        // Floyd-Warshall algorithm
375        for k in 0..self.num_vertices {
376            for i in 0..self.num_vertices {
377                for j in 0..self.num_vertices {
378                    let via_k = distances[[i, k]] + distances[[k, j]];
379                    if via_k < distances[[i, j]] {
380                        distances[[i, j]] = via_k;
381                    }
382                }
383            }
384        }
385
386        distances
387    }
388
389    /// Create a graph from an adjacency matrix
390    pub fn from_adjacency_matrix(matrix: &Array2<f64>) -> QuantRS2Result<Self> {
391        let (rows, cols) = matrix.dim();
392        if rows != cols {
393            return Err(QuantRS2Error::InvalidInput(
394                "Adjacency matrix must be square".to_string(),
395            ));
396        }
397
398        let mut graph = Self::new_empty(rows);
399        let mut has_weights = false;
400
401        for i in 0..rows {
402            for j in i + 1..cols {
403                let weight = matrix[[i, j]];
404                if weight != 0.0 {
405                    if weight != 1.0 {
406                        has_weights = true;
407                    }
408                    if has_weights {
409                        graph.add_weighted_edge(i, j, weight);
410                    } else {
411                        graph.add_edge(i, j);
412                    }
413                }
414            }
415        }
416
417        Ok(graph)
418    }
419}
420
421/// Discrete-time quantum walk
422pub struct DiscreteQuantumWalk {
423    graph: Graph,
424    coin_operator: CoinOperator,
425    coin_dimension: usize,
426    /// Total Hilbert space dimension: coin_dimension * num_vertices
427    hilbert_dim: usize,
428    /// Current state vector
429    state: Vec<Complex64>,
430}
431
432impl DiscreteQuantumWalk {
433    /// Create a new discrete quantum walk with specified coin operator
434    pub fn new(graph: Graph, coin_operator: CoinOperator) -> Self {
435        // Coin dimension is the maximum degree for standard walks
436        // For hypercube, it's the dimension
437        let coin_dimension = match graph.num_vertices {
438            n if n > 0 => {
439                (0..graph.num_vertices)
440                    .map(|v| graph.degree(v))
441                    .max()
442                    .unwrap_or(2)
443                    .max(2) // At least 2-dimensional coin
444            }
445            _ => 2,
446        };
447
448        let hilbert_dim = coin_dimension * graph.num_vertices;
449
450        Self {
451            graph,
452            coin_operator,
453            coin_dimension,
454            hilbert_dim,
455            state: vec![Complex64::new(0.0, 0.0); hilbert_dim],
456        }
457    }
458
459    /// Initialize walker at a specific position
460    pub fn initialize_position(&mut self, position: usize) {
461        self.state = vec![Complex64::new(0.0, 0.0); self.hilbert_dim];
462
463        // Equal superposition over all coin states at the position
464        let degree = self.graph.degree(position) as f64;
465        if degree > 0.0 {
466            let amplitude = Complex64::new(1.0 / degree.sqrt(), 0.0);
467
468            for coin in 0..self.coin_dimension.min(self.graph.degree(position)) {
469                let index = self.state_index(position, coin);
470                if index < self.state.len() {
471                    self.state[index] = amplitude;
472                }
473            }
474        }
475    }
476
477    /// Perform one step of the quantum walk
478    pub fn step(&mut self) {
479        // Apply coin operator
480        self.apply_coin();
481
482        // Apply shift operator
483        self.apply_shift();
484    }
485
486    /// Get position probabilities
487    pub fn position_probabilities(&self) -> Vec<f64> {
488        let mut probs = vec![0.0; self.graph.num_vertices];
489
490        for (vertex, prob) in probs.iter_mut().enumerate() {
491            for coin in 0..self.coin_dimension {
492                let idx = self.state_index(vertex, coin);
493                if idx < self.state.len() {
494                    *prob += self.state[idx].norm_sqr();
495                }
496            }
497        }
498
499        probs
500    }
501
502    /// Get the index in the state vector for (vertex, coin) pair
503    const fn state_index(&self, vertex: usize, coin: usize) -> usize {
504        vertex * self.coin_dimension + coin
505    }
506
507    /// Apply the coin operator
508    fn apply_coin(&mut self) {
509        match &self.coin_operator {
510            CoinOperator::Hadamard => self.apply_hadamard_coin(),
511            CoinOperator::Grover => self.apply_grover_coin(),
512            CoinOperator::DFT => self.apply_dft_coin(),
513            CoinOperator::Custom(matrix) => self.apply_custom_coin(matrix.clone()),
514        }
515    }
516
517    /// Apply Hadamard coin
518    fn apply_hadamard_coin(&mut self) {
519        let h = 1.0 / std::f64::consts::SQRT_2;
520
521        for vertex in 0..self.graph.num_vertices {
522            if self.coin_dimension == 2 {
523                let idx0 = self.state_index(vertex, 0);
524                let idx1 = self.state_index(vertex, 1);
525
526                if idx1 < self.state.len() {
527                    let a0 = self.state[idx0];
528                    let a1 = self.state[idx1];
529
530                    self.state[idx0] = h * (a0 + a1);
531                    self.state[idx1] = h * (a0 - a1);
532                }
533            }
534        }
535    }
536
537    /// Apply Grover coin
538    fn apply_grover_coin(&mut self) {
539        // Grover coin: 2|s><s| - I, where |s> is uniform superposition
540        for vertex in 0..self.graph.num_vertices {
541            let degree = self.graph.degree(vertex);
542            if degree <= 1 {
543                continue; // No coin needed for degree 0 or 1
544            }
545
546            // Calculate sum of amplitudes for this vertex
547            let mut sum = Complex64::new(0.0, 0.0);
548            for coin in 0..degree.min(self.coin_dimension) {
549                let idx = self.state_index(vertex, coin);
550                if idx < self.state.len() {
551                    sum += self.state[idx];
552                }
553            }
554
555            // Apply Grover coin
556            let factor = Complex64::new(2.0 / degree as f64, 0.0);
557            for coin in 0..degree.min(self.coin_dimension) {
558                let idx = self.state_index(vertex, coin);
559                if idx < self.state.len() {
560                    let old_amp = self.state[idx];
561                    self.state[idx] = factor * sum - old_amp;
562                }
563            }
564        }
565    }
566
567    /// Apply DFT coin
568    fn apply_dft_coin(&mut self) {
569        // DFT coin for 2-dimensional coin space
570        if self.coin_dimension == 2 {
571            self.apply_hadamard_coin(); // DFT is same as Hadamard for 2D
572        }
573        // For higher dimensions, would implement full DFT
574    }
575
576    /// Apply custom coin operator
577    fn apply_custom_coin(&mut self, matrix: Array2<Complex64>) {
578        if matrix.shape() != [self.coin_dimension, self.coin_dimension] {
579            return; // Matrix size mismatch
580        }
581
582        for vertex in 0..self.graph.num_vertices {
583            let mut coin_state = vec![Complex64::new(0.0, 0.0); self.coin_dimension];
584
585            // Extract coin state for this vertex
586            for (coin, cs) in coin_state.iter_mut().enumerate() {
587                let idx = self.state_index(vertex, coin);
588                if idx < self.state.len() {
589                    *cs = self.state[idx];
590                }
591            }
592
593            // Apply coin operator
594            let new_coin_state = matrix.dot(&Array1::from(coin_state));
595
596            // Write back
597            for coin in 0..self.coin_dimension {
598                let idx = self.state_index(vertex, coin);
599                if idx < self.state.len() {
600                    self.state[idx] = new_coin_state[coin];
601                }
602            }
603        }
604    }
605
606    /// Apply the shift operator
607    fn apply_shift(&mut self) {
608        let mut new_state = vec![Complex64::new(0.0, 0.0); self.hilbert_dim];
609
610        for vertex in 0..self.graph.num_vertices {
611            for (coin, &neighbor) in self.graph.edges[vertex].iter().enumerate() {
612                if coin < self.coin_dimension {
613                    let from_idx = self.state_index(vertex, coin);
614
615                    // Find which coin state corresponds to coming from 'vertex' at 'neighbor'
616                    let to_coin = self.graph.edges[neighbor]
617                        .iter()
618                        .position(|&v| v == vertex)
619                        .unwrap_or(0);
620
621                    if to_coin < self.coin_dimension && from_idx < self.state.len() {
622                        let to_idx = self.state_index(neighbor, to_coin);
623                        if to_idx < new_state.len() {
624                            new_state[to_idx] = self.state[from_idx];
625                        }
626                    }
627                }
628            }
629        }
630
631        self.state.copy_from_slice(&new_state);
632    }
633}
634
635/// Continuous-time quantum walk
636pub struct ContinuousQuantumWalk {
637    graph: Graph,
638    hamiltonian: Array2<Complex64>,
639    state: Vec<Complex64>,
640}
641
642impl ContinuousQuantumWalk {
643    /// Create a new continuous quantum walk
644    pub fn new(graph: Graph) -> Self {
645        let adj_matrix = graph.adjacency_matrix();
646        let hamiltonian = adj_matrix.mapv(|x| Complex64::new(x, 0.0));
647        let num_vertices = graph.num_vertices;
648
649        Self {
650            graph,
651            hamiltonian,
652            state: vec![Complex64::new(0.0, 0.0); num_vertices],
653        }
654    }
655
656    /// Initialize walker at a specific vertex
657    pub fn initialize_vertex(&mut self, vertex: usize) {
658        self.state = vec![Complex64::new(0.0, 0.0); self.graph.num_vertices];
659        if vertex < self.graph.num_vertices {
660            self.state[vertex] = Complex64::new(1.0, 0.0);
661        }
662    }
663
664    /// Evolve the quantum walk for time t
665    pub fn evolve(&mut self, time: f64) {
666        // This is a simplified version using first-order approximation
667        // For a full implementation, we would diagonalize the Hamiltonian
668
669        let dt = 0.01; // Time step
670        let steps = (time / dt) as usize;
671
672        for _ in 0..steps {
673            let mut new_state = self.state.clone();
674
675            // Apply exp(-iHt) ≈ I - iHdt for small dt
676            for (i, ns) in new_state
677                .iter_mut()
678                .enumerate()
679                .take(self.graph.num_vertices)
680            {
681                let mut sum = Complex64::new(0.0, 0.0);
682                for j in 0..self.graph.num_vertices {
683                    sum += self.hamiltonian[[i, j]] * self.state[j];
684                }
685                *ns = self.state[i] - Complex64::new(0.0, dt) * sum;
686            }
687
688            // Normalize
689            let norm: f64 = new_state.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
690
691            if norm > 0.0 {
692                for amp in &mut new_state {
693                    *amp /= norm;
694                }
695            }
696
697            self.state = new_state;
698        }
699    }
700
701    /// Get vertex probabilities
702    pub fn vertex_probabilities(&self) -> Vec<f64> {
703        self.state.iter().map(|c| c.probability()).collect()
704    }
705
706    /// Calculate transport probability between two vertices at time t
707    pub fn transport_probability(&mut self, from: usize, to: usize, time: f64) -> f64 {
708        // Initialize at 'from' vertex
709        self.initialize_vertex(from);
710
711        // Evolve for time t
712        self.evolve(time);
713
714        // Return probability at 'to' vertex
715        if to < self.state.len() {
716            self.state[to].probability()
717        } else {
718            0.0
719        }
720    }
721
722    /// Get the probability distribution
723    pub fn get_probabilities(&self, state: &[Complex64]) -> Vec<f64> {
724        state.iter().map(|c| c.probability()).collect()
725    }
726}
727
728/// Szegedy quantum walk for arbitrary graphs
729/// This provides better mixing properties on irregular graphs
730pub struct SzegedyQuantumWalk {
731    graph: Graph,
732    /// State lives on edges: |u,v> where edge (u,v) exists
733    state: HashMap<(usize, usize), Complex64>,
734    num_edges: usize,
735}
736
737impl SzegedyQuantumWalk {
738    /// Create a new Szegedy quantum walk
739    pub fn new(graph: Graph) -> Self {
740        let mut num_edges = 0;
741        for v in 0..graph.num_vertices {
742            num_edges += graph.edges[v].len();
743        }
744
745        Self {
746            graph,
747            state: HashMap::new(),
748            num_edges,
749        }
750    }
751
752    /// Initialize in uniform superposition over all edges
753    pub fn initialize_uniform(&mut self) {
754        self.state.clear();
755
756        if self.num_edges == 0 {
757            return;
758        }
759
760        let amplitude = Complex64::new(1.0 / (self.num_edges as f64).sqrt(), 0.0);
761
762        for u in 0..self.graph.num_vertices {
763            for &v in &self.graph.edges[u] {
764                self.state.insert((u, v), amplitude);
765            }
766        }
767    }
768
769    /// Initialize at a specific edge
770    pub fn initialize_edge(&mut self, u: usize, v: usize) {
771        self.state.clear();
772
773        if u < self.graph.num_vertices && self.graph.edges[u].contains(&v) {
774            self.state.insert((u, v), Complex64::new(1.0, 0.0));
775        }
776    }
777
778    /// Perform one step of Szegedy walk
779    pub fn step(&mut self) {
780        // Szegedy walk: (2P - I)(2Q - I) where P and Q are projections
781
782        // Apply reflection around vertex-uniform states
783        self.reflect_vertex_uniform();
784
785        // Apply reflection around edge-uniform states
786        self.reflect_edge_uniform();
787    }
788
789    /// Reflect around vertex-uniform subspaces
790    fn reflect_vertex_uniform(&mut self) {
791        let mut vertex_sums: Vec<Complex64> =
792            vec![Complex64::new(0.0, 0.0); self.graph.num_vertices];
793
794        // Calculate sum of amplitudes for each vertex
795        for (&(u, _), &amplitude) in &self.state {
796            vertex_sums[u] += amplitude;
797        }
798
799        // Apply reflection: 2|psi_u><psi_u| - I
800        let mut new_state = HashMap::new();
801
802        for (&(u, v), &old_amp) in &self.state {
803            let degree = self.graph.degree(u) as f64;
804            if degree > 0.0 {
805                let vertex_avg = vertex_sums[u] / degree;
806                let new_amp = 2.0 * vertex_avg - old_amp;
807                new_state.insert((u, v), new_amp);
808            }
809        }
810
811        self.state = new_state;
812    }
813
814    /// Reflect around edge-uniform subspace
815    fn reflect_edge_uniform(&mut self) {
816        if self.num_edges == 0 {
817            return;
818        }
819
820        // Calculate total amplitude
821        let total_amp: Complex64 = self.state.values().sum();
822        let uniform_amp = total_amp / self.num_edges as f64;
823
824        // Apply reflection: 2|uniform><uniform| - I
825        for amplitude in self.state.values_mut() {
826            *amplitude = 2.0 * uniform_amp - *amplitude;
827        }
828    }
829
830    /// Get vertex probabilities by summing over outgoing edges
831    pub fn vertex_probabilities(&self) -> Vec<f64> {
832        let mut probs = vec![0.0; self.graph.num_vertices];
833
834        for (&(u, _), &amplitude) in &self.state {
835            probs[u] += amplitude.norm_sqr();
836        }
837
838        probs
839    }
840
841    /// Get edge probabilities
842    pub fn edge_probabilities(&self) -> Vec<((usize, usize), f64)> {
843        self.state
844            .iter()
845            .map(|(&edge, &amplitude)| (edge, amplitude.norm_sqr()))
846            .collect()
847    }
848
849    /// Calculate mixing time to epsilon-close to uniform distribution
850    pub fn estimate_mixing_time(&mut self, epsilon: f64) -> usize {
851        let uniform_prob = 1.0 / self.graph.num_vertices as f64;
852
853        // Reset to uniform
854        self.initialize_uniform();
855
856        for steps in 1..1000 {
857            self.step();
858
859            let probs = self.vertex_probabilities();
860            let max_deviation = probs
861                .iter()
862                .map(|&p| (p - uniform_prob).abs())
863                .fold(0.0, f64::max);
864
865            if max_deviation < epsilon {
866                return steps;
867            }
868        }
869
870        1000 // Return max if not converged
871    }
872}
873
874/// Multi-walker quantum walk for studying entanglement and correlations
875pub struct MultiWalkerQuantumWalk {
876    graph: Graph,
877    num_walkers: usize,
878    /// State tensor product space: walker1_pos ⊗ walker2_pos ⊗ ...
879    state: Array1<Complex64>,
880    /// Dimension of single walker space
881    single_walker_dim: usize,
882}
883
884impl MultiWalkerQuantumWalk {
885    /// Create a new multi-walker quantum walk
886    pub fn new(graph: Graph, num_walkers: usize) -> Self {
887        let single_walker_dim = graph.num_vertices;
888        let total_dim = single_walker_dim.pow(num_walkers as u32);
889
890        Self {
891            graph,
892            num_walkers,
893            state: Array1::zeros(total_dim),
894            single_walker_dim,
895        }
896    }
897
898    /// Initialize walkers at specific positions
899    pub fn initialize_positions(&mut self, positions: &[usize]) -> QuantRS2Result<()> {
900        if positions.len() != self.num_walkers {
901            return Err(QuantRS2Error::InvalidInput(
902                "Number of positions must match number of walkers".to_string(),
903            ));
904        }
905
906        for &pos in positions {
907            if pos >= self.single_walker_dim {
908                return Err(QuantRS2Error::InvalidInput(format!(
909                    "Position {pos} out of bounds"
910                )));
911            }
912        }
913
914        // Reset state
915        self.state.fill(Complex64::new(0.0, 0.0));
916
917        // Set amplitude for initial configuration
918        let index = self.positions_to_index(positions);
919        self.state[index] = Complex64::new(1.0, 0.0);
920
921        Ok(())
922    }
923
924    /// Initialize in entangled superposition
925    pub fn initialize_entangled_bell_state(
926        &mut self,
927        pos1: usize,
928        pos2: usize,
929    ) -> QuantRS2Result<()> {
930        if self.num_walkers != 2 {
931            return Err(QuantRS2Error::InvalidInput(
932                "Bell state initialization only works for 2 walkers".to_string(),
933            ));
934        }
935
936        self.state.fill(Complex64::new(0.0, 0.0));
937
938        let amplitude = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0);
939
940        // |pos1,pos2> + |pos2,pos1>
941        let idx1 = self.positions_to_index(&[pos1, pos2]);
942        let idx2 = self.positions_to_index(&[pos2, pos1]);
943
944        self.state[idx1] = amplitude;
945        self.state[idx2] = amplitude;
946
947        Ok(())
948    }
949
950    /// Convert walker positions to state vector index
951    fn positions_to_index(&self, positions: &[usize]) -> usize {
952        let mut index = 0;
953        let mut multiplier = 1;
954
955        for &pos in positions.iter().rev() {
956            index += pos * multiplier;
957            multiplier *= self.single_walker_dim;
958        }
959
960        index
961    }
962
963    /// Convert state vector index to walker positions
964    fn index_to_positions(&self, mut index: usize) -> Vec<usize> {
965        let mut positions = Vec::with_capacity(self.num_walkers);
966
967        for _ in 0..self.num_walkers {
968            positions.push(index % self.single_walker_dim);
969            index /= self.single_walker_dim;
970        }
971
972        positions.reverse();
973        positions
974    }
975
976    /// Perform one step (simplified version - each walker evolves independently)
977    pub fn step_independent(&mut self) {
978        let mut new_state = Array1::zeros(self.state.len());
979
980        for (index, &amplitude) in self.state.iter().enumerate() {
981            if amplitude.norm_sqr() < 1e-15 {
982                continue;
983            }
984
985            let positions = self.index_to_positions(index);
986
987            // Each walker moves to neighboring vertices with equal probability
988            let mut total_neighbors = 1;
989            for &pos in &positions {
990                total_neighbors *= self.graph.degree(pos).max(1);
991            }
992
993            let neighbor_amplitude = amplitude / (total_neighbors as f64).sqrt();
994
995            // Generate all possible neighbor configurations
996            self.add_neighbor_amplitudes(
997                &positions,
998                0,
999                &mut Vec::new(),
1000                neighbor_amplitude,
1001                &mut new_state,
1002            );
1003        }
1004
1005        self.state = new_state;
1006    }
1007
1008    /// Recursively add amplitudes for all neighbor configurations
1009    fn add_neighbor_amplitudes(
1010        &self,
1011        original_positions: &[usize],
1012        walker_idx: usize,
1013        current_positions: &mut Vec<usize>,
1014        amplitude: Complex64,
1015        new_state: &mut Array1<Complex64>,
1016    ) {
1017        if walker_idx >= self.num_walkers {
1018            let index = self.positions_to_index(current_positions);
1019            new_state[index] += amplitude;
1020            return;
1021        }
1022
1023        let pos = original_positions[walker_idx];
1024        let neighbors = &self.graph.edges[pos];
1025
1026        if neighbors.is_empty() {
1027            // Stay at same position if no neighbors
1028            current_positions.push(pos);
1029            self.add_neighbor_amplitudes(
1030                original_positions,
1031                walker_idx + 1,
1032                current_positions,
1033                amplitude,
1034                new_state,
1035            );
1036            current_positions.pop();
1037        } else {
1038            for &neighbor in neighbors {
1039                current_positions.push(neighbor);
1040                self.add_neighbor_amplitudes(
1041                    original_positions,
1042                    walker_idx + 1,
1043                    current_positions,
1044                    amplitude,
1045                    new_state,
1046                );
1047                current_positions.pop();
1048            }
1049        }
1050    }
1051
1052    /// Get marginal probability distribution for a specific walker
1053    pub fn marginal_probabilities(&self, walker_idx: usize) -> Vec<f64> {
1054        let mut probs = vec![0.0; self.single_walker_dim];
1055
1056        for (index, &amplitude) in self.state.iter().enumerate() {
1057            let positions = self.index_to_positions(index);
1058            probs[positions[walker_idx]] += amplitude.norm_sqr();
1059        }
1060
1061        probs
1062    }
1063
1064    /// Calculate entanglement entropy between walkers
1065    pub fn entanglement_entropy(&self) -> f64 {
1066        if self.num_walkers != 2 {
1067            return 0.0; // Only implemented for 2 walkers
1068        }
1069
1070        // Compute reduced density matrix for walker 1
1071        let mut reduced_dm = Array2::zeros((self.single_walker_dim, self.single_walker_dim));
1072
1073        for i in 0..self.single_walker_dim {
1074            for j in 0..self.single_walker_dim {
1075                for k in 0..self.single_walker_dim {
1076                    let idx1 = self.positions_to_index(&[i, k]);
1077                    let idx2 = self.positions_to_index(&[j, k]);
1078
1079                    reduced_dm[[i, j]] += self.state[idx1].conj() * self.state[idx2];
1080                }
1081            }
1082        }
1083
1084        // Calculate von Neumann entropy (simplified - would use eigenvalues in practice)
1085        let trace = reduced_dm.diag().mapv(|x: Complex64| x.re).sum();
1086        -trace * trace.ln() // Simplified approximation
1087    }
1088}
1089
1090/// Quantum walk with environmental decoherence
1091pub struct DecoherentQuantumWalk {
1092    base_walk: DiscreteQuantumWalk,
1093    decoherence_rate: f64,
1094    measurement_probability: f64,
1095}
1096
1097impl DecoherentQuantumWalk {
1098    /// Create a new decoherent quantum walk
1099    pub fn new(graph: Graph, coin_operator: CoinOperator, decoherence_rate: f64) -> Self {
1100        Self {
1101            base_walk: DiscreteQuantumWalk::new(graph, coin_operator),
1102            decoherence_rate,
1103            measurement_probability: 0.0,
1104        }
1105    }
1106
1107    /// Initialize walker position
1108    pub fn initialize_position(&mut self, position: usize) {
1109        self.base_walk.initialize_position(position);
1110    }
1111
1112    /// Perform one step with decoherence
1113    pub fn step(&mut self) {
1114        // Apply unitary evolution
1115        self.base_walk.step();
1116
1117        // Apply decoherence
1118        self.apply_decoherence();
1119    }
1120
1121    /// Apply decoherence by mixing with classical random walk
1122    fn apply_decoherence(&mut self) {
1123        if self.decoherence_rate <= 0.0 {
1124            return;
1125        }
1126
1127        // Get current probabilities
1128        let quantum_probs = self.base_walk.position_probabilities();
1129
1130        // Classical random walk step
1131        let mut classical_probs = vec![0.0; quantum_probs.len()];
1132        for (v, &prob) in quantum_probs.iter().enumerate() {
1133            if prob > 0.0 {
1134                let degree = self.base_walk.graph.degree(v) as f64;
1135                if degree > 0.0 {
1136                    for &neighbor in &self.base_walk.graph.edges[v] {
1137                        classical_probs[neighbor] += prob / degree;
1138                    }
1139                } else {
1140                    classical_probs[v] += prob; // Stay if isolated
1141                }
1142            }
1143        }
1144
1145        // Mix quantum and classical
1146        let quantum_weight = 1.0 - self.decoherence_rate;
1147        let classical_weight = self.decoherence_rate;
1148
1149        // Update quantum state to match mixed probabilities (approximate)
1150        for v in 0..quantum_probs.len() {
1151            let mixed_prob =
1152                quantum_weight * quantum_probs[v] + classical_weight * classical_probs[v];
1153
1154            // Scale amplitudes to match mixed probabilities (simplified)
1155            if quantum_probs[v] > 0.0 {
1156                let scale_factor = (mixed_prob / quantum_probs[v]).sqrt();
1157
1158                for coin in 0..self.base_walk.coin_dimension {
1159                    let idx = self.base_walk.state_index(v, coin);
1160                    if idx < self.base_walk.state.len() {
1161                        self.base_walk.state[idx] *= scale_factor;
1162                    }
1163                }
1164            }
1165        }
1166
1167        // Renormalize
1168        let total_norm: f64 = self.base_walk.state.iter().map(|c| c.norm_sqr()).sum();
1169        if total_norm > 0.0 {
1170            let norm_factor = 1.0 / total_norm.sqrt();
1171            for amplitude in &mut self.base_walk.state {
1172                *amplitude *= norm_factor;
1173            }
1174        }
1175    }
1176
1177    /// Get position probabilities
1178    pub fn position_probabilities(&self) -> Vec<f64> {
1179        self.base_walk.position_probabilities()
1180    }
1181
1182    /// Set decoherence rate
1183    pub const fn set_decoherence_rate(&mut self, rate: f64) {
1184        self.decoherence_rate = rate.clamp(0.0, 1.0);
1185    }
1186}
1187
1188/// Search algorithm using quantum walks
1189pub struct QuantumWalkSearch {
1190    #[allow(dead_code)]
1191    graph: Graph,
1192    oracle: SearchOracle,
1193    walk: DiscreteQuantumWalk,
1194}
1195
1196impl QuantumWalkSearch {
1197    /// Create a new quantum walk search
1198    pub fn new(graph: Graph, oracle: SearchOracle) -> Self {
1199        let walk = DiscreteQuantumWalk::new(graph.clone(), CoinOperator::Grover);
1200        Self {
1201            graph,
1202            oracle,
1203            walk,
1204        }
1205    }
1206
1207    /// Apply the oracle that marks vertices
1208    fn apply_oracle(&mut self) {
1209        for &vertex in &self.oracle.marked {
1210            for coin in 0..self.walk.coin_dimension {
1211                let idx = self.walk.state_index(vertex, coin);
1212                if idx < self.walk.state.len() {
1213                    self.walk.state[idx] = -self.walk.state[idx]; // Phase flip
1214                }
1215            }
1216        }
1217    }
1218
1219    /// Run the search algorithm
1220    pub fn run(&mut self, max_steps: usize) -> (usize, f64, usize) {
1221        // Start in uniform superposition
1222        let amplitude = Complex64::new(1.0 / (self.walk.hilbert_dim as f64).sqrt(), 0.0);
1223        self.walk.state.fill(amplitude);
1224
1225        let mut best_vertex = 0;
1226        let mut best_prob = 0.0;
1227        let mut best_step = 0;
1228
1229        // Alternate between walk and oracle
1230        for step in 1..=max_steps {
1231            self.walk.step();
1232            self.apply_oracle();
1233
1234            // Check probabilities at marked vertices
1235            let probs = self.walk.position_probabilities();
1236            for &marked in &self.oracle.marked {
1237                if probs[marked] > best_prob {
1238                    best_prob = probs[marked];
1239                    best_vertex = marked;
1240                    best_step = step;
1241                }
1242            }
1243
1244            // Early stopping if we have high probability
1245            if best_prob > 0.5 {
1246                break;
1247            }
1248        }
1249
1250        (best_vertex, best_prob, best_step)
1251    }
1252
1253    /// Get vertex probabilities
1254    pub fn vertex_probabilities(&self) -> Vec<f64> {
1255        self.walk.position_probabilities()
1256    }
1257}
1258
1259/// Example: Quantum walk on a line
1260pub fn quantum_walk_line_example() {
1261    println!("Quantum Walk on a Line (10 vertices)");
1262
1263    let graph = Graph::new(GraphType::Line, 10);
1264    let walk = DiscreteQuantumWalk::new(graph, CoinOperator::Hadamard);
1265
1266    // Start at vertex 5 (middle)
1267    let mut walk = walk;
1268    walk.initialize_position(5);
1269
1270    // Evolve for different time steps
1271    for steps in [0, 5, 10, 20, 30] {
1272        // Reset and evolve
1273        walk.initialize_position(5);
1274        for _ in 0..steps {
1275            walk.step();
1276        }
1277        let probs = walk.position_probabilities();
1278
1279        println!("\nAfter {steps} steps:");
1280        print!("Probabilities: ");
1281        for (v, p) in probs.iter().enumerate() {
1282            if *p > 0.01 {
1283                print!("v{v}: {p:.3} ");
1284            }
1285        }
1286        println!();
1287    }
1288}
1289
1290/// Example: Search on a complete graph
1291pub fn quantum_walk_search_example() {
1292    println!("\nQuantum Walk Search on Complete Graph (8 vertices)");
1293
1294    let graph = Graph::new(GraphType::Complete, 8);
1295    let marked = vec![3, 5]; // Mark vertices 3 and 5
1296    let oracle = SearchOracle::new(marked.clone());
1297
1298    let mut search = QuantumWalkSearch::new(graph, oracle);
1299
1300    println!("Marked vertices: {marked:?}");
1301
1302    // Run search
1303    let (found, prob, steps) = search.run(50);
1304
1305    println!("\nFound vertex {found} with probability {prob:.3} after {steps} steps");
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310    use super::*;
1311    // use approx::assert_relative_eq;
1312
1313    #[test]
1314    fn test_graph_creation() {
1315        let graph = Graph::new(GraphType::Cycle, 4);
1316        assert_eq!(graph.num_vertices, 4);
1317        assert_eq!(graph.degree(0), 2);
1318
1319        let complete = Graph::new(GraphType::Complete, 5);
1320        assert_eq!(complete.degree(0), 4);
1321    }
1322
1323    #[test]
1324    fn test_discrete_walk_initialization() {
1325        let graph = Graph::new(GraphType::Line, 5);
1326        let mut walk = DiscreteQuantumWalk::new(graph, CoinOperator::Hadamard);
1327
1328        walk.initialize_position(2);
1329        let probs = walk.position_probabilities();
1330
1331        assert!((probs[2] - 1.0).abs() < 1e-10);
1332    }
1333
1334    #[test]
1335    fn test_continuous_walk() {
1336        let graph = Graph::new(GraphType::Cycle, 4);
1337        let mut walk = ContinuousQuantumWalk::new(graph);
1338
1339        walk.initialize_vertex(0);
1340        walk.evolve(1.0);
1341
1342        let probs = walk.vertex_probabilities();
1343        let total: f64 = probs.iter().sum();
1344        assert!((total - 1.0).abs() < 1e-10);
1345    }
1346
1347    #[test]
1348    fn test_weighted_graph() {
1349        let mut graph = Graph::new_empty(3);
1350        graph.add_weighted_edge(0, 1, 2.0);
1351        graph.add_weighted_edge(1, 2, 3.0);
1352
1353        let adj_matrix = graph.adjacency_matrix();
1354        assert_eq!(adj_matrix[[0, 1]], 2.0);
1355        assert_eq!(adj_matrix[[1, 2]], 3.0);
1356        assert_eq!(adj_matrix[[0, 2]], 0.0);
1357    }
1358
1359    #[test]
1360    fn test_graph_from_adjacency_matrix() {
1361        let mut matrix = Array2::zeros((3, 3));
1362        matrix[[0, 1]] = 1.0;
1363        matrix[[1, 0]] = 1.0;
1364        matrix[[1, 2]] = 2.0;
1365        matrix[[2, 1]] = 2.0;
1366
1367        let graph = Graph::from_adjacency_matrix(&matrix).expect(
1368            "Failed to create graph from adjacency matrix in test_graph_from_adjacency_matrix",
1369        );
1370        assert_eq!(graph.num_vertices, 3);
1371        assert_eq!(graph.degree(0), 1);
1372        assert_eq!(graph.degree(1), 2);
1373        assert_eq!(graph.degree(2), 1);
1374    }
1375
1376    #[test]
1377    fn test_laplacian_matrix() {
1378        let graph = Graph::new(GraphType::Cycle, 3);
1379        let laplacian = graph.laplacian_matrix();
1380
1381        // Each vertex in a 3-cycle has degree 2
1382        assert_eq!(laplacian[[0, 0]], 2.0);
1383        assert_eq!(laplacian[[1, 1]], 2.0);
1384        assert_eq!(laplacian[[2, 2]], 2.0);
1385
1386        // Adjacent vertices have -1
1387        assert_eq!(laplacian[[0, 1]], -1.0);
1388        assert_eq!(laplacian[[1, 2]], -1.0);
1389        assert_eq!(laplacian[[2, 0]], -1.0);
1390    }
1391
1392    #[test]
1393    fn test_bipartite_detection() {
1394        let bipartite = Graph::new(GraphType::Cycle, 4); // Even cycle is bipartite
1395        assert!(bipartite.is_bipartite());
1396
1397        let non_bipartite = Graph::new(GraphType::Cycle, 3); // Odd cycle is not bipartite
1398        assert!(!non_bipartite.is_bipartite());
1399
1400        let complete = Graph::new(GraphType::Complete, 3); // Complete graph with >2 vertices is not bipartite
1401        assert!(!complete.is_bipartite());
1402    }
1403
1404    #[test]
1405    fn test_shortest_paths() {
1406        let graph = Graph::new(GraphType::Line, 4); // 0-1-2-3
1407        let distances = graph.all_pairs_shortest_paths();
1408
1409        assert_eq!(distances[[0, 0]], 0.0);
1410        assert_eq!(distances[[0, 1]], 1.0);
1411        assert_eq!(distances[[0, 2]], 2.0);
1412        assert_eq!(distances[[0, 3]], 3.0);
1413        assert_eq!(distances[[1, 3]], 2.0);
1414    }
1415
1416    #[test]
1417    fn test_szegedy_walk() {
1418        let graph = Graph::new(GraphType::Cycle, 4);
1419        let mut szegedy = SzegedyQuantumWalk::new(graph);
1420
1421        szegedy.initialize_uniform();
1422        let initial_probs = szegedy.vertex_probabilities();
1423
1424        // Should have some probability on each vertex
1425        for &prob in &initial_probs {
1426            assert!(prob > 0.0);
1427        }
1428
1429        // Take a few steps
1430        for _ in 0..5 {
1431            szegedy.step();
1432        }
1433
1434        let final_probs = szegedy.vertex_probabilities();
1435        let total: f64 = final_probs.iter().sum();
1436        assert!((total - 1.0).abs() < 1e-10);
1437    }
1438
1439    #[test]
1440    fn test_szegedy_edge_initialization() {
1441        let mut graph = Graph::new_empty(3);
1442        graph.add_edge(0, 1);
1443        graph.add_edge(1, 2);
1444
1445        let mut szegedy = SzegedyQuantumWalk::new(graph);
1446        szegedy.initialize_edge(0, 1);
1447
1448        let edge_probs = szegedy.edge_probabilities();
1449        assert_eq!(edge_probs.len(), 1);
1450        assert_eq!(edge_probs[0].0, (0, 1));
1451        assert!((edge_probs[0].1 - 1.0).abs() < 1e-10);
1452    }
1453
1454    #[test]
1455    fn test_multi_walker_quantum_walk() {
1456        let graph = Graph::new(GraphType::Cycle, 3);
1457        let mut multi_walk = MultiWalkerQuantumWalk::new(graph, 2);
1458
1459        // Initialize two walkers at positions 0 and 1
1460        multi_walk
1461            .initialize_positions(&[0, 1])
1462            .expect("Failed to initialize positions in test_multi_walker_quantum_walk");
1463
1464        let marginal_0 = multi_walk.marginal_probabilities(0);
1465        let marginal_1 = multi_walk.marginal_probabilities(1);
1466
1467        assert!((marginal_0[0] - 1.0).abs() < 1e-10);
1468        assert!((marginal_1[1] - 1.0).abs() < 1e-10);
1469
1470        // Take a step
1471        multi_walk.step_independent();
1472
1473        // Probabilities should have spread
1474        let new_marginal_0 = multi_walk.marginal_probabilities(0);
1475        let new_marginal_1 = multi_walk.marginal_probabilities(1);
1476
1477        assert!(new_marginal_0[0] < 1.0);
1478        assert!(new_marginal_1[1] < 1.0);
1479    }
1480
1481    #[test]
1482    fn test_multi_walker_bell_state() {
1483        let graph = Graph::new(GraphType::Cycle, 4);
1484        let mut multi_walk = MultiWalkerQuantumWalk::new(graph, 2);
1485
1486        multi_walk
1487            .initialize_entangled_bell_state(0, 1)
1488            .expect("Failed to initialize entangled Bell state in test_multi_walker_bell_state");
1489
1490        let marginal_0 = multi_walk.marginal_probabilities(0);
1491        let marginal_1 = multi_walk.marginal_probabilities(1);
1492
1493        // Each walker should have 50% probability at each of their initial positions
1494        assert!((marginal_0[0] - 0.5).abs() < 1e-10);
1495        assert!((marginal_0[1] - 0.5).abs() < 1e-10);
1496        assert!((marginal_1[0] - 0.5).abs() < 1e-10);
1497        assert!((marginal_1[1] - 0.5).abs() < 1e-10);
1498    }
1499
1500    #[test]
1501    fn test_multi_walker_error_handling() {
1502        let graph = Graph::new(GraphType::Line, 3);
1503        let mut multi_walk = MultiWalkerQuantumWalk::new(graph.clone(), 2);
1504
1505        // Wrong number of positions
1506        assert!(multi_walk.initialize_positions(&[0]).is_err());
1507
1508        // Position out of bounds
1509        assert!(multi_walk.initialize_positions(&[0, 5]).is_err());
1510
1511        // Bell state with wrong number of walkers
1512        let mut single_walk = MultiWalkerQuantumWalk::new(graph, 1);
1513        assert!(single_walk.initialize_entangled_bell_state(0, 1).is_err());
1514    }
1515
1516    #[test]
1517    fn test_decoherent_quantum_walk() {
1518        let graph = Graph::new(GraphType::Line, 5);
1519        let mut decoherent = DecoherentQuantumWalk::new(graph, CoinOperator::Hadamard, 0.1);
1520
1521        decoherent.initialize_position(2);
1522        let initial_probs = decoherent.position_probabilities();
1523        assert!((initial_probs[2] - 1.0).abs() < 1e-10);
1524
1525        // Take steps with decoherence
1526        for _ in 0..10 {
1527            decoherent.step();
1528        }
1529
1530        let final_probs = decoherent.position_probabilities();
1531        let total: f64 = final_probs.iter().sum();
1532        assert!((total - 1.0).abs() < 1e-10);
1533
1534        // Should have spread from initial position
1535        assert!(final_probs[2] < 1.0);
1536    }
1537
1538    #[test]
1539    fn test_decoherence_rate_bounds() {
1540        let graph = Graph::new(GraphType::Cycle, 4);
1541        let mut decoherent = DecoherentQuantumWalk::new(graph, CoinOperator::Grover, 0.5);
1542
1543        // Test clamping
1544        decoherent.set_decoherence_rate(-0.1);
1545        decoherent.initialize_position(0);
1546        decoherent.step(); // Should work without panicking
1547
1548        decoherent.set_decoherence_rate(1.5);
1549        decoherent.step(); // Should work without panicking
1550    }
1551
1552    #[test]
1553    fn test_transition_matrix() {
1554        let graph = Graph::new(GraphType::Cycle, 3);
1555        let transition = graph.transition_matrix();
1556
1557        // Each vertex has degree 2, so each transition probability is 1/2
1558        for i in 0..3 {
1559            let mut row_sum = 0.0;
1560            for j in 0..3 {
1561                row_sum += transition[[i, j]];
1562            }
1563            assert!((row_sum - 1.0).abs() < 1e-10);
1564        }
1565    }
1566
1567    #[test]
1568    fn test_normalized_laplacian() {
1569        let graph = Graph::new(GraphType::Complete, 3);
1570        let norm_laplacian = graph.normalized_laplacian_matrix();
1571
1572        // Diagonal entries should be 1
1573        for i in 0..3 {
1574            assert!((norm_laplacian[[i, i]] - 1.0).abs() < 1e-10);
1575        }
1576
1577        // Off-diagonal entries for complete graph K_3
1578        let expected_off_diag = -1.0 / 2.0; // -1/sqrt(2*2)
1579        assert!((norm_laplacian[[0, 1]] - expected_off_diag).abs() < 1e-10);
1580        assert!((norm_laplacian[[1, 2]] - expected_off_diag).abs() < 1e-10);
1581        assert!((norm_laplacian[[0, 2]] - expected_off_diag).abs() < 1e-10);
1582    }
1583
1584    #[test]
1585    fn test_algebraic_connectivity() {
1586        let complete_3 = Graph::new(GraphType::Complete, 3);
1587        let connectivity = complete_3.algebraic_connectivity();
1588        assert!(connectivity > 0.0); // Complete graphs have positive algebraic connectivity
1589
1590        let line_5 = Graph::new(GraphType::Line, 5);
1591        let line_connectivity = line_5.algebraic_connectivity();
1592        assert!(line_connectivity > 0.0);
1593    }
1594
1595    #[test]
1596    fn test_mixing_time_estimation() {
1597        let graph = Graph::new(GraphType::Complete, 4);
1598        let mut szegedy = SzegedyQuantumWalk::new(graph);
1599
1600        let mixing_time = szegedy.estimate_mixing_time(0.1);
1601        assert!(mixing_time > 0);
1602        assert!(mixing_time <= 1000); // Should converge within max steps
1603    }
1604
1605    #[test]
1606    fn test_quantum_walk_search_on_custom_graph() {
1607        // Create a star graph: central vertex connected to all others
1608        let mut graph = Graph::new_empty(5);
1609        for i in 1..5 {
1610            graph.add_edge(0, i);
1611        }
1612
1613        let oracle = SearchOracle::new(vec![3]); // Mark vertex 3
1614        let mut search = QuantumWalkSearch::new(graph, oracle);
1615
1616        let (found_vertex, prob, steps) = search.run(20);
1617        assert_eq!(found_vertex, 3);
1618        assert!(prob > 0.0);
1619        assert!(steps <= 20);
1620    }
1621
1622    #[test]
1623    fn test_custom_coin_operator() {
1624        let graph = Graph::new(GraphType::Line, 3);
1625
1626        // Create a custom 2x2 coin (Pauli-X)
1627        let mut coin_matrix = Array2::zeros((2, 2));
1628        coin_matrix[[0, 1]] = Complex64::new(1.0, 0.0);
1629        coin_matrix[[1, 0]] = Complex64::new(1.0, 0.0);
1630
1631        let custom_coin = CoinOperator::Custom(coin_matrix);
1632        let mut walk = DiscreteQuantumWalk::new(graph, custom_coin);
1633
1634        walk.initialize_position(1);
1635        walk.step();
1636
1637        let probs = walk.position_probabilities();
1638        let total: f64 = probs.iter().sum();
1639        assert!((total - 1.0).abs() < 1e-10);
1640    }
1641
1642    #[test]
1643    fn test_empty_graph_edge_cases() {
1644        let empty_graph = Graph::new_empty(3);
1645        let mut szegedy = SzegedyQuantumWalk::new(empty_graph);
1646
1647        szegedy.initialize_uniform();
1648        let probs = szegedy.vertex_probabilities();
1649
1650        // No edges means no probability distribution
1651        for &prob in &probs {
1652            assert_eq!(prob, 0.0);
1653        }
1654    }
1655
1656    #[test]
1657    fn test_hypercube_graph() {
1658        let hypercube = Graph::new(GraphType::Hypercube, 3); // 2^3 = 8 vertices
1659        assert_eq!(hypercube.num_vertices, 8);
1660
1661        // Each vertex in a 3D hypercube has degree 3
1662        for i in 0..8 {
1663            assert_eq!(hypercube.degree(i), 3);
1664        }
1665    }
1666
1667    #[test]
1668    fn test_grid_2d_graph() {
1669        let grid = Graph::new(GraphType::Grid2D, 3); // 3x3 grid
1670        assert_eq!(grid.num_vertices, 9);
1671
1672        // Corner vertices have degree 2
1673        assert_eq!(grid.degree(0), 2); // Top-left
1674        assert_eq!(grid.degree(2), 2); // Top-right
1675        assert_eq!(grid.degree(6), 2); // Bottom-left
1676        assert_eq!(grid.degree(8), 2); // Bottom-right
1677
1678        // Center vertex has degree 4
1679        assert_eq!(grid.degree(4), 4);
1680    }
1681}