1use super::{
24 is_integer_valued,
25 lp_relaxation::{LpRelaxationSolver, LpResult},
26 most_fractional_variable, IntegerVariableSet, LinearProgram, MipResult,
27};
28use crate::error::{OptimizeError, OptimizeResult};
29use scirs2_core::ndarray::{s, Array1, Array2};
30
31#[derive(Debug, Clone)]
33pub struct CuttingPlaneOptions {
34 pub max_iter: usize,
36 pub int_tol: f64,
38 pub cuts_per_iter: usize,
40 pub cut_violation_tol: f64,
42 pub fallback_bb: bool,
44 pub max_cuts: usize,
46}
47
48impl Default for CuttingPlaneOptions {
49 fn default() -> Self {
50 CuttingPlaneOptions {
51 max_iter: 100,
52 int_tol: 1e-6,
53 cuts_per_iter: 5,
54 cut_violation_tol: 1e-8,
55 fallback_bb: true,
56 max_cuts: 500,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63struct GomoryCut {
64 a: Vec<f64>,
65 b: f64,
66}
67
68pub struct CuttingPlaneSolver {
70 pub options: CuttingPlaneOptions,
71}
72
73impl CuttingPlaneSolver {
74 pub fn new() -> Self {
76 CuttingPlaneSolver {
77 options: CuttingPlaneOptions::default(),
78 }
79 }
80
81 pub fn with_options(options: CuttingPlaneOptions) -> Self {
83 CuttingPlaneSolver { options }
84 }
85
86 fn generate_gomory_cut(
96 &self,
97 x: &[f64],
98 branch_var: usize,
99 n: usize,
100 lp: &LinearProgram,
101 existing_cuts: &[GomoryCut],
102 ) -> Option<GomoryCut> {
103 let xi = x[branch_var];
104 let fi = xi - xi.floor(); if fi < self.options.int_tol || fi > 1.0 - self.options.int_tol {
107 return None;
108 }
109
110 let cx: f64 = lp.c.iter().zip(x.iter()).map(|(&ci, &xi)| ci * xi).sum();
119 let cut_rhs = -cx.ceil();
120 let cut_lhs: Vec<f64> = lp.c.iter().map(|&ci| -ci).collect();
121
122 let violation: f64 = cut_lhs
124 .iter()
125 .zip(x.iter())
126 .map(|(&ai, &xi)| ai * xi)
127 .sum::<f64>()
128 - cut_rhs;
129 if violation <= self.options.cut_violation_tol {
130 let mut a = vec![0.0; n];
134 a[branch_var] = -1.0;
135 let b = -xi.ceil();
136
137 return Some(GomoryCut { a, b });
139 }
140
141 Some(GomoryCut {
142 a: cut_lhs,
143 b: cut_rhs,
144 })
145 }
146
147 fn augment_lp(lp: &LinearProgram, cuts: &[GomoryCut]) -> LinearProgram {
149 if cuts.is_empty() {
150 return lp.clone();
151 }
152 let n = lp.n_vars();
153 let n_new_cuts = cuts.len();
154
155 let augmented = match (&lp.a_ub, &lp.b_ub) {
156 (Some(aub), Some(bub)) => {
157 let m = aub.nrows();
158 let mut new_a = Array2::zeros((m + n_new_cuts, n));
159 let mut new_b = Array1::zeros(m + n_new_cuts);
160
161 for i in 0..m {
163 for j in 0..n {
164 new_a[[i, j]] = aub[[i, j]];
165 }
166 new_b[i] = bub[i];
167 }
168
169 for (k, cut) in cuts.iter().enumerate() {
171 for j in 0..n {
172 new_a[[m + k, j]] = cut.a[j];
173 }
174 new_b[m + k] = cut.b;
175 }
176
177 (new_a, new_b)
178 }
179 _ => {
180 let mut new_a = Array2::zeros((n_new_cuts, n));
181 let mut new_b = Array1::zeros(n_new_cuts);
182 for (k, cut) in cuts.iter().enumerate() {
183 for j in 0..n {
184 new_a[[k, j]] = cut.a[j];
185 }
186 new_b[k] = cut.b;
187 }
188 (new_a, new_b)
189 }
190 };
191
192 LinearProgram {
193 c: lp.c.clone(),
194 a_ub: Some(augmented.0),
195 b_ub: Some(augmented.1),
196 a_eq: lp.a_eq.clone(),
197 b_eq: lp.b_eq.clone(),
198 lower: lp.lower.clone(),
199 upper: lp.upper.clone(),
200 }
201 }
202
203 pub fn solve(&self, lp: &LinearProgram, ivs: &IntegerVariableSet) -> OptimizeResult<MipResult> {
205 let n = lp.n_vars();
206 if n == 0 {
207 return Err(OptimizeError::InvalidInput("Empty problem".to_string()));
208 }
209
210 let lb: Vec<f64> = lp.lower.as_ref().map_or(vec![0.0; n], |l| l.to_vec());
211 let ub: Vec<f64> = lp
212 .upper
213 .as_ref()
214 .map_or(vec![f64::INFINITY; n], |u| u.to_vec());
215
216 let mut current_lp = lp.clone();
217 let mut all_cuts: Vec<GomoryCut> = Vec::new();
218 let mut lp_solves = 0usize;
219 let mut total_cuts = 0usize;
220
221 for iter in 0..self.options.max_iter {
222 if total_cuts >= self.options.max_cuts {
223 break;
224 }
225
226 let lp_result = match LpRelaxationSolver::solve(¤t_lp, &lb, &ub) {
228 Ok(r) => r,
229 Err(e) => return Err(e),
230 };
231 lp_solves += 1;
232
233 if !lp_result.success {
234 return Ok(MipResult {
235 x: Array1::zeros(n),
236 fun: f64::INFINITY,
237 success: false,
238 message: "LP relaxation became infeasible after cuts".to_string(),
239 nodes_explored: iter,
240 lp_solves,
241 lower_bound: f64::INFINITY,
242 });
243 }
244
245 let x: Vec<f64> = lp_result.x.to_vec();
246 let obj = lp_result.fun;
247
248 let all_integer = ivs.kinds.iter().enumerate().all(|(i, k)| {
250 matches!(k, super::IntegerKind::Continuous)
251 || is_integer_valued(x[i], self.options.int_tol)
252 });
253
254 if all_integer {
255 return Ok(MipResult {
256 x: Array1::from_vec(x),
257 fun: obj,
258 success: true,
259 message: format!(
260 "Integer feasible solution found (cuts={}, lp_solves={})",
261 total_cuts, lp_solves
262 ),
263 nodes_explored: iter,
264 lp_solves,
265 lower_bound: obj,
266 });
267 }
268
269 let mut new_cuts_added = 0;
271 let fractional_vars: Vec<usize> = (0..n)
272 .filter(|&i| ivs.is_integer(i) && !is_integer_valued(x[i], self.options.int_tol))
273 .collect();
274
275 for &var in &fractional_vars {
276 if new_cuts_added >= self.options.cuts_per_iter {
277 break;
278 }
279 if total_cuts >= self.options.max_cuts {
280 break;
281 }
282
283 if let Some(cut) = self.generate_gomory_cut(&x, var, n, ¤t_lp, &all_cuts) {
284 all_cuts.push(cut);
285 new_cuts_added += 1;
286 total_cuts += 1;
287 }
288 }
289
290 if new_cuts_added == 0 {
291 break;
293 }
294
295 current_lp = Self::augment_lp(lp, &all_cuts);
297 }
298
299 if self.options.fallback_bb {
301 use super::branch_and_bound::{BranchAndBoundOptions, BranchAndBoundSolver};
302 let bb_opts = BranchAndBoundOptions {
303 max_nodes: 10000,
304 ..Default::default()
305 };
306 let bb = BranchAndBoundSolver::with_options(bb_opts);
307 let bb_result = bb.solve(¤t_lp, ivs)?;
308 return Ok(MipResult {
309 lp_solves: bb_result.lp_solves + lp_solves,
310 ..bb_result
311 });
312 }
313
314 Ok(MipResult {
315 x: Array1::zeros(n),
316 fun: f64::INFINITY,
317 success: false,
318 message: "No integer feasible solution found in cutting plane phase".to_string(),
319 nodes_explored: self.options.max_iter,
320 lp_solves,
321 lower_bound: f64::NEG_INFINITY,
322 })
323 }
324}
325
326impl Default for CuttingPlaneSolver {
327 fn default() -> Self {
328 CuttingPlaneSolver::new()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use approx::assert_abs_diff_eq;
336 use scirs2_core::ndarray::{array, Array2};
337
338 #[test]
339 fn test_cutting_plane_simple_integer() {
340 let c = array![1.0, 1.0];
342 let mut lp = LinearProgram::new(c);
343 lp.a_ub = Some(Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("shape"));
344 lp.b_ub = Some(array![-3.5]);
345 lp.lower = Some(array![0.0, 0.0]);
346 lp.upper = Some(array![10.0, 10.0]);
347
348 let ivs = IntegerVariableSet::all_integer(2);
349 let solver = CuttingPlaneSolver::new();
350 let result = solver.solve(&lp, &ivs).expect("solve failed");
351
352 assert!(result.success);
353 assert_abs_diff_eq!(result.fun, 4.0, epsilon = 1e-3);
354 }
355
356 #[test]
357 fn test_cutting_plane_already_integer() {
358 let c = array![1.0, 2.0];
360 let mut lp = LinearProgram::new(c);
361 lp.lower = Some(array![2.0, 3.0]);
362 lp.upper = Some(array![5.0, 6.0]);
363
364 let ivs = IntegerVariableSet::all_integer(2);
365 let solver = CuttingPlaneSolver::new();
366 let result = solver.solve(&lp, &ivs).expect("solve failed");
367
368 assert!(result.success);
369 assert_abs_diff_eq!(result.fun, 8.0, epsilon = 1e-3);
371 }
372
373 #[test]
374 fn test_cutting_plane_no_fallback() {
375 let opts = CuttingPlaneOptions {
376 fallback_bb: false,
377 max_iter: 50,
378 ..Default::default()
379 };
380 let c = array![1.0, 1.0];
381 let mut lp = LinearProgram::new(c);
382 lp.lower = Some(array![0.0, 0.0]);
383 lp.upper = Some(array![5.0, 5.0]);
384 let ivs = IntegerVariableSet::all_integer(2);
385 let solver = CuttingPlaneSolver::with_options(opts);
386 let _result = solver.solve(&lp, &ivs).expect("solve failed");
388 }
389}