1use crate::error::{OptimizeError, OptimizeResult};
47
48#[non_exhaustive]
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum VariableSelectionStrategy {
57 #[default]
60 MostFractional,
61 FirstFractional,
64 DeepestCut,
67}
68
69#[derive(Debug, Clone)]
73pub struct LiftProjectConfig {
74 pub max_cuts: usize,
77 pub variable_selection: VariableSelectionStrategy,
79 pub cut_violation_tol: f64,
83 pub ls_strengthening: bool,
87 pub int_tol: f64,
91 pub max_rows_per_var: usize,
95}
96
97impl Default for LiftProjectConfig {
98 fn default() -> Self {
99 LiftProjectConfig {
100 max_cuts: 50,
101 variable_selection: VariableSelectionStrategy::MostFractional,
102 cut_violation_tol: 1e-6,
103 ls_strengthening: false,
104 int_tol: 1e-8,
105 max_rows_per_var: 1000,
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
117pub struct LiftProjectCut {
118 pub pi: Vec<f64>,
120 pub pi0: f64,
123 pub source_var: usize,
125 pub source_row: usize,
128 pub violation: f64,
130}
131
132pub struct LiftProjectGenerator {
156 config: LiftProjectConfig,
157}
158
159impl LiftProjectGenerator {
160 pub fn new(config: LiftProjectConfig) -> Self {
162 LiftProjectGenerator { config }
163 }
164
165 pub fn default_generator() -> Self {
167 LiftProjectGenerator::new(LiftProjectConfig::default())
168 }
169
170 pub fn generate_cuts(
186 &self,
187 a: &[Vec<f64>],
188 b: &[f64],
189 x_bar: &[f64],
190 integer_vars: &[usize],
191 ) -> OptimizeResult<Vec<LiftProjectCut>> {
192 let n = x_bar.len();
193 if n == 0 {
194 return Err(OptimizeError::InvalidInput(
195 "x_bar must be non-empty".to_string(),
196 ));
197 }
198 if a.len() != b.len() {
199 return Err(OptimizeError::InvalidInput(format!(
200 "Constraint matrix has {} rows but b has {} entries",
201 a.len(),
202 b.len()
203 )));
204 }
205 for (i, row) in a.iter().enumerate() {
206 if row.len() != n {
207 return Err(OptimizeError::InvalidInput(format!(
208 "Row {} has {} columns but x_bar has {} components",
209 i,
210 row.len(),
211 n
212 )));
213 }
214 }
215
216 let (a_aug, b_aug) = build_augmented_constraints(a, b, x_bar, integer_vars);
218
219 let fractional_vars: Vec<usize> = integer_vars
221 .iter()
222 .copied()
223 .filter(|&j| {
224 j < n && {
225 let xj = x_bar[j];
226 xj > self.config.int_tol && xj < 1.0 - self.config.int_tol
227 }
228 })
229 .collect();
230
231 if fractional_vars.is_empty() {
232 return Ok(Vec::new());
233 }
234
235 let mut all_cuts: Vec<LiftProjectCut> = Vec::new();
236
237 match self.config.variable_selection {
238 VariableSelectionStrategy::MostFractional => {
239 let mut ranked: Vec<(usize, f64)> = fractional_vars
240 .iter()
241 .map(|&j| {
242 let frac = x_bar[j];
243 let dist = frac.min(1.0 - frac);
244 (j, dist)
245 })
246 .collect();
247 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
248 for (j, _) in ranked {
249 if all_cuts.len() >= self.config.max_cuts {
250 break;
251 }
252 self.append_cuts_for_var(&a_aug, &b_aug, x_bar, j, &mut all_cuts)?;
253 }
254 }
255 VariableSelectionStrategy::FirstFractional => {
256 for &j in &fractional_vars {
257 if all_cuts.len() >= self.config.max_cuts {
258 break;
259 }
260 self.append_cuts_for_var(&a_aug, &b_aug, x_bar, j, &mut all_cuts)?;
261 }
262 }
263 VariableSelectionStrategy::DeepestCut => {
264 let mut candidates: Vec<LiftProjectCut> = Vec::new();
265 for &j in &fractional_vars {
266 let mut tmp: Vec<LiftProjectCut> = Vec::new();
267 self.append_cuts_for_var(&a_aug, &b_aug, x_bar, j, &mut tmp)?;
268 if let Some(best) = tmp.into_iter().max_by(|c1, c2| {
269 c1.violation
270 .partial_cmp(&c2.violation)
271 .unwrap_or(std::cmp::Ordering::Equal)
272 }) {
273 candidates.push(best);
274 }
275 }
276 candidates.sort_by(|c1, c2| {
277 c2.violation
278 .partial_cmp(&c1.violation)
279 .unwrap_or(std::cmp::Ordering::Equal)
280 });
281 candidates.truncate(self.config.max_cuts);
282 all_cuts = candidates;
283 }
284 }
285
286 all_cuts.sort_by(|c1, c2| {
287 c2.violation
288 .partial_cmp(&c1.violation)
289 .unwrap_or(std::cmp::Ordering::Equal)
290 });
291
292 Ok(all_cuts)
293 }
294
295 pub fn select_variable(&self, x_bar: &[f64], integer_vars: &[usize]) -> Option<usize> {
299 let n = x_bar.len();
300 match self.config.variable_selection {
301 VariableSelectionStrategy::FirstFractional => integer_vars.iter().copied().find(|&j| {
302 j < n && x_bar[j] > self.config.int_tol && x_bar[j] < 1.0 - self.config.int_tol
303 }),
304 VariableSelectionStrategy::MostFractional | VariableSelectionStrategy::DeepestCut => {
305 let mut best_idx = None;
306 let mut best_dist = -1.0_f64;
307 for &j in integer_vars {
308 if j >= n {
309 continue;
310 }
311 let xj = x_bar[j];
312 if xj > self.config.int_tol && xj < 1.0 - self.config.int_tol {
313 let dist = xj.min(1.0 - xj);
314 if dist > best_dist {
315 best_dist = dist;
316 best_idx = Some(j);
317 }
318 }
319 }
320 best_idx
321 }
322 }
323 }
324
325 pub fn generate_cut_for_var(
334 &self,
335 a: &[Vec<f64>],
336 b: &[f64],
337 x_bar: &[f64],
338 j: usize,
339 ) -> Option<LiftProjectCut> {
340 let f_j = x_bar[j];
341 if f_j <= self.config.int_tol || f_j >= 1.0 - self.config.int_tol {
342 return None;
343 }
344 let n = x_bar.len();
347 let integer_vars_for_j: Vec<usize> = (0..n).collect();
348 let (a_aug, b_aug) = build_augmented_constraints(a, b, x_bar, &integer_vars_for_j);
349 self.best_cut_from_rows(&a_aug, &b_aug, x_bar, j)
350 }
351
352 pub fn cut_violation(&self, cut: &LiftProjectCut, x_bar: &[f64]) -> f64 {
357 cut.pi
358 .iter()
359 .zip(x_bar.iter())
360 .map(|(&pi_k, &xk)| pi_k * xk)
361 .sum::<f64>()
362 - cut.pi0
363 }
364
365 fn bcc_cut_from_row(
371 &self,
372 row: &[f64],
373 bi: f64,
374 x_bar: &[f64],
375 j: usize,
376 row_index: usize,
377 ) -> Option<LiftProjectCut> {
378 let a_ij = row[j];
379 if a_ij.abs() < 1e-12 {
380 return None;
381 }
382 let dot_ax: f64 = row
383 .iter()
384 .zip(x_bar.iter())
385 .map(|(&aik, &xk)| aik * xk)
386 .sum();
387 let r_i = bi - dot_ax; let f_j = x_bar[j];
389
390 let violation = if a_ij > 0.0 {
393 r_i * f_j / (1.0 - f_j)
394 } else {
395 r_i * (1.0 - f_j) / f_j
396 };
397
398 if violation <= self.config.cut_violation_tol {
399 return None;
400 }
401
402 let pi0 = dot_ax - violation;
403
404 Some(LiftProjectCut {
405 pi: row.to_vec(),
406 pi0,
407 source_var: j,
408 source_row: row_index,
409 violation,
410 })
411 }
412
413 fn best_cut_from_rows(
415 &self,
416 a: &[Vec<f64>],
417 b: &[f64],
418 x_bar: &[f64],
419 j: usize,
420 ) -> Option<LiftProjectCut> {
421 let mut best: Option<LiftProjectCut> = None;
422 let row_limit = self.config.max_rows_per_var.min(a.len());
423 for (i, (row, &bi)) in a.iter().zip(b.iter()).enumerate().take(row_limit) {
424 if let Some(cut) = self.bcc_cut_from_row(row, bi, x_bar, j, i) {
425 let better = best
426 .as_ref()
427 .map_or(true, |prev| cut.violation > prev.violation);
428 if better {
429 best = Some(cut);
430 }
431 }
432 }
433 best
434 }
435
436 fn append_cuts_for_var(
438 &self,
439 a: &[Vec<f64>],
440 b: &[f64],
441 x_bar: &[f64],
442 j: usize,
443 out: &mut Vec<LiftProjectCut>,
444 ) -> OptimizeResult<()> {
445 let f_j = x_bar[j];
446 if f_j <= self.config.int_tol || f_j >= 1.0 - self.config.int_tol {
447 return Ok(());
448 }
449 let row_limit = self.config.max_rows_per_var.min(a.len());
450 for (i, (row, &bi)) in a.iter().zip(b.iter()).enumerate().take(row_limit) {
451 if out.len() >= self.config.max_cuts {
452 break;
453 }
454 if let Some(cut) = self.bcc_cut_from_row(row, bi, x_bar, j, i) {
455 out.push(cut);
456 }
457 }
458 Ok(())
459 }
460}
461
462fn build_augmented_constraints(
484 a: &[Vec<f64>],
485 b: &[f64],
486 x_bar: &[f64],
487 integer_vars: &[usize],
488) -> (Vec<Vec<f64>>, Vec<f64>) {
489 let n = x_bar.len();
490 let mut a_aug: Vec<Vec<f64>> = a.to_vec();
491 let mut b_aug: Vec<f64> = b.to_vec();
492
493 for &k in integer_vars {
494 if k >= n {
495 continue;
496 }
497 let mut ub_row = vec![0.0; n];
499 ub_row[k] = 1.0;
500 a_aug.push(ub_row);
501 b_aug.push(1.0);
502
503 let mut lb_row = vec![0.0; n];
505 lb_row[k] = -1.0;
506 a_aug.push(lb_row);
507 b_aug.push(0.0);
508 }
509
510 (a_aug, b_aug)
511}
512
513pub fn ls_strengthen(
529 cut: &LiftProjectCut,
530 x_bar: &[f64],
531 integer_vars: &[usize],
532 j: usize,
533) -> LiftProjectCut {
534 let n = cut.pi.len();
535 let f_j = if j < x_bar.len() { x_bar[j] } else { 0.5 };
536
537 let mut new_pi = cut.pi.clone();
538 let mut delta_pi0 = 0.0_f64;
539
540 for &k in integer_vars {
541 if k >= n || k == j {
542 continue;
543 }
544 let x_k = if k < x_bar.len() { x_bar[k] } else { continue };
545 let pi_k = cut.pi[k];
546 if pi_k > 0.0 && x_k > f_j {
549 let tightening = pi_k * (x_k - f_j) * f_j;
550 delta_pi0 += tightening;
551 let scale_denom = x_k + 1e-12;
552 new_pi[k] = pi_k + tightening / scale_denom;
553 }
554 }
555
556 LiftProjectCut {
557 pi: new_pi,
558 pi0: cut.pi0 + delta_pi0,
559 source_var: cut.source_var,
560 source_row: cut.source_row,
561 violation: cut.violation - delta_pi0,
562 }
563}
564
565pub fn cut_satisfied_at_integer(cut: &LiftProjectCut, x: &[f64]) -> bool {
571 let dot: f64 = cut
572 .pi
573 .iter()
574 .zip(x.iter())
575 .map(|(&pi_k, &xk)| pi_k * xk)
576 .sum();
577 dot >= cut.pi0 - 1e-9
578}
579
580#[cfg(test)]
585mod tests {
586 use super::*;
587
588 fn simple_constraints() -> (Vec<Vec<f64>>, Vec<f64>) {
593 let a = vec![vec![1.0, 1.0]];
594 let b = vec![1.0];
595 (a, b)
596 }
597
598 fn simple_x_bar() -> Vec<f64> {
599 vec![0.5, 0.5]
600 }
601
602 fn simple_integer_vars() -> Vec<usize> {
603 vec![0, 1]
604 }
605
606 #[test]
609 fn test_most_fractional_selects_closest_to_half() {
610 let config = LiftProjectConfig {
611 variable_selection: VariableSelectionStrategy::MostFractional,
612 ..Default::default()
613 };
614 let gen = LiftProjectGenerator::new(config);
615 let x_bar = vec![0.2, 0.5, 0.4];
617 let integer_vars = vec![0, 1, 2];
618 let selected = gen.select_variable(&x_bar, &integer_vars);
619 assert_eq!(selected, Some(1));
621 }
622
623 #[test]
624 fn test_first_fractional_selects_first() {
625 let config = LiftProjectConfig {
626 variable_selection: VariableSelectionStrategy::FirstFractional,
627 ..Default::default()
628 };
629 let gen = LiftProjectGenerator::new(config);
630 let x_bar = vec![0.8, 0.5, 0.3];
631 let integer_vars = vec![0, 1, 2];
632 let selected = gen.select_variable(&x_bar, &integer_vars);
633 assert_eq!(selected, Some(0));
634 }
635
636 #[test]
637 fn test_select_variable_returns_none_when_all_integer() {
638 let config = LiftProjectConfig::default();
639 let gen = LiftProjectGenerator::new(config);
640 let x_bar = vec![0.0, 1.0, 0.0, 1.0];
642 let integer_vars = vec![0, 1, 2, 3];
643 assert_eq!(gen.select_variable(&x_bar, &integer_vars), None);
644 }
645
646 #[test]
647 fn test_select_variable_skips_continuous_vars() {
648 let config = LiftProjectConfig {
649 variable_selection: VariableSelectionStrategy::MostFractional,
650 ..Default::default()
651 };
652 let gen = LiftProjectGenerator::new(config);
653 let x_bar = vec![0.3, 0.5, 0.4];
654 let integer_vars = vec![2];
656 let selected = gen.select_variable(&x_bar, &integer_vars);
657 assert_eq!(selected, Some(2));
658 }
659
660 #[test]
663 fn test_generate_cuts_empty_for_integer_solution() {
664 let gen = LiftProjectGenerator::default_generator();
665 let (a, b) = simple_constraints();
666 let x_bar = vec![1.0, 0.0]; let integer_vars = simple_integer_vars();
668 let cuts = gen.generate_cuts(&a, &b, &x_bar, &integer_vars).unwrap();
669 assert!(cuts.is_empty());
670 }
671
672 #[test]
673 fn test_generate_cuts_empty_for_no_integer_vars() {
674 let gen = LiftProjectGenerator::default_generator();
675 let (a, b) = simple_constraints();
676 let x_bar = vec![0.5, 0.5];
677 let cuts = gen.generate_cuts(&a, &b, &x_bar, &[]).unwrap();
678 assert!(cuts.is_empty());
679 }
680
681 #[test]
684 fn test_cuts_violated_at_x_bar() {
685 let gen = LiftProjectGenerator::default_generator();
686 let (a, b) = simple_constraints();
687 let x_bar = simple_x_bar();
688 let integer_vars = simple_integer_vars();
689 let cuts = gen.generate_cuts(&a, &b, &x_bar, &integer_vars).unwrap();
690 assert!(!cuts.is_empty(), "Expected at least one cut");
691 for cut in &cuts {
692 let violation = gen.cut_violation(cut, &x_bar);
693 assert!(
694 violation > gen.config.cut_violation_tol,
695 "Cut should be violated at x̄, got violation = {}",
696 violation
697 );
698 }
699 }
700
701 #[test]
702 fn test_cut_violation_is_positive_at_x_bar() {
703 let gen = LiftProjectGenerator::default_generator();
704 let (a, b) = simple_constraints();
705 let x_bar = simple_x_bar();
706 let cut = gen
707 .generate_cut_for_var(&a, &b, &x_bar, 0)
708 .expect("Should generate a cut for variable 0");
709 assert!(
710 cut.violation > 0.0,
711 "violation field should be positive, got {}",
712 cut.violation
713 );
714 let v2 = gen.cut_violation(&cut, &x_bar);
716 assert!(
717 (cut.violation - v2).abs() < 1e-12,
718 "violation field and cut_violation must agree: {} vs {}",
719 cut.violation,
720 v2
721 );
722 }
723
724 #[test]
727 fn test_cuts_satisfied_at_zero_vector() {
728 let gen = LiftProjectGenerator::default_generator();
729 let (a, b) = simple_constraints();
730 let x_bar = simple_x_bar();
731 let integer_vars = simple_integer_vars();
732 let cuts = gen.generate_cuts(&a, &b, &x_bar, &integer_vars).unwrap();
733 let zero = vec![0.0, 0.0];
734 for cut in &cuts {
735 let dot: f64 = cut.pi.iter().zip(zero.iter()).map(|(&p, &x)| p * x).sum();
736 assert!(
737 cut_satisfied_at_integer(cut, &zero),
738 "Cut should hold at x=(0,0): π·x={:.6} ≥ π₀={:.6}",
739 dot,
740 cut.pi0
741 );
742 }
743 }
744
745 #[test]
746 fn test_cuts_satisfied_at_ones_vector() {
747 let a = vec![vec![1.0, 1.0]];
749 let b = vec![2.0];
750 let x_bar = vec![0.4, 0.6];
751 let gen = LiftProjectGenerator::default_generator();
752 let cuts = gen.generate_cuts(&a, &b, &x_bar, &[0, 1]).unwrap();
753 let ones = vec![1.0, 1.0];
754 for cut in &cuts {
755 assert!(
756 cut_satisfied_at_integer(cut, &ones),
757 "Cut should hold at x=(1,1): π·x={:.6} ≥ π₀={:.6}",
758 cut.pi
759 .iter()
760 .zip(ones.iter())
761 .map(|(&p, &x)| p * x)
762 .sum::<f64>(),
763 cut.pi0
764 );
765 }
766 }
767
768 #[test]
769 fn test_cuts_satisfied_at_unit_vectors() {
770 let gen = LiftProjectGenerator::default_generator();
771 let (a, b) = simple_constraints();
772 let x_bar = simple_x_bar();
773 let integer_vars = simple_integer_vars();
774 let cuts = gen.generate_cuts(&a, &b, &x_bar, &integer_vars).unwrap();
775 let e0 = vec![1.0, 0.0];
776 let e1 = vec![0.0, 1.0];
777 for cut in &cuts {
778 assert!(cut_satisfied_at_integer(cut, &e0), "Cut should hold at e0");
779 assert!(cut_satisfied_at_integer(cut, &e1), "Cut should hold at e1");
780 }
781 }
782
783 #[test]
786 fn test_max_cuts_limits_output() {
787 let config = LiftProjectConfig {
788 max_cuts: 1,
789 ..Default::default()
790 };
791 let gen = LiftProjectGenerator::new(config);
792 let a = vec![
793 vec![1.0, 0.0],
794 vec![0.0, 1.0],
795 vec![1.0, 1.0],
796 vec![2.0, 1.0],
797 ];
798 let b = vec![1.0, 1.0, 1.5, 2.0];
799 let x_bar = vec![0.4, 0.6];
800 let cuts = gen.generate_cuts(&a, &b, &x_bar, &[0, 1]).unwrap();
801 assert!(
802 cuts.len() <= 1,
803 "Expected at most 1 cut, got {}",
804 cuts.len()
805 );
806 }
807
808 #[test]
809 fn test_zero_max_cuts_returns_empty() {
810 let config = LiftProjectConfig {
811 max_cuts: 0,
812 ..Default::default()
813 };
814 let gen = LiftProjectGenerator::new(config);
815 let (a, b) = simple_constraints();
816 let x_bar = simple_x_bar();
817 let cuts = gen.generate_cuts(&a, &b, &x_bar, &[0, 1]).unwrap();
818 assert!(cuts.is_empty());
819 }
820
821 #[test]
824 fn test_generate_cuts_error_on_empty_x_bar() {
825 let gen = LiftProjectGenerator::default_generator();
826 let result = gen.generate_cuts(&[], &[], &[], &[]);
827 assert!(result.is_err());
828 }
829
830 #[test]
831 fn test_generate_cuts_error_on_mismatched_a_b() {
832 let gen = LiftProjectGenerator::default_generator();
833 let a = vec![vec![1.0, 1.0], vec![1.0, 0.0]];
834 let b = vec![1.0]; let result = gen.generate_cuts(&a, &b, &[0.5, 0.5], &[0, 1]);
836 assert!(result.is_err());
837 }
838
839 #[test]
840 fn test_generate_cuts_error_on_row_length_mismatch() {
841 let gen = LiftProjectGenerator::default_generator();
842 let a = vec![vec![1.0, 1.0, 0.5]]; let b = vec![1.0];
844 let x_bar = vec![0.5, 0.5]; let result = gen.generate_cuts(&a, &b, &x_bar, &[0, 1]);
846 assert!(result.is_err());
847 }
848
849 #[test]
852 fn test_deepest_cut_strategy_returns_most_violated() {
853 let config = LiftProjectConfig {
854 variable_selection: VariableSelectionStrategy::DeepestCut,
855 max_cuts: 10,
856 ..Default::default()
857 };
858 let gen = LiftProjectGenerator::new(config);
859 let a = vec![vec![1.0, 1.0], vec![2.0, 1.0], vec![1.0, 2.0]];
860 let b = vec![1.5, 2.0, 2.0];
861 let x_bar = vec![0.4, 0.6];
862 let cuts = gen.generate_cuts(&a, &b, &x_bar, &[0, 1]).unwrap();
863 for w in cuts.windows(2) {
865 assert!(
866 w[0].violation >= w[1].violation - 1e-12,
867 "Cuts should be sorted by decreasing violation"
868 );
869 }
870 }
871
872 #[test]
875 fn test_generate_cut_for_var_negative_coefficient() {
876 let gen = LiftProjectGenerator::default_generator();
877 let a = vec![vec![-1.0, 1.0]];
880 let b = vec![0.5];
881 let x_bar = vec![0.3, 0.7];
882 let cut = gen.generate_cut_for_var(&a, &b, &x_bar, 0);
883 if let Some(c) = cut {
885 assert_eq!(c.pi.len(), 2);
886 assert!(c.violation >= 0.0);
887 }
888 }
889
890 #[test]
891 fn test_generate_cut_for_var_no_cut_when_no_structural_row_has_coeff_but_bound_exists() {
892 let gen = LiftProjectGenerator::default_generator();
893 let a = vec![vec![0.0, 1.0]];
895 let b = vec![0.8];
896 let x_bar = vec![0.4, 0.6];
897 let cut = gen.generate_cut_for_var(&a, &b, &x_bar, 0);
900 assert!(
902 cut.is_some(),
903 "Should get a cut from the bound row for variable 0"
904 );
905 }
906
907 #[test]
910 fn test_ls_strengthen_does_not_decrease_coefficients_to_negative() {
911 let gen = LiftProjectGenerator::default_generator();
912 let (a, b) = simple_constraints();
913 let x_bar = simple_x_bar();
914 let cut = gen
915 .generate_cut_for_var(&a, &b, &x_bar, 0)
916 .expect("Should generate a cut for variable 0");
917 let strengthened = ls_strengthen(&cut, &x_bar, &[0, 1], 0);
918 for &pi_k in &strengthened.pi {
920 assert!(
921 pi_k >= 0.0 - 1e-12,
922 "Coefficient should not become negative: {}",
923 pi_k
924 );
925 }
926 }
927
928 #[test]
931 fn test_cut_satisfied_at_integer_utility() {
932 let cut = LiftProjectCut {
933 pi: vec![1.0, 1.0],
934 pi0: 0.0,
935 source_var: 0,
936 source_row: 0,
937 violation: 0.5,
938 };
939 assert!(cut_satisfied_at_integer(&cut, &[0.0, 0.0]));
940 assert!(cut_satisfied_at_integer(&cut, &[1.0, 0.0]));
941 assert!(cut_satisfied_at_integer(&cut, &[0.0, 1.0]));
942 assert!(cut_satisfied_at_integer(&cut, &[1.0, 1.0]));
943 }
944
945 #[test]
948 fn test_bcc_violation_equals_stored_violation() {
949 let gen = LiftProjectGenerator::default_generator();
950 let a = vec![vec![2.0, 1.0], vec![1.0, 2.0]];
951 let b = vec![3.0, 3.0];
952 let x_bar = vec![0.6, 0.8];
953 let cuts = gen.generate_cuts(&a, &b, &x_bar, &[0, 1]).unwrap();
954 for cut in &cuts {
955 let recomputed = gen.cut_violation(cut, &x_bar);
956 assert!(
957 (cut.violation - recomputed).abs() < 1e-12,
958 "Stored violation {} != recomputed {}",
959 cut.violation,
960 recomputed
961 );
962 }
963 }
964
965 #[test]
968 fn test_augmented_system_size() {
969 let a = vec![vec![1.0, 1.0]];
970 let b = vec![1.5];
971 let x_bar = vec![0.5, 0.5];
972 let integer_vars = vec![0, 1];
973 let (a_aug, b_aug) = build_augmented_constraints(&a, &b, &x_bar, &integer_vars);
974 assert_eq!(a_aug.len(), 5);
976 assert_eq!(b_aug.len(), 5);
977 }
978}