quantrs2_anneal/
qubo_decomposition.rs

1//! Large-scale QUBO decomposition for efficient solving
2//!
3//! This module provides methods for decomposing large QUBO problems into
4//! smaller sub-problems that can be solved more efficiently using various
5//! decomposition strategies.
6
7use 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/// Errors that can occur during QUBO decomposition
16#[derive(Error, Debug)]
17pub enum DecompositionError {
18    /// Ising model error
19    #[error("Ising error: {0}")]
20    IsingError(#[from] IsingError),
21
22    /// Invalid decomposition parameter
23    #[error("Invalid parameter: {0}")]
24    InvalidParameter(String),
25
26    /// Decomposition failed
27    #[error("Decomposition failed: {0}")]
28    DecompositionFailed(String),
29
30    /// Solving failed
31    #[error("Solving failed: {0}")]
32    SolvingFailed(String),
33
34    /// Reconstruction failed
35    #[error("Reconstruction failed: {0}")]
36    ReconstructionFailed(String),
37}
38
39/// Result type for decomposition operations
40pub type DecompositionResult<T> = Result<T, DecompositionError>;
41
42/// Decomposition strategy for QUBO problems
43#[derive(Debug, Clone)]
44pub enum DecompositionStrategy {
45    /// Spectral decomposition using eigenvector-based partitioning
46    Spectral {
47        num_partitions: usize,
48        overlap_size: usize,
49    },
50
51    /// Block decomposition based on problem structure
52    Block {
53        block_size: usize,
54        overlap_size: usize,
55    },
56
57    /// Hierarchical decomposition using recursive bisection
58    Hierarchical {
59        min_partition_size: usize,
60        max_depth: usize,
61        overlap_size: usize,
62    },
63
64    /// Variable clustering based on interaction strength
65    Clustering {
66        num_clusters: usize,
67        strength_threshold: f64,
68        overlap_size: usize,
69    },
70}
71
72/// Configuration for QUBO decomposition
73#[derive(Debug, Clone)]
74pub struct DecompositionConfig {
75    /// Decomposition strategy
76    pub strategy: DecompositionStrategy,
77
78    /// Maximum sub-problem size
79    pub max_subproblem_size: usize,
80
81    /// Minimum sub-problem size
82    pub min_subproblem_size: usize,
83
84    /// Number of iterations for iterative refinement
85    pub refinement_iterations: usize,
86
87    /// Convergence tolerance for iterative methods
88    pub convergence_tolerance: f64,
89
90    /// Solver parameters for sub-problems
91    pub solver_params: AnnealingParams,
92
93    /// Enable parallel solving of sub-problems
94    pub parallel_solving: bool,
95
96    /// Maximum runtime for decomposition
97    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/// Sub-problem extracted from the original QUBO
123#[derive(Debug, Clone)]
124pub struct SubProblem {
125    /// Variables in this sub-problem
126    pub variables: Vec<usize>,
127
128    /// Mapping from sub-problem indices to original indices
129    pub variable_mapping: HashMap<usize, usize>,
130
131    /// QUBO model for this sub-problem
132    pub qubo: QuboModel,
133
134    /// Fixed variables from other sub-problems
135    pub fixed_variables: HashMap<usize, bool>,
136
137    /// Sub-problem identifier
138    pub id: usize,
139}
140
141/// Solution for a sub-problem
142#[derive(Debug, Clone)]
143pub struct SubSolution {
144    /// Sub-problem identifier
145    pub subproblem_id: usize,
146
147    /// Solution values for variables in this sub-problem
148    pub values: HashMap<usize, bool>,
149
150    /// Objective value achieved
151    pub objective_value: f64,
152
153    /// Solver statistics
154    pub solver_info: String,
155}
156
157/// Complete decomposition solution
158#[derive(Debug, Clone)]
159pub struct DecomposedSolution {
160    /// Variable assignments for the complete problem
161    pub variable_values: HashMap<usize, bool>,
162
163    /// Objective value of the complete solution
164    pub objective_value: f64,
165
166    /// Sub-problem solutions
167    pub sub_solutions: Vec<SubSolution>,
168
169    /// Decomposition statistics
170    pub stats: DecompositionStats,
171}
172
173/// Statistics about the decomposition process
174#[derive(Debug, Clone)]
175pub struct DecompositionStats {
176    /// Number of sub-problems created
177    pub num_subproblems: usize,
178
179    /// Average sub-problem size
180    pub avg_subproblem_size: f64,
181
182    /// Maximum sub-problem size
183    pub max_subproblem_size: usize,
184
185    /// Minimum sub-problem size
186    pub min_subproblem_size: usize,
187
188    /// Total decomposition time
189    pub decomposition_time: Duration,
190
191    /// Total solving time
192    pub solving_time: Duration,
193
194    /// Number of refinement iterations performed
195    pub refinement_iterations: usize,
196
197    /// Convergence achieved
198    pub converged: bool,
199}
200
201/// QUBO decomposition solver
202pub struct QuboDecomposer {
203    /// Configuration
204    config: DecompositionConfig,
205}
206
207impl QuboDecomposer {
208    /// Create a new QUBO decomposer
209    #[must_use]
210    pub const fn new(config: DecompositionConfig) -> Self {
211        Self { config }
212    }
213
214    /// Solve a large QUBO problem using decomposition
215    pub fn solve(&self, qubo: &QuboModel) -> DecompositionResult<DecomposedSolution> {
216        let total_start = Instant::now();
217
218        // Step 1: Decompose the problem
219        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        // Step 2: Solve sub-problems
229        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        // Step 3: Iterative refinement
234        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            // Check timeout
254            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        // Step 4: Reconstruct complete solution
268        let (variable_values, objective_value) = self.reconstruct_solution(qubo, &sub_solutions)?;
269
270        // Calculate statistics
271        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    /// Decompose the QUBO problem into sub-problems
295    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    /// Spectral decomposition using graph partitioning
329    fn spectral_decomposition(
330        &self,
331        qubo: &QuboModel,
332        num_partitions: usize,
333        overlap_size: usize,
334    ) -> DecompositionResult<Vec<SubProblem>> {
335        // Convert QUBO to graph representation
336        let (ising, _) = qubo.to_ising();
337
338        // Use spectral partitioner
339        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        // Create sub-problems with overlap
350        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    /// Block decomposition based on variable indices
375    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    /// Hierarchical decomposition using recursive bisection
414    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    /// Recursive bisection helper
440    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            // Create leaf sub-problem
453            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        // Bisect the variables
473        let mid = variables.len() / 2;
474        let left_vars = variables[..mid].to_vec();
475        let right_vars = variables[mid..].to_vec();
476
477        // Add overlap
478        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        // Recurse
482        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    /// Clustering decomposition based on interaction strength
508    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        // Build interaction graph
516        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        // Simple clustering based on strong interactions
521        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        // Assign remaining variables to nearest clusters
553        for i in 0..qubo.num_variables {
554            if !visited[i] {
555                clusters[i] = i % num_clusters;
556            }
557        }
558
559        // Create sub-problems from clusters
560        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            // Add overlap
568            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    /// Add overlap to a set of variables
593    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            // Find neighbors through couplings
605            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            // Add the strongest neighbors up to overlap_size
614            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    /// Add overlap to a list of variables
633    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        // Add some variables from the boundary
642        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    /// Extract a sub-problem from the original QUBO
660    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        // Create variable index mapping
668        let var_map: HashMap<usize, usize> = variables
669            .iter()
670            .enumerate()
671            .map(|(i, &var)| (var, i))
672            .collect();
673
674        // Copy linear terms
675        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        // Copy quadratic terms
683        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    /// Solve all sub-problems
700    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            // Convert spin solution to binary
717            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    /// Refine solutions through iterative improvement
741    fn refine_solutions(
742        &self,
743        _qubo: &QuboModel,
744        _sub_problems: &[SubProblem],
745        sub_solutions: &[SubSolution],
746    ) -> DecompositionResult<(Vec<SubSolution>, f64)> {
747        // For now, return the same solutions with zero improvement
748        // In a real implementation, this would:
749        // 1. Fix variables based on current solutions
750        // 2. Re-solve sub-problems with fixed boundary conditions
751        // 3. Calculate improvement in objective value
752
753        Ok((sub_solutions.to_vec(), 0.0))
754    }
755
756    /// Reconstruct the complete solution from sub-solutions
757    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(); // (true_votes, false_votes)
764
765        // Collect votes from all sub-solutions
766        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        // Decide final values based on majority vote
778        for (var, (true_votes, false_votes)) in vote_counts {
779            variable_values.insert(var, true_votes > false_votes);
780        }
781
782        // Calculate objective value
783        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); // Increase size to 20 variables
808        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, // Reduce minimum size to allow small blocks
818            ..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        // Create a small test problem
837        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}