quantrs2_tytan/
advanced_problem_decomposition.rs

1//! Advanced Problem Decomposition for QUBO Optimization
2//!
3//! This module provides sophisticated problem decomposition techniques for large-scale
4//! QUBO problems, enabling efficient parallel solving and scalability to problems
5//! beyond the capacity of single solvers.
6//!
7//! # Features
8//!
9//! - **Spectral Clustering**: Graph partitioning using eigenvalue decomposition
10//! - **Community Detection**: Identification of tightly connected variable groups
11//! - **Overlapping Decomposition**: Consensus-based solving with overlap handling
12//! - **Adaptive Granularity**: Dynamic adjustment of partition sizes
13//! - **Parallel Orchestration**: Coordinated solving across multiple solvers
14//!
15//! # Example
16//!
17//! ```rust
18//! use quantrs2_tytan::advanced_problem_decomposition::{
19//!     AdvancedDecomposer, DecompositionConfig, SpectralMethod
20//! };
21//! use scirs2_core::ndarray::Array2;
22//!
23//! // Create a QUBO matrix
24//! let qubo = Array2::from_shape_vec(
25//!     (4, 4),
26//!     vec![
27//!         -1.0, 2.0, 0.0, 0.0,
28//!         2.0, -2.0, 1.0, 0.0,
29//!         0.0, 1.0, -1.0, 3.0,
30//!         0.0, 0.0, 3.0, -2.0,
31//!     ]
32//! ).expect("valid 4x4 shape");
33//!
34//! // Configure decomposer
35//! let config = DecompositionConfig::default()
36//!     .with_num_partitions(2)
37//!     .with_method(SpectralMethod::NormalizedCut);
38//!
39//! // Decompose problem
40//! let decomposer = AdvancedDecomposer::new(config);
41//! let partitions = decomposer.spectral_partition(&qubo).expect("decomposition should succeed");
42//! ```
43
44use scirs2_core::ndarray::{Array1, Array2};
45use scirs2_core::random::prelude::*;
46use std::collections::{HashMap, HashSet, VecDeque};
47use std::fmt;
48
49/// Error types for problem decomposition
50#[derive(Debug, Clone)]
51pub enum DecompositionError {
52    /// Matrix is not square
53    InvalidMatrix(String),
54    /// Eigenvalue computation failed
55    EigenvalueFailed(String),
56    /// Invalid partition specification
57    InvalidPartition(String),
58    /// Decomposition produced no valid partitions
59    NoValidPartitions,
60    /// Consensus solving failed
61    ConsensusFailed(String),
62}
63
64impl fmt::Display for DecompositionError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::InvalidMatrix(msg) => write!(f, "Invalid matrix: {msg}"),
68            Self::EigenvalueFailed(msg) => write!(f, "Eigenvalue computation failed: {msg}"),
69            Self::InvalidPartition(msg) => write!(f, "Invalid partition: {msg}"),
70            Self::NoValidPartitions => write!(f, "No valid partitions produced"),
71            Self::ConsensusFailed(msg) => write!(f, "Consensus solving failed: {msg}"),
72        }
73    }
74}
75
76impl std::error::Error for DecompositionError {}
77
78/// Result type for decomposition operations
79pub type DecompositionResult<T> = Result<T, DecompositionError>;
80
81/// Spectral partitioning method
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum SpectralMethod {
84    /// Standard spectral clustering
85    Standard,
86    /// Normalized cut
87    NormalizedCut,
88    /// Ratio cut
89    RatioCut,
90}
91
92/// Community detection algorithm
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum CommunityDetection {
95    /// Louvain method for modularity optimization
96    Louvain,
97    /// Label propagation
98    LabelPropagation,
99    /// Girvan-Newman edge betweenness
100    GirvanNewman,
101}
102
103/// Configuration for advanced decomposer
104#[derive(Debug, Clone)]
105pub struct DecompositionConfig {
106    /// Number of partitions to create
107    pub num_partitions: usize,
108    /// Spectral method to use
109    pub spectral_method: SpectralMethod,
110    /// Community detection algorithm
111    pub community_algorithm: CommunityDetection,
112    /// Overlap size between partitions (number of variables)
113    pub overlap_size: usize,
114    /// Enable adaptive granularity control
115    pub adaptive_granularity: bool,
116    /// Target partition size for adaptive mode
117    pub target_partition_size: usize,
118    /// Maximum number of consensus iterations
119    pub max_consensus_iterations: usize,
120    /// Convergence tolerance for consensus
121    pub consensus_tolerance: f64,
122}
123
124impl Default for DecompositionConfig {
125    fn default() -> Self {
126        Self {
127            num_partitions: 4,
128            spectral_method: SpectralMethod::NormalizedCut,
129            community_algorithm: CommunityDetection::Louvain,
130            overlap_size: 2,
131            adaptive_granularity: true,
132            target_partition_size: 50,
133            max_consensus_iterations: 10,
134            consensus_tolerance: 1e-6,
135        }
136    }
137}
138
139impl DecompositionConfig {
140    /// Set the number of partitions
141    #[must_use]
142    pub const fn with_num_partitions(mut self, n: usize) -> Self {
143        self.num_partitions = n;
144        self
145    }
146
147    /// Set the spectral method
148    #[must_use]
149    pub const fn with_method(mut self, method: SpectralMethod) -> Self {
150        self.spectral_method = method;
151        self
152    }
153
154    /// Set the community detection algorithm
155    #[must_use]
156    pub const fn with_community_algorithm(mut self, alg: CommunityDetection) -> Self {
157        self.community_algorithm = alg;
158        self
159    }
160
161    /// Set the overlap size
162    #[must_use]
163    pub const fn with_overlap_size(mut self, size: usize) -> Self {
164        self.overlap_size = size;
165        self
166    }
167
168    /// Enable or disable adaptive granularity
169    #[must_use]
170    pub const fn with_adaptive_granularity(mut self, enable: bool) -> Self {
171        self.adaptive_granularity = enable;
172        self
173    }
174
175    /// Set the target partition size
176    #[must_use]
177    pub const fn with_target_partition_size(mut self, size: usize) -> Self {
178        self.target_partition_size = size;
179        self
180    }
181}
182
183/// Represents a partition of variables
184#[derive(Debug, Clone)]
185pub struct Partition {
186    /// Variable indices in this partition
187    pub variables: Vec<usize>,
188    /// Overlap variables (shared with other partitions)
189    pub overlap_variables: Vec<usize>,
190    /// Subproblem QUBO matrix
191    pub subproblem: Option<Array2<f64>>,
192}
193
194/// Result of consensus solving
195#[derive(Debug, Clone)]
196pub struct ConsensusResult {
197    /// Final variable assignments
198    pub solution: Vec<i32>,
199    /// Energy of the solution
200    pub energy: f64,
201    /// Number of iterations to convergence
202    pub iterations: usize,
203    /// Whether consensus was reached
204    pub converged: bool,
205}
206
207/// Advanced problem decomposer
208pub struct AdvancedDecomposer {
209    config: DecompositionConfig,
210}
211
212impl AdvancedDecomposer {
213    /// Create a new advanced decomposer
214    pub const fn new(config: DecompositionConfig) -> Self {
215        Self { config }
216    }
217
218    /// Partition a QUBO problem using spectral clustering
219    ///
220    /// Constructs a graph Laplacian from the QUBO matrix and uses its
221    /// eigenvectors to partition variables into groups
222    pub fn spectral_partition(&self, qubo: &Array2<f64>) -> DecompositionResult<Vec<Partition>> {
223        let n = qubo.nrows();
224        if n != qubo.ncols() {
225            return Err(DecompositionError::InvalidMatrix(
226                "Matrix must be square".to_string(),
227            ));
228        }
229
230        // Construct graph Laplacian
231        let laplacian = self.compute_laplacian(qubo)?;
232
233        // Compute eigenvectors (simplified - in practice use proper eigendecomposition)
234        let eigenvector = self.compute_fiedler_vector(&laplacian)?;
235
236        // Partition based on eigenvector signs/values
237        let assignments = self.partition_by_eigenvector(&eigenvector, self.config.num_partitions);
238
239        // Create partitions with overlap
240        let partitions = self.create_overlapping_partitions(&assignments, n)?;
241
242        // Extract subproblems
243        let partitions_with_subproblems: Vec<Partition> = partitions
244            .into_iter()
245            .map(|mut p| {
246                p.subproblem = Some(self.extract_subproblem(qubo, &p.variables));
247                p
248            })
249            .collect();
250
251        Ok(partitions_with_subproblems)
252    }
253
254    /// Compute graph Laplacian matrix
255    fn compute_laplacian(&self, qubo: &Array2<f64>) -> DecompositionResult<Array2<f64>> {
256        let n = qubo.nrows();
257
258        // Compute degree matrix and adjacency
259        let mut degree = Array1::zeros(n);
260        let mut adjacency = Array2::zeros((n, n));
261
262        for i in 0..n {
263            for j in 0..n {
264                if i != j {
265                    let weight = qubo[[i, j]].abs();
266                    adjacency[[i, j]] = weight;
267                    degree[i] += weight;
268                }
269            }
270        }
271
272        // Compute Laplacian based on method
273        let mut laplacian = Array2::zeros((n, n));
274        match self.config.spectral_method {
275            SpectralMethod::Standard => {
276                // L = D - A
277                for i in 0..n {
278                    laplacian[[i, i]] = degree[i];
279                    for j in 0..n {
280                        if i != j {
281                            laplacian[[i, j]] = -adjacency[[i, j]];
282                        }
283                    }
284                }
285            }
286            SpectralMethod::NormalizedCut => {
287                // L = D^(-1/2) (D - A) D^(-1/2)
288                for i in 0..n {
289                    let d_inv_sqrt = if degree[i] > 1e-10 {
290                        1.0 / degree[i].sqrt()
291                    } else {
292                        0.0
293                    };
294
295                    for j in 0..n {
296                        if i == j {
297                            laplacian[[i, j]] = if degree[i] > 1e-10 { 1.0 } else { 0.0 };
298                        } else {
299                            let d_j_inv_sqrt = if degree[j] > 1e-10 {
300                                1.0 / degree[j].sqrt()
301                            } else {
302                                0.0
303                            };
304                            laplacian[[i, j]] = -adjacency[[i, j]] * d_inv_sqrt * d_j_inv_sqrt;
305                        }
306                    }
307                }
308            }
309            SpectralMethod::RatioCut => {
310                // Same as standard Laplacian for our purposes
311                for i in 0..n {
312                    laplacian[[i, i]] = degree[i];
313                    for j in 0..n {
314                        if i != j {
315                            laplacian[[i, j]] = -adjacency[[i, j]];
316                        }
317                    }
318                }
319            }
320        }
321
322        Ok(laplacian)
323    }
324
325    /// Compute Fiedler vector (second smallest eigenvector of Laplacian)
326    ///
327    /// This is a simplified implementation using power iteration
328    fn compute_fiedler_vector(&self, laplacian: &Array2<f64>) -> DecompositionResult<Array1<f64>> {
329        let n = laplacian.nrows();
330        let mut rng = thread_rng();
331
332        // Random initialization
333        let mut v = Array1::from_shape_fn(n, |_| rng.gen::<f64>().mul_add(2.0, -1.0));
334
335        // Orthogonalize against constant vector (first eigenvector)
336        let ones = Array1::ones(n);
337        let projection = v.dot(&ones) / (n as f64);
338        v = &v - &(&ones * projection);
339
340        // Normalize
341        let norm = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
342        if norm > 1e-10 {
343            v = &v / norm;
344        }
345
346        // Power iteration (simplified - finds dominant eigenvector of (I - L))
347        // In practice, use proper eigensolvers for Fiedler vector
348        for _ in 0..100 {
349            let mut new_v = Array1::zeros(n);
350
351            // Multiply by (I - αL) to shift spectrum
352            let alpha = 0.5;
353            for i in 0..n {
354                new_v[i] = v[i];
355                for j in 0..n {
356                    new_v[i] -= alpha * laplacian[[i, j]] * v[j];
357                }
358            }
359
360            // Orthogonalize against constant vector
361            let projection = new_v.dot(&ones) / (n as f64);
362            new_v = &new_v - &(&ones * projection);
363
364            // Normalize
365            let norm = new_v.iter().map(|&x| x * x).sum::<f64>().sqrt();
366            if norm > 1e-10 {
367                new_v = &new_v / norm;
368            }
369
370            // Check convergence
371            let diff = (&new_v - &v).iter().map(|&x| x.abs()).sum::<f64>();
372            v = new_v;
373
374            if diff < 1e-6 {
375                break;
376            }
377        }
378
379        Ok(v)
380    }
381
382    /// Partition variables based on eigenvector values
383    fn partition_by_eigenvector(&self, eigenvector: &Array1<f64>, k: usize) -> Vec<usize> {
384        let n = eigenvector.len();
385        let mut assignments = vec![0; n];
386
387        if k == 2 {
388            // Simple bisection
389            for i in 0..n {
390                assignments[i] = usize::from(eigenvector[i] > 0.0);
391            }
392        } else {
393            // K-way partitioning using k-means on eigenvector values
394            let sorted_indices: Vec<usize> = {
395                let mut indices: Vec<usize> = (0..n).collect();
396                indices.sort_by(|&a, &b| {
397                    eigenvector[a]
398                        .partial_cmp(&eigenvector[b])
399                        .unwrap_or(std::cmp::Ordering::Equal)
400                });
401                indices
402            };
403
404            let partition_size = n.div_ceil(k);
405            for (i, &idx) in sorted_indices.iter().enumerate() {
406                assignments[idx] = (i / partition_size).min(k - 1);
407            }
408        }
409
410        assignments
411    }
412
413    /// Create overlapping partitions from assignments
414    fn create_overlapping_partitions(
415        &self,
416        assignments: &[usize],
417        n: usize,
418    ) -> DecompositionResult<Vec<Partition>> {
419        let k = self.config.num_partitions;
420        let mut partitions = Vec::new();
421
422        for partition_id in 0..k {
423            let mut variables: Vec<usize> = assignments
424                .iter()
425                .enumerate()
426                .filter(|(_, &p)| p == partition_id)
427                .map(|(i, _)| i)
428                .collect();
429
430            if variables.is_empty() {
431                continue;
432            }
433
434            // Add overlap variables from neighboring partitions
435            let mut overlap_variables = Vec::new();
436            if self.config.overlap_size > 0 {
437                // Find boundary variables
438                for &var in &variables {
439                    // Check if this variable should be in overlap
440                    // (simplified - in practice, use connectivity analysis)
441                    if var > 0 && assignments[var - 1] != partition_id {
442                        overlap_variables.push(var);
443                    }
444                    if var < n - 1 && assignments[var + 1] != partition_id {
445                        overlap_variables.push(var);
446                    }
447                }
448                overlap_variables.sort_unstable();
449                overlap_variables.dedup();
450            }
451
452            partitions.push(Partition {
453                variables,
454                overlap_variables,
455                subproblem: None,
456            });
457        }
458
459        if partitions.is_empty() {
460            return Err(DecompositionError::NoValidPartitions);
461        }
462
463        Ok(partitions)
464    }
465
466    /// Extract subproblem QUBO for a partition
467    fn extract_subproblem(&self, qubo: &Array2<f64>, variables: &[usize]) -> Array2<f64> {
468        let m = variables.len();
469        let mut subqubo = Array2::zeros((m, m));
470
471        for (i, &var_i) in variables.iter().enumerate() {
472            for (j, &var_j) in variables.iter().enumerate() {
473                subqubo[[i, j]] = qubo[[var_i, var_j]];
474            }
475        }
476
477        subqubo
478    }
479
480    /// Detect communities using Louvain method
481    pub fn detect_communities(&self, qubo: &Array2<f64>) -> DecompositionResult<Vec<usize>> {
482        let n = qubo.nrows();
483
484        match self.config.community_algorithm {
485            CommunityDetection::Louvain => self.louvain_communities(qubo),
486            CommunityDetection::LabelPropagation => self.label_propagation(qubo),
487            CommunityDetection::GirvanNewman => self.girvan_newman(qubo),
488        }
489    }
490
491    /// Louvain community detection
492    fn louvain_communities(&self, qubo: &Array2<f64>) -> DecompositionResult<Vec<usize>> {
493        let n = qubo.nrows();
494        let mut communities = (0..n).collect::<Vec<usize>>();
495
496        // Compute total edge weight
497        let total_weight: f64 = qubo.iter().map(|&x| x.abs()).sum::<f64>() / 2.0;
498
499        // Iterative optimization
500        let mut improved = true;
501        let max_iterations = 10;
502        let mut iteration = 0;
503
504        while improved && iteration < max_iterations {
505            improved = false;
506            iteration += 1;
507
508            for node in 0..n {
509                let current_community = communities[node];
510                let mut best_community = current_community;
511                let mut best_gain = 0.0;
512
513                // Try moving node to each neighboring community
514                let neighbors = self.get_neighbors(qubo, node);
515                let neighbor_communities: HashSet<usize> =
516                    neighbors.iter().map(|&i| communities[i]).collect();
517
518                for &community in &neighbor_communities {
519                    let gain =
520                        self.modularity_gain(qubo, &communities, node, community, total_weight);
521                    if gain > best_gain {
522                        best_gain = gain;
523                        best_community = community;
524                    }
525                }
526
527                if best_community != current_community && best_gain > 1e-10 {
528                    communities[node] = best_community;
529                    improved = true;
530                }
531            }
532        }
533
534        Ok(communities)
535    }
536
537    /// Get neighbors of a node
538    fn get_neighbors(&self, qubo: &Array2<f64>, node: usize) -> Vec<usize> {
539        let n = qubo.nrows();
540        let mut neighbors = Vec::new();
541
542        for i in 0..n {
543            if i != node && qubo[[node, i]].abs() > 1e-10 {
544                neighbors.push(i);
545            }
546        }
547
548        neighbors
549    }
550
551    /// Compute modularity gain from moving a node to a community
552    fn modularity_gain(
553        &self,
554        qubo: &Array2<f64>,
555        communities: &[usize],
556        node: usize,
557        new_community: usize,
558        total_weight: f64,
559    ) -> f64 {
560        let n = qubo.nrows();
561
562        // Compute edge weight to new community
563        let mut edge_weight_to_new = 0.0;
564        for i in 0..n {
565            if communities[i] == new_community {
566                edge_weight_to_new += qubo[[node, i]].abs();
567            }
568        }
569
570        // Simplified modularity gain (actual Louvain is more complex)
571        edge_weight_to_new / total_weight
572    }
573
574    /// Label propagation community detection
575    fn label_propagation(&self, qubo: &Array2<f64>) -> DecompositionResult<Vec<usize>> {
576        let n = qubo.nrows();
577        let mut labels: Vec<usize> = (0..n).collect();
578        let mut rng = thread_rng();
579
580        let max_iterations = 100;
581        for _ in 0..max_iterations {
582            let mut changed = false;
583
584            // Random order
585            let mut order: Vec<usize> = (0..n).collect();
586            order.shuffle(&mut rng);
587
588            for &node in &order {
589                // Count neighbor labels
590                let mut label_counts: HashMap<usize, f64> = HashMap::new();
591
592                for i in 0..n {
593                    if i != node {
594                        let weight = qubo[[node, i]].abs();
595                        if weight > 1e-10 {
596                            *label_counts.entry(labels[i]).or_insert(0.0) += weight;
597                        }
598                    }
599                }
600
601                if !label_counts.is_empty() {
602                    // Choose most common label
603                    // Since label_counts is non-empty, max_by is guaranteed to return Some
604                    let new_label = *label_counts
605                        .iter()
606                        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
607                        .map(|(k, _)| k)
608                        .expect("label_counts verified non-empty");
609
610                    if new_label != labels[node] {
611                        labels[node] = new_label;
612                        changed = true;
613                    }
614                }
615            }
616
617            if !changed {
618                break;
619            }
620        }
621
622        // Renumber communities to be contiguous
623        let unique_labels: HashSet<usize> = labels.iter().copied().collect();
624        let mut label_map: HashMap<usize, usize> = HashMap::new();
625        for (i, &label) in unique_labels.iter().enumerate() {
626            label_map.insert(label, i);
627        }
628
629        Ok(labels.iter().map(|&l| label_map[&l]).collect())
630    }
631
632    /// Girvan-Newman edge betweenness community detection
633    fn girvan_newman(&self, qubo: &Array2<f64>) -> DecompositionResult<Vec<usize>> {
634        // Simplified implementation - just use spectral partitioning
635        let partitions = self.spectral_partition(qubo)?;
636        let n = qubo.nrows();
637        let mut communities = vec![0; n];
638
639        for (comm_id, partition) in partitions.iter().enumerate() {
640            for &var in &partition.variables {
641                communities[var] = comm_id;
642            }
643        }
644
645        Ok(communities)
646    }
647
648    /// Solve using consensus over overlapping partitions
649    pub fn consensus_solve(
650        &self,
651        partitions: &[Partition],
652        subsolvers: Vec<Vec<i32>>,
653    ) -> DecompositionResult<ConsensusResult> {
654        if partitions.len() != subsolvers.len() {
655            return Err(DecompositionError::ConsensusFailed(
656                "Number of partitions and subsolutions mismatch".to_string(),
657            ));
658        }
659
660        // Determine total number of variables
661        let num_vars = partitions
662            .iter()
663            .flat_map(|p| p.variables.iter())
664            .max()
665            .map_or(0, |&m| m + 1);
666
667        let mut solution = vec![0; num_vars];
668        let mut vote_counts: Vec<HashMap<i32, usize>> = vec![HashMap::new(); num_vars];
669
670        // Aggregate votes from all subsolvers
671        for (partition, subsolution) in partitions.iter().zip(subsolvers.iter()) {
672            for (local_idx, &var) in partition.variables.iter().enumerate() {
673                if local_idx < subsolution.len() {
674                    let value = subsolution[local_idx];
675                    *vote_counts[var].entry(value).or_insert(0) += 1;
676                }
677            }
678        }
679
680        // Consensus by majority vote
681        for (var, counts) in vote_counts.iter().enumerate() {
682            if let Some((&value, _)) = counts.iter().max_by_key(|(_, &count)| count) {
683                solution[var] = value;
684            }
685        }
686
687        Ok(ConsensusResult {
688            solution,
689            energy: 0.0, // Would compute actual energy
690            iterations: 1,
691            converged: true,
692        })
693    }
694
695    /// Get configuration
696    pub const fn config(&self) -> &DecompositionConfig {
697        &self.config
698    }
699}
700
701#[cfg(test)]
702mod tests {
703    use super::*;
704
705    #[test]
706    fn test_decomposer_creation() {
707        let config = DecompositionConfig::default();
708        let decomposer = AdvancedDecomposer::new(config);
709        assert_eq!(decomposer.config().num_partitions, 4);
710    }
711
712    #[test]
713    fn test_spectral_partition() {
714        let qubo = Array2::from_shape_vec(
715            (4, 4),
716            vec![
717                -1.0, 2.0, 0.0, 0.0, 2.0, -2.0, 1.0, 0.0, 0.0, 1.0, -1.0, 3.0, 0.0, 0.0, 3.0, -2.0,
718            ],
719        )
720        .expect("valid 4x4 QUBO matrix shape");
721
722        let config = DecompositionConfig::default().with_num_partitions(2);
723        let decomposer = AdvancedDecomposer::new(config);
724
725        let partitions = decomposer
726            .spectral_partition(&qubo)
727            .expect("spectral partition should succeed");
728        assert!(!partitions.is_empty());
729        assert!(partitions.len() <= 2);
730    }
731
732    #[test]
733    fn test_laplacian_computation() {
734        let qubo =
735            Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 0.0, 2.0, 3.0, 1.0, 0.0, 1.0, 2.0])
736                .expect("valid 3x3 QUBO matrix shape");
737
738        let config = DecompositionConfig::default();
739        let decomposer = AdvancedDecomposer::new(config);
740
741        let laplacian = decomposer
742            .compute_laplacian(&qubo)
743            .expect("laplacian computation should succeed");
744        assert_eq!(laplacian.nrows(), 3);
745        assert_eq!(laplacian.ncols(), 3);
746    }
747
748    #[test]
749    fn test_community_detection() {
750        let qubo = Array2::from_shape_vec(
751            (4, 4),
752            vec![
753                1.0, 2.0, 0.1, 0.1, 2.0, 1.0, 0.1, 0.1, 0.1, 0.1, 1.0, 2.0, 0.1, 0.1, 2.0, 1.0,
754            ],
755        )
756        .expect("valid 4x4 QUBO matrix shape");
757
758        let config = DecompositionConfig::default();
759        let decomposer = AdvancedDecomposer::new(config);
760
761        let communities = decomposer
762            .detect_communities(&qubo)
763            .expect("community detection should succeed");
764        assert_eq!(communities.len(), 4);
765    }
766
767    #[test]
768    fn test_label_propagation() {
769        let qubo = Array2::from_shape_vec(
770            (4, 4),
771            vec![
772                0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
773            ],
774        )
775        .expect("valid 4x4 QUBO matrix shape");
776
777        let config = DecompositionConfig::default()
778            .with_community_algorithm(CommunityDetection::LabelPropagation);
779        let decomposer = AdvancedDecomposer::new(config);
780
781        let _labels = decomposer
782            .label_propagation(&qubo)
783            .expect("label propagation should succeed");
784    }
785
786    #[test]
787    fn test_consensus_solve() {
788        let config = DecompositionConfig::default();
789        let decomposer = AdvancedDecomposer::new(config);
790
791        let partitions = vec![
792            Partition {
793                variables: vec![0, 1],
794                overlap_variables: vec![1],
795                subproblem: None,
796            },
797            Partition {
798                variables: vec![1, 2],
799                overlap_variables: vec![1],
800                subproblem: None,
801            },
802        ];
803
804        let subsolvers = vec![vec![1, 0], vec![0, 1]];
805
806        let consensus = decomposer
807            .consensus_solve(&partitions, subsolvers)
808            .expect("consensus solve should succeed");
809        assert_eq!(consensus.solution.len(), 3);
810        assert!(consensus.converged);
811    }
812
813    #[test]
814    fn test_config_builder() {
815        let config = DecompositionConfig::default()
816            .with_num_partitions(8)
817            .with_method(SpectralMethod::RatioCut)
818            .with_overlap_size(3)
819            .with_adaptive_granularity(false);
820
821        assert_eq!(config.num_partitions, 8);
822        assert_eq!(config.spectral_method, SpectralMethod::RatioCut);
823        assert_eq!(config.overlap_size, 3);
824        assert!(!config.adaptive_granularity);
825    }
826
827    #[test]
828    fn test_subproblem_extraction() {
829        let qubo = Array2::from_shape_vec(
830            (4, 4),
831            vec![
832                1.0, 2.0, 3.0, 4.0, 2.0, 5.0, 6.0, 7.0, 3.0, 6.0, 8.0, 9.0, 4.0, 7.0, 9.0, 10.0,
833            ],
834        )
835        .expect("valid 4x4 QUBO matrix shape");
836
837        let config = DecompositionConfig::default();
838        let decomposer = AdvancedDecomposer::new(config);
839
840        let variables = vec![0, 2];
841        let subqubo = decomposer.extract_subproblem(&qubo, &variables);
842
843        assert_eq!(subqubo.nrows(), 2);
844        assert_eq!(subqubo.ncols(), 2);
845        assert_eq!(subqubo[[0, 0]], 1.0);
846        assert_eq!(subqubo[[0, 1]], 3.0);
847        assert_eq!(subqubo[[1, 0]], 3.0);
848        assert_eq!(subqubo[[1, 1]], 8.0);
849    }
850
851    #[test]
852    fn test_spectral_methods() {
853        let qubo = Array2::eye(4);
854
855        for method in &[
856            SpectralMethod::Standard,
857            SpectralMethod::NormalizedCut,
858            SpectralMethod::RatioCut,
859        ] {
860            let config = DecompositionConfig::default().with_method(*method);
861            let decomposer = AdvancedDecomposer::new(config);
862
863            let result = decomposer.compute_laplacian(&qubo);
864            assert!(result.is_ok());
865        }
866    }
867
868    #[test]
869    fn test_overlapping_partitions() {
870        let assignments = vec![0, 0, 1, 1];
871        let config = DecompositionConfig::default()
872            .with_num_partitions(2)
873            .with_overlap_size(1);
874        let decomposer = AdvancedDecomposer::new(config);
875
876        let partitions = decomposer
877            .create_overlapping_partitions(&assignments, 4)
878            .expect("overlapping partitions creation should succeed");
879        assert_eq!(partitions.len(), 2);
880    }
881}