1use crate::error::{OptimizeError, OptimizeResult};
47use scirs2_core::ndarray::{Array1, Array2};
48
49pub use crate::integer::milp_branch_and_bound::{
52 branch_and_bound, BnbConfig, BranchingStrategy, MilpProblem as MilpProblemInner,
53 MilpResult as MilpResultInner,
54};
55pub use crate::integer::{
56 is_integer_valued, BranchAndBoundOptions, BranchAndBoundSolver, CuttingPlaneOptions,
57 CuttingPlaneSolver, IntegerKind, IntegerVariableSet, LinearProgram, MipResult,
58};
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum IntegerConstraint {
65 Continuous,
67 Integer,
69 Binary,
71 NonNegativeInteger,
73}
74
75impl IntegerConstraint {
76 pub fn to_kind(self) -> IntegerKind {
78 match self {
79 IntegerConstraint::Continuous => IntegerKind::Continuous,
80 IntegerConstraint::Integer | IntegerConstraint::NonNegativeInteger => {
81 IntegerKind::Integer
82 }
83 IntegerConstraint::Binary => IntegerKind::Binary,
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
94pub struct MilpProblem {
95 pub c: Array1<f64>,
97 pub a_ub: Option<Array2<f64>>,
99 pub b_ub: Option<Array1<f64>>,
101 pub a_eq: Option<Array2<f64>>,
103 pub b_eq: Option<Array1<f64>>,
105 pub lb: Array1<f64>,
107 pub ub: Array1<f64>,
109 pub constraints: Vec<IntegerConstraint>,
111}
112
113impl MilpProblem {
114 pub fn builder(c: Array1<f64>) -> MilpProblemBuilder {
116 let n = c.len();
117 MilpProblemBuilder {
118 c,
119 a_ub: None,
120 b_ub: None,
121 a_eq: None,
122 b_eq: None,
123 lb: Array1::zeros(n),
124 ub: Array1::from_elem(n, f64::INFINITY),
125 constraints: vec![IntegerConstraint::Continuous; n],
126 }
127 }
128
129 #[inline]
131 pub fn n_vars(&self) -> usize {
132 self.c.len()
133 }
134
135 #[inline]
137 pub fn n_ineq(&self) -> usize {
138 self.a_ub.as_ref().map_or(0, |a| a.nrows())
139 }
140
141 #[inline]
143 pub fn n_eq(&self) -> usize {
144 self.a_eq.as_ref().map_or(0, |a| a.nrows())
145 }
146
147 pub fn integer_indices(&self) -> Vec<usize> {
149 self.constraints
150 .iter()
151 .enumerate()
152 .filter(|(_, c)| **c != IntegerConstraint::Continuous)
153 .map(|(i, _)| i)
154 .collect()
155 }
156
157 pub fn to_linear_program(&self) -> LinearProgram {
159 LinearProgram {
160 c: self.c.clone(),
161 a_ub: self.a_ub.clone(),
162 b_ub: self.b_ub.clone(),
163 a_eq: self.a_eq.clone(),
164 b_eq: self.b_eq.clone(),
165 lower: Some(self.lb.clone()),
166 upper: Some(self.ub.clone()),
167 }
168 }
169
170 pub fn to_integer_variable_set(&self) -> IntegerVariableSet {
172 IntegerVariableSet {
173 kinds: self.constraints.iter().map(|c| c.to_kind()).collect(),
174 }
175 }
176
177 pub fn to_inner(&self) -> OptimizeResult<MilpProblemInner> {
182 let (a_combined, b_combined) = self.build_combined_ineq()?;
183 MilpProblemInner::new(
184 self.c.clone(),
185 a_combined,
186 b_combined,
187 self.lb.clone(),
188 self.ub.clone(),
189 self.integer_indices(),
190 )
191 }
192
193 fn build_combined_ineq(&self) -> OptimizeResult<(Array2<f64>, Array1<f64>)> {
194 let n = self.n_vars();
195 let m_ub = self.n_ineq();
196 let m_eq = self.n_eq();
197 let total = m_ub + 2 * m_eq;
198
199 if total == 0 {
200 return Ok((Array2::zeros((0, n)), Array1::zeros(0)));
201 }
202
203 let mut a = Array2::<f64>::zeros((total, n));
204 let mut b = Array1::<f64>::zeros(total);
205
206 if let (Some(aub), Some(bub)) = (&self.a_ub, &self.b_ub) {
207 for i in 0..m_ub {
208 for j in 0..n {
209 a[[i, j]] = aub[[i, j]];
210 }
211 b[i] = bub[i];
212 }
213 }
214
215 if let (Some(aeq), Some(beq)) = (&self.a_eq, &self.b_eq) {
216 for k in 0..m_eq {
217 let rp = m_ub + k;
218 let rm = m_ub + m_eq + k;
219 for j in 0..n {
220 a[[rp, j]] = aeq[[k, j]];
221 a[[rm, j]] = -aeq[[k, j]];
222 }
223 b[rp] = beq[k];
224 b[rm] = -beq[k];
225 }
226 }
227
228 Ok((a, b))
229 }
230}
231
232pub struct MilpProblemBuilder {
236 c: Array1<f64>,
237 a_ub: Option<Array2<f64>>,
238 b_ub: Option<Array1<f64>>,
239 a_eq: Option<Array2<f64>>,
240 b_eq: Option<Array1<f64>>,
241 lb: Array1<f64>,
242 ub: Array1<f64>,
243 constraints: Vec<IntegerConstraint>,
244}
245
246impl MilpProblemBuilder {
247 pub fn inequalities(mut self, a: Array2<f64>, b: Array1<f64>) -> Self {
249 self.a_ub = Some(a);
250 self.b_ub = Some(b);
251 self
252 }
253
254 pub fn equalities(mut self, a: Array2<f64>, b: Array1<f64>) -> Self {
256 self.a_eq = Some(a);
257 self.b_eq = Some(b);
258 self
259 }
260
261 pub fn bounds(mut self, lb: Array1<f64>, ub: Array1<f64>) -> Self {
263 self.lb = lb;
264 self.ub = ub;
265 self
266 }
267
268 pub fn integer_constraints(mut self, c: Vec<IntegerConstraint>) -> Self {
270 self.constraints = c;
271 self
272 }
273
274 pub fn all_binary(mut self) -> Self {
276 let n = self.c.len();
277 self.constraints = vec![IntegerConstraint::Binary; n];
278 self.lb = Array1::zeros(n);
279 self.ub = Array1::ones(n);
280 self
281 }
282
283 pub fn all_integer(mut self) -> Self {
285 let n = self.c.len();
286 self.constraints = vec![IntegerConstraint::Integer; n];
287 self
288 }
289
290 pub fn build(self) -> OptimizeResult<MilpProblem> {
292 let n = self.c.len();
293
294 if n == 0 {
295 return Err(OptimizeError::InvalidInput(
296 "Objective vector c is empty".to_string(),
297 ));
298 }
299 if self.lb.len() != n || self.ub.len() != n {
300 return Err(OptimizeError::InvalidInput(format!(
301 "lb/ub length ({}/{}) must equal n={}",
302 self.lb.len(),
303 self.ub.len(),
304 n
305 )));
306 }
307 if self.constraints.len() != n {
308 return Err(OptimizeError::InvalidInput(format!(
309 "constraints length ({}) must equal n={}",
310 self.constraints.len(),
311 n
312 )));
313 }
314 if let (Some(a), Some(b)) = (&self.a_ub, &self.b_ub) {
315 if a.ncols() != n {
316 return Err(OptimizeError::InvalidInput(format!(
317 "A_ub has {} columns but n={}",
318 a.ncols(),
319 n
320 )));
321 }
322 if a.nrows() != b.len() {
323 return Err(OptimizeError::InvalidInput(format!(
324 "A_ub rows ({}) ≠ b_ub len ({})",
325 a.nrows(),
326 b.len()
327 )));
328 }
329 }
330 if let (Some(a), Some(b)) = (&self.a_eq, &self.b_eq) {
331 if a.ncols() != n {
332 return Err(OptimizeError::InvalidInput(format!(
333 "A_eq has {} columns but n={}",
334 a.ncols(),
335 n
336 )));
337 }
338 if a.nrows() != b.len() {
339 return Err(OptimizeError::InvalidInput(format!(
340 "A_eq rows ({}) ≠ b_eq len ({})",
341 a.nrows(),
342 b.len()
343 )));
344 }
345 }
346
347 Ok(MilpProblem {
348 c: self.c,
349 a_ub: self.a_ub,
350 b_ub: self.b_ub,
351 a_eq: self.a_eq,
352 b_eq: self.b_eq,
353 lb: self.lb,
354 ub: self.ub,
355 constraints: self.constraints,
356 })
357 }
358}
359
360#[derive(Debug, Clone)]
364pub struct MilpSolveResult {
365 pub x: Array1<f64>,
367 pub objective: f64,
369 pub success: bool,
371 pub message: String,
373 pub nodes_explored: usize,
375 pub lower_bound: f64,
377 pub lp_solves: usize,
379 pub gap: f64,
381}
382
383impl MilpSolveResult {
384 fn from_bnb(r: MilpResultInner) -> Self {
385 let gap = if r.success {
386 (r.obj - r.lower_bound).abs() / (1.0 + r.obj.abs())
387 } else {
388 f64::INFINITY
389 };
390 MilpSolveResult {
391 x: r.x,
392 objective: r.obj,
393 success: r.success,
394 message: r.message,
395 nodes_explored: r.nodes_explored,
396 lower_bound: r.lower_bound,
397 lp_solves: 0,
398 gap,
399 }
400 }
401
402 fn from_mip(r: MipResult) -> Self {
403 let gap = if r.success {
404 (r.fun - r.lower_bound).abs() / (1.0 + r.fun.abs())
405 } else {
406 f64::INFINITY
407 };
408 MilpSolveResult {
409 x: r.x,
410 objective: r.fun,
411 success: r.success,
412 message: r.message,
413 nodes_explored: r.nodes_explored,
414 lower_bound: r.lower_bound,
415 lp_solves: r.lp_solves,
416 gap,
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
425pub enum MilpAlgorithm {
426 BranchAndBound,
428 CuttingPlanes,
430}
431
432#[derive(Debug, Clone)]
436pub struct MilpSolverConfig {
437 pub algorithm: MilpAlgorithm,
439 pub max_nodes: usize,
441 pub int_tol: f64,
443 pub opt_tol: f64,
445 pub branching: BranchingStrategy,
447 pub max_cut_rounds: usize,
449 pub verbose: bool,
451}
452
453impl Default for MilpSolverConfig {
454 fn default() -> Self {
455 MilpSolverConfig {
456 algorithm: MilpAlgorithm::BranchAndBound,
457 max_nodes: 50_000,
458 int_tol: 1e-6,
459 opt_tol: 1e-6,
460 branching: BranchingStrategy::MostFractional,
461 max_cut_rounds: 50,
462 verbose: false,
463 }
464 }
465}
466
467pub struct MilpSolver {
474 config: MilpSolverConfig,
475}
476
477impl MilpSolver {
478 pub fn new(config: MilpSolverConfig) -> Self {
480 MilpSolver { config }
481 }
482
483 pub fn solve(&self, problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
485 match self.config.algorithm {
486 MilpAlgorithm::BranchAndBound => self.solve_bnb(problem),
487 MilpAlgorithm::CuttingPlanes => self.solve_cutting_planes(problem),
488 }
489 }
490
491 fn bnb_config(&self) -> BnbConfig {
492 BnbConfig {
493 max_nodes: self.config.max_nodes,
494 int_tol: self.config.int_tol,
495 abs_gap: self.config.opt_tol,
496 rel_gap: self.config.opt_tol,
497 branching: self.config.branching,
498 ..Default::default()
499 }
500 }
501
502 fn solve_bnb(&self, problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
503 let inner = problem.to_inner()?;
504 let r = branch_and_bound(&inner, &self.bnb_config())?;
505 Ok(MilpSolveResult::from_bnb(r))
506 }
507
508 fn solve_cutting_planes(&self, problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
509 let lp = problem.to_linear_program();
510 let ivs = problem.to_integer_variable_set();
511 let opts = CuttingPlaneOptions {
512 max_iter: self.config.max_cut_rounds,
513 int_tol: self.config.int_tol,
514 fallback_bb: true,
515 ..Default::default()
516 };
517 let r = CuttingPlaneSolver::with_options(opts).solve(&lp, &ivs)?;
518 Ok(MilpSolveResult::from_mip(r))
519 }
520}
521
522impl Default for MilpSolver {
523 fn default() -> Self {
524 MilpSolver::new(MilpSolverConfig::default())
525 }
526}
527
528pub fn solve_milp(problem: &MilpProblem) -> OptimizeResult<MilpSolveResult> {
532 MilpSolver::default().solve(problem)
533}
534
535#[cfg(test)]
538mod tests {
539 use super::*;
540 use approx::assert_abs_diff_eq;
541 use scirs2_core::ndarray::{array, Array2};
542
543 fn knapsack_problem(values: &[f64], weights: &[f64], cap: f64) -> MilpProblem {
544 let n = values.len();
545 let c = Array1::from_vec(values.iter().map(|&v| -v).collect());
546 let a = Array2::from_shape_vec((1, n), weights.to_vec()).expect("shape");
547 let b = Array1::from_vec(vec![cap]);
548 MilpProblem::builder(c)
549 .inequalities(a, b)
550 .bounds(Array1::zeros(n), Array1::ones(n))
551 .all_binary()
552 .build()
553 .expect("valid problem")
554 }
555
556 #[test]
557 fn test_builder_basic_shape() {
558 let c = array![-4.0, -5.0, -3.0];
559 let a = Array2::from_shape_vec((1, 3), vec![2.0, 3.0, 1.0]).expect("shape");
560 let b = array![5.0];
561 let p = MilpProblem::builder(c)
562 .inequalities(a, b)
563 .all_binary()
564 .build()
565 .expect("valid");
566 assert_eq!(p.n_vars(), 3);
567 assert_eq!(p.n_ineq(), 1);
568 assert_eq!(p.n_eq(), 0);
569 }
570
571 #[test]
572 fn test_builder_dimension_mismatch() {
573 let c = array![1.0, 2.0];
574 let a = Array2::from_shape_vec((1, 3), vec![1.0, 1.0, 1.0]).expect("shape");
576 let b = array![5.0];
577 let result = MilpProblem::builder(c)
578 .inequalities(a, b)
579 .all_binary()
580 .build();
581 assert!(result.is_err());
582 }
583
584 #[test]
585 fn test_solve_binary_knapsack() {
586 let p = knapsack_problem(&[4.0, 5.0], &[2.0, 3.0], 5.0);
589 let r = solve_milp(&p).expect("solve ok");
590 assert!(r.success);
591 assert!(r.objective <= -8.0 + 1e-4, "obj={}", r.objective);
592 }
593
594 #[test]
595 fn test_solve_pure_integer() {
596 let c = array![1.0, 1.0];
598 let a = Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("shape");
599 let b = array![-3.5];
600 let p = MilpProblem::builder(c)
601 .inequalities(a, b)
602 .bounds(array![0.0, 0.0], array![10.0, 10.0])
603 .all_integer()
604 .build()
605 .expect("valid");
606 let r = solve_milp(&p).expect("solve ok");
607 assert!(r.success);
608 assert_abs_diff_eq!(r.objective, 4.0, epsilon = 1e-3);
609 }
610
611 #[test]
612 fn test_solve_with_equalities() {
613 let c = array![1.0, 1.0];
615 let aeq = Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("shape");
616 let beq = array![5.0];
617 let p = MilpProblem::builder(c)
618 .equalities(aeq, beq)
619 .bounds(array![0.0, 0.0], array![10.0, 10.0])
620 .all_integer()
621 .build()
622 .expect("valid");
623 let r = solve_milp(&p).expect("solve ok");
624 assert!(r.success);
625 assert_abs_diff_eq!(r.objective, 5.0, epsilon = 1e-3);
626 }
627
628 #[test]
629 fn test_solve_infeasible() {
630 let c = array![-1.0];
632 let a = Array2::from_shape_vec((1, 1), vec![-1.0]).expect("shape");
633 let b = array![-2.0];
634 let p = MilpProblem::builder(c)
635 .inequalities(a, b)
636 .bounds(array![0.0], array![1.0])
637 .all_binary()
638 .build()
639 .expect("valid");
640 let r = solve_milp(&p).expect("runs");
641 assert!(!r.success);
642 }
643
644 #[test]
645 fn test_solve_cutting_planes() {
646 let c = array![1.0, 1.0];
647 let a = Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("shape");
648 let b = array![-3.5];
649 let p = MilpProblem::builder(c)
650 .inequalities(a, b)
651 .bounds(array![0.0, 0.0], array![10.0, 10.0])
652 .all_integer()
653 .build()
654 .expect("valid");
655 let cfg = MilpSolverConfig {
656 algorithm: MilpAlgorithm::CuttingPlanes,
657 ..Default::default()
658 };
659 let r = MilpSolver::new(cfg).solve(&p).expect("ok");
660 assert!(r.success);
661 assert_abs_diff_eq!(r.objective, 4.0, epsilon = 1e-3);
662 }
663
664 #[test]
665 fn test_integer_constraint_to_kind() {
666 assert_eq!(
667 IntegerConstraint::Continuous.to_kind(),
668 IntegerKind::Continuous
669 );
670 assert_eq!(IntegerConstraint::Binary.to_kind(), IntegerKind::Binary);
671 assert_eq!(IntegerConstraint::Integer.to_kind(), IntegerKind::Integer);
672 assert_eq!(
673 IntegerConstraint::NonNegativeInteger.to_kind(),
674 IntegerKind::Integer
675 );
676 }
677
678 #[test]
679 fn test_gap_computation() {
680 let p = knapsack_problem(&[5.0, 3.0], &[3.0, 2.0], 4.0);
681 let r = solve_milp(&p).expect("ok");
682 if r.success {
683 assert!(r.gap >= 0.0);
684 assert!(r.gap < 1.0 + 1e-10);
685 }
686 }
687}