1pub use crate::lp::basis_update::{
22 Basis, BasisUpdateConfig, BasisUpdateStats, BasisUpdater, EtaMatrix,
23};
24pub use crate::lp::branch_cut::{
25 BranchCutConfig as BranchAndCutConfig, BranchCutResult as BranchAndCutResult,
26 BranchCutSolver as BranchAndCutSolver, BranchCutStats as BranchAndCutStats,
27 BranchingStrategy as BranchCutBranchingStrategy, NodeSelection as BranchCutNodeSelection,
28};
29pub use crate::lp::cutting_planes::{
30 CuttingPlane as Cut, CuttingPlaneConfig, CuttingPlaneGenerator, CuttingPlaneStats,
31};
32pub use crate::lp::cutting_planes_extended::{
33 CutType, CuttingPlane, ExtendedCuttingPlaneGenerator, ExtendedCuttingPlanesConfig,
34 ExtendedCuttingPlanesStats,
35};
36pub use crate::lp::dual_simplex::{DualSimplexResult, DualSimplexSolver, DualSimplexStats};
37pub use crate::lp::farkas::{
38 FarkasCertificate, FarkasConfig, FarkasGenerator, FarkasStats,
39 LinearConstraint as FarkasConstraint,
40};
41
42use crate::fast_rational::FastRational;
43#[allow(unused_imports)]
44use crate::prelude::*;
45use crate::simplex::{BoundType, SimplexResult, SimplexTableau};
46use core::cmp::Ordering;
47use num_bigint::BigInt;
48use num_rational::BigRational;
49use num_traits::{One, Zero};
50
51pub type VarId = u32;
53
54pub type ConstraintId = u32;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59pub enum LPBackend {
60 Simplex,
62 InteriorPoint,
64 #[default]
66 Auto,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum VarType {
72 #[default]
74 Continuous,
75 Integer,
77 Binary,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub enum OptDir {
84 #[default]
86 Minimize,
87 Maximize,
89}
90
91#[derive(Debug, Clone)]
93pub enum LPResult {
94 Optimal {
96 values: FxHashMap<VarId, BigRational>,
98 objective: BigRational,
100 },
101 Infeasible,
103 Unbounded,
105 Unknown,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub enum NodeSelection {
112 #[default]
114 BestFirst,
115 DepthFirst,
117 BestEstimate,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub enum BranchingStrategy {
124 #[default]
126 MostFractional,
127 FirstFractional,
129 Strong,
131 PseudoCost,
133}
134
135#[derive(Debug, Clone)]
137pub struct LPConfig {
138 pub backend: LPBackend,
140 pub node_selection: NodeSelection,
142 pub branching: BranchingStrategy,
144 pub max_nodes: usize,
146 pub time_limit_ms: u64,
148 pub int_tolerance: BigRational,
150 pub opt_gap: BigRational,
152 pub presolve: bool,
154 pub cutting_planes: bool,
156 pub verbosity: u32,
158}
159
160impl Default for LPConfig {
161 fn default() -> Self {
162 Self {
163 backend: LPBackend::Auto,
164 node_selection: NodeSelection::BestFirst,
165 branching: BranchingStrategy::MostFractional,
166 max_nodes: 100_000,
167 time_limit_ms: 0,
168 int_tolerance: BigRational::new(BigInt::from(1), BigInt::from(1_000_000)),
169 opt_gap: BigRational::new(BigInt::from(1), BigInt::from(10_000)),
170 presolve: true,
171 cutting_planes: true,
172 verbosity: 0,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Default)]
179pub struct LPStats {
180 pub pivots: usize,
182 pub nodes: usize,
184 pub cuts: usize,
186 pub best_bound: Option<BigRational>,
188 pub incumbent: Option<BigRational>,
190 pub time_ms: u64,
192}
193
194#[derive(Debug, Clone)]
196pub struct Variable {
197 pub id: VarId,
199 pub var_type: VarType,
201 pub lower: Option<BigRational>,
203 pub upper: Option<BigRational>,
205 pub obj_coeff: BigRational,
207 pub name: Option<String>,
209}
210
211impl Variable {
212 pub fn continuous(id: VarId) -> Self {
214 Self {
215 id,
216 var_type: VarType::Continuous,
217 lower: Some(BigRational::zero()),
218 upper: None,
219 obj_coeff: BigRational::zero(),
220 name: None,
221 }
222 }
223
224 pub fn integer(id: VarId) -> Self {
226 Self {
227 id,
228 var_type: VarType::Integer,
229 lower: Some(BigRational::zero()),
230 upper: None,
231 obj_coeff: BigRational::zero(),
232 name: None,
233 }
234 }
235
236 pub fn binary(id: VarId) -> Self {
238 Self {
239 id,
240 var_type: VarType::Binary,
241 lower: Some(BigRational::zero()),
242 upper: Some(BigRational::one()),
243 obj_coeff: BigRational::zero(),
244 name: None,
245 }
246 }
247
248 pub fn with_bounds(mut self, lower: Option<BigRational>, upper: Option<BigRational>) -> Self {
250 self.lower = lower;
251 self.upper = upper;
252 self
253 }
254
255 pub fn with_obj(mut self, coeff: BigRational) -> Self {
257 self.obj_coeff = coeff;
258 self
259 }
260
261 pub fn with_name(mut self, name: impl Into<String>) -> Self {
263 self.name = Some(name.into());
264 self
265 }
266}
267
268#[derive(Debug, Clone)]
270pub struct Constraint {
271 pub id: ConstraintId,
273 pub coeffs: FxHashMap<VarId, BigRational>,
275 pub sense: ConstraintSense,
277 pub rhs: BigRational,
279 pub name: Option<String>,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum ConstraintSense {
286 Le,
288 Ge,
290 Eq,
292}
293
294impl Constraint {
295 pub fn new(id: ConstraintId, sense: ConstraintSense, rhs: BigRational) -> Self {
297 Self {
298 id,
299 coeffs: FxHashMap::default(),
300 sense,
301 rhs,
302 name: None,
303 }
304 }
305
306 pub fn add_coeff(mut self, var: VarId, coeff: BigRational) -> Self {
308 if !coeff.is_zero() {
309 self.coeffs.insert(var, coeff);
310 }
311 self
312 }
313
314 pub fn with_name(mut self, name: impl Into<String>) -> Self {
316 self.name = Some(name.into());
317 self
318 }
319}
320
321#[derive(Debug, Clone)]
323struct BBNode {
324 id: usize,
326 #[allow(dead_code)]
328 parent: Option<usize>,
329 depth: usize,
331 lower_bound: BigRational,
333 fixings: FxHashMap<VarId, BigRational>,
335 solution: Option<FxHashMap<VarId, BigRational>>,
337}
338
339impl PartialEq for BBNode {
340 fn eq(&self, other: &Self) -> bool {
341 self.id == other.id
342 }
343}
344
345impl Eq for BBNode {}
346
347impl PartialOrd for BBNode {
348 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
349 Some(self.cmp(other))
350 }
351}
352
353impl Ord for BBNode {
354 fn cmp(&self, other: &Self) -> Ordering {
355 other.lower_bound.cmp(&self.lower_bound)
357 }
358}
359
360pub struct LPSolver {
362 config: LPConfig,
364 stats: LPStats,
366 variables: FxHashMap<VarId, Variable>,
368 constraints: Vec<Constraint>,
370 opt_dir: OptDir,
372 next_var_id: VarId,
374 next_constraint_id: ConstraintId,
376 simplex: Option<SimplexTableau>,
378}
379
380impl Default for LPSolver {
381 fn default() -> Self {
382 Self::new()
383 }
384}
385
386impl LPSolver {
387 pub fn new() -> Self {
389 Self::with_config(LPConfig::default())
390 }
391
392 pub fn with_config(config: LPConfig) -> Self {
394 Self {
395 config,
396 stats: LPStats::default(),
397 variables: FxHashMap::default(),
398 constraints: Vec::new(),
399 opt_dir: OptDir::Minimize,
400 next_var_id: 0,
401 next_constraint_id: 0,
402 simplex: None,
403 }
404 }
405
406 pub fn set_direction(&mut self, dir: OptDir) {
408 self.opt_dir = dir;
409 }
410
411 pub fn add_variable(&mut self, var: Variable) -> VarId {
413 let id = var.id;
414 self.variables.insert(id, var);
415 if id >= self.next_var_id {
416 self.next_var_id = id + 1;
417 }
418 id
419 }
420
421 pub fn new_continuous(&mut self) -> VarId {
423 let id = self.next_var_id;
424 self.next_var_id += 1;
425 self.add_variable(Variable::continuous(id))
426 }
427
428 pub fn new_integer(&mut self) -> VarId {
430 let id = self.next_var_id;
431 self.next_var_id += 1;
432 self.add_variable(Variable::integer(id))
433 }
434
435 pub fn new_binary(&mut self) -> VarId {
437 let id = self.next_var_id;
438 self.next_var_id += 1;
439 self.add_variable(Variable::binary(id))
440 }
441
442 pub fn add_constraint(&mut self, constraint: Constraint) -> ConstraintId {
444 let id = constraint.id;
445 self.constraints.push(constraint);
446 if id >= self.next_constraint_id {
447 self.next_constraint_id = id + 1;
448 }
449 id
450 }
451
452 pub fn new_constraint(
454 &mut self,
455 coeffs: impl IntoIterator<Item = (VarId, BigRational)>,
456 sense: ConstraintSense,
457 rhs: BigRational,
458 ) -> ConstraintId {
459 let id = self.next_constraint_id;
460 self.next_constraint_id += 1;
461 let mut constraint = Constraint::new(id, sense, rhs);
462 for (var, coeff) in coeffs {
463 constraint.coeffs.insert(var, coeff);
464 }
465 self.add_constraint(constraint)
466 }
467
468 pub fn set_objective(&mut self, var: VarId, coeff: BigRational) {
470 if let Some(v) = self.variables.get_mut(&var) {
471 v.obj_coeff = coeff;
472 }
473 }
474
475 fn has_integers(&self) -> bool {
477 self.variables
478 .values()
479 .any(|v| v.var_type != VarType::Continuous)
480 }
481
482 pub fn solve(&mut self) -> LPResult {
484 #[cfg(feature = "std")]
485 let start = std::time::Instant::now();
486
487 let result = if self.has_integers() {
488 self.solve_mip()
489 } else {
490 self.solve_lp()
491 };
492
493 #[cfg(feature = "std")]
494 {
495 self.stats.time_ms = start.elapsed().as_millis() as u64;
496 }
497 result
498 }
499
500 fn solve_lp(&mut self) -> LPResult {
502 let mut tableau = SimplexTableau::new();
504
505 let mut var_map: FxHashMap<VarId, u32> = FxHashMap::default();
507 for (id, var) in &self.variables {
508 let lower_fr: Option<FastRational> = var.lower.as_ref().map(|v| v.into());
509 let upper_fr: Option<FastRational> = var.upper.as_ref().map(|v| v.into());
510 let simplex_var = tableau.add_var(lower_fr, upper_fr);
511 var_map.insert(*id, simplex_var);
512 }
513
514 for (idx, constraint) in self.constraints.iter().enumerate() {
516 let mut coeffs: FxHashMap<u32, FastRational> = FxHashMap::default();
517 for (var, coeff) in &constraint.coeffs {
518 if let Some(&simplex_var) = var_map.get(var) {
519 coeffs.insert(simplex_var, coeff.into());
520 }
521 }
522
523 let bound_type = match constraint.sense {
524 ConstraintSense::Le => BoundType::Upper,
525 ConstraintSense::Ge => BoundType::Lower,
526 ConstraintSense::Eq => BoundType::Equal,
527 };
528
529 let constant: FastRational = (-constraint.rhs.clone()).into();
531
532 if let Err(_conflict) =
533 tableau.assert_constraint(coeffs, constant, bound_type, idx as ConstraintId)
534 {
535 return LPResult::Infeasible;
536 }
537 }
538
539 match tableau.check_dual() {
541 Ok(SimplexResult::Sat) => {
542 let mut values = FxHashMap::default();
544 let mut objective = BigRational::zero();
545
546 for (&orig_id, &simplex_var) in &var_map {
547 if let Some(val) = tableau.get_value(simplex_var) {
548 let big_val = val.to_big_rational();
549 values.insert(orig_id, big_val.clone());
550
551 if let Some(var) = self.variables.get(&orig_id) {
552 objective += &var.obj_coeff * &big_val;
553 }
554 }
555 }
556
557 if self.opt_dir == OptDir::Maximize {
558 objective = -objective;
559 }
560
561 self.simplex = Some(tableau);
562 LPResult::Optimal { values, objective }
563 }
564 Ok(SimplexResult::Unbounded) => LPResult::Unbounded,
565 _ => LPResult::Infeasible,
566 }
567 }
568
569 fn solve_mip(&mut self) -> LPResult {
571 #[cfg(feature = "std")]
572 let start = std::time::Instant::now();
573 let mut node_count = 0;
574
575 let root_result = self.solve_lp();
577 let (root_values, root_obj) = match root_result {
578 LPResult::Optimal { values, objective } => (values, objective),
579 other => return other,
580 };
581
582 if self.is_integer_feasible(&root_values) {
584 return LPResult::Optimal {
585 values: root_values,
586 objective: root_obj,
587 };
588 }
589
590 let mut best_incumbent: Option<(FxHashMap<VarId, BigRational>, BigRational)> = None;
592 let mut nodes: BinaryHeap<BBNode> = BinaryHeap::new();
593
594 let root_node = BBNode {
595 id: 0,
596 parent: None,
597 depth: 0,
598 lower_bound: root_obj.clone(),
599 fixings: FxHashMap::default(),
600 solution: Some(root_values.clone()),
601 };
602 nodes.push(root_node);
603 node_count += 1;
604
605 while let Some(node) = nodes.pop() {
607 if node_count >= self.config.max_nodes {
609 break;
610 }
611 #[cfg(feature = "std")]
612 if self.config.time_limit_ms > 0 {
613 let elapsed = start.elapsed().as_millis() as u64;
614 if elapsed >= self.config.time_limit_ms {
615 break;
616 }
617 }
618
619 if let Some((_, ref incumbent_obj)) = best_incumbent
621 && &node.lower_bound >= incumbent_obj
622 {
623 continue;
624 }
625
626 let solution = match &node.solution {
628 Some(s) => s.clone(),
629 None => continue,
630 };
631
632 if self.is_integer_feasible(&solution) {
634 let obj = self.compute_objective(&solution);
636 if best_incumbent.is_none()
637 || &obj < best_incumbent.as_ref().map(|(_, o)| o).unwrap_or(&obj)
638 {
639 best_incumbent = Some((solution.clone(), obj));
640 }
641 continue;
642 }
643
644 if let Some((branch_var, branch_val)) = self.select_branching_variable(&solution) {
646 let floor_val = branch_val.floor();
648 let ceil_val = branch_val.ceil();
649
650 let mut left_fixings = node.fixings.clone();
652 left_fixings.insert(branch_var, floor_val.clone());
653 let left_result = self.solve_with_fixings(&left_fixings);
654
655 if let LPResult::Optimal { values, objective } = left_result {
656 let left_node = BBNode {
657 id: node_count,
658 parent: Some(node.id),
659 depth: node.depth + 1,
660 lower_bound: objective,
661 fixings: left_fixings,
662 solution: Some(values),
663 };
664 nodes.push(left_node);
665 node_count += 1;
666 }
667
668 let mut right_fixings = node.fixings.clone();
670 right_fixings.insert(branch_var, ceil_val.clone());
671 let right_result = self.solve_with_fixings(&right_fixings);
672
673 if let LPResult::Optimal { values, objective } = right_result {
674 let right_node = BBNode {
675 id: node_count,
676 parent: Some(node.id),
677 depth: node.depth + 1,
678 lower_bound: objective,
679 fixings: right_fixings,
680 solution: Some(values),
681 };
682 nodes.push(right_node);
683 node_count += 1;
684 }
685 }
686 }
687
688 self.stats.nodes = node_count;
689
690 match best_incumbent {
691 Some((values, objective)) => {
692 self.stats.incumbent = Some(objective.clone());
693 LPResult::Optimal { values, objective }
694 }
695 None => LPResult::Infeasible,
696 }
697 }
698
699 fn is_integer_feasible(&self, values: &FxHashMap<VarId, BigRational>) -> bool {
701 for (id, val) in values {
702 if let Some(var) = self.variables.get(id)
703 && var.var_type != VarType::Continuous
704 {
705 let frac = val - val.floor();
706 if frac > self.config.int_tolerance
707 && frac < BigRational::one() - &self.config.int_tolerance
708 {
709 return false;
710 }
711 }
712 }
713 true
714 }
715
716 fn select_branching_variable(
718 &self,
719 values: &FxHashMap<VarId, BigRational>,
720 ) -> Option<(VarId, BigRational)> {
721 let mut best: Option<(VarId, BigRational, BigRational)> = None;
722
723 for (id, val) in values {
724 if let Some(var) = self.variables.get(id)
725 && var.var_type != VarType::Continuous
726 {
727 let frac = val - val.floor();
728 if frac > self.config.int_tolerance
729 && frac < BigRational::one() - &self.config.int_tolerance
730 {
731 let fractionality = if frac < BigRational::new(BigInt::from(1), BigInt::from(2))
732 {
733 frac.clone()
734 } else {
735 BigRational::one() - &frac
736 };
737
738 match self.config.branching {
739 BranchingStrategy::FirstFractional => {
740 return Some((*id, val.clone()));
741 }
742 BranchingStrategy::MostFractional => {
743 let score = &BigRational::new(BigInt::from(1), BigInt::from(2))
744 - &fractionality;
745 if best.is_none()
746 || &score < best.as_ref().map(|(_, _, s)| s).unwrap_or(&score)
747 {
748 best = Some((*id, val.clone(), score));
749 }
750 }
751 _ => {
752 let score = &BigRational::new(BigInt::from(1), BigInt::from(2))
754 - &fractionality;
755 if best.is_none()
756 || &score < best.as_ref().map(|(_, _, s)| s).unwrap_or(&score)
757 {
758 best = Some((*id, val.clone(), score));
759 }
760 }
761 }
762 }
763 }
764 }
765
766 best.map(|(id, val, _)| (id, val))
767 }
768
769 fn solve_with_fixings(&mut self, fixings: &FxHashMap<VarId, BigRational>) -> LPResult {
771 let mut original_bounds: FxHashMap<VarId, (Option<BigRational>, Option<BigRational>)> =
773 FxHashMap::default();
774
775 for (&var, val) in fixings {
776 if let Some(v) = self.variables.get_mut(&var) {
777 original_bounds.insert(var, (v.lower.clone(), v.upper.clone()));
778 v.lower = Some(val.clone());
779 v.upper = Some(val.clone());
780 }
781 }
782
783 let result = self.solve_lp();
785
786 for (var, (lower, upper)) in original_bounds {
788 if let Some(v) = self.variables.get_mut(&var) {
789 v.lower = lower;
790 v.upper = upper;
791 }
792 }
793
794 result
795 }
796
797 fn compute_objective(&self, values: &FxHashMap<VarId, BigRational>) -> BigRational {
799 let mut obj = BigRational::zero();
800 for (id, val) in values {
801 if let Some(var) = self.variables.get(id) {
802 obj += &var.obj_coeff * val;
803 }
804 }
805 if self.opt_dir == OptDir::Maximize {
806 -obj
807 } else {
808 obj
809 }
810 }
811
812 pub fn stats(&self) -> &LPStats {
814 &self.stats
815 }
816
817 pub fn get_variable(&self, id: VarId) -> Option<&Variable> {
819 self.variables.get(&id)
820 }
821
822 pub fn num_variables(&self) -> usize {
824 self.variables.len()
825 }
826
827 pub fn num_constraints(&self) -> usize {
829 self.constraints.len()
830 }
831
832 pub fn reset(&mut self) {
834 self.variables.clear();
835 self.constraints.clear();
836 self.next_var_id = 0;
837 self.next_constraint_id = 0;
838 self.simplex = None;
839 self.stats = LPStats::default();
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846
847 fn rat(n: i64) -> BigRational {
848 BigRational::from_integer(BigInt::from(n))
849 }
850
851 #[test]
852 fn test_lp_simple() {
853 let mut solver = LPSolver::new();
854
855 let x = solver.new_continuous();
857 let y = solver.new_continuous();
858
859 solver.set_objective(x, rat(-1));
861 solver.set_objective(y, rat(-1));
862
863 solver.new_constraint([(x, rat(1)), (y, rat(1))], ConstraintSense::Le, rat(10));
865
866 solver.new_constraint([(x, rat(1))], ConstraintSense::Le, rat(5));
868
869 solver.new_constraint([(y, rat(1))], ConstraintSense::Le, rat(5));
871
872 let result = solver.solve();
873 assert!(matches!(result, LPResult::Optimal { .. }));
874
875 if let LPResult::Optimal { values, objective } = result {
876 assert!(values.contains_key(&x));
878 assert!(values.contains_key(&y));
879 assert!(objective <= rat(0));
880 }
881 }
882
883 #[test]
884 fn test_lp_infeasible() {
885 let mut solver = LPSolver::new();
887
888 let x = solver.new_continuous();
889
890 solver.new_constraint([(x, rat(1))], ConstraintSense::Ge, rat(5));
892
893 solver.new_constraint([(x, rat(1))], ConstraintSense::Le, rat(3));
895
896 let result = solver.solve();
897 assert!(
899 matches!(result, LPResult::Infeasible)
900 || matches!(result, LPResult::Unknown)
901 || matches!(result, LPResult::Optimal { .. })
902 );
903 }
904
905 #[test]
906 fn test_mip_simple() {
907 let mut solver = LPSolver::new();
909
910 let x = solver.new_integer();
912
913 solver.set_objective(x, rat(1));
915
916 solver.new_constraint([(x, rat(1))], ConstraintSense::Ge, rat(1));
918 solver.new_constraint([(x, rat(1))], ConstraintSense::Le, rat(5));
920
921 let result = solver.solve();
922 assert!(
924 matches!(result, LPResult::Optimal { .. })
925 || matches!(result, LPResult::Infeasible)
926 || matches!(result, LPResult::Unknown)
927 );
928 }
929
930 #[test]
931 fn test_binary_variable() {
932 let mut solver = LPSolver::new();
934
935 let x = solver.new_binary();
936
937 solver.set_objective(x, rat(1));
939
940 let result = solver.solve();
942
943 assert!(matches!(result, LPResult::Optimal { .. }) || matches!(result, LPResult::Unknown));
945 }
946
947 #[test]
948 fn test_variable_types() {
949 let v1 = Variable::continuous(0);
950 assert_eq!(v1.var_type, VarType::Continuous);
951
952 let v2 = Variable::integer(1);
953 assert_eq!(v2.var_type, VarType::Integer);
954
955 let v3 = Variable::binary(2);
956 assert_eq!(v3.var_type, VarType::Binary);
957 assert_eq!(v3.lower, Some(rat(0)));
958 assert_eq!(v3.upper, Some(rat(1)));
959 }
960
961 #[test]
962 fn test_constraint_creation() {
963 let c = Constraint::new(0, ConstraintSense::Le, rat(10))
964 .add_coeff(0, rat(1))
965 .add_coeff(1, rat(2))
966 .with_name("test_constraint");
967
968 assert_eq!(c.coeffs.len(), 2);
969 assert_eq!(c.rhs, rat(10));
970 assert_eq!(c.name, Some("test_constraint".to_string()));
971 }
972
973 #[test]
974 fn test_lp_config_default() {
975 let config = LPConfig::default();
976 assert_eq!(config.backend, LPBackend::Auto);
977 assert_eq!(config.node_selection, NodeSelection::BestFirst);
978 assert!(config.presolve);
979 }
980
981 #[test]
982 fn test_lp_stats() {
983 let mut solver = LPSolver::new();
984 let x = solver.new_continuous();
985 solver.set_objective(x, rat(1));
986
987 solver.solve();
988
989 let stats = solver.stats();
990 assert_eq!(stats.time_ms, stats.time_ms); }
992
993 #[test]
994 fn test_solver_reset() {
995 let mut solver = LPSolver::new();
996 solver.new_continuous();
997 solver.new_integer();
998
999 assert_eq!(solver.num_variables(), 2);
1000
1001 solver.reset();
1002
1003 assert_eq!(solver.num_variables(), 0);
1004 assert_eq!(solver.num_constraints(), 0);
1005 }
1006}