quantrs2_anneal/
embedding.rs

1//! Graph embedding algorithms for quantum annealing hardware
2//!
3//! This module implements minorminer-like embedding algorithms that map
4//! logical problem graphs onto physical quantum annealing hardware topologies.
5//! The embedding process finds chains of physical qubits to represent each
6//! logical variable, ensuring connectivity constraints are satisfied.
7
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::hash::Hash;
10use thiserror::Error;
11
12use crate::ising::{IsingError, IsingResult};
13
14/// Errors that can occur during embedding
15#[derive(Error, Debug)]
16pub enum EmbeddingError {
17    /// Ising model error
18    #[error("Ising error: {0}")]
19    IsingError(#[from] IsingError),
20
21    /// Embedding not found
22    #[error("Embedding not found: {0}")]
23    EmbeddingNotFound(String),
24
25    /// Invalid embedding
26    #[error("Invalid embedding: {0}")]
27    InvalidEmbedding(String),
28
29    /// Topology error
30    #[error("Topology error: {0}")]
31    TopologyError(String),
32}
33
34/// Result of an embedding operation
35#[derive(Debug, Clone)]
36pub struct EmbeddingResult {
37    /// The actual embedding mapping logical variables to physical qubits
38    pub embedding: HashMap<usize, Vec<usize>>,
39    /// Recommended chain strength
40    pub chain_strength: f64,
41    /// Whether embedding was successful
42    pub success: bool,
43    /// Optional error message
44    pub error_message: Option<String>,
45}
46
47/// Hardware graph topology types
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum HardwareTopology {
50    /// Chimera topology with given dimensions (m, n, t)
51    /// where m×n is the grid size and t is the shore size
52    Chimera(usize, usize, usize),
53    /// Pegasus topology with given dimension
54    Pegasus(usize),
55    /// Zephyr topology with given dimension
56    Zephyr(usize),
57    /// Custom topology defined by adjacency
58    Custom,
59}
60
61/// Represents a hardware graph for quantum annealing
62#[derive(Debug, Clone)]
63pub struct HardwareGraph {
64    /// Number of physical qubits
65    pub num_qubits: usize,
66    /// Adjacency matrix (stored as adjacency list for efficiency)
67    pub adjacency: HashMap<usize, Vec<usize>>,
68    /// Hardware topology type
69    pub topology: HardwareTopology,
70    /// Coordinates for each qubit (if applicable)
71    pub coordinates: Option<HashMap<usize, Vec<usize>>>,
72}
73
74impl HardwareGraph {
75    /// Create a new hardware graph with custom adjacency
76    #[must_use]
77    pub fn new_custom(num_qubits: usize, edges: Vec<(usize, usize)>) -> Self {
78        let mut adjacency = HashMap::new();
79
80        for i in 0..num_qubits {
81            adjacency.insert(i, Vec::new());
82        }
83
84        for (u, v) in edges {
85            adjacency
86                .get_mut(&u)
87                .expect("key was just inserted")
88                .push(v);
89            adjacency
90                .get_mut(&v)
91                .expect("key was just inserted")
92                .push(u);
93        }
94
95        Self {
96            num_qubits,
97            adjacency,
98            topology: HardwareTopology::Custom,
99            coordinates: None,
100        }
101    }
102
103    /// Create a Chimera graph with given dimensions
104    pub fn new_chimera(m: usize, n: usize, t: usize) -> IsingResult<Self> {
105        let num_qubits = 2 * m * n * t;
106        let mut adjacency = HashMap::new();
107
108        // Initialize adjacency lists
109        for q in 0..num_qubits {
110            adjacency.insert(q, Vec::new());
111        }
112
113        // Create Chimera connectivity
114        for i in 0..m {
115            for j in 0..n {
116                let cell_offset = 2 * t * (i * n + j);
117
118                // Internal bipartite connections within unit cell
119                for k in 0..t {
120                    for l in 0..t {
121                        let q1 = cell_offset + k;
122                        let q2 = cell_offset + t + l;
123                        adjacency
124                            .get_mut(&q1)
125                            .expect("key was just inserted")
126                            .push(q2);
127                        adjacency
128                            .get_mut(&q2)
129                            .expect("key was just inserted")
130                            .push(q1);
131                    }
132                }
133
134                // Horizontal connections
135                if j < n - 1 {
136                    for k in 0..t {
137                        let q1 = cell_offset + k;
138                        let q2 = cell_offset + 2 * t + k;
139                        adjacency
140                            .get_mut(&q1)
141                            .expect("key was just inserted")
142                            .push(q2);
143                        adjacency
144                            .get_mut(&q2)
145                            .expect("key was just inserted")
146                            .push(q1);
147                    }
148                }
149
150                // Vertical connections
151                if i < m - 1 {
152                    for k in 0..t {
153                        let q1 = cell_offset + t + k;
154                        let q2 = cell_offset + 2 * t * n + t + k;
155                        adjacency
156                            .get_mut(&q1)
157                            .expect("key was just inserted")
158                            .push(q2);
159                        adjacency
160                            .get_mut(&q2)
161                            .expect("key was just inserted")
162                            .push(q1);
163                    }
164                }
165            }
166        }
167
168        // Generate coordinates
169        let mut coordinates = HashMap::new();
170        for i in 0..m {
171            for j in 0..n {
172                let cell_offset = 2 * t * (i * n + j);
173                for k in 0..t {
174                    coordinates.insert(cell_offset + k, vec![i, j, 0, k]);
175                    coordinates.insert(cell_offset + t + k, vec![i, j, 1, k]);
176                }
177            }
178        }
179
180        Ok(Self {
181            num_qubits,
182            adjacency,
183            topology: HardwareTopology::Chimera(m, n, t),
184            coordinates: Some(coordinates),
185        })
186    }
187
188    /// Get the neighbors of a qubit
189    #[must_use]
190    pub fn neighbors(&self, qubit: usize) -> Vec<usize> {
191        if qubit >= self.num_qubits {
192            return Vec::new();
193        }
194
195        self.adjacency.get(&qubit).cloned().unwrap_or_default()
196    }
197
198    /// Check if two qubits are connected
199    #[must_use]
200    pub fn are_connected(&self, q1: usize, q2: usize) -> bool {
201        if q1 >= self.num_qubits || q2 >= self.num_qubits {
202            return false;
203        }
204        self.adjacency
205            .get(&q1)
206            .is_some_and(|neighbors| neighbors.contains(&q2))
207    }
208}
209
210/// Represents an embedding of logical variables onto physical qubits
211#[derive(Debug, Clone)]
212pub struct Embedding {
213    /// Maps logical variable index to chain of physical qubits
214    pub chains: HashMap<usize, Vec<usize>>,
215    /// Reverse mapping: physical qubit to logical variable
216    pub qubit_to_variable: HashMap<usize, usize>,
217}
218
219impl Embedding {
220    /// Create a new empty embedding
221    #[must_use]
222    pub fn new() -> Self {
223        Self {
224            chains: HashMap::new(),
225            qubit_to_variable: HashMap::new(),
226        }
227    }
228
229    /// Add a chain for a logical variable
230    pub fn add_chain(&mut self, variable: usize, chain: Vec<usize>) -> IsingResult<()> {
231        // Check for overlapping chains
232        for &qubit in &chain {
233            if let Some(&existing_var) = self.qubit_to_variable.get(&qubit) {
234                if existing_var != variable {
235                    return Err(IsingError::InvalidQubit(qubit));
236                }
237            }
238        }
239
240        // Update mappings
241        for &qubit in &chain {
242            self.qubit_to_variable.insert(qubit, variable);
243        }
244        self.chains.insert(variable, chain);
245
246        Ok(())
247    }
248
249    /// Verify the embedding is valid for given logical and hardware graphs
250    pub fn verify(
251        &self,
252        logical_edges: &[(usize, usize)],
253        num_vars: usize,
254        hardware: &HardwareGraph,
255    ) -> IsingResult<()> {
256        // Check all variables are embedded
257        for var in 0..num_vars {
258            if !self.chains.contains_key(&var) {
259                return Err(IsingError::InvalidQubit(var));
260            }
261        }
262
263        // Check chains are connected
264        for (var, chain) in &self.chains {
265            if !is_chain_connected(chain, hardware) {
266                return Err(IsingError::HardwareConstraint(format!(
267                    "Chain for variable {var} is not connected"
268                )));
269            }
270        }
271
272        // Check logical edges are preserved
273        for &(u, v) in logical_edges {
274            let Some(chain1) = self.chains.get(&u) else {
275                return Err(IsingError::InvalidQubit(u));
276            };
277            let Some(chain2) = self.chains.get(&v) else {
278                return Err(IsingError::InvalidQubit(v));
279            };
280
281            let mut connected = false;
282            'outer: for &q1 in chain1 {
283                for &q2 in chain2 {
284                    if hardware.are_connected(q1, q2) {
285                        connected = true;
286                        break 'outer;
287                    }
288                }
289            }
290
291            if !connected {
292                return Err(IsingError::HardwareConstraint(format!(
293                    "Logical edge ({u}, {v}) has no physical connection"
294                )));
295            }
296        }
297
298        Ok(())
299    }
300}
301
302/// MinorMiner-like embedding algorithm
303pub struct MinorMiner {
304    /// Maximum number of embedding attempts
305    pub max_tries: usize,
306    /// Chain length penalty weight
307    pub chain_length_penalty: f64,
308    /// Use random initial chains
309    pub random_init: bool,
310    /// Seed for random number generation
311    pub seed: Option<u64>,
312}
313
314impl Default for MinorMiner {
315    fn default() -> Self {
316        Self {
317            max_tries: 10,
318            chain_length_penalty: 1.0,
319            random_init: true,
320            seed: None,
321        }
322    }
323}
324
325impl MinorMiner {
326    /// Find an embedding of logical graph into hardware graph
327    pub fn find_embedding(
328        &self,
329        logical_edges: &[(usize, usize)],
330        num_vars: usize,
331        hardware: &HardwareGraph,
332    ) -> IsingResult<Embedding> {
333        // Use a heuristic approach similar to minorminer
334        for attempt in 0..self.max_tries {
335            let mut embedding = Embedding::new();
336            let mut used_qubits = HashSet::new();
337
338            // Order variables by degree (higher degree first)
339            let mut var_degrees: Vec<(usize, usize)> = (0..num_vars)
340                .map(|v| {
341                    let degree = logical_edges
342                        .iter()
343                        .filter(|&&(u, w)| u == v || w == v)
344                        .count();
345                    (v, degree)
346                })
347                .collect();
348            var_degrees.sort_by_key(|&(_, d)| std::cmp::Reverse(d));
349
350            // Try to embed each variable
351            let mut success = true;
352            for (var, _) in var_degrees {
353                // Find neighbors that are already embedded
354                let embedded_neighbors = get_embedded_neighbors(var, logical_edges, &embedding);
355
356                // Find a chain for this variable
357                if let Some(chain) = find_chain_for_variable(
358                    var,
359                    &embedded_neighbors,
360                    &embedding,
361                    hardware,
362                    &used_qubits,
363                ) {
364                    for &q in &chain {
365                        used_qubits.insert(q);
366                    }
367                    embedding.add_chain(var, chain)?;
368                } else {
369                    success = false;
370                    break;
371                }
372            }
373
374            if success {
375                // Verify and return the embedding
376                embedding.verify(logical_edges, num_vars, hardware)?;
377                return Ok(embedding);
378            }
379        }
380
381        Err(IsingError::HardwareConstraint(
382            "Failed to find valid embedding".to_string(),
383        ))
384    }
385}
386
387/// Check if a chain of qubits is connected in the hardware graph
388fn is_chain_connected(chain: &[usize], hardware: &HardwareGraph) -> bool {
389    if chain.is_empty() {
390        return false;
391    }
392    if chain.len() == 1 {
393        return true;
394    }
395
396    // Use BFS to check connectivity
397    let mut visited = HashSet::new();
398    let mut queue = VecDeque::new();
399    queue.push_back(chain[0]);
400    visited.insert(chain[0]);
401
402    while let Some(qubit) = queue.pop_front() {
403        for neighbor in hardware.neighbors(qubit) {
404            if chain.contains(&neighbor) && visited.insert(neighbor) {
405                queue.push_back(neighbor);
406            }
407        }
408    }
409
410    visited.len() == chain.len()
411}
412
413/// Get embedded neighbors of a variable
414fn get_embedded_neighbors(
415    var: usize,
416    logical_edges: &[(usize, usize)],
417    embedding: &Embedding,
418) -> Vec<usize> {
419    let mut neighbors = Vec::new();
420
421    for &(u, v) in logical_edges {
422        if u == var && embedding.chains.contains_key(&v) {
423            neighbors.push(v);
424        } else if v == var && embedding.chains.contains_key(&u) {
425            neighbors.push(u);
426        }
427    }
428
429    neighbors
430}
431
432/// Find a chain for a variable given embedded neighbors
433fn find_chain_for_variable(
434    var: usize,
435    embedded_neighbors: &[usize],
436    embedding: &Embedding,
437    hardware: &HardwareGraph,
438    used_qubits: &HashSet<usize>,
439) -> Option<Vec<usize>> {
440    // If no embedded neighbors, pick any available qubit
441    if embedded_neighbors.is_empty() {
442        for q in 0..hardware.num_qubits {
443            if !used_qubits.contains(&q) {
444                return Some(vec![q]);
445            }
446        }
447        return None;
448    }
449
450    // Find qubits that can connect to all embedded neighbors
451    let mut candidate_qubits = HashSet::new();
452
453    // Collect all qubits adjacent to neighbor chains
454    for &neighbor_var in embedded_neighbors {
455        if let Some(neighbor_chain) = embedding.chains.get(&neighbor_var) {
456            for &q in neighbor_chain {
457                for &adj_q in &hardware.neighbors(q) {
458                    if !used_qubits.contains(&adj_q) {
459                        candidate_qubits.insert(adj_q);
460                    }
461                }
462            }
463        }
464    }
465
466    // If no candidates adjacent to neighbors, use any available qubit
467    if candidate_qubits.is_empty() {
468        for q in 0..hardware.num_qubits {
469            if !used_qubits.contains(&q) {
470                candidate_qubits.insert(q);
471            }
472        }
473    }
474
475    // Try to find a connected chain from candidates
476    let candidates: Vec<_> = candidate_qubits.into_iter().collect();
477    for &start_q in &candidates {
478        let chain = grow_chain(
479            start_q,
480            embedded_neighbors,
481            embedding,
482            hardware,
483            used_qubits,
484        );
485
486        if is_valid_chain(&chain, embedded_neighbors, embedding, hardware) {
487            return Some(chain);
488        }
489    }
490
491    // If simple approach fails, try more complex chain building
492    if !embedded_neighbors.is_empty() {
493        // Try to build a chain that connects to at least one neighbor
494        for &start_q in &candidates {
495            let mut chain = vec![start_q];
496            let mut chain_set = HashSet::new();
497            chain_set.insert(start_q);
498
499            // Check if we can connect to any neighbor with just this qubit
500            for &neighbor_var in embedded_neighbors {
501                if let Some(neighbor_chain) = embedding.chains.get(&neighbor_var) {
502                    for &nq in neighbor_chain {
503                        if hardware.are_connected(start_q, nq) {
504                            return Some(chain);
505                        }
506                    }
507                }
508            }
509        }
510    }
511
512    None
513}
514
515/// Grow a chain starting from a qubit
516fn grow_chain(
517    start: usize,
518    embedded_neighbors: &[usize],
519    embedding: &Embedding,
520    hardware: &HardwareGraph,
521    used_qubits: &HashSet<usize>,
522) -> Vec<usize> {
523    let mut chain = vec![start];
524    let mut chain_set = HashSet::new();
525    chain_set.insert(start);
526
527    // Try to connect to each embedded neighbor
528    for &neighbor_var in embedded_neighbors {
529        if let Some(neighbor_chain) = embedding.chains.get(&neighbor_var) {
530            // Check if already connected
531            let mut connected = false;
532            for &q in &chain {
533                for &nq in neighbor_chain {
534                    if hardware.are_connected(q, nq) {
535                        connected = true;
536                        break;
537                    }
538                }
539                if connected {
540                    break;
541                }
542            }
543
544            if !connected {
545                // Try to extend chain to connect
546                if let Some(path) =
547                    find_path_to_chain(&chain, neighbor_chain, hardware, used_qubits, &chain_set)
548                {
549                    for q in path {
550                        if chain_set.insert(q) {
551                            chain.push(q);
552                        }
553                    }
554                }
555            }
556        }
557    }
558
559    chain
560}
561
562/// Find a path from a chain to another chain
563fn find_path_to_chain(
564    from_chain: &[usize],
565    to_chain: &[usize],
566    hardware: &HardwareGraph,
567    used_qubits: &HashSet<usize>,
568    chain_set: &HashSet<usize>,
569) -> Option<Vec<usize>> {
570    // Use BFS to find shortest path
571    let mut queue = VecDeque::new();
572    let mut parent = HashMap::new();
573    let mut visited = HashSet::new();
574
575    // Start from all qubits in from_chain
576    for &q in from_chain {
577        queue.push_back(q);
578        visited.insert(q);
579    }
580
581    while let Some(current) = queue.pop_front() {
582        for neighbor in hardware.neighbors(current) {
583            if to_chain.contains(&neighbor) {
584                // Found a connection, reconstruct path
585                let mut path = Vec::new();
586                let mut node = Some(neighbor);
587
588                while let Some(n) = node {
589                    if !chain_set.contains(&n) {
590                        path.push(n);
591                    }
592                    node = parent.get(&n).copied();
593                    if from_chain.contains(&n) {
594                        break;
595                    }
596                }
597
598                path.reverse();
599                return Some(path);
600            }
601
602            if !used_qubits.contains(&neighbor)
603                && !chain_set.contains(&neighbor)
604                && visited.insert(neighbor)
605            {
606                parent.insert(neighbor, current);
607                queue.push_back(neighbor);
608            }
609        }
610    }
611
612    None
613}
614
615/// Check if a chain is valid for given embedded neighbors
616fn is_valid_chain(
617    chain: &[usize],
618    embedded_neighbors: &[usize],
619    embedding: &Embedding,
620    hardware: &HardwareGraph,
621) -> bool {
622    if chain.is_empty() {
623        return false;
624    }
625
626    // For a chain to be valid, it should connect to all embedded neighbors if possible
627    // But if that's not possible, at least one connection is acceptable
628    if embedded_neighbors.is_empty() {
629        return true;
630    }
631
632    let mut connections = 0;
633    for &neighbor_var in embedded_neighbors {
634        if let Some(neighbor_chain) = embedding.chains.get(&neighbor_var) {
635            let mut connected = false;
636
637            'outer: for &q1 in chain {
638                for &q2 in neighbor_chain {
639                    if hardware.are_connected(q1, q2) {
640                        connected = true;
641                        break 'outer;
642                    }
643                }
644            }
645
646            if connected {
647                connections += 1;
648            }
649        }
650    }
651
652    // Accept if we connect to at least one neighbor
653    connections > 0
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn test_chimera_graph() {
662        let graph = HardwareGraph::new_chimera(2, 2, 4).expect("should create Chimera graph");
663        assert_eq!(graph.num_qubits, 32);
664
665        // Test internal bipartite connections
666        assert!(graph.are_connected(0, 4));
667        assert!(graph.are_connected(0, 5));
668        assert!(!graph.are_connected(0, 1));
669
670        // Test that each qubit has the right number of neighbors
671        let q0_neighbors = graph.neighbors(0);
672        assert!(q0_neighbors.len() > 0);
673    }
674
675    #[test]
676    fn test_simple_embedding() {
677        // Create a simple 2-node edge logical graph (simpler test case)
678        let logical_edges = vec![(0, 1)];
679        let num_vars = 2;
680
681        // Create a small chimera graph
682        let hardware = HardwareGraph::new_chimera(1, 1, 2).expect("should create Chimera graph");
683
684        // Find embedding
685        let embedder = MinorMiner::default();
686        let result = embedder.find_embedding(&logical_edges, num_vars, &hardware);
687
688        // For this small example, embedding should be possible
689        assert!(
690            result.is_ok(),
691            "Failed to find embedding: {:?}",
692            result.err()
693        );
694
695        if let Ok(embedding) = result {
696            // Verify the embedding
697            let verify_result = embedding.verify(&logical_edges, num_vars, &hardware);
698            assert!(
699                verify_result.is_ok(),
700                "Verification failed: {:?}",
701                verify_result.err()
702            );
703            assert_eq!(embedding.chains.len(), 2);
704
705            // Check that the chains are connected by an edge
706            let chain0 = &embedding.chains[&0];
707            let chain1 = &embedding.chains[&1];
708            let mut found_connection = false;
709            for &q0 in chain0 {
710                for &q1 in chain1 {
711                    if hardware.are_connected(q0, q1) {
712                        found_connection = true;
713                        break;
714                    }
715                }
716            }
717            assert!(found_connection, "Chains are not connected");
718        }
719    }
720
721    #[test]
722    fn test_triangle_embedding() {
723        // Create a 3-node triangle logical graph
724        let logical_edges = vec![(0, 1), (0, 2), (1, 2)];
725        let num_vars = 3;
726
727        // Create a larger chimera graph for triangle
728        let hardware = HardwareGraph::new_chimera(2, 2, 2).expect("should create Chimera graph");
729
730        // Find embedding
731        let embedder = MinorMiner {
732            max_tries: 20, // More tries for harder problem
733            ..Default::default()
734        };
735        let result = embedder.find_embedding(&logical_edges, num_vars, &hardware);
736
737        // Triangle embedding in chimera is possible but may require multiple tries
738        if let Ok(embedding) = result {
739            // Verify the embedding
740            assert!(embedding
741                .verify(&logical_edges, num_vars, &hardware)
742                .is_ok());
743            assert_eq!(embedding.chains.len(), 3);
744        }
745    }
746
747    #[test]
748    fn test_chain_connectivity() {
749        let hardware = HardwareGraph::new_chimera(2, 2, 2).expect("should create Chimera graph");
750
751        // Test connected chain
752        let chain1 = vec![0, 2]; // Connected in chimera
753        assert!(is_chain_connected(&chain1, &hardware));
754
755        // Test disconnected chain
756        let chain2 = vec![0, 7]; // Not directly connected
757        assert!(!is_chain_connected(&chain2, &hardware));
758
759        // Test single qubit chain
760        let chain3 = vec![0];
761        assert!(is_chain_connected(&chain3, &hardware));
762    }
763}