1use std::collections::{HashMap, HashSet};
8use std::time::{Duration, Instant};
9use thiserror::Error;
10
11use crate::ising::{IsingError, IsingModel, QuboModel};
12use crate::partitioning::{Partition, SpectralPartitioner};
13use crate::simulator::{AnnealingParams, AnnealingSolution, QuantumAnnealingSimulator};
14
15#[derive(Error, Debug)]
17pub enum DecompositionError {
18 #[error("Ising error: {0}")]
20 IsingError(#[from] IsingError),
21
22 #[error("Invalid parameter: {0}")]
24 InvalidParameter(String),
25
26 #[error("Decomposition failed: {0}")]
28 DecompositionFailed(String),
29
30 #[error("Solving failed: {0}")]
32 SolvingFailed(String),
33
34 #[error("Reconstruction failed: {0}")]
36 ReconstructionFailed(String),
37}
38
39pub type DecompositionResult<T> = Result<T, DecompositionError>;
41
42#[derive(Debug, Clone)]
44pub enum DecompositionStrategy {
45 Spectral {
47 num_partitions: usize,
48 overlap_size: usize,
49 },
50
51 Block {
53 block_size: usize,
54 overlap_size: usize,
55 },
56
57 Hierarchical {
59 min_partition_size: usize,
60 max_depth: usize,
61 overlap_size: usize,
62 },
63
64 Clustering {
66 num_clusters: usize,
67 strength_threshold: f64,
68 overlap_size: usize,
69 },
70}
71
72#[derive(Debug, Clone)]
74pub struct DecompositionConfig {
75 pub strategy: DecompositionStrategy,
77
78 pub max_subproblem_size: usize,
80
81 pub min_subproblem_size: usize,
83
84 pub refinement_iterations: usize,
86
87 pub convergence_tolerance: f64,
89
90 pub solver_params: AnnealingParams,
92
93 pub parallel_solving: bool,
95
96 pub timeout: Option<Duration>,
98}
99
100impl Default for DecompositionConfig {
101 fn default() -> Self {
102 Self {
103 strategy: DecompositionStrategy::Spectral {
104 num_partitions: 4,
105 overlap_size: 2,
106 },
107 max_subproblem_size: 100,
108 min_subproblem_size: 10,
109 refinement_iterations: 5,
110 convergence_tolerance: 1e-6,
111 solver_params: AnnealingParams {
112 num_sweeps: 1000,
113 num_repetitions: 5,
114 ..Default::default()
115 },
116 parallel_solving: true,
117 timeout: Some(Duration::from_secs(3600)),
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct SubProblem {
125 pub variables: Vec<usize>,
127
128 pub variable_mapping: HashMap<usize, usize>,
130
131 pub qubo: QuboModel,
133
134 pub fixed_variables: HashMap<usize, bool>,
136
137 pub id: usize,
139}
140
141#[derive(Debug, Clone)]
143pub struct SubSolution {
144 pub subproblem_id: usize,
146
147 pub values: HashMap<usize, bool>,
149
150 pub objective_value: f64,
152
153 pub solver_info: String,
155}
156
157#[derive(Debug, Clone)]
159pub struct DecomposedSolution {
160 pub variable_values: HashMap<usize, bool>,
162
163 pub objective_value: f64,
165
166 pub sub_solutions: Vec<SubSolution>,
168
169 pub stats: DecompositionStats,
171}
172
173#[derive(Debug, Clone)]
175pub struct DecompositionStats {
176 pub num_subproblems: usize,
178
179 pub avg_subproblem_size: f64,
181
182 pub max_subproblem_size: usize,
184
185 pub min_subproblem_size: usize,
187
188 pub decomposition_time: Duration,
190
191 pub solving_time: Duration,
193
194 pub refinement_iterations: usize,
196
197 pub converged: bool,
199}
200
201pub struct QuboDecomposer {
203 config: DecompositionConfig,
205}
206
207impl QuboDecomposer {
208 #[must_use]
210 pub const fn new(config: DecompositionConfig) -> Self {
211 Self { config }
212 }
213
214 pub fn solve(&self, qubo: &QuboModel) -> DecompositionResult<DecomposedSolution> {
216 let total_start = Instant::now();
217
218 let decompose_start = Instant::now();
220 let sub_problems = self.decompose_problem(qubo)?;
221 let decomposition_time = decompose_start.elapsed();
222
223 println!(
224 "Decomposed problem into {} sub-problems",
225 sub_problems.len()
226 );
227
228 let solve_start = Instant::now();
230 let mut sub_solutions = self.solve_subproblems(&sub_problems)?;
231 let mut solving_time = solve_start.elapsed();
232
233 let mut converged = false;
235 let mut refinement_iterations = 0;
236
237 for iteration in 0..self.config.refinement_iterations {
238 let refine_start = Instant::now();
239
240 let (new_solutions, improvement) =
241 self.refine_solutions(qubo, &sub_problems, &sub_solutions)?;
242
243 solving_time += refine_start.elapsed();
244 refinement_iterations += 1;
245
246 if improvement < self.config.convergence_tolerance {
247 converged = true;
248 break;
249 }
250
251 sub_solutions = new_solutions;
252
253 if let Some(timeout) = self.config.timeout {
255 if total_start.elapsed() > timeout {
256 break;
257 }
258 }
259
260 println!(
261 "Refinement iteration {}: improvement = {:.6}",
262 iteration + 1,
263 improvement
264 );
265 }
266
267 let (variable_values, objective_value) = self.reconstruct_solution(qubo, &sub_solutions)?;
269
270 let subproblem_sizes: Vec<usize> =
272 sub_problems.iter().map(|sp| sp.variables.len()).collect();
273
274 let stats = DecompositionStats {
275 num_subproblems: sub_problems.len(),
276 avg_subproblem_size: subproblem_sizes.iter().sum::<usize>() as f64
277 / subproblem_sizes.len() as f64,
278 max_subproblem_size: *subproblem_sizes.iter().max().unwrap_or(&0),
279 min_subproblem_size: *subproblem_sizes.iter().min().unwrap_or(&0),
280 decomposition_time,
281 solving_time,
282 refinement_iterations,
283 converged,
284 };
285
286 Ok(DecomposedSolution {
287 variable_values,
288 objective_value,
289 sub_solutions,
290 stats,
291 })
292 }
293
294 fn decompose_problem(&self, qubo: &QuboModel) -> DecompositionResult<Vec<SubProblem>> {
296 match &self.config.strategy {
297 DecompositionStrategy::Spectral {
298 num_partitions,
299 overlap_size,
300 } => self.spectral_decomposition(qubo, *num_partitions, *overlap_size),
301 DecompositionStrategy::Block {
302 block_size,
303 overlap_size,
304 } => self.block_decomposition(qubo, *block_size, *overlap_size),
305 DecompositionStrategy::Hierarchical {
306 min_partition_size,
307 max_depth,
308 overlap_size,
309 } => self.hierarchical_decomposition(
310 qubo,
311 *min_partition_size,
312 *max_depth,
313 *overlap_size,
314 ),
315 DecompositionStrategy::Clustering {
316 num_clusters,
317 strength_threshold,
318 overlap_size,
319 } => self.clustering_decomposition(
320 qubo,
321 *num_clusters,
322 *strength_threshold,
323 *overlap_size,
324 ),
325 }
326 }
327
328 fn spectral_decomposition(
330 &self,
331 qubo: &QuboModel,
332 num_partitions: usize,
333 overlap_size: usize,
334 ) -> DecompositionResult<Vec<SubProblem>> {
335 let (ising, _) = qubo.to_ising();
337
338 let partitioner = SpectralPartitioner::new();
340 let edges: Vec<(usize, usize, f64)> = ising
341 .couplings()
342 .into_iter()
343 .map(|coupling| (coupling.i, coupling.j, coupling.strength))
344 .collect();
345 let partition = partitioner
346 .partition_graph(ising.num_qubits, &edges, num_partitions)
347 .map_err(|e| DecompositionError::DecompositionFailed(e.to_string()))?;
348
349 let mut sub_problems = Vec::new();
351
352 for id in 0..num_partitions {
353 let nodes = partition.get_partition(id);
354 let variables = self.add_overlap(&nodes, &ising, overlap_size);
355 let sub_qubo = self.extract_subproblem(qubo, &variables)?;
356 let variable_mapping = variables
357 .iter()
358 .enumerate()
359 .map(|(i, &var)| (i, var))
360 .collect();
361
362 sub_problems.push(SubProblem {
363 variables: (0..variables.len()).collect(),
364 variable_mapping,
365 qubo: sub_qubo,
366 fixed_variables: HashMap::new(),
367 id,
368 });
369 }
370
371 Ok(sub_problems)
372 }
373
374 fn block_decomposition(
376 &self,
377 qubo: &QuboModel,
378 block_size: usize,
379 overlap_size: usize,
380 ) -> DecompositionResult<Vec<SubProblem>> {
381 let mut sub_problems = Vec::new();
382 let mut id = 0;
383
384 for start in (0..qubo.num_variables).step_by(block_size - overlap_size) {
385 let end = std::cmp::min(start + block_size, qubo.num_variables);
386
387 if end - start < self.config.min_subproblem_size {
388 break;
389 }
390
391 let variables: Vec<usize> = (start..end).collect();
392 let sub_qubo = self.extract_subproblem(qubo, &variables)?;
393 let variable_mapping = variables
394 .iter()
395 .enumerate()
396 .map(|(i, &var)| (i, var))
397 .collect();
398
399 sub_problems.push(SubProblem {
400 variables: (0..variables.len()).collect(),
401 variable_mapping,
402 qubo: sub_qubo,
403 fixed_variables: HashMap::new(),
404 id,
405 });
406
407 id += 1;
408 }
409
410 Ok(sub_problems)
411 }
412
413 fn hierarchical_decomposition(
415 &self,
416 qubo: &QuboModel,
417 min_partition_size: usize,
418 max_depth: usize,
419 overlap_size: usize,
420 ) -> DecompositionResult<Vec<SubProblem>> {
421 let all_variables: Vec<usize> = (0..qubo.num_variables).collect();
422 let mut sub_problems = Vec::new();
423 let mut id = 0;
424
425 self.recursive_bisection(
426 qubo,
427 all_variables,
428 0,
429 max_depth,
430 min_partition_size,
431 overlap_size,
432 &mut sub_problems,
433 &mut id,
434 )?;
435
436 Ok(sub_problems)
437 }
438
439 fn recursive_bisection(
441 &self,
442 qubo: &QuboModel,
443 variables: Vec<usize>,
444 depth: usize,
445 max_depth: usize,
446 min_size: usize,
447 overlap_size: usize,
448 sub_problems: &mut Vec<SubProblem>,
449 id: &mut usize,
450 ) -> DecompositionResult<()> {
451 if variables.len() <= min_size || depth >= max_depth {
452 let sub_qubo = self.extract_subproblem(qubo, &variables)?;
454 let variable_mapping = variables
455 .iter()
456 .enumerate()
457 .map(|(i, &var)| (i, var))
458 .collect();
459
460 sub_problems.push(SubProblem {
461 variables: (0..variables.len()).collect(),
462 variable_mapping,
463 qubo: sub_qubo,
464 fixed_variables: HashMap::new(),
465 id: *id,
466 });
467
468 *id += 1;
469 return Ok(());
470 }
471
472 let mid = variables.len() / 2;
474 let left_vars = variables[..mid].to_vec();
475 let right_vars = variables[mid..].to_vec();
476
477 let left_with_overlap = self.add_overlap_to_vars(&left_vars, &variables, overlap_size);
479 let right_with_overlap = self.add_overlap_to_vars(&right_vars, &variables, overlap_size);
480
481 self.recursive_bisection(
483 qubo,
484 left_with_overlap,
485 depth + 1,
486 max_depth,
487 min_size,
488 overlap_size,
489 sub_problems,
490 id,
491 )?;
492
493 self.recursive_bisection(
494 qubo,
495 right_with_overlap,
496 depth + 1,
497 max_depth,
498 min_size,
499 overlap_size,
500 sub_problems,
501 id,
502 )?;
503
504 Ok(())
505 }
506
507 fn clustering_decomposition(
509 &self,
510 qubo: &QuboModel,
511 num_clusters: usize,
512 strength_threshold: f64,
513 overlap_size: usize,
514 ) -> DecompositionResult<Vec<SubProblem>> {
515 let mut clusters = vec![0; qubo.num_variables];
517 let mut current_cluster = 0;
518 let mut visited = vec![false; qubo.num_variables];
519
520 for i in 0..qubo.num_variables {
522 if visited[i] {
523 continue;
524 }
525
526 let mut cluster_vars = vec![i];
527 let mut stack = vec![i];
528 visited[i] = true;
529
530 while let Some(var) = stack.pop() {
531 for j in 0..qubo.num_variables {
532 if i != j && !visited[j] {
533 let interaction = qubo.get_quadratic(var, j).unwrap_or(0.0).abs();
534 if interaction > strength_threshold {
535 visited[j] = true;
536 clusters[j] = current_cluster;
537 cluster_vars.push(j);
538 stack.push(j);
539 }
540 }
541 }
542 }
543
544 clusters[i] = current_cluster;
545 current_cluster += 1;
546
547 if current_cluster >= num_clusters {
548 break;
549 }
550 }
551
552 for i in 0..qubo.num_variables {
554 if !visited[i] {
555 clusters[i] = i % num_clusters;
556 }
557 }
558
559 let mut cluster_vars: HashMap<usize, Vec<usize>> = HashMap::new();
561 for (var, &cluster) in clusters.iter().enumerate() {
562 cluster_vars.entry(cluster).or_default().push(var);
563 }
564
565 let mut sub_problems = Vec::new();
566 for (id, mut variables) in cluster_vars {
567 let (ising, _) = qubo.to_ising();
569 variables = self.add_overlap(&variables, &ising, overlap_size);
570
571 if variables.len() >= self.config.min_subproblem_size {
572 let sub_qubo = self.extract_subproblem(qubo, &variables)?;
573 let variable_mapping = variables
574 .iter()
575 .enumerate()
576 .map(|(i, &var)| (i, var))
577 .collect();
578
579 sub_problems.push(SubProblem {
580 variables: (0..variables.len()).collect(),
581 variable_mapping,
582 qubo: sub_qubo,
583 fixed_variables: HashMap::new(),
584 id,
585 });
586 }
587 }
588
589 Ok(sub_problems)
590 }
591
592 fn add_overlap(
594 &self,
595 variables: &[usize],
596 ising: &IsingModel,
597 overlap_size: usize,
598 ) -> Vec<usize> {
599 let mut result: HashSet<usize> = variables.iter().copied().collect();
600
601 for &var in variables {
602 let mut neighbors = Vec::new();
603
604 for coupling in ising.couplings() {
606 if coupling.i == var && !result.contains(&coupling.j) {
607 neighbors.push(coupling.j);
608 } else if coupling.j == var && !result.contains(&coupling.i) {
609 neighbors.push(coupling.i);
610 }
611 }
612
613 neighbors.sort_by(|&a, &b| {
615 let strength_a = ising.get_coupling(var, a).unwrap_or(0.0).abs();
616 let strength_b = ising.get_coupling(var, b).unwrap_or(0.0).abs();
617 strength_b
618 .partial_cmp(&strength_a)
619 .unwrap_or(std::cmp::Ordering::Equal)
620 });
621
622 for &neighbor in neighbors.iter().take(overlap_size) {
623 result.insert(neighbor);
624 }
625 }
626
627 let mut result_vec: Vec<usize> = result.into_iter().collect();
628 result_vec.sort_unstable();
629 result_vec
630 }
631
632 fn add_overlap_to_vars(
634 &self,
635 variables: &[usize],
636 all_vars: &[usize],
637 overlap_size: usize,
638 ) -> Vec<usize> {
639 let mut result: HashSet<usize> = variables.iter().copied().collect();
640
641 let boundary_start = if variables.len() >= overlap_size {
643 variables.len() - overlap_size
644 } else {
645 0
646 };
647
648 for i in boundary_start..std::cmp::min(variables.len() + overlap_size, all_vars.len()) {
649 if i < all_vars.len() {
650 result.insert(all_vars[i]);
651 }
652 }
653
654 let mut result_vec: Vec<usize> = result.into_iter().collect();
655 result_vec.sort_unstable();
656 result_vec
657 }
658
659 fn extract_subproblem(
661 &self,
662 qubo: &QuboModel,
663 variables: &[usize],
664 ) -> DecompositionResult<QuboModel> {
665 let mut sub_qubo = QuboModel::new(variables.len());
666
667 let var_map: HashMap<usize, usize> = variables
669 .iter()
670 .enumerate()
671 .map(|(i, &var)| (var, i))
672 .collect();
673
674 for (i, &var) in variables.iter().enumerate() {
676 let linear = qubo.get_linear(var)?;
677 if linear != 0.0 {
678 sub_qubo.set_linear(i, linear)?;
679 }
680 }
681
682 for &var1 in variables {
684 for &var2 in variables {
685 if var1 < var2 {
686 let quadratic = qubo.get_quadratic(var1, var2)?;
687 if quadratic != 0.0 {
688 let sub_var1 = var_map[&var1];
689 let sub_var2 = var_map[&var2];
690 sub_qubo.set_quadratic(sub_var1, sub_var2, quadratic)?;
691 }
692 }
693 }
694 }
695
696 Ok(sub_qubo)
697 }
698
699 fn solve_subproblems(
701 &self,
702 sub_problems: &[SubProblem],
703 ) -> DecompositionResult<Vec<SubSolution>> {
704 let mut sub_solutions = Vec::new();
705
706 for sub_problem in sub_problems {
707 let (ising, offset) = sub_problem.qubo.to_ising();
708
709 let mut simulator = QuantumAnnealingSimulator::new(self.config.solver_params.clone())
710 .map_err(|e| DecompositionError::SolvingFailed(e.to_string()))?;
711
712 let result = simulator
713 .solve(&ising)
714 .map_err(|e| DecompositionError::SolvingFailed(e.to_string()))?;
715
716 let values: HashMap<usize, bool> = result
718 .best_spins
719 .iter()
720 .enumerate()
721 .map(|(i, &spin)| {
722 let original_var = sub_problem.variable_mapping[&i];
723 (original_var, spin > 0)
724 })
725 .collect();
726
727 let objective_value = result.best_energy + offset;
728
729 sub_solutions.push(SubSolution {
730 subproblem_id: sub_problem.id,
731 values,
732 objective_value,
733 solver_info: result.info,
734 });
735 }
736
737 Ok(sub_solutions)
738 }
739
740 fn refine_solutions(
742 &self,
743 _qubo: &QuboModel,
744 _sub_problems: &[SubProblem],
745 sub_solutions: &[SubSolution],
746 ) -> DecompositionResult<(Vec<SubSolution>, f64)> {
747 Ok((sub_solutions.to_vec(), 0.0))
754 }
755
756 fn reconstruct_solution(
758 &self,
759 qubo: &QuboModel,
760 sub_solutions: &[SubSolution],
761 ) -> DecompositionResult<(HashMap<usize, bool>, f64)> {
762 let mut variable_values = HashMap::new();
763 let mut vote_counts: HashMap<usize, (usize, usize)> = HashMap::new(); for sub_solution in sub_solutions {
767 for (&var, &value) in &sub_solution.values {
768 let (true_votes, false_votes) = vote_counts.entry(var).or_insert((0, 0));
769 if value {
770 *true_votes += 1;
771 } else {
772 *false_votes += 1;
773 }
774 }
775 }
776
777 for (var, (true_votes, false_votes)) in vote_counts {
779 variable_values.insert(var, true_votes > false_votes);
780 }
781
782 let binary_vars: Vec<bool> = (0..qubo.num_variables)
784 .map(|i| variable_values.get(&i).copied().unwrap_or(false))
785 .collect();
786
787 let objective_value = qubo.objective(&binary_vars)?;
788
789 Ok((variable_values, objective_value))
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::qubo::QuboBuilder;
797
798 #[test]
799 fn test_decomposition_config() {
800 let config = DecompositionConfig::default();
801 assert!(config.max_subproblem_size > 0);
802 assert!(config.refinement_iterations > 0);
803 }
804
805 #[test]
806 fn test_block_decomposition() {
807 let mut qubo = QuboModel::new(20); for i in 0..20 {
809 qubo.set_linear(i, 1.0).expect("set_linear should succeed");
810 }
811
812 let config = DecompositionConfig {
813 strategy: DecompositionStrategy::Block {
814 block_size: 4,
815 overlap_size: 1,
816 },
817 min_subproblem_size: 3, ..Default::default()
819 };
820
821 let decomposer = QuboDecomposer::new(config);
822 let sub_problems = decomposer
823 .block_decomposition(&qubo, 4, 1)
824 .expect("block_decomposition should succeed");
825
826 assert!(!sub_problems.is_empty());
827 for sub_problem in &sub_problems {
828 assert!(sub_problem.variables.len() <= 4);
829 }
830 }
831
832 #[test]
833 fn test_small_qubo_decomposition() {
834 let mut builder = QuboBuilder::new();
835
836 let vars: Vec<_> = (0..6)
838 .map(|i| {
839 builder
840 .add_variable(format!("x{}", i))
841 .expect("add_variable should succeed")
842 })
843 .collect();
844
845 for i in 0..6 {
846 builder
847 .set_linear_term(&vars[i], 1.0)
848 .expect("set_linear_term should succeed");
849 }
850
851 for i in 0..5 {
852 builder
853 .set_quadratic_term(&vars[i], &vars[i + 1], -2.0)
854 .expect("set_quadratic_term should succeed");
855 }
856
857 let qubo_formulation = builder.build();
858 let qubo = qubo_formulation.to_qubo_model();
859
860 let config = DecompositionConfig {
861 strategy: DecompositionStrategy::Block {
862 block_size: 3,
863 overlap_size: 1,
864 },
865 max_subproblem_size: 5,
866 min_subproblem_size: 2,
867 refinement_iterations: 2,
868 ..Default::default()
869 };
870
871 let decomposer = QuboDecomposer::new(config);
872 let result = decomposer.solve(&qubo).expect("solve should succeed");
873
874 assert_eq!(result.variable_values.len(), 6);
875 assert!(result.stats.num_subproblems > 0);
876 }
877}