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::complex_ext::QuantumComplexExt;
12use ndarray::{Array1, Array2};
13use num_complex::Complex64;
14
15/// Types of graphs for quantum walks
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum GraphType {
18    /// Line graph (path graph)
19    Line,
20    /// Cycle graph
21    Cycle,
22    /// Complete graph
23    Complete,
24    /// Hypercube graph
25    Hypercube,
26    /// Grid graph (2D lattice)
27    Grid2D,
28    /// Custom graph
29    Custom,
30}
31
32/// Coin operators for discrete quantum walks
33#[derive(Debug, Clone)]
34pub enum CoinOperator {
35    /// Hadamard coin
36    Hadamard,
37    /// Grover coin
38    Grover,
39    /// DFT (Discrete Fourier Transform) coin
40    DFT,
41    /// Custom coin operator
42    Custom(Array2<Complex64>),
43}
44
45/// Search oracle for quantum walk search
46#[derive(Debug, Clone)]
47pub struct SearchOracle {
48    /// Marked vertices
49    pub marked: Vec<usize>,
50}
51
52impl SearchOracle {
53    /// Create a new search oracle with marked vertices
54    pub fn new(marked: Vec<usize>) -> Self {
55        Self { marked }
56    }
57
58    /// Check if a vertex is marked
59    pub fn is_marked(&self, vertex: usize) -> bool {
60        self.marked.contains(&vertex)
61    }
62}
63
64/// Graph representation for quantum walks
65#[derive(Debug, Clone)]
66pub struct Graph {
67    /// Number of vertices
68    pub num_vertices: usize,
69    /// Adjacency list representation
70    pub edges: Vec<Vec<usize>>,
71    /// Optional edge weights
72    pub weights: Option<Vec<Vec<f64>>>,
73}
74
75impl Graph {
76    /// Create a new graph of a specific type
77    pub fn new(graph_type: GraphType, size: usize) -> Self {
78        let mut graph = Self {
79            num_vertices: match graph_type {
80                GraphType::Hypercube => 1 << size, // 2^size vertices
81                GraphType::Grid2D => size * size,  // size x size grid
82                _ => size,
83            },
84            edges: vec![],
85            weights: None,
86        };
87
88        // Initialize edges based on graph type
89        graph.edges = vec![Vec::new(); graph.num_vertices];
90
91        match graph_type {
92            GraphType::Line => {
93                for i in 0..size.saturating_sub(1) {
94                    graph.add_edge(i, i + 1);
95                }
96            }
97            GraphType::Cycle => {
98                for i in 0..size {
99                    graph.add_edge(i, (i + 1) % size);
100                }
101            }
102            GraphType::Complete => {
103                for i in 0..size {
104                    for j in i + 1..size {
105                        graph.add_edge(i, j);
106                    }
107                }
108            }
109            GraphType::Hypercube => {
110                let n = size; // dimension
111                for i in 0..(1 << n) {
112                    for j in 0..n {
113                        let neighbor = i ^ (1 << j);
114                        if neighbor > i {
115                            graph.add_edge(i, neighbor);
116                        }
117                    }
118                }
119            }
120            GraphType::Grid2D => {
121                for i in 0..size {
122                    for j in 0..size {
123                        let idx = i * size + j;
124                        // Right neighbor
125                        if j < size - 1 {
126                            graph.add_edge(idx, idx + 1);
127                        }
128                        // Bottom neighbor
129                        if i < size - 1 {
130                            graph.add_edge(idx, idx + size);
131                        }
132                    }
133                }
134            }
135            GraphType::Custom => {
136                // Empty graph, user will add edges manually
137            }
138        }
139
140        graph
141    }
142
143    /// Create an empty graph with given number of vertices
144    pub fn new_empty(num_vertices: usize) -> Self {
145        Self {
146            num_vertices,
147            edges: vec![Vec::new(); num_vertices],
148            weights: None,
149        }
150    }
151
152    /// Add an undirected edge
153    pub fn add_edge(&mut self, u: usize, v: usize) {
154        if u < self.num_vertices && v < self.num_vertices && u != v && !self.edges[u].contains(&v) {
155            self.edges[u].push(v);
156            self.edges[v].push(u);
157        }
158    }
159
160    /// Add a weighted edge
161    pub fn add_weighted_edge(&mut self, u: usize, v: usize, weight: f64) {
162        if self.weights.is_none() {
163            self.weights = Some(vec![vec![0.0; self.num_vertices]; self.num_vertices]);
164        }
165
166        self.add_edge(u, v);
167
168        if let Some(ref mut weights) = self.weights {
169            weights[u][v] = weight;
170            weights[v][u] = weight;
171        }
172    }
173
174    /// Get the degree of a vertex
175    pub fn degree(&self, vertex: usize) -> usize {
176        if vertex < self.num_vertices {
177            self.edges[vertex].len()
178        } else {
179            0
180        }
181    }
182
183    /// Get the adjacency matrix
184    pub fn adjacency_matrix(&self) -> Array2<f64> {
185        let mut matrix = Array2::zeros((self.num_vertices, self.num_vertices));
186
187        for (u, neighbors) in self.edges.iter().enumerate() {
188            for &v in neighbors {
189                if let Some(ref weights) = self.weights {
190                    matrix[[u, v]] = weights[u][v];
191                } else {
192                    matrix[[u, v]] = 1.0;
193                }
194            }
195        }
196
197        matrix
198    }
199}
200
201/// Discrete-time quantum walk
202pub struct DiscreteQuantumWalk {
203    graph: Graph,
204    coin_operator: CoinOperator,
205    coin_dimension: usize,
206    /// Total Hilbert space dimension: coin_dimension * num_vertices
207    hilbert_dim: usize,
208    /// Current state vector
209    state: Vec<Complex64>,
210}
211
212impl DiscreteQuantumWalk {
213    /// Create a new discrete quantum walk with specified coin operator
214    pub fn new(graph: Graph, coin_operator: CoinOperator) -> Self {
215        // Coin dimension is the maximum degree for standard walks
216        // For hypercube, it's the dimension
217        let coin_dimension = match graph.num_vertices {
218            n if n > 0 => {
219                (0..graph.num_vertices)
220                    .map(|v| graph.degree(v))
221                    .max()
222                    .unwrap_or(2)
223                    .max(2) // At least 2-dimensional coin
224            }
225            _ => 2,
226        };
227
228        let hilbert_dim = coin_dimension * graph.num_vertices;
229
230        Self {
231            graph,
232            coin_operator,
233            coin_dimension,
234            hilbert_dim,
235            state: vec![Complex64::new(0.0, 0.0); hilbert_dim],
236        }
237    }
238
239    /// Initialize walker at a specific position
240    pub fn initialize_position(&mut self, position: usize) {
241        self.state = vec![Complex64::new(0.0, 0.0); self.hilbert_dim];
242
243        // Equal superposition over all coin states at the position
244        let degree = self.graph.degree(position) as f64;
245        if degree > 0.0 {
246            let amplitude = Complex64::new(1.0 / degree.sqrt(), 0.0);
247
248            for coin in 0..self.coin_dimension.min(self.graph.degree(position)) {
249                let index = self.state_index(position, coin);
250                if index < self.state.len() {
251                    self.state[index] = amplitude;
252                }
253            }
254        }
255    }
256
257    /// Perform one step of the quantum walk
258    pub fn step(&mut self) {
259        // Apply coin operator
260        self.apply_coin();
261
262        // Apply shift operator
263        self.apply_shift();
264    }
265
266    /// Get position probabilities
267    pub fn position_probabilities(&self) -> Vec<f64> {
268        let mut probs = vec![0.0; self.graph.num_vertices];
269
270        for (vertex, prob) in probs.iter_mut().enumerate() {
271            for coin in 0..self.coin_dimension {
272                let idx = self.state_index(vertex, coin);
273                if idx < self.state.len() {
274                    *prob += self.state[idx].norm_sqr();
275                }
276            }
277        }
278
279        probs
280    }
281
282    /// Get the index in the state vector for (vertex, coin) pair
283    fn state_index(&self, vertex: usize, coin: usize) -> usize {
284        vertex * self.coin_dimension + coin
285    }
286
287    /// Apply the coin operator
288    fn apply_coin(&mut self) {
289        match &self.coin_operator {
290            CoinOperator::Hadamard => self.apply_hadamard_coin(),
291            CoinOperator::Grover => self.apply_grover_coin(),
292            CoinOperator::DFT => self.apply_dft_coin(),
293            CoinOperator::Custom(matrix) => self.apply_custom_coin(matrix.clone()),
294        }
295    }
296
297    /// Apply Hadamard coin
298    fn apply_hadamard_coin(&mut self) {
299        let h = 1.0 / std::f64::consts::SQRT_2;
300
301        for vertex in 0..self.graph.num_vertices {
302            if self.coin_dimension == 2 {
303                let idx0 = self.state_index(vertex, 0);
304                let idx1 = self.state_index(vertex, 1);
305
306                if idx1 < self.state.len() {
307                    let a0 = self.state[idx0];
308                    let a1 = self.state[idx1];
309
310                    self.state[idx0] = h * (a0 + a1);
311                    self.state[idx1] = h * (a0 - a1);
312                }
313            }
314        }
315    }
316
317    /// Apply Grover coin
318    fn apply_grover_coin(&mut self) {
319        // Grover coin: 2|s><s| - I, where |s> is uniform superposition
320        for vertex in 0..self.graph.num_vertices {
321            let degree = self.graph.degree(vertex);
322            if degree <= 1 {
323                continue; // No coin needed for degree 0 or 1
324            }
325
326            // Calculate sum of amplitudes for this vertex
327            let mut sum = Complex64::new(0.0, 0.0);
328            for coin in 0..degree.min(self.coin_dimension) {
329                let idx = self.state_index(vertex, coin);
330                if idx < self.state.len() {
331                    sum += self.state[idx];
332                }
333            }
334
335            // Apply Grover coin
336            let factor = Complex64::new(2.0 / degree as f64, 0.0);
337            for coin in 0..degree.min(self.coin_dimension) {
338                let idx = self.state_index(vertex, coin);
339                if idx < self.state.len() {
340                    let old_amp = self.state[idx];
341                    self.state[idx] = factor * sum - old_amp;
342                }
343            }
344        }
345    }
346
347    /// Apply DFT coin
348    fn apply_dft_coin(&mut self) {
349        // DFT coin for 2-dimensional coin space
350        if self.coin_dimension == 2 {
351            self.apply_hadamard_coin(); // DFT is same as Hadamard for 2D
352        }
353        // For higher dimensions, would implement full DFT
354    }
355
356    /// Apply custom coin operator
357    fn apply_custom_coin(&mut self, matrix: Array2<Complex64>) {
358        if matrix.shape() != [self.coin_dimension, self.coin_dimension] {
359            return; // Matrix size mismatch
360        }
361
362        for vertex in 0..self.graph.num_vertices {
363            let mut coin_state = vec![Complex64::new(0.0, 0.0); self.coin_dimension];
364
365            // Extract coin state for this vertex
366            for (coin, cs) in coin_state.iter_mut().enumerate() {
367                let idx = self.state_index(vertex, coin);
368                if idx < self.state.len() {
369                    *cs = self.state[idx];
370                }
371            }
372
373            // Apply coin operator
374            let new_coin_state = matrix.dot(&Array1::from(coin_state));
375
376            // Write back
377            for coin in 0..self.coin_dimension {
378                let idx = self.state_index(vertex, coin);
379                if idx < self.state.len() {
380                    self.state[idx] = new_coin_state[coin];
381                }
382            }
383        }
384    }
385
386    /// Apply the shift operator
387    fn apply_shift(&mut self) {
388        let mut new_state = vec![Complex64::new(0.0, 0.0); self.hilbert_dim];
389
390        for vertex in 0..self.graph.num_vertices {
391            for (coin, &neighbor) in self.graph.edges[vertex].iter().enumerate() {
392                if coin < self.coin_dimension {
393                    let from_idx = self.state_index(vertex, coin);
394
395                    // Find which coin state corresponds to coming from 'vertex' at 'neighbor'
396                    let to_coin = self.graph.edges[neighbor]
397                        .iter()
398                        .position(|&v| v == vertex)
399                        .unwrap_or(0);
400
401                    if to_coin < self.coin_dimension && from_idx < self.state.len() {
402                        let to_idx = self.state_index(neighbor, to_coin);
403                        if to_idx < new_state.len() {
404                            new_state[to_idx] = self.state[from_idx];
405                        }
406                    }
407                }
408            }
409        }
410
411        self.state.copy_from_slice(&new_state);
412    }
413}
414
415/// Continuous-time quantum walk
416pub struct ContinuousQuantumWalk {
417    graph: Graph,
418    hamiltonian: Array2<Complex64>,
419    state: Vec<Complex64>,
420}
421
422impl ContinuousQuantumWalk {
423    /// Create a new continuous quantum walk
424    pub fn new(graph: Graph) -> Self {
425        let adj_matrix = graph.adjacency_matrix();
426        let hamiltonian = adj_matrix.mapv(|x| Complex64::new(x, 0.0));
427        let num_vertices = graph.num_vertices;
428
429        Self {
430            graph,
431            hamiltonian,
432            state: vec![Complex64::new(0.0, 0.0); num_vertices],
433        }
434    }
435
436    /// Initialize walker at a specific vertex
437    pub fn initialize_vertex(&mut self, vertex: usize) {
438        self.state = vec![Complex64::new(0.0, 0.0); self.graph.num_vertices];
439        if vertex < self.graph.num_vertices {
440            self.state[vertex] = Complex64::new(1.0, 0.0);
441        }
442    }
443
444    /// Evolve the quantum walk for time t
445    pub fn evolve(&mut self, time: f64) {
446        // This is a simplified version using first-order approximation
447        // For a full implementation, we would diagonalize the Hamiltonian
448
449        let dt = 0.01; // Time step
450        let steps = (time / dt) as usize;
451
452        for _ in 0..steps {
453            let mut new_state = self.state.clone();
454
455            // Apply exp(-iHt) ≈ I - iHdt for small dt
456            for (i, ns) in new_state
457                .iter_mut()
458                .enumerate()
459                .take(self.graph.num_vertices)
460            {
461                let mut sum = Complex64::new(0.0, 0.0);
462                for j in 0..self.graph.num_vertices {
463                    sum += self.hamiltonian[[i, j]] * self.state[j];
464                }
465                *ns = self.state[i] - Complex64::new(0.0, dt) * sum;
466            }
467
468            // Normalize
469            let norm: f64 = new_state.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
470
471            if norm > 0.0 {
472                for amp in new_state.iter_mut() {
473                    *amp /= norm;
474                }
475            }
476
477            self.state = new_state;
478        }
479    }
480
481    /// Get vertex probabilities
482    pub fn vertex_probabilities(&self) -> Vec<f64> {
483        self.state.iter().map(|c| c.probability()).collect()
484    }
485
486    /// Calculate transport probability between two vertices at time t
487    pub fn transport_probability(&mut self, from: usize, to: usize, time: f64) -> f64 {
488        // Initialize at 'from' vertex
489        self.initialize_vertex(from);
490
491        // Evolve for time t
492        self.evolve(time);
493
494        // Return probability at 'to' vertex
495        if to < self.state.len() {
496            self.state[to].probability()
497        } else {
498            0.0
499        }
500    }
501
502    /// Get the probability distribution
503    pub fn get_probabilities(&self, state: &[Complex64]) -> Vec<f64> {
504        state.iter().map(|c| c.probability()).collect()
505    }
506}
507
508/// Search algorithm using quantum walks
509pub struct QuantumWalkSearch {
510    #[allow(dead_code)]
511    graph: Graph,
512    oracle: SearchOracle,
513    walk: DiscreteQuantumWalk,
514}
515
516impl QuantumWalkSearch {
517    /// Create a new quantum walk search
518    pub fn new(graph: Graph, oracle: SearchOracle) -> Self {
519        let walk = DiscreteQuantumWalk::new(graph.clone(), CoinOperator::Grover);
520        Self {
521            graph,
522            oracle,
523            walk,
524        }
525    }
526
527    /// Apply the oracle that marks vertices
528    fn apply_oracle(&mut self) {
529        for &vertex in &self.oracle.marked {
530            for coin in 0..self.walk.coin_dimension {
531                let idx = self.walk.state_index(vertex, coin);
532                if idx < self.walk.state.len() {
533                    self.walk.state[idx] = -self.walk.state[idx]; // Phase flip
534                }
535            }
536        }
537    }
538
539    /// Run the search algorithm
540    pub fn run(&mut self, max_steps: usize) -> (usize, f64, usize) {
541        // Start in uniform superposition
542        let amplitude = Complex64::new(1.0 / (self.walk.hilbert_dim as f64).sqrt(), 0.0);
543        self.walk.state.fill(amplitude);
544
545        let mut best_vertex = 0;
546        let mut best_prob = 0.0;
547        let mut best_step = 0;
548
549        // Alternate between walk and oracle
550        for step in 1..=max_steps {
551            self.walk.step();
552            self.apply_oracle();
553
554            // Check probabilities at marked vertices
555            let probs = self.walk.position_probabilities();
556            for &marked in &self.oracle.marked {
557                if probs[marked] > best_prob {
558                    best_prob = probs[marked];
559                    best_vertex = marked;
560                    best_step = step;
561                }
562            }
563
564            // Early stopping if we have high probability
565            if best_prob > 0.5 {
566                break;
567            }
568        }
569
570        (best_vertex, best_prob, best_step)
571    }
572
573    /// Get vertex probabilities
574    pub fn vertex_probabilities(&self) -> Vec<f64> {
575        self.walk.position_probabilities()
576    }
577}
578
579/// Example: Quantum walk on a line
580pub fn quantum_walk_line_example() {
581    println!("Quantum Walk on a Line (10 vertices)");
582
583    let graph = Graph::new(GraphType::Line, 10);
584    let walk = DiscreteQuantumWalk::new(graph, CoinOperator::Hadamard);
585
586    // Start at vertex 5 (middle)
587    let mut walk = walk;
588    walk.initialize_position(5);
589
590    // Evolve for different time steps
591    for steps in [0, 5, 10, 20, 30] {
592        // Reset and evolve
593        walk.initialize_position(5);
594        for _ in 0..steps {
595            walk.step();
596        }
597        let probs = walk.position_probabilities();
598
599        println!("\nAfter {} steps:", steps);
600        print!("Probabilities: ");
601        for (v, p) in probs.iter().enumerate() {
602            if *p > 0.01 {
603                print!("v{}: {:.3} ", v, p);
604            }
605        }
606        println!();
607    }
608}
609
610/// Example: Search on a complete graph
611pub fn quantum_walk_search_example() {
612    println!("\nQuantum Walk Search on Complete Graph (8 vertices)");
613
614    let graph = Graph::new(GraphType::Complete, 8);
615    let marked = vec![3, 5]; // Mark vertices 3 and 5
616    let oracle = SearchOracle::new(marked.clone());
617
618    let mut search = QuantumWalkSearch::new(graph, oracle);
619
620    println!("Marked vertices: {:?}", marked);
621
622    // Run search
623    let (found, prob, steps) = search.run(50);
624
625    println!(
626        "\nFound vertex {} with probability {:.3} after {} steps",
627        found, prob, steps
628    );
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn test_graph_creation() {
637        let graph = Graph::new(GraphType::Cycle, 4);
638        assert_eq!(graph.num_vertices, 4);
639        assert_eq!(graph.degree(0), 2);
640
641        let complete = Graph::new(GraphType::Complete, 5);
642        assert_eq!(complete.degree(0), 4);
643    }
644
645    #[test]
646    fn test_discrete_walk_initialization() {
647        let graph = Graph::new(GraphType::Line, 5);
648        let mut walk = DiscreteQuantumWalk::new(graph, CoinOperator::Hadamard);
649
650        walk.initialize_position(2);
651        let probs = walk.position_probabilities();
652
653        assert!((probs[2] - 1.0).abs() < 1e-10);
654    }
655
656    #[test]
657    fn test_continuous_walk() {
658        let graph = Graph::new(GraphType::Cycle, 4);
659        let mut walk = ContinuousQuantumWalk::new(graph);
660
661        walk.initialize_vertex(0);
662        walk.evolve(1.0);
663
664        let probs = walk.vertex_probabilities();
665        let total: f64 = probs.iter().sum();
666        assert!((total - 1.0).abs() < 1e-10);
667    }
668}