1use crate::ising::{IsingError, IsingResult};
7use std::collections::{HashMap, HashSet};
8
9#[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#[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#[derive(Debug, Clone)]
120pub struct CompressedQubo {
121 pub num_vars: usize,
123 pub linear_terms: SparseVector<f64>,
125 pub quadratic_terms: CooMatrix<f64>,
128 pub offset: f64,
130 pub stats: CompressionStats,
132}
133
134#[derive(Debug, Clone, Default)]
136pub struct CompressionStats {
137 pub original_nnz: usize,
139 pub compressed_nnz: usize,
141 pub merged_vars: usize,
143 pub eliminated_vars: usize,
145 pub compression_ratio: f64,
147}
148
149impl CompressedQubo {
150 #[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 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 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 #[must_use]
183 pub fn evaluate(&self, solution: &[bool]) -> f64 {
184 let mut energy = self.offset;
185
186 for (var, coeff) in self.linear_terms.iter() {
188 if var < solution.len() && solution[var] {
189 energy += coeff;
190 }
191 }
192
193 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 #[must_use]
205 pub fn nnz(&self) -> usize {
206 self.linear_terms.nnz() + self.quadratic_terms.nnz()
207 }
208
209 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
217pub struct CooCompressor {
219 pub zero_threshold: f64,
221 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 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 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 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 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
298pub struct VariableReducer {
300 pub fixing_threshold: f64,
302 pub enable_merging: bool,
304 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 pub fn reduce(&self, qubo: &mut CompressedQubo) -> IsingResult<ReductionMapping> {
321 let mut mapping = ReductionMapping::new(qubo.num_vars);
322
323 if self.enable_elimination {
325 self.fix_variables(qubo, &mut mapping)?;
326 }
327
328 if self.enable_merging {
330 self.merge_equivalent_variables(qubo, &mut mapping)?;
331 }
332
333 if self.enable_elimination {
335 self.eliminate_degree_one_variables(qubo, &mut mapping)?;
336 }
337
338 Ok(mapping)
339 }
340
341 fn fix_variables(
343 &self,
344 qubo: &mut CompressedQubo,
345 mapping: &mut ReductionMapping,
346 ) -> IsingResult<()> {
347 let mut to_fix = Vec::new();
348
349 for (var, coeff) in qubo.linear_terms.iter() {
351 if coeff.abs() > self.fixing_threshold {
352 let value = *coeff < 0.0;
354 to_fix.push((var, value));
355 }
356 }
357
358 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 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 if value {
379 let linear_coeff = *qubo.linear_terms.get(var).unwrap_or(&0.0);
380 qubo.offset += linear_coeff;
381
382 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 qubo.linear_terms.remove(var);
399 qubo.quadratic_terms.filter_inplace(|_| true); 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 fn merge_equivalent_variables(
414 &self,
415 qubo: &mut CompressedQubo,
416 mapping: &mut ReductionMapping,
417 ) -> IsingResult<()> {
418 let signatures = self.compute_variable_signatures(qubo);
420
421 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 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 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 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 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 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 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 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 qubo.quadratic_terms = new_quadratic;
507
508 for (i, j, coeff) in updates {
510 qubo.add_quadratic(i, j, coeff);
511 }
512
513 Ok(())
514 }
515
516 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 let degrees = self.compute_variable_degrees(qubo);
527
528 for (var, degree) in degrees {
529 if degree == 1 && !mapping.is_reduced(var) {
530 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 let linear_coeff = *qubo.linear_terms.get(var).unwrap_or(&0.0);
549
550 if connection_coeff > 0.0 {
553 mapping.merge_variables_negated(var, other);
555 } else {
556 mapping.merge_variables(var, other);
558 }
559
560 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 if linear_coeff.abs() > 1e-10 {
572 if connection_coeff > 0.0 {
573 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 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 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#[derive(Debug, Clone)]
619pub struct ReductionMapping {
620 pub variable_map: HashMap<usize, (usize, bool)>,
623 pub fixed_vars: HashMap<usize, bool>,
625 pub original_vars: usize,
627}
628
629impl ReductionMapping {
630 #[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 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 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 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 #[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 #[must_use]
681 pub fn expand_solution(&self, reduced_solution: &[bool]) -> Vec<bool> {
682 let mut solution = vec![false; self.original_vars];
683
684 for (&var, &value) in &self.fixed_vars {
686 if var < solution.len() {
687 solution[var] = value;
688 }
689 }
690
691 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
706pub struct BlockDetector {
708 pub min_block_size: usize,
710 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 #[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 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 while let Some(var) = to_process.pop() {
743 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 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); qubo.add_linear(1, -1e7); 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]); assert!(mapping.fixed_vars.contains_key(&1));
822 assert!(mapping.fixed_vars[&1]); }
824
825 #[test]
826 fn test_block_detection() {
827 let mut qubo = CompressedQubo::new(6);
828 qubo.add_quadratic(0, 1, 1.0);
830 qubo.add_quadratic(1, 2, 1.0);
831 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}