quantrs2_anneal/
compression.rs

1//! QUBO matrix compression algorithms
2//!
3//! This module provides compression techniques for QUBO matrices to reduce
4//! memory usage and improve computational efficiency for large problems.
5
6use crate::ising::{IsingError, IsingResult};
7use std::collections::{HashMap, HashSet};
8
9/// Simple sparse vector implementation
10#[derive(Debug, Clone)]
11pub struct SparseVector<T> {
12    data: HashMap<usize, T>,
13    size: usize,
14}
15
16impl<T: Clone + Default + PartialEq> SparseVector<T> {
17    #[must_use]
18    pub fn new(size: usize) -> Self {
19        Self {
20            data: HashMap::new(),
21            size,
22        }
23    }
24
25    #[must_use]
26    pub fn get(&self, index: usize) -> Option<&T> {
27        self.data.get(&index)
28    }
29
30    pub fn set(&mut self, index: usize, value: T) {
31        if index < self.size {
32            if value == T::default() {
33                self.data.remove(&index);
34            } else {
35                self.data.insert(index, value);
36            }
37        }
38    }
39
40    pub fn iter(&self) -> impl Iterator<Item = (usize, &T)> {
41        self.data.iter().map(|(&k, v)| (k, v))
42    }
43
44    pub fn remove(&mut self, index: usize) -> Option<T> {
45        self.data.remove(&index)
46    }
47
48    #[must_use]
49    pub fn nnz(&self) -> usize {
50        self.data.len()
51    }
52
53    pub fn filter_inplace<F>(&mut self, mut predicate: F)
54    where
55        F: FnMut(&T) -> bool,
56    {
57        self.data.retain(|_, v| predicate(v));
58    }
59}
60
61/// Simple COO sparse matrix implementation
62#[derive(Debug, Clone)]
63pub struct CooMatrix<T> {
64    data: HashMap<(usize, usize), T>,
65    rows: usize,
66    cols: usize,
67}
68
69impl<T: Clone + Default + PartialEq> CooMatrix<T> {
70    #[must_use]
71    pub fn new(rows: usize, cols: usize) -> Self {
72        Self {
73            data: HashMap::new(),
74            rows,
75            cols,
76        }
77    }
78
79    #[must_use]
80    pub fn get(&self, row: usize, col: usize) -> Option<&T> {
81        self.data.get(&(row, col))
82    }
83
84    pub fn set(&mut self, row: usize, col: usize, value: T) {
85        if row < self.rows && col < self.cols {
86            if value == T::default() {
87                self.data.remove(&(row, col));
88            } else {
89                self.data.insert((row, col), value);
90            }
91        }
92    }
93
94    pub fn iter(&self) -> impl Iterator<Item = (usize, usize, &T)> {
95        self.data.iter().map(|(&(i, j), v)| (i, j, v))
96    }
97
98    pub fn retain<F>(&mut self, mut predicate: F)
99    where
100        F: FnMut(&(usize, usize), &mut T) -> bool,
101    {
102        self.data.retain(predicate);
103    }
104
105    #[must_use]
106    pub fn nnz(&self) -> usize {
107        self.data.len()
108    }
109
110    pub fn filter_inplace<F>(&mut self, mut predicate: F)
111    where
112        F: FnMut(&T) -> bool,
113    {
114        self.data.retain(|_, v| predicate(v));
115    }
116}
117
118/// Compressed QUBO representation using various sparse formats
119#[derive(Debug, Clone)]
120pub struct CompressedQubo {
121    /// Number of variables
122    pub num_vars: usize,
123    /// Linear terms (diagonal elements) as sparse vector
124    pub linear_terms: SparseVector<f64>,
125    /// Quadratic terms (off-diagonal elements) as COO sparse matrix
126    /// Stored as upper triangular: (i, j) where i < j
127    pub quadratic_terms: CooMatrix<f64>,
128    /// Constant offset
129    pub offset: f64,
130    /// Compression statistics
131    pub stats: CompressionStats,
132}
133
134/// Statistics about compression
135#[derive(Debug, Clone, Default)]
136pub struct CompressionStats {
137    /// Original number of non-zero elements
138    pub original_nnz: usize,
139    /// Compressed number of non-zero elements
140    pub compressed_nnz: usize,
141    /// Number of merged variables
142    pub merged_vars: usize,
143    /// Number of eliminated variables
144    pub eliminated_vars: usize,
145    /// Compression ratio
146    pub compression_ratio: f64,
147}
148
149impl CompressedQubo {
150    /// Create a new compressed QUBO
151    #[must_use]
152    pub fn new(num_vars: usize) -> Self {
153        Self {
154            num_vars,
155            linear_terms: SparseVector::new(num_vars),
156            quadratic_terms: CooMatrix::new(num_vars, num_vars),
157            offset: 0.0,
158            stats: CompressionStats::default(),
159        }
160    }
161
162    /// Add or update a linear term
163    pub fn add_linear(&mut self, var: usize, coefficient: f64) {
164        if coefficient.abs() > 1e-10 {
165            let current_value = *self.linear_terms.get(var).unwrap_or(&0.0);
166            self.linear_terms.set(var, current_value + coefficient);
167        }
168    }
169
170    /// Add or update a quadratic term
171    pub fn add_quadratic(&mut self, i: usize, j: usize, coefficient: f64) {
172        if i == j {
173            self.add_linear(i, coefficient);
174        } else if coefficient.abs() > 1e-10 {
175            let (i, j) = if i < j { (i, j) } else { (j, i) };
176            let current_value = *self.quadratic_terms.get(i, j).unwrap_or(&0.0);
177            self.quadratic_terms.set(i, j, current_value + coefficient);
178        }
179    }
180
181    /// Get the energy for a given solution
182    #[must_use]
183    pub fn evaluate(&self, solution: &[bool]) -> f64 {
184        let mut energy = self.offset;
185
186        // Linear terms
187        for (var, coeff) in self.linear_terms.iter() {
188            if var < solution.len() && solution[var] {
189                energy += coeff;
190            }
191        }
192
193        // Quadratic terms
194        for (i, j, coeff) in self.quadratic_terms.iter() {
195            if i < solution.len() && j < solution.len() && solution[i] && solution[j] {
196                energy += coeff;
197            }
198        }
199
200        energy
201    }
202
203    /// Get total number of non-zero elements
204    #[must_use]
205    pub fn nnz(&self) -> usize {
206        self.linear_terms.nnz() + self.quadratic_terms.nnz()
207    }
208
209    /// Apply threshold to remove small coefficients
210    pub fn apply_threshold(&mut self, threshold: f64) {
211        self.linear_terms.filter_inplace(|&v| v.abs() > threshold);
212        self.quadratic_terms
213            .filter_inplace(|&v| v.abs() > threshold);
214    }
215}
216
217/// QUBO compression using Coordinate (COO) format
218pub struct CooCompressor {
219    /// Threshold for considering coefficients as zero
220    pub zero_threshold: f64,
221    /// Whether to sort by magnitude for better compression
222    pub sort_by_magnitude: bool,
223}
224
225impl Default for CooCompressor {
226    fn default() -> Self {
227        Self {
228            zero_threshold: 1e-10,
229            sort_by_magnitude: true,
230        }
231    }
232}
233
234impl CooCompressor {
235    /// Compress a QUBO matrix from dense format
236    pub fn compress_dense(&self, matrix: &[Vec<f64>], offset: f64) -> IsingResult<CompressedQubo> {
237        let n = matrix.len();
238        let mut compressed = CompressedQubo::new(n);
239        compressed.offset = offset;
240
241        let mut original_nnz = 0;
242
243        // Extract non-zero elements
244        for i in 0..n {
245            for j in i..n {
246                let value = matrix[i][j];
247                if value.abs() > self.zero_threshold {
248                    original_nnz += 1;
249                    if i == j {
250                        compressed.add_linear(i, value);
251                    } else {
252                        compressed.add_quadratic(i, j, value);
253                    }
254                }
255            }
256        }
257
258        // Update statistics
259        compressed.stats.original_nnz = original_nnz;
260        compressed.stats.compressed_nnz = compressed.nnz();
261        compressed.stats.compression_ratio = if original_nnz > 0 {
262            compressed.stats.compressed_nnz as f64 / original_nnz as f64
263        } else {
264            1.0
265        };
266
267        Ok(compressed)
268    }
269
270    /// Compress from edge list format
271    pub fn compress_edges(
272        &self,
273        num_vars: usize,
274        edges: &[(usize, usize, f64)],
275        offset: f64,
276    ) -> IsingResult<CompressedQubo> {
277        let mut compressed = CompressedQubo::new(num_vars);
278        compressed.offset = offset;
279
280        for &(i, j, weight) in edges {
281            if weight.abs() > self.zero_threshold {
282                compressed.add_quadratic(i, j, weight);
283            }
284        }
285
286        compressed.stats.original_nnz = edges.len();
287        compressed.stats.compressed_nnz = compressed.nnz();
288        compressed.stats.compression_ratio = if edges.len() > 0 {
289            compressed.stats.compressed_nnz as f64 / edges.len() as f64
290        } else {
291            1.0
292        };
293
294        Ok(compressed)
295    }
296}
297
298/// Variable reduction techniques for QUBO compression
299pub struct VariableReducer {
300    /// Threshold for considering variables as fixed
301    pub fixing_threshold: f64,
302    /// Enable variable merging
303    pub enable_merging: bool,
304    /// Enable variable elimination
305    pub enable_elimination: bool,
306}
307
308impl Default for VariableReducer {
309    fn default() -> Self {
310        Self {
311            fixing_threshold: 1e6,
312            enable_merging: true,
313            enable_elimination: true,
314        }
315    }
316}
317
318impl VariableReducer {
319    /// Reduce QUBO by fixing, merging, and eliminating variables
320    pub fn reduce(&self, qubo: &mut CompressedQubo) -> IsingResult<ReductionMapping> {
321        let mut mapping = ReductionMapping::new(qubo.num_vars);
322
323        // Fix variables with very large linear coefficients
324        if self.enable_elimination {
325            self.fix_variables(qubo, &mut mapping)?;
326        }
327
328        // Merge equivalent variables
329        if self.enable_merging {
330            self.merge_equivalent_variables(qubo, &mut mapping)?;
331        }
332
333        // Eliminate variables with degree 1
334        if self.enable_elimination {
335            self.eliminate_degree_one_variables(qubo, &mut mapping)?;
336        }
337
338        Ok(mapping)
339    }
340
341    /// Fix variables with large coefficients
342    fn fix_variables(
343        &self,
344        qubo: &mut CompressedQubo,
345        mapping: &mut ReductionMapping,
346    ) -> IsingResult<()> {
347        let mut to_fix = Vec::new();
348
349        // Find variables to fix
350        for (var, coeff) in qubo.linear_terms.iter() {
351            if coeff.abs() > self.fixing_threshold {
352                // Fix to 0 if coefficient is positive, 1 if negative
353                let value = *coeff < 0.0;
354                to_fix.push((var, value));
355            }
356        }
357
358        // Apply fixings
359        for (var, value) in to_fix {
360            self.fix_variable(qubo, mapping, var, value)?;
361            qubo.stats.eliminated_vars += 1;
362        }
363
364        Ok(())
365    }
366
367    /// Fix a single variable and update the QUBO
368    fn fix_variable(
369        &self,
370        qubo: &mut CompressedQubo,
371        mapping: &mut ReductionMapping,
372        var: usize,
373        value: bool,
374    ) -> IsingResult<()> {
375        mapping.fix_variable(var, value);
376
377        // Update offset if variable is set to 1
378        if value {
379            let linear_coeff = *qubo.linear_terms.get(var).unwrap_or(&0.0);
380            qubo.offset += linear_coeff;
381
382            // Update other linear terms
383            let mut updates = Vec::new();
384            for (i, j, coeff) in qubo.quadratic_terms.iter() {
385                if i == var {
386                    updates.push((j, *coeff));
387                } else if j == var {
388                    updates.push((i, *coeff));
389                }
390            }
391
392            for (other_var, coeff) in updates {
393                qubo.add_linear(other_var, coeff);
394            }
395        }
396
397        // Remove variable from QUBO
398        qubo.linear_terms.remove(var);
399        qubo.quadratic_terms.filter_inplace(|_| true); // Remove terms involving var
400                                                       // Note: We need a more specific filter for removing terms involving var
401        let mut new_quadratic = CooMatrix::new(qubo.num_vars, qubo.num_vars);
402        for (i, j, coeff) in qubo.quadratic_terms.iter() {
403            if i != var && j != var {
404                new_quadratic.set(i, j, *coeff);
405            }
406        }
407        qubo.quadratic_terms = new_quadratic;
408
409        Ok(())
410    }
411
412    /// Merge variables that appear with the same coefficients
413    fn merge_equivalent_variables(
414        &self,
415        qubo: &mut CompressedQubo,
416        mapping: &mut ReductionMapping,
417    ) -> IsingResult<()> {
418        // Build variable signature based on coefficients
419        let signatures = self.compute_variable_signatures(qubo);
420
421        // Group variables by signature
422        let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
423        for (var, sig) in signatures {
424            groups.entry(sig).or_insert_with(Vec::new).push(var);
425        }
426
427        // Merge equivalent variables
428        for (_, vars) in groups {
429            if vars.len() > 1 {
430                let representative = vars[0];
431                for &var in &vars[1..] {
432                    self.merge_variables(qubo, mapping, var, representative)?;
433                    qubo.stats.merged_vars += 1;
434                }
435            }
436        }
437
438        Ok(())
439    }
440
441    /// Compute signature for each variable based on its coefficients
442    fn compute_variable_signatures(&self, qubo: &CompressedQubo) -> HashMap<usize, String> {
443        let mut signatures = HashMap::new();
444
445        for var in 0..qubo.num_vars {
446            let mut sig_parts = Vec::new();
447
448            // Linear coefficient
449            let linear_coeff = *qubo.linear_terms.get(var).unwrap_or(&0.0);
450            if linear_coeff.abs() > 1e-10 {
451                sig_parts.push(format!("L:{linear_coeff:.6}"));
452            }
453
454            // Quadratic coefficients
455            let mut quad_coeffs = Vec::new();
456            for (i, j, coeff) in qubo.quadratic_terms.iter() {
457                if i == var || j == var {
458                    let other = if i == var { j } else { i };
459                    quad_coeffs.push((other, coeff));
460                }
461            }
462            quad_coeffs.sort_by_key(|&(v, _)| v);
463
464            for (other, coeff) in quad_coeffs {
465                sig_parts.push(format!("Q{other}:{coeff:.6}"));
466            }
467
468            signatures.insert(var, sig_parts.join("|"));
469        }
470
471        signatures
472    }
473
474    /// Merge two variables
475    fn merge_variables(
476        &self,
477        qubo: &mut CompressedQubo,
478        mapping: &mut ReductionMapping,
479        from_var: usize,
480        to_var: usize,
481    ) -> IsingResult<()> {
482        mapping.merge_variables(from_var, to_var);
483
484        // Merge linear terms
485        let from_coeff = *qubo.linear_terms.get(from_var).unwrap_or(&0.0);
486        qubo.linear_terms.remove(from_var);
487        if from_coeff.abs() > 1e-10 {
488            qubo.add_linear(to_var, from_coeff);
489        }
490
491        // Update quadratic terms
492        let mut updates = Vec::new();
493        let mut new_quadratic = CooMatrix::new(qubo.num_vars, qubo.num_vars);
494
495        for (i, j, coeff) in qubo.quadratic_terms.iter() {
496            if i == from_var || j == from_var {
497                let new_i = if i == from_var { to_var } else { i };
498                let new_j = if j == from_var { to_var } else { j };
499                updates.push((new_i, new_j, *coeff));
500            } else {
501                new_quadratic.set(i, j, *coeff);
502            }
503        }
504
505        // Set the new quadratic matrix
506        qubo.quadratic_terms = new_quadratic;
507
508        // Add updated terms
509        for (i, j, coeff) in updates {
510            qubo.add_quadratic(i, j, coeff);
511        }
512
513        Ok(())
514    }
515
516    /// Eliminate variables with degree 1
517    fn eliminate_degree_one_variables(
518        &self,
519        qubo: &mut CompressedQubo,
520        mapping: &mut ReductionMapping,
521    ) -> IsingResult<()> {
522        loop {
523            let mut eliminated = false;
524
525            // Find degree-1 variables
526            let degrees = self.compute_variable_degrees(qubo);
527
528            for (var, degree) in degrees {
529                if degree == 1 && !mapping.is_reduced(var) {
530                    // Find the connected variable
531                    let mut connected_var = None;
532                    let mut connection_coeff = 0.0;
533
534                    for (i, j, coeff) in qubo.quadratic_terms.iter() {
535                        if i == var {
536                            connected_var = Some(j);
537                            connection_coeff = *coeff;
538                            break;
539                        } else if j == var {
540                            connected_var = Some(i);
541                            connection_coeff = *coeff;
542                            break;
543                        }
544                    }
545
546                    if let Some(other) = connected_var {
547                        // Determine optimal value based on coefficients
548                        let linear_coeff = *qubo.linear_terms.get(var).unwrap_or(&0.0);
549
550                        // If connection is positive, variables should have opposite values
551                        // If connection is negative, variables should have same value
552                        if connection_coeff > 0.0 {
553                            // Variables should be opposite
554                            mapping.merge_variables_negated(var, other);
555                        } else {
556                            // Variables should be same
557                            mapping.merge_variables(var, other);
558                        }
559
560                        // Update QUBO
561                        qubo.linear_terms.remove(var);
562                        let mut new_quadratic = CooMatrix::new(qubo.num_vars, qubo.num_vars);
563                        for (i, j, coeff) in qubo.quadratic_terms.iter() {
564                            if i != var && j != var {
565                                new_quadratic.set(i, j, *coeff);
566                            }
567                        }
568                        qubo.quadratic_terms = new_quadratic;
569
570                        // Update linear term of connected variable
571                        if linear_coeff.abs() > 1e-10 {
572                            if connection_coeff > 0.0 {
573                                // Opposite values: adjust offset
574                                qubo.offset += linear_coeff.min(0.0);
575                                qubo.add_linear(
576                                    other,
577                                    -(linear_coeff.abs() as f64).min(connection_coeff.abs() as f64),
578                                );
579                            } else {
580                                // Same values: just add linear coefficient
581                                qubo.add_linear(other, linear_coeff);
582                            }
583                        }
584
585                        eliminated = true;
586                        qubo.stats.eliminated_vars += 1;
587                        break;
588                    }
589                }
590            }
591
592            if !eliminated {
593                break;
594            }
595        }
596
597        Ok(())
598    }
599
600    /// Compute degree of each variable
601    fn compute_variable_degrees(&self, qubo: &CompressedQubo) -> HashMap<usize, usize> {
602        let mut degrees = HashMap::new();
603
604        for (var, _) in qubo.linear_terms.iter() {
605            degrees.insert(var, 0);
606        }
607
608        for (i, j, _) in qubo.quadratic_terms.iter() {
609            *degrees.entry(i).or_insert(0) += 1;
610            *degrees.entry(j).or_insert(0) += 1;
611        }
612
613        degrees
614    }
615}
616
617/// Mapping from original to reduced variables
618#[derive(Debug, Clone)]
619pub struct ReductionMapping {
620    /// Maps original variable to (representative, negated)
621    /// If negated is true, the variable equals NOT(representative)
622    pub variable_map: HashMap<usize, (usize, bool)>,
623    /// Fixed variables
624    pub fixed_vars: HashMap<usize, bool>,
625    /// Original number of variables
626    pub original_vars: usize,
627}
628
629impl ReductionMapping {
630    /// Create a new identity mapping
631    #[must_use]
632    pub fn new(num_vars: usize) -> Self {
633        let mut variable_map = HashMap::new();
634        for i in 0..num_vars {
635            variable_map.insert(i, (i, false));
636        }
637
638        Self {
639            variable_map,
640            fixed_vars: HashMap::new(),
641            original_vars: num_vars,
642        }
643    }
644
645    /// Fix a variable to a specific value
646    pub fn fix_variable(&mut self, var: usize, value: bool) {
647        self.fixed_vars.insert(var, value);
648        self.variable_map.remove(&var);
649    }
650
651    /// Merge two variables (they take the same value)
652    pub fn merge_variables(&mut self, from: usize, to: usize) {
653        if let Some((repr, negated)) = self.variable_map.get(&to).copied() {
654            self.variable_map.insert(from, (repr, negated));
655        } else {
656            self.variable_map.insert(from, (to, false));
657        }
658    }
659
660    /// Merge two variables with negation (from = NOT(to))
661    pub fn merge_variables_negated(&mut self, from: usize, to: usize) {
662        if let Some((repr, negated)) = self.variable_map.get(&to).copied() {
663            self.variable_map.insert(from, (repr, !negated));
664        } else {
665            self.variable_map.insert(from, (to, true));
666        }
667    }
668
669    /// Check if a variable has been reduced
670    #[must_use]
671    pub fn is_reduced(&self, var: usize) -> bool {
672        self.fixed_vars.contains_key(&var)
673            || self
674                .variable_map
675                .get(&var)
676                .map_or(false, |&(repr, _)| repr != var)
677    }
678
679    /// Map solution from reduced to original variables
680    #[must_use]
681    pub fn expand_solution(&self, reduced_solution: &[bool]) -> Vec<bool> {
682        let mut solution = vec![false; self.original_vars];
683
684        // Set fixed variables
685        for (&var, &value) in &self.fixed_vars {
686            if var < solution.len() {
687                solution[var] = value;
688            }
689        }
690
691        // Set mapped variables
692        for (&var, &(repr, negated)) in &self.variable_map {
693            if var < solution.len() && repr < reduced_solution.len() {
694                solution[var] = if negated {
695                    !reduced_solution[repr]
696                } else {
697                    reduced_solution[repr]
698                };
699            }
700        }
701
702        solution
703    }
704}
705
706/// Block structure detection for QUBO matrices
707pub struct BlockDetector {
708    /// Minimum block size to consider
709    pub min_block_size: usize,
710    /// Threshold for considering blocks as independent
711    pub independence_threshold: f64,
712}
713
714impl Default for BlockDetector {
715    fn default() -> Self {
716        Self {
717            min_block_size: 3,
718            independence_threshold: 0.01,
719        }
720    }
721}
722
723impl BlockDetector {
724    /// Detect block structure in QUBO
725    #[must_use]
726    pub fn detect_blocks(&self, qubo: &CompressedQubo) -> Vec<Vec<usize>> {
727        let mut blocks = Vec::new();
728        let mut unassigned: HashSet<usize> = (0..qubo.num_vars).collect();
729
730        while !unassigned.is_empty() {
731            // Start a new block with an arbitrary unassigned variable
732            // SAFETY: The while loop guarantees unassigned is not empty
733            let start = *unassigned
734                .iter()
735                .next()
736                .expect("unassigned is not empty as checked by while loop");
737            let mut block = vec![start];
738            let mut to_process = vec![start];
739            unassigned.remove(&start);
740
741            // Grow block using BFS
742            while let Some(var) = to_process.pop() {
743                // Find connected variables
744                for (i, j, coeff) in qubo.quadratic_terms.iter() {
745                    if coeff.abs() > self.independence_threshold {
746                        let connected = if i == var && unassigned.contains(&j) {
747                            Some(j)
748                        } else if j == var && unassigned.contains(&i) {
749                            Some(i)
750                        } else {
751                            None
752                        };
753
754                        if let Some(connected_var) = connected {
755                            block.push(connected_var);
756                            to_process.push(connected_var);
757                            unassigned.remove(&connected_var);
758                        }
759                    }
760                }
761            }
762
763            if block.len() >= self.min_block_size {
764                blocks.push(block);
765            } else {
766                // Merge small blocks with the last block if possible
767                if let Some(last_block) = blocks.last_mut() {
768                    last_block.extend(block);
769                } else {
770                    blocks.push(block);
771                }
772            }
773        }
774
775        blocks
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782
783    #[test]
784    fn test_coo_compression() {
785        let matrix = vec![
786            vec![1.0, 0.0, -0.5],
787            vec![0.0, 2.0, 0.0],
788            vec![-0.5, 0.0, 3.0],
789        ];
790
791        let compressor = CooCompressor::default();
792        let compressed = compressor
793            .compress_dense(&matrix, 0.5)
794            .expect("compression should succeed for valid matrix");
795
796        assert_eq!(compressed.num_vars, 3);
797        assert_eq!(compressed.offset, 0.5);
798        assert_eq!(*compressed.linear_terms.get(0).unwrap_or(&0.0), 1.0);
799        assert_eq!(*compressed.linear_terms.get(1).unwrap_or(&0.0), 2.0);
800        assert_eq!(*compressed.linear_terms.get(2).unwrap_or(&0.0), 3.0);
801        assert_eq!(*compressed.quadratic_terms.get(0, 2).unwrap_or(&0.0), -0.5);
802    }
803
804    #[test]
805    fn test_variable_reduction() {
806        let mut qubo = CompressedQubo::new(4);
807        qubo.add_linear(0, 1e7); // Should be fixed to 0 (large positive)
808        qubo.add_linear(1, -1e7); // Should be fixed to 1 (large negative)
809        qubo.add_quadratic(2, 3, -1.0);
810
811        let reducer = VariableReducer::default();
812        let mapping = reducer
813            .reduce(&mut qubo)
814            .expect("reduction should succeed for valid QUBO");
815
816        println!("Fixed vars: {:?}", mapping.fixed_vars);
817        println!("Fixing threshold: {}", reducer.fixing_threshold);
818
819        assert!(mapping.fixed_vars.contains_key(&0));
820        assert!(!mapping.fixed_vars[&0]); // Fixed to 0
821        assert!(mapping.fixed_vars.contains_key(&1));
822        assert!(mapping.fixed_vars[&1]); // Fixed to 1
823    }
824
825    #[test]
826    fn test_block_detection() {
827        let mut qubo = CompressedQubo::new(6);
828        // Block 1: 0-1-2
829        qubo.add_quadratic(0, 1, 1.0);
830        qubo.add_quadratic(1, 2, 1.0);
831        // Block 2: 3-4-5
832        qubo.add_quadratic(3, 4, 1.0);
833        qubo.add_quadratic(4, 5, 1.0);
834
835        let detector = BlockDetector::default();
836        let blocks = detector.detect_blocks(&qubo);
837
838        assert_eq!(blocks.len(), 2);
839        assert_eq!(blocks[0].len(), 3);
840        assert_eq!(blocks[1].len(), 3);
841    }
842}