1use scirs2_core::ndarray::{Array1, Array2};
45use scirs2_core::random::prelude::*;
46use std::collections::{HashMap, HashSet, VecDeque};
47use std::fmt;
48
49#[derive(Debug, Clone)]
51pub enum DecompositionError {
52 InvalidMatrix(String),
54 EigenvalueFailed(String),
56 InvalidPartition(String),
58 NoValidPartitions,
60 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
78pub type DecompositionResult<T> = Result<T, DecompositionError>;
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum SpectralMethod {
84 Standard,
86 NormalizedCut,
88 RatioCut,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum CommunityDetection {
95 Louvain,
97 LabelPropagation,
99 GirvanNewman,
101}
102
103#[derive(Debug, Clone)]
105pub struct DecompositionConfig {
106 pub num_partitions: usize,
108 pub spectral_method: SpectralMethod,
110 pub community_algorithm: CommunityDetection,
112 pub overlap_size: usize,
114 pub adaptive_granularity: bool,
116 pub target_partition_size: usize,
118 pub max_consensus_iterations: usize,
120 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 #[must_use]
142 pub const fn with_num_partitions(mut self, n: usize) -> Self {
143 self.num_partitions = n;
144 self
145 }
146
147 #[must_use]
149 pub const fn with_method(mut self, method: SpectralMethod) -> Self {
150 self.spectral_method = method;
151 self
152 }
153
154 #[must_use]
156 pub const fn with_community_algorithm(mut self, alg: CommunityDetection) -> Self {
157 self.community_algorithm = alg;
158 self
159 }
160
161 #[must_use]
163 pub const fn with_overlap_size(mut self, size: usize) -> Self {
164 self.overlap_size = size;
165 self
166 }
167
168 #[must_use]
170 pub const fn with_adaptive_granularity(mut self, enable: bool) -> Self {
171 self.adaptive_granularity = enable;
172 self
173 }
174
175 #[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#[derive(Debug, Clone)]
185pub struct Partition {
186 pub variables: Vec<usize>,
188 pub overlap_variables: Vec<usize>,
190 pub subproblem: Option<Array2<f64>>,
192}
193
194#[derive(Debug, Clone)]
196pub struct ConsensusResult {
197 pub solution: Vec<i32>,
199 pub energy: f64,
201 pub iterations: usize,
203 pub converged: bool,
205}
206
207pub struct AdvancedDecomposer {
209 config: DecompositionConfig,
210}
211
212impl AdvancedDecomposer {
213 pub const fn new(config: DecompositionConfig) -> Self {
215 Self { config }
216 }
217
218 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 let laplacian = self.compute_laplacian(qubo)?;
232
233 let eigenvector = self.compute_fiedler_vector(&laplacian)?;
235
236 let assignments = self.partition_by_eigenvector(&eigenvector, self.config.num_partitions);
238
239 let partitions = self.create_overlapping_partitions(&assignments, n)?;
241
242 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 fn compute_laplacian(&self, qubo: &Array2<f64>) -> DecompositionResult<Array2<f64>> {
256 let n = qubo.nrows();
257
258 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 let mut laplacian = Array2::zeros((n, n));
274 match self.config.spectral_method {
275 SpectralMethod::Standard => {
276 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 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 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 fn compute_fiedler_vector(&self, laplacian: &Array2<f64>) -> DecompositionResult<Array1<f64>> {
329 let n = laplacian.nrows();
330 let mut rng = thread_rng();
331
332 let mut v = Array1::from_shape_fn(n, |_| rng.gen::<f64>().mul_add(2.0, -1.0));
334
335 let ones = Array1::ones(n);
337 let projection = v.dot(&ones) / (n as f64);
338 v = &v - &(&ones * projection);
339
340 let norm = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
342 if norm > 1e-10 {
343 v = &v / norm;
344 }
345
346 for _ in 0..100 {
349 let mut new_v = Array1::zeros(n);
350
351 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 let projection = new_v.dot(&ones) / (n as f64);
362 new_v = &new_v - &(&ones * projection);
363
364 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 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 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 for i in 0..n {
390 assignments[i] = usize::from(eigenvector[i] > 0.0);
391 }
392 } else {
393 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 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 let mut overlap_variables = Vec::new();
436 if self.config.overlap_size > 0 {
437 for &var in &variables {
439 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 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 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 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 let total_weight: f64 = qubo.iter().map(|&x| x.abs()).sum::<f64>() / 2.0;
498
499 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 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 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 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 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 edge_weight_to_new / total_weight
572 }
573
574 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 let mut order: Vec<usize> = (0..n).collect();
586 order.shuffle(&mut rng);
587
588 for &node in &order {
589 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 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 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 fn girvan_newman(&self, qubo: &Array2<f64>) -> DecompositionResult<Vec<usize>> {
634 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 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 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 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 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, iterations: 1,
691 converged: true,
692 })
693 }
694
695 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}