quantrs2_anneal/
layout_embedding.rs

1//! Layout-aware graph embedding algorithms
2//!
3//! This module implements advanced graph embedding techniques that take into
4//! account the physical layout of quantum annealing hardware to optimize
5//! embedding quality, minimize chain lengths, and improve solution quality.
6
7use std::cmp::Ordering;
8use std::collections::{BinaryHeap, HashMap, HashSet};
9
10use crate::embedding::{Embedding, HardwareGraph, HardwareTopology};
11use crate::ising::{IsingError, IsingResult};
12
13/// Configuration for layout-aware embedding
14#[derive(Debug, Clone)]
15pub struct LayoutConfig {
16    /// Weight for physical distance in cost function
17    pub distance_weight: f64,
18    /// Weight for chain length in cost function
19    pub chain_length_weight: f64,
20    /// Weight for chain degree (number of connections) in cost function
21    pub chain_degree_weight: f64,
22    /// Maximum allowed chain length
23    pub max_chain_length: usize,
24    /// Use spectral placement for initial embedding
25    pub use_spectral_placement: bool,
26    /// Number of refinement iterations
27    pub refinement_iterations: usize,
28}
29
30impl Default for LayoutConfig {
31    fn default() -> Self {
32        Self {
33            distance_weight: 1.0,
34            chain_length_weight: 2.0,
35            chain_degree_weight: 0.5,
36            max_chain_length: 5,
37            use_spectral_placement: true,
38            refinement_iterations: 10,
39        }
40    }
41}
42
43/// Statistics from layout-aware embedding
44#[derive(Debug, Clone)]
45pub struct LayoutStats {
46    /// Average chain length
47    pub avg_chain_length: f64,
48    /// Maximum chain length
49    pub max_chain_length: usize,
50    /// Total chain length
51    pub total_chain_length: usize,
52    /// Number of chains exceeding threshold
53    pub long_chains: usize,
54    /// Embedding quality score
55    pub quality_score: f64,
56}
57
58/// Layout-aware embedder that considers hardware topology
59pub struct LayoutAwareEmbedder {
60    config: LayoutConfig,
61    /// Cache of shortest paths in hardware graph
62    shortest_paths: HashMap<(usize, usize), Vec<usize>>,
63    /// Physical coordinates of qubits (if available)
64    qubit_coordinates: Option<HashMap<usize, Vec<f64>>>,
65}
66
67impl LayoutAwareEmbedder {
68    /// Create a new layout-aware embedder
69    #[must_use]
70    pub fn new(config: LayoutConfig) -> Self {
71        Self {
72            config,
73            shortest_paths: HashMap::new(),
74            qubit_coordinates: None,
75        }
76    }
77
78    /// Set physical coordinates for qubits
79    pub fn set_coordinates(&mut self, coordinates: HashMap<usize, Vec<f64>>) {
80        self.qubit_coordinates = Some(coordinates);
81    }
82
83    /// Find a layout-aware embedding
84    pub fn find_embedding(
85        &mut self,
86        logical_edges: &[(usize, usize)],
87        num_vars: usize,
88        hardware: &HardwareGraph,
89    ) -> IsingResult<(Embedding, LayoutStats)> {
90        // Initialize coordinates from hardware if available
91        if self.qubit_coordinates.is_none() && hardware.coordinates.is_some() {
92            self.qubit_coordinates = hardware.coordinates.as_ref().map(|coords| {
93                coords
94                    .iter()
95                    .map(|(q, coord)| (*q, coord.iter().map(|&x| x as f64).collect()))
96                    .collect()
97            });
98        }
99
100        // Phase 1: Initial placement
101        let initial_placement = if self.config.use_spectral_placement {
102            self.spectral_placement(logical_edges, num_vars, hardware)?
103        } else {
104            self.greedy_placement(logical_edges, num_vars, hardware)?
105        };
106
107        // Phase 2: Chain building
108        let mut embedding = self.build_chains(&initial_placement, logical_edges, hardware)?;
109
110        // Phase 3: Iterative refinement
111        for _ in 0..self.config.refinement_iterations {
112            let improved = self.refine_embedding(&mut embedding, logical_edges, hardware)?;
113            if !improved {
114                break;
115            }
116        }
117
118        // Compute statistics
119        let stats = self.compute_stats(&embedding);
120
121        Ok((embedding, stats))
122    }
123
124    /// Spectral placement using eigenvectors of the logical graph Laplacian
125    fn spectral_placement(
126        &self,
127        logical_edges: &[(usize, usize)],
128        num_vars: usize,
129        hardware: &HardwareGraph,
130    ) -> IsingResult<HashMap<usize, usize>> {
131        // Simplified spectral placement
132        // In practice, would compute eigenvectors of graph Laplacian
133
134        let mut placement = HashMap::new();
135        let mut available_qubits: Vec<_> = (0..hardware.num_qubits).collect();
136
137        // Sort variables by degree
138        let mut var_degrees: Vec<_> = (0..num_vars)
139            .map(|v| {
140                let degree = logical_edges
141                    .iter()
142                    .filter(|&&(u, w)| u == v || w == v)
143                    .count();
144                (v, degree)
145            })
146            .collect();
147        var_degrees.sort_by_key(|&(_, d)| std::cmp::Reverse(d));
148
149        // Place high-degree variables first
150        for (var, _) in var_degrees {
151            if let Some(best_qubit) =
152                self.find_best_qubit(var, &placement, logical_edges, &available_qubits, hardware)
153            {
154                placement.insert(var, best_qubit);
155                available_qubits.retain(|&q| q != best_qubit);
156            }
157        }
158
159        Ok(placement)
160    }
161
162    /// Greedy placement algorithm
163    fn greedy_placement(
164        &self,
165        logical_edges: &[(usize, usize)],
166        num_vars: usize,
167        hardware: &HardwareGraph,
168    ) -> IsingResult<HashMap<usize, usize>> {
169        let mut placement = HashMap::new();
170        let mut available_qubits: HashSet<_> = (0..hardware.num_qubits).collect();
171
172        // Order variables by connectivity
173        let mut var_order = self.order_variables_by_connectivity(logical_edges, num_vars);
174
175        for var in var_order {
176            let neighbors = self.get_logical_neighbors(var, logical_edges);
177            let placed_neighbors: Vec<_> = neighbors
178                .iter()
179                .filter_map(|&n| placement.get(&n).copied())
180                .collect();
181
182            let best_qubit = if placed_neighbors.is_empty() {
183                // No placed neighbors, choose central qubit
184                self.find_central_qubit(&available_qubits, hardware)
185            } else {
186                // Find qubit closest to placed neighbors
187                self.find_closest_qubit(&placed_neighbors, &available_qubits, hardware)?
188            };
189
190            placement.insert(var, best_qubit);
191            available_qubits.remove(&best_qubit);
192        }
193
194        Ok(placement)
195    }
196
197    /// Build chains from initial placement
198    fn build_chains(
199        &self,
200        placement: &HashMap<usize, usize>,
201        logical_edges: &[(usize, usize)],
202        hardware: &HardwareGraph,
203    ) -> IsingResult<Embedding> {
204        let mut embedding = Embedding::new();
205        let mut used_qubits = HashSet::new();
206
207        // First, assign single-qubit chains for placed variables
208        for (&var, &qubit) in placement {
209            embedding.add_chain(var, vec![qubit])?;
210            used_qubits.insert(qubit);
211        }
212
213        // Extend chains to ensure connectivity
214        for &(u, v) in logical_edges {
215            if let (Some(chain_u), Some(chain_v)) =
216                (embedding.chains.get(&u), embedding.chains.get(&v))
217            {
218                if !self.chains_connected(chain_u, chain_v, hardware) {
219                    // Need to extend one or both chains
220                    let path =
221                        self.find_connection_path(chain_u, chain_v, hardware, &used_qubits)?;
222
223                    // Decide which chain to extend based on current lengths
224                    if chain_u.len() <= chain_v.len() {
225                        self.extend_chain(&mut embedding, u, &path, &mut used_qubits)?;
226                    } else {
227                        self.extend_chain(&mut embedding, v, &path, &mut used_qubits)?;
228                    }
229                }
230            }
231        }
232
233        Ok(embedding)
234    }
235
236    /// Refine embedding to improve quality
237    fn refine_embedding(
238        &self,
239        embedding: &mut Embedding,
240        logical_edges: &[(usize, usize)],
241        hardware: &HardwareGraph,
242    ) -> IsingResult<bool> {
243        let mut improved = false;
244
245        // Try to shorten long chains
246        for (var, chain) in embedding.chains.clone() {
247            if chain.len() > self.config.max_chain_length {
248                if let Some(shorter_chain) =
249                    self.find_shorter_chain(var, &chain, embedding, logical_edges, hardware)
250                {
251                    embedding.chains.insert(var, shorter_chain);
252                    improved = true;
253                }
254            }
255        }
256
257        // Try local swaps to improve layout
258        for var in 0..embedding.chains.len() {
259            if self.try_local_swap(embedding, var, logical_edges, hardware)? {
260                improved = true;
261            }
262        }
263
264        Ok(improved)
265    }
266
267    /// Find the best qubit for a variable during placement
268    fn find_best_qubit(
269        &self,
270        var: usize,
271        placement: &HashMap<usize, usize>,
272        logical_edges: &[(usize, usize)],
273        available: &[usize],
274        hardware: &HardwareGraph,
275    ) -> Option<usize> {
276        let neighbors = self.get_logical_neighbors(var, logical_edges);
277        let placed_neighbors: Vec<_> = neighbors
278            .iter()
279            .filter_map(|&n| placement.get(&n))
280            .collect();
281
282        if placed_neighbors.is_empty() {
283            // No constraints, pick any available qubit
284            available.first().copied()
285        } else {
286            // Find qubit minimizing distance to placed neighbors
287            available
288                .iter()
289                .min_by_key(|&&q| {
290                    let total_dist: usize = placed_neighbors
291                        .iter()
292                        .map(|&&nq| self.qubit_distance(q, nq, hardware))
293                        .sum();
294                    total_dist
295                })
296                .copied()
297        }
298    }
299
300    /// Find central qubit in available set
301    fn find_central_qubit(&self, available: &HashSet<usize>, hardware: &HardwareGraph) -> usize {
302        // Choose qubit with maximum connectivity
303        available
304            .iter()
305            .max_by_key(|&&q| hardware.neighbors(q).len())
306            .copied()
307            .unwrap_or(0)
308    }
309
310    /// Find closest qubit to a set of target qubits
311    fn find_closest_qubit(
312        &self,
313        targets: &[usize],
314        available: &HashSet<usize>,
315        hardware: &HardwareGraph,
316    ) -> IsingResult<usize> {
317        available
318            .iter()
319            .min_by_key(|&&q| {
320                targets
321                    .iter()
322                    .map(|&t| self.qubit_distance(q, t, hardware))
323                    .sum::<usize>()
324            })
325            .copied()
326            .ok_or_else(|| IsingError::HardwareConstraint("No available qubits".to_string()))
327    }
328
329    /// Compute distance between two qubits
330    fn qubit_distance(&self, q1: usize, q2: usize, hardware: &HardwareGraph) -> usize {
331        if let Some(coords) = &self.qubit_coordinates {
332            // Use Euclidean distance if coordinates available
333            if let (Some(c1), Some(c2)) = (coords.get(&q1), coords.get(&q2)) {
334                let dist_sq: f64 = c1
335                    .iter()
336                    .zip(c2.iter())
337                    .map(|(x1, x2)| (x1 - x2).powi(2))
338                    .sum();
339                return dist_sq.sqrt() as usize;
340            }
341        }
342
343        // Fall back to graph distance
344        self.graph_distance(q1, q2, hardware)
345    }
346
347    /// Compute graph distance between qubits
348    fn graph_distance(&self, q1: usize, q2: usize, hardware: &HardwareGraph) -> usize {
349        if q1 == q2 {
350            return 0;
351        }
352
353        // BFS to find shortest path
354        let mut visited = HashSet::new();
355        let mut queue = vec![(q1, 0)];
356        visited.insert(q1);
357
358        while let Some((current, dist)) = queue.pop() {
359            for neighbor in hardware.neighbors(current) {
360                if neighbor == q2 {
361                    return dist + 1;
362                }
363                if visited.insert(neighbor) {
364                    queue.push((neighbor, dist + 1));
365                }
366            }
367        }
368
369        usize::MAX // No path found
370    }
371
372    /// Check if two chains are connected
373    fn chains_connected(
374        &self,
375        chain1: &[usize],
376        chain2: &[usize],
377        hardware: &HardwareGraph,
378    ) -> bool {
379        for &q1 in chain1 {
380            for &q2 in chain2 {
381                if hardware.are_connected(q1, q2) {
382                    return true;
383                }
384            }
385        }
386        false
387    }
388
389    /// Find a path to connect two chains
390    fn find_connection_path(
391        &self,
392        chain1: &[usize],
393        chain2: &[usize],
394        hardware: &HardwareGraph,
395        used_qubits: &HashSet<usize>,
396    ) -> IsingResult<Vec<usize>> {
397        // A* search for shortest path
398        let mut best_path = None;
399        let mut best_cost = usize::MAX;
400
401        for &start in chain1 {
402            for &goal in chain2 {
403                if let Some(path) = self.astar_path(start, goal, hardware, used_qubits) {
404                    if path.len() < best_cost {
405                        best_cost = path.len();
406                        best_path = Some(path);
407                    }
408                }
409            }
410        }
411
412        best_path.ok_or_else(|| IsingError::HardwareConstraint("Cannot connect chains".to_string()))
413    }
414
415    /// A* pathfinding algorithm
416    fn astar_path(
417        &self,
418        start: usize,
419        goal: usize,
420        hardware: &HardwareGraph,
421        used_qubits: &HashSet<usize>,
422    ) -> Option<Vec<usize>> {
423        #[derive(Eq, PartialEq)]
424        struct State {
425            cost: usize,
426            position: usize,
427        }
428
429        impl Ord for State {
430            fn cmp(&self, other: &Self) -> Ordering {
431                other.cost.cmp(&self.cost)
432            }
433        }
434
435        impl PartialOrd for State {
436            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
437                Some(self.cmp(other))
438            }
439        }
440
441        let mut heap = BinaryHeap::new();
442        let mut dist = HashMap::new();
443        let mut parent = HashMap::new();
444
445        heap.push(State {
446            cost: 0,
447            position: start,
448        });
449        dist.insert(start, 0);
450
451        while let Some(State { cost, position }) = heap.pop() {
452            if position == goal {
453                // Reconstruct path
454                let mut path = Vec::new();
455                let mut current = goal;
456                while current != start {
457                    if !used_qubits.contains(&current) {
458                        path.push(current);
459                    }
460                    current = parent[&current];
461                }
462                path.reverse();
463                return Some(path);
464            }
465
466            if cost > dist[&position] {
467                continue;
468            }
469
470            for neighbor in hardware.neighbors(position) {
471                let next_cost = cost + 1;
472
473                if next_cost < *dist.get(&neighbor).unwrap_or(&usize::MAX) {
474                    dist.insert(neighbor, next_cost);
475                    parent.insert(neighbor, position);
476                    let heuristic = self.qubit_distance(neighbor, goal, hardware);
477                    heap.push(State {
478                        cost: next_cost + heuristic,
479                        position: neighbor,
480                    });
481                }
482            }
483        }
484
485        None
486    }
487
488    /// Extend a chain with additional qubits
489    fn extend_chain(
490        &self,
491        embedding: &mut Embedding,
492        var: usize,
493        extension: &[usize],
494        used_qubits: &mut HashSet<usize>,
495    ) -> IsingResult<()> {
496        if let Some(chain) = embedding.chains.get_mut(&var) {
497            for &qubit in extension {
498                if used_qubits.insert(qubit) {
499                    chain.push(qubit);
500                    embedding.qubit_to_variable.insert(qubit, var);
501                }
502            }
503        }
504        Ok(())
505    }
506
507    /// Find a shorter chain for a variable
508    fn find_shorter_chain(
509        &self,
510        var: usize,
511        current_chain: &[usize],
512        embedding: &Embedding,
513        logical_edges: &[(usize, usize)],
514        hardware: &HardwareGraph,
515    ) -> Option<Vec<usize>> {
516        // Find required connections
517        let neighbors = self.get_logical_neighbors(var, logical_edges);
518        let neighbor_chains: Vec<_> = neighbors
519            .iter()
520            .filter_map(|&n| embedding.chains.get(&n))
521            .collect();
522
523        // Try to find a shorter path that maintains all connections
524        // This is a simplified version - full implementation would be more sophisticated
525        None
526    }
527
528    /// Try a local swap to improve embedding
529    const fn try_local_swap(
530        &self,
531        embedding: &Embedding,
532        var: usize,
533        logical_edges: &[(usize, usize)],
534        hardware: &HardwareGraph,
535    ) -> IsingResult<bool> {
536        // Simplified local search - in practice would try various swaps
537        Ok(false)
538    }
539
540    /// Get logical neighbors of a variable
541    fn get_logical_neighbors(&self, var: usize, logical_edges: &[(usize, usize)]) -> Vec<usize> {
542        let mut neighbors = Vec::new();
543        for &(u, v) in logical_edges {
544            if u == var {
545                neighbors.push(v);
546            } else if v == var {
547                neighbors.push(u);
548            }
549        }
550        neighbors
551    }
552
553    /// Order variables by connectivity for placement
554    fn order_variables_by_connectivity(
555        &self,
556        logical_edges: &[(usize, usize)],
557        num_vars: usize,
558    ) -> Vec<usize> {
559        let mut degrees: Vec<_> = (0..num_vars)
560            .map(|v| {
561                let degree = logical_edges
562                    .iter()
563                    .filter(|&&(u, w)| u == v || w == v)
564                    .count();
565                (v, degree)
566            })
567            .collect();
568
569        degrees.sort_by_key(|&(_, d)| std::cmp::Reverse(d));
570        degrees.into_iter().map(|(v, _)| v).collect()
571    }
572
573    /// Compute embedding statistics
574    fn compute_stats(&self, embedding: &Embedding) -> LayoutStats {
575        let chain_lengths: Vec<_> = embedding.chains.values().map(std::vec::Vec::len).collect();
576
577        let total_length: usize = chain_lengths.iter().sum();
578        let max_length = chain_lengths.iter().copied().max().unwrap_or(0);
579        let avg_length = if chain_lengths.is_empty() {
580            0.0
581        } else {
582            total_length as f64 / chain_lengths.len() as f64
583        };
584
585        let long_chains = chain_lengths
586            .iter()
587            .filter(|&&len| len > self.config.max_chain_length)
588            .count();
589
590        // Simple quality score (lower is better)
591        let quality_score =
592            avg_length * self.config.chain_length_weight + long_chains as f64 * 10.0;
593
594        LayoutStats {
595            avg_chain_length: avg_length,
596            max_chain_length: max_length,
597            total_chain_length: total_length,
598            long_chains,
599            quality_score,
600        }
601    }
602}
603
604/// Multi-level embedding for hierarchical problems
605pub struct MultiLevelEmbedder {
606    /// Base embedder for each level
607    base_embedder: LayoutAwareEmbedder,
608    /// Number of coarsening levels
609    num_levels: usize,
610    /// Coarsening ratio at each level
611    coarsening_ratio: f64,
612}
613
614impl MultiLevelEmbedder {
615    /// Create a new multi-level embedder
616    #[must_use]
617    pub fn new(config: LayoutConfig, num_levels: usize) -> Self {
618        Self {
619            base_embedder: LayoutAwareEmbedder::new(config),
620            num_levels,
621            coarsening_ratio: 0.5,
622        }
623    }
624
625    /// Find embedding using multi-level approach
626    pub fn find_embedding(
627        &mut self,
628        logical_edges: &[(usize, usize)],
629        num_vars: usize,
630        hardware: &HardwareGraph,
631    ) -> IsingResult<(Embedding, LayoutStats)> {
632        // Coarsen the logical graph
633        let coarsened_graphs = self.coarsen_graph(logical_edges, num_vars);
634
635        // Get the coarsest graph (coarsen_graph always returns at least one element)
636        let coarsest = coarsened_graphs
637            .last()
638            .expect("coarsen_graph should return at least the original graph");
639
640        // Embed coarsest graph
641        let (mut embedding, _) =
642            self.base_embedder
643                .find_embedding(&coarsest.0, coarsest.1, hardware)?;
644
645        // Refine through levels
646        for level in (0..coarsened_graphs.len() - 1).rev() {
647            embedding = self.refine_level(
648                embedding,
649                &coarsened_graphs[level],
650                &coarsened_graphs[level + 1],
651                hardware,
652            )?;
653        }
654
655        let stats = self.base_embedder.compute_stats(&embedding);
656        Ok((embedding, stats))
657    }
658
659    /// Coarsen the logical graph into multiple levels
660    fn coarsen_graph(
661        &self,
662        logical_edges: &[(usize, usize)],
663        num_vars: usize,
664    ) -> Vec<(Vec<(usize, usize)>, usize)> {
665        // Simplified coarsening - in practice would use graph clustering
666        vec![(logical_edges.to_vec(), num_vars)]
667    }
668
669    /// Refine embedding from coarse to fine level
670    const fn refine_level(
671        &self,
672        coarse_embedding: Embedding,
673        fine_graph: &(Vec<(usize, usize)>, usize),
674        coarse_graph: &(Vec<(usize, usize)>, usize),
675        hardware: &HardwareGraph,
676    ) -> IsingResult<Embedding> {
677        // Simplified refinement - in practice would uncoarsen properly
678        Ok(coarse_embedding)
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    #[test]
687    fn test_layout_embedder_creation() {
688        let config = LayoutConfig::default();
689        let embedder = LayoutAwareEmbedder::new(config);
690        assert!(embedder.shortest_paths.is_empty());
691    }
692
693    #[test]
694    fn test_qubit_distance() {
695        let config = LayoutConfig::default();
696        let embedder = LayoutAwareEmbedder::new(config);
697
698        let hardware = HardwareGraph::new_chimera(2, 2, 2)
699            .expect("should create Chimera hardware graph for testing");
700
701        // Same qubit
702        assert_eq!(embedder.qubit_distance(0, 0, &hardware), 0);
703
704        // Adjacent qubits
705        let dist = embedder.qubit_distance(0, 2, &hardware);
706        assert!(dist > 0);
707    }
708}