1use crate::compression::CompressedQubo;
8use crate::ising::{IsingError, IsingResult};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Clone)]
13pub struct HigherOrderTerm {
14 pub variables: Vec<usize>,
16 pub coefficient: f64,
18}
19
20impl HigherOrderTerm {
21 #[must_use]
23 pub fn new(mut variables: Vec<usize>, coefficient: f64) -> Self {
24 variables.sort_unstable();
25 variables.dedup();
26 Self {
27 variables,
28 coefficient,
29 }
30 }
31
32 #[must_use]
34 pub fn order(&self) -> usize {
35 self.variables.len()
36 }
37
38 #[must_use]
40 pub fn contains(&self, var: usize) -> bool {
41 self.variables.binary_search(&var).is_ok()
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct HoboProblem {
48 pub num_vars: usize,
50 pub linear_terms: HashMap<usize, f64>,
52 pub quadratic_terms: HashMap<(usize, usize), f64>,
54 pub higher_order_terms: Vec<HigherOrderTerm>,
56 pub offset: f64,
58}
59
60impl HoboProblem {
61 #[must_use]
63 pub fn new(num_vars: usize) -> Self {
64 Self {
65 num_vars,
66 linear_terms: HashMap::new(),
67 quadratic_terms: HashMap::new(),
68 higher_order_terms: Vec::new(),
69 offset: 0.0,
70 }
71 }
72
73 pub fn add_linear(&mut self, var: usize, coefficient: f64) {
75 if var >= self.num_vars {
76 self.num_vars = var + 1;
77 }
78 *self.linear_terms.entry(var).or_insert(0.0) += coefficient;
79 }
80
81 pub fn add_quadratic(&mut self, var1: usize, var2: usize, coefficient: f64) {
83 let max_var = var1.max(var2);
84 if max_var >= self.num_vars {
85 self.num_vars = max_var + 1;
86 }
87
88 if var1 == var2 {
89 self.add_linear(var1, coefficient);
90 } else {
91 let (i, j) = if var1 < var2 {
92 (var1, var2)
93 } else {
94 (var2, var1)
95 };
96 *self.quadratic_terms.entry((i, j)).or_insert(0.0) += coefficient;
97 }
98 }
99
100 pub fn add_higher_order(&mut self, variables: Vec<usize>, coefficient: f64) {
102 if variables.len() < 3 {
103 match variables.len() {
105 0 => self.offset += coefficient,
106 1 => self.add_linear(variables[0], coefficient),
107 2 => self.add_quadratic(variables[0], variables[1], coefficient),
108 _ => unreachable!(),
109 }
110 } else {
111 let term = HigherOrderTerm::new(variables, coefficient);
112
113 if let Some(&max_var) = term.variables.last() {
115 if max_var >= self.num_vars {
116 self.num_vars = max_var + 1;
117 }
118 }
119
120 self.higher_order_terms.push(term);
121 }
122 }
123
124 pub fn max_order(&self) -> usize {
126 self.higher_order_terms
127 .iter()
128 .map(HigherOrderTerm::order)
129 .max()
130 .unwrap_or(2)
131 .max(if self.quadratic_terms.is_empty() {
132 0
133 } else {
134 2
135 })
136 .max(usize::from(!self.linear_terms.is_empty()))
137 }
138
139 #[must_use]
141 pub fn is_qubo(&self) -> bool {
142 self.higher_order_terms.is_empty()
143 }
144
145 pub fn to_qubo(&self, reduction_method: ReductionMethod) -> IsingResult<QuboReduction> {
147 match reduction_method {
148 ReductionMethod::SubstitutionMethod => self.reduce_by_substitution(),
149 ReductionMethod::MinimumVertexCover => self.reduce_by_mvc(),
150 ReductionMethod::BooleanProduct => self.reduce_by_boolean_product(),
151 }
152 }
153
154 fn reduce_by_substitution(&self) -> IsingResult<QuboReduction> {
156 let mut reduction = QuboReduction::new(self.num_vars);
157
158 for (&var, &coeff) in &self.linear_terms {
160 reduction.qubo.add_linear(var, coeff);
161 }
162 for (&(i, j), &coeff) in &self.quadratic_terms {
163 reduction.qubo.add_quadratic(i, j, coeff);
164 }
165 reduction.qubo.offset = self.offset;
166
167 for hot in &self.higher_order_terms {
169 if hot.order() < 3 {
170 continue;
171 }
172
173 self.reduce_term_substitution(hot, &mut reduction)?;
175 }
176
177 Ok(reduction)
178 }
179
180 fn reduce_term_substitution(
182 &self,
183 term: &HigherOrderTerm,
184 reduction: &mut QuboReduction,
185 ) -> IsingResult<()> {
186 let mut current_vars = term.variables.clone();
187 let mut current_coeff = term.coefficient;
188
189 while current_vars.len() > 2 {
191 let v1 = current_vars[0];
193 let v2 = current_vars[1];
194
195 let aux_var = reduction.qubo.num_vars;
197 reduction.qubo.num_vars += 1;
198
199 reduction.auxiliary_vars.push(AuxiliaryVariable {
201 index: aux_var,
202 reduction_type: ReductionType::Product(v1, v2),
203 penalty_weight: 3.0 * current_coeff.abs(),
204 });
205
206 let penalty = 3.0 * current_coeff.abs();
208 reduction.qubo.add_quadratic(v1, v2, penalty * 3.0);
209 reduction.qubo.add_quadratic(v1, aux_var, -penalty * 2.0);
210 reduction.qubo.add_quadratic(v2, aux_var, -penalty * 2.0);
211 reduction.qubo.add_linear(aux_var, penalty);
212
213 current_vars = current_vars[2..].to_vec();
215 current_vars.push(aux_var);
216 current_vars.sort_unstable();
217 }
218
219 if current_vars.len() == 2 {
221 reduction
222 .qubo
223 .add_quadratic(current_vars[0], current_vars[1], current_coeff);
224 } else if current_vars.len() == 1 {
225 reduction.qubo.add_linear(current_vars[0], current_coeff);
226 }
227
228 Ok(())
229 }
230
231 fn reduce_by_mvc(&self) -> IsingResult<QuboReduction> {
233 self.reduce_by_substitution()
237 }
238
239 fn reduce_by_boolean_product(&self) -> IsingResult<QuboReduction> {
241 let mut reduction = QuboReduction::new(self.num_vars);
242
243 for (&var, &coeff) in &self.linear_terms {
245 reduction.qubo.add_linear(var, coeff);
246 }
247 for (&(i, j), &coeff) in &self.quadratic_terms {
248 reduction.qubo.add_quadratic(i, j, coeff);
249 }
250 reduction.qubo.offset = self.offset;
251
252 for hot in &self.higher_order_terms {
254 if hot.order() < 3 {
255 continue;
256 }
257
258 let aux_var = reduction.qubo.num_vars;
260 reduction.qubo.num_vars += 1;
261
262 reduction.auxiliary_vars.push(AuxiliaryVariable {
264 index: aux_var,
265 reduction_type: ReductionType::MultiProduct(hot.variables.clone()),
266 penalty_weight: hot.coefficient.abs() * (hot.order() as f64),
267 });
268
269 let k = hot.order();
274 let penalty = hot.coefficient.abs() * (k as f64);
275
276 reduction.qubo.add_linear(aux_var, penalty * (k - 1) as f64);
278
279 for &var in &hot.variables {
280 reduction.qubo.add_linear(var, penalty);
281 reduction.qubo.add_quadratic(aux_var, var, -2.0 * penalty);
282 }
283
284 reduction.qubo.add_linear(aux_var, hot.coefficient);
286 }
287
288 Ok(reduction)
289 }
290
291 #[must_use]
293 pub fn evaluate(&self, solution: &[bool]) -> f64 {
294 let mut energy = self.offset;
295
296 for (&var, &coeff) in &self.linear_terms {
298 if var < solution.len() && solution[var] {
299 energy += coeff;
300 }
301 }
302
303 for (&(i, j), &coeff) in &self.quadratic_terms {
305 if i < solution.len() && j < solution.len() && solution[i] && solution[j] {
306 energy += coeff;
307 }
308 }
309
310 for term in &self.higher_order_terms {
312 let mut product = true;
313 for &var in &term.variables {
314 if var >= solution.len() || !solution[var] {
315 product = false;
316 break;
317 }
318 }
319 if product {
320 energy += term.coefficient;
321 }
322 }
323
324 energy
325 }
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum ReductionMethod {
331 SubstitutionMethod,
333 MinimumVertexCover,
335 BooleanProduct,
337}
338
339#[derive(Debug, Clone)]
341pub enum ReductionType {
342 Product(usize, usize),
344 MultiProduct(Vec<usize>),
346 Custom(String),
348}
349
350#[derive(Debug, Clone)]
352pub struct AuxiliaryVariable {
353 pub index: usize,
355 pub reduction_type: ReductionType,
357 pub penalty_weight: f64,
359}
360
361#[derive(Debug, Clone)]
363pub struct QuboReduction {
364 pub qubo: CompressedQubo,
366 pub auxiliary_vars: Vec<AuxiliaryVariable>,
368 pub variable_mapping: HashMap<usize, usize>,
370}
371
372impl QuboReduction {
373 fn new(original_vars: usize) -> Self {
375 let mut variable_mapping = HashMap::new();
376 for i in 0..original_vars {
377 variable_mapping.insert(i, i);
378 }
379
380 Self {
381 qubo: CompressedQubo::new(original_vars),
382 auxiliary_vars: Vec::new(),
383 variable_mapping,
384 }
385 }
386
387 #[must_use]
389 pub fn extract_original_solution(&self, qubo_solution: &[bool]) -> Vec<bool> {
390 let mut original_solution = vec![false; self.variable_mapping.len()];
391
392 for (&orig_var, &new_var) in &self.variable_mapping {
393 if new_var < qubo_solution.len() {
394 original_solution[orig_var] = qubo_solution[new_var];
395 }
396 }
397
398 original_solution
399 }
400
401 #[must_use]
403 pub fn verify_constraints(&self, solution: &[bool]) -> ConstraintViolations {
404 let mut violations = ConstraintViolations::default();
405
406 for aux in &self.auxiliary_vars {
407 if aux.index >= solution.len() {
408 violations.missing_variables += 1;
409 continue;
410 }
411
412 let aux_value = solution[aux.index];
413
414 match &aux.reduction_type {
415 ReductionType::Product(v1, v2) => {
416 if *v1 < solution.len() && *v2 < solution.len() {
417 let expected = solution[*v1] && solution[*v2];
418 if aux_value != expected {
419 violations.product_violations += 1;
420 }
421 }
422 }
423 ReductionType::MultiProduct(vars) => {
424 let expected = vars.iter().all(|&v| v < solution.len() && solution[v]);
425 if aux_value != expected {
426 violations.multi_product_violations += 1;
427 }
428 }
429 ReductionType::Custom(_) => {}
430 }
431 }
432
433 violations.total = violations.product_violations
434 + violations.multi_product_violations
435 + violations.missing_variables;
436
437 violations
438 }
439}
440
441#[derive(Debug, Clone, Default)]
443pub struct ConstraintViolations {
444 pub total: usize,
446 pub product_violations: usize,
448 pub multi_product_violations: usize,
450 pub missing_variables: usize,
452}
453
454pub struct HoboAnalyzer;
456
457impl HoboAnalyzer {
458 #[must_use]
460 pub fn analyze(problem: &HoboProblem) -> HoboStats {
461 let mut stats = HoboStats::default();
462
463 stats.num_variables = problem.num_vars;
464 stats.num_linear_terms = problem.linear_terms.len();
465 stats.num_quadratic_terms = problem.quadratic_terms.len();
466 stats.num_higher_order_terms = problem.higher_order_terms.len();
467
468 let mut order_counts = HashMap::new();
470 for term in &problem.higher_order_terms {
471 *order_counts.entry(term.order()).or_insert(0) += 1;
472 }
473
474 if !order_counts.is_empty() {
475 stats.max_order = order_counts.keys().copied().max().unwrap_or(0);
477 stats.order_distribution = order_counts;
478 } else if !problem.quadratic_terms.is_empty() {
479 stats.max_order = 2;
480 } else if !problem.linear_terms.is_empty() {
481 stats.max_order = 1;
482 }
483
484 stats.estimated_aux_vars_substitution = problem
486 .higher_order_terms
487 .iter()
488 .map(|term| term.order().saturating_sub(2))
489 .sum();
490
491 stats.estimated_aux_vars_boolean = problem.higher_order_terms.len();
492
493 stats
494 }
495}
496
497#[derive(Debug, Clone, Default)]
499pub struct HoboStats {
500 pub num_variables: usize,
502 pub num_linear_terms: usize,
504 pub num_quadratic_terms: usize,
506 pub num_higher_order_terms: usize,
508 pub max_order: usize,
510 pub order_distribution: HashMap<usize, usize>,
512 pub estimated_aux_vars_substitution: usize,
514 pub estimated_aux_vars_boolean: usize,
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn test_hobo_creation() {
524 let mut hobo = HoboProblem::new(4);
525
526 hobo.add_linear(0, 1.0);
528 hobo.add_quadratic(0, 1, -2.0);
529 hobo.add_higher_order(vec![0, 1, 2], 3.0);
530 hobo.add_higher_order(vec![1, 2, 3], -1.5);
531
532 assert_eq!(hobo.num_vars, 4);
533 assert_eq!(hobo.linear_terms.len(), 1);
534 assert_eq!(hobo.quadratic_terms.len(), 1);
535 assert_eq!(hobo.higher_order_terms.len(), 2);
536 assert_eq!(hobo.max_order(), 3);
537 }
538
539 #[test]
540 fn test_substitution_reduction() {
541 let mut hobo = HoboProblem::new(3);
542 hobo.add_higher_order(vec![0, 1, 2], 1.0);
543
544 let reduction = hobo
545 .to_qubo(ReductionMethod::SubstitutionMethod)
546 .expect("substitution reduction should succeed");
547
548 assert_eq!(reduction.auxiliary_vars.len(), 1);
550 assert_eq!(reduction.qubo.num_vars, 4); let test_solution = vec![true, true, true, true]; let violations = reduction.verify_constraints(&test_solution);
555 assert_eq!(violations.total, 0);
556 }
557
558 #[test]
559 fn test_boolean_product_reduction() {
560 let mut hobo = HoboProblem::new(4);
561 hobo.add_higher_order(vec![0, 1, 2, 3], 2.0);
562
563 let reduction = hobo
564 .to_qubo(ReductionMethod::BooleanProduct)
565 .expect("boolean product reduction should succeed");
566
567 assert_eq!(reduction.auxiliary_vars.len(), 1);
569 assert_eq!(reduction.qubo.num_vars, 5); }
571
572 #[test]
573 fn test_hobo_evaluation() {
574 let mut hobo = HoboProblem::new(3);
575 hobo.add_linear(0, 1.0);
576 hobo.add_quadratic(0, 1, -2.0);
577 hobo.add_higher_order(vec![0, 1, 2], 3.0);
578
579 assert_eq!(hobo.evaluate(&[false, false, false]), 0.0);
581 assert_eq!(hobo.evaluate(&[true, false, false]), 1.0);
582 assert_eq!(hobo.evaluate(&[true, true, false]), 1.0 - 2.0);
583 assert_eq!(hobo.evaluate(&[true, true, true]), 1.0 - 2.0 + 3.0);
584 }
585
586 #[test]
587 fn test_hobo_analyzer() {
588 let mut hobo = HoboProblem::new(5);
589 hobo.add_linear(0, 1.0);
590 hobo.add_quadratic(0, 1, -1.0);
591 hobo.add_higher_order(vec![0, 1, 2], 1.0);
592 hobo.add_higher_order(vec![1, 2, 3], 1.0);
593 hobo.add_higher_order(vec![0, 1, 2, 3, 4], 1.0);
594
595 let stats = HoboAnalyzer::analyze(&hobo);
596
597 assert_eq!(stats.num_variables, 5);
598 assert_eq!(stats.num_higher_order_terms, 3);
599 assert_eq!(stats.max_order, 5);
600 assert_eq!(stats.estimated_aux_vars_substitution, 5); assert_eq!(stats.estimated_aux_vars_boolean, 3);
602 }
603}