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