algos/math/integer_linear/
branch_and_price.rs1use crate::math::integer_linear::{ILPSolution, ILPSolver, ILPStatus, IntegerLinearProgram};
2use crate::math::optimization::simplex::{minimize, LinearProgram};
3use crate::math::optimization::OptimizationConfig;
4use std::error::Error;
5
6pub struct BranchAndPriceSolver {
7 max_iterations: usize,
8 tolerance: f64,
9 max_columns_per_node: usize,
10}
11
12impl BranchAndPriceSolver {
13 pub fn new(max_iterations: usize, tolerance: f64, max_columns_per_node: usize) -> Self {
14 Self {
15 max_iterations,
16 tolerance,
17 max_columns_per_node,
18 }
19 }
20
21 fn is_integer(&self, value: f64) -> bool {
22 (value - value.round()).abs() < self.tolerance
23 }
24
25 fn solve_relaxation(
26 &self,
27 problem: &IntegerLinearProgram,
28 ) -> Result<ILPSolution, Box<dyn Error>> {
29 let original_objective = problem.objective.clone();
30 let n = problem.objective.len();
31 let m = problem.constraints.len();
32
33 let mut lp_objective: Vec<f64> = original_objective.iter().map(|x| -x).collect();
35 lp_objective.extend(vec![0.0; m]);
36
37 let lp_constraints: Vec<Vec<f64>> = problem
38 .constraints
39 .iter()
40 .enumerate()
41 .map(|(i, row)| {
42 let mut new_row = row.clone();
43 new_row.resize(n + m, 0.0);
44 new_row[n + i] = 1.0;
45 new_row
46 })
47 .collect();
48
49 let lp = LinearProgram {
50 objective: lp_objective,
51 constraints: lp_constraints,
52 rhs: problem.bounds.clone(),
53 };
54
55 let config = OptimizationConfig {
56 max_iterations: self.max_iterations,
57 tolerance: self.tolerance,
58 learning_rate: 1.0,
59 };
60 let result = minimize(&lp, &config);
61
62 if !result.converged
63 || result.optimal_point.is_empty()
64 || result.optimal_value.is_infinite()
65 || result.optimal_value.is_nan()
66 {
67 return Ok(ILPSolution {
68 values: vec![],
69 objective_value: f64::NEG_INFINITY,
70 status: ILPStatus::Infeasible,
71 });
72 }
73
74 let x = result.optimal_point[..n].to_vec();
76 for (i, constraint) in problem.constraints.iter().enumerate() {
78 let lhs: f64 = constraint.iter().zip(&x).map(|(&a, &xi)| a * xi).sum();
79 if lhs > problem.bounds[i] + self.tolerance {
80 return Ok(ILPSolution {
81 values: vec![],
82 objective_value: f64::NEG_INFINITY,
83 status: ILPStatus::Infeasible,
84 });
85 }
86 }
87
88 Ok(ILPSolution {
90 values: x,
91 objective_value: -result.optimal_value,
92 status: ILPStatus::Optimal,
93 })
94 }
95
96 fn solve_pricing_problem(
97 &self,
98 dual_values: &[f64],
99 problem: &IntegerLinearProgram,
100 ) -> Option<Vec<f64>> {
101 let mut best_column = None;
104 let mut best_reduced_cost = -self.tolerance;
105 for i in 0..problem.constraints.len() {
107 let mut new_column = vec![0.0; problem.constraints.len()];
108 new_column[i] = 1.0;
109 let mut reduced_cost = -1.0; for (j, &dual) in dual_values.iter().enumerate() {
112 reduced_cost += dual * new_column[j];
113 }
114 if reduced_cost < best_reduced_cost {
115 let mut feasible = true;
117 for (constraint, &bound) in problem.constraints.iter().zip(problem.bounds.iter()) {
118 let lhs: f64 = constraint
119 .iter()
120 .zip(&new_column)
121 .map(|(&a, &x)| a * x)
122 .sum();
123 if lhs > bound + self.tolerance {
124 feasible = false;
125 break;
126 }
127 }
128 if feasible {
129 best_reduced_cost = reduced_cost;
130 best_column = Some(new_column);
131 }
132 }
133 }
134 best_column
135 }
136
137 fn generate_columns(&self, problem: &mut IntegerLinearProgram, solution: &ILPSolution) -> bool {
138 if problem.objective.len() >= self.max_columns_per_node {
139 return false;
140 }
141 let mut columns_added = 0;
142 let mut dual_values = vec![0.0; problem.constraints.len()];
143 for (i, constraint) in problem.constraints.iter().enumerate() {
145 let lhs: f64 = constraint
146 .iter()
147 .zip(&solution.values)
148 .map(|(&a, &x)| a * x)
149 .sum();
150 let slack = problem.bounds[i] - lhs;
151 if slack.abs() < self.tolerance {
152 dual_values[i] = if problem.bounds[i] > 0.0 { 1.0 } else { -1.0 };
153 }
154 }
155 if let Some(new_column) = self.solve_pricing_problem(&dual_values, problem) {
157 for (i, constraint) in problem.constraints.iter_mut().enumerate() {
159 constraint.push(new_column[i]);
160 }
161 let obj_coeff = new_column
163 .iter()
164 .zip(problem.objective.iter())
165 .map(|(&a, &c)| a * c)
166 .sum::<f64>();
167 problem.objective.push(obj_coeff);
168 problem.integer_vars.push(problem.objective.len() - 1);
169 columns_added += 1;
170 }
171 columns_added > 0
172 }
173
174 fn branch(
175 &self,
176 problem: &IntegerLinearProgram,
177 var_idx: usize,
178 value: f64,
179 ) -> (IntegerLinearProgram, IntegerLinearProgram) {
180 let mut lower_branch = problem.clone();
181 let mut upper_branch = problem.clone();
182 let mut lower_constraint = vec![0.0; problem.objective.len()];
184 lower_constraint[var_idx] = 1.0;
185 lower_branch.constraints.push(lower_constraint);
186 lower_branch.bounds.push(value.floor());
187 let mut upper_constraint = vec![0.0; problem.objective.len()];
189 upper_constraint[var_idx] = -1.0; upper_branch.constraints.push(upper_constraint);
191 upper_branch.bounds.push(-value.ceil()); (lower_branch, upper_branch)
193 }
194}
195
196impl ILPSolver for BranchAndPriceSolver {
197 fn solve(&self, problem: &IntegerLinearProgram) -> Result<ILPSolution, Box<dyn Error>> {
198 let mut stack = vec![problem.clone()];
199 let mut iterations = 0;
200 let mut best_objective = f64::NEG_INFINITY;
201 let mut best_solution = None;
202
203 while let Some(node) = stack.pop() {
204 iterations += 1;
205 if iterations > self.max_iterations {
206 break;
207 }
208
209 let relaxation = match self.solve_relaxation(&node) {
211 Ok(sol) if sol.status == ILPStatus::Optimal => sol,
212 _ => continue,
213 };
214
215 let mut solution_is_integer = true;
217 for &i in &node.integer_vars {
218 if (relaxation.values[i] - relaxation.values[i].round()).abs() > self.tolerance {
219 solution_is_integer = false;
220 break;
221 }
222 }
223 if solution_is_integer {
224 return Ok(relaxation);
225 }
226
227 let mut current_node = node.clone();
229 if self.generate_columns(&mut current_node, &relaxation) {
230 stack.push(current_node);
231 continue;
232 }
233
234 if relaxation.objective_value <= best_objective {
236 continue;
237 }
238
239 let mut all_integer = true;
241 let mut most_fractional = None;
242 let mut max_frac_diff = 0.0;
243 for (i, &v) in relaxation.values.iter().enumerate() {
244 if node.integer_vars.contains(&i) && !self.is_integer(v) {
245 all_integer = false;
246 let frac = (v - v.floor()).abs();
247 let diff = (frac - 0.5).abs();
248 if diff > max_frac_diff {
249 max_frac_diff = diff;
250 most_fractional = Some((i, v));
251 }
252 }
253 }
254 if !all_integer {
255 if let Some((idx, val)) = most_fractional {
256 let (lower, upper) = self.branch(&node, idx, val);
257 stack.push(lower);
258 stack.push(upper);
259 }
260 }
261
262 if all_integer && relaxation.objective_value > best_objective {
264 best_objective = relaxation.objective_value;
265 best_solution = Some(relaxation);
266 }
267 }
268
269 Ok(best_solution.unwrap_or(ILPSolution {
270 values: vec![],
271 objective_value: f64::NEG_INFINITY,
272 status: ILPStatus::Infeasible,
273 }))
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_simple_ilp() -> Result<(), Box<dyn Error>> {
283 let problem = IntegerLinearProgram {
289 objective: vec![2.0, 1.0],
290 constraints: vec![
291 vec![1.0, 1.0],
292 vec![1.0, 0.0],
293 vec![-1.0, 0.0],
294 vec![0.0, -1.0],
295 ],
296 bounds: vec![4.0, 2.0, 0.0, 0.0],
297 integer_vars: vec![0, 1],
298 };
299 let solver = BranchAndPriceSolver::new(100, 1e-6, 5);
300 let solution = solver.solve(&problem)?;
301 assert_eq!(solution.status, ILPStatus::Optimal);
302 assert!((solution.objective_value - 6.0).abs() < 1e-1);
304 for &v in &solution.values {
305 assert!((v - v.round()).abs() < 1e-6 && v >= 0.0);
306 }
307 Ok(())
308 }
309
310 #[test]
311 fn test_infeasible_ilp() -> Result<(), Box<dyn Error>> {
312 let problem = IntegerLinearProgram {
318 objective: vec![1.0, 1.0],
319 constraints: vec![
320 vec![1.0, 1.0],
321 vec![-1.0, -1.0],
322 vec![-1.0, 0.0],
323 vec![0.0, -1.0],
324 ],
325 bounds: vec![5.0, -6.0, 0.0, 0.0],
326 integer_vars: vec![0, 1],
327 };
328 let solver = BranchAndPriceSolver::new(100, 1e-6, 5);
329 let solution = solver.solve(&problem)?;
330 assert_eq!(solution.status, ILPStatus::Infeasible);
331 Ok(())
332 }
333}