1use crate::options::{SimplexMethod, SolverOptions, WarmStartBasis};
4use crate::presolve;
5use crate::problem::{LpProblem, SolveStatus, SolverResult};
6use crate::qp::certificate::guard_lp_optimal;
7use crate::tolerances::PIVOT_TOL;
8
9use super::dual;
10use super::dual_advanced;
11use super::primal::two_phase_simplex;
12use super::standard_form::build_standard_form;
13
14pub fn solve(problem: &LpProblem) -> SolverResult {
16 solve_with(problem, &SolverOptions::default())
17}
18
19pub fn solve_with(problem: &LpProblem, options: &SolverOptions) -> SolverResult {
25 if options.validate().is_err() {
26 return SolverResult::numerical_error();
27 }
28 let mut opts_with_deadline;
30 let options = if let (Some(secs), true) = (options.timeout_secs, options.deadline.is_none()) {
31 opts_with_deadline = options.clone();
32 opts_with_deadline.deadline = Some(
33 std::time::Instant::now() + std::time::Duration::from_secs_f64(secs),
34 );
35 &opts_with_deadline
36 } else {
37 options
38 };
39
40 let prof_t0 = std::time::Instant::now();
41 let mut non_reduced_presolve_us: Option<u64> = None;
44
45 if options.presolve {
46 match presolve::run_presolve(problem, options.deadline) {
47 Err(presolve::PresolveStatus::Infeasible) => {
48 return SolverResult {
49 status: SolveStatus::Infeasible,
50 objective: 0.0,
51 solution: vec![],
52 dual_solution: vec![],
53 reduced_costs: vec![],
54 slack: vec![],
55 warm_start_basis: None,
56 ..Default::default()
57 };
58 }
59 Err(presolve::PresolveStatus::Unbounded) => {
60 return SolverResult {
61 status: SolveStatus::Unbounded,
62 objective: f64::NEG_INFINITY,
63 solution: vec![],
64 dual_solution: vec![],
65 reduced_costs: vec![],
66 slack: vec![],
67 warm_start_basis: None,
68 ..Default::default()
69 };
70 }
71 Ok(presolve_result) if presolve_result.was_reduced => {
72 let opts_no_ws = if options.warm_start.is_some() {
74 let mut o = options.clone();
75 o.warm_start = None;
76 o.presolve = false;
77 Some(o)
78 } else {
79 None
80 };
81 let eff_opts = opts_no_ws.as_ref().unwrap_or(options);
82 let t_presolve_done = std::time::Instant::now();
83 let presolve_us = t_presolve_done.duration_since(prof_t0).as_micros() as u64;
84 let raw = solve_without_presolve(&presolve_result.reduced_problem, eff_opts);
85 let t_solve_done = std::time::Instant::now();
86 let solve_us = t_solve_done.duration_since(t_presolve_done).as_micros() as u64;
87 if matches!(raw.status, SolveStatus::NumericalError | SolveStatus::SuboptimalSolution) {
92 return solve_without_presolve(problem, options);
93 }
94 let mut res = presolve::postsolve::run_postsolve(
95 &raw,
96 &presolve_result,
97 problem,
98 eff_opts.deadline,
99 options.recover_warm_start_basis,
100 );
101 res = guard_lp_optimal(res, problem);
102 let postsolve_us = t_solve_done.elapsed().as_micros() as u64;
103 res.timing_breakdown = Some(crate::problem::TimingBreakdown {
104 presolve_us, solve_us, postsolve_us,
105 ..Default::default()
106 });
107 let postsolve_bad = res.postsolve_dfeas.is_some_and(|d| d > PIVOT_TOL)
111 || res.status == SolveStatus::SuboptimalSolution;
112 if matches!(res.status, SolveStatus::Optimal | SolveStatus::SuboptimalSolution)
113 && postsolve_bad
114 {
115 let deadline_ok = options.deadline
116 .is_none_or(|d| std::time::Instant::now() < d);
117 if deadline_ok {
118 let mut opts_off = options.clone();
119 opts_off.presolve = false;
120 opts_off.simplex_method = crate::options::SimplexMethod::Primal;
122 let t_alt_start = std::time::Instant::now();
123 let mut alt = solve_without_presolve(problem, &opts_off);
124 let alt_solve_us = t_alt_start.elapsed().as_micros() as u64;
125 if alt.status == SolveStatus::Optimal
126 && alt.postsolve_dfeas.is_none()
127 && alt.objective.is_finite()
128 {
129 alt.timing_breakdown = Some(crate::problem::TimingBreakdown {
133 presolve_us,
134 solve_us: alt_solve_us,
135 postsolve_us,
136 ..Default::default()
137 });
138 return alt;
139 }
140 }
141 }
142 return res;
143 }
144 Ok(_) => {
145 non_reduced_presolve_us = Some(prof_t0.elapsed().as_micros() as u64);
147 }
148 }
149 }
150
151 if options.deadline.is_some_and(|d| std::time::Instant::now() >= d) {
154 return SolverResult {
155 status: SolveStatus::Timeout,
156 objective: f64::INFINITY,
157 solution: vec![],
158 dual_solution: vec![],
159 reduced_costs: vec![],
160 slack: vec![],
161 warm_start_basis: None,
162 ..Default::default()
163 };
164 }
165
166 let t_solve_start = std::time::Instant::now();
167 let mut result = solve_without_presolve(problem, options);
168 if let Some(presolve_us) = non_reduced_presolve_us {
169 let solve_us = t_solve_start.elapsed().as_micros() as u64;
170 result.timing_breakdown = Some(crate::problem::TimingBreakdown {
171 presolve_us,
172 solve_us,
173 postsolve_us: 0,
174 ..Default::default()
175 });
176 }
177 result
178}
179
180pub(crate) fn solve_without_presolve(problem: &LpProblem, options: &SolverOptions) -> SolverResult {
182 let m = problem.num_constraints;
183 let n = problem.num_vars;
184
185 if n == 0 {
186 for i in 0..m {
187 if problem.b[i] < -options.primal_tol {
188 return SolverResult {
189 status: SolveStatus::Infeasible,
190 objective: 0.0,
191 solution: vec![],
192 dual_solution: vec![],
193 reduced_costs: vec![],
194 slack: vec![],
195 warm_start_basis: None,
196 ..Default::default()
197 };
198 }
199 }
200 return SolverResult {
201 status: SolveStatus::Optimal,
202 objective: 0.0,
203 solution: vec![],
204 dual_solution: vec![0.0; m],
205 reduced_costs: vec![],
206 slack: problem.b.clone(),
207 warm_start_basis: None,
208 ..Default::default()
209 };
210 }
211
212 if m == 0 {
213 let mut x = vec![0.0; n];
214 let mut obj = 0.0;
215 for (j, x_j) in x.iter_mut().enumerate() {
216 if problem.c[j] < -options.primal_tol {
217 let ub = problem.bounds[j].1;
219 if ub.is_infinite() {
220 return SolverResult {
221 status: SolveStatus::Unbounded,
222 objective: f64::NEG_INFINITY,
223 solution: vec![],
224 dual_solution: vec![],
225 reduced_costs: vec![],
226 slack: vec![],
227 warm_start_basis: None,
228 ..Default::default()
229 };
230 }
231 *x_j = ub;
232 }
233 obj += problem.c[j] * *x_j;
234 }
235 return SolverResult {
236 status: SolveStatus::Optimal,
237 objective: obj,
238 solution: x,
239 dual_solution: vec![],
240 reduced_costs: problem.c.clone(),
241 slack: vec![],
242 warm_start_basis: None,
243 ..Default::default()
244 };
245 }
246
247 let sf = build_standard_form(problem);
248
249 let warm_lp_opts;
252 let options = if let Some(ws_lp) = options.warm_start_lp.as_ref() {
253 if options.warm_start.is_none() {
254 warm_lp_opts = SolverOptions {
255 warm_start: Some(WarmStartBasis {
256 basis: ws_lp.basis.clone(),
257 x_b: Vec::new(),
258 }),
259 ..options.clone()
260 };
261 &warm_lp_opts
262 } else {
263 options
264 }
265 } else {
266 options
267 };
268
269 let result = match options.simplex_method {
270 SimplexMethod::Primal => two_phase_simplex(&sf, problem, options),
271 SimplexMethod::Dual => dual::two_phase_dual_simplex(&sf, problem, options),
272 SimplexMethod::DualAdvanced | SimplexMethod::Auto => {
273 dual_advanced::solve_dual_advanced(&sf, problem, options)
276 }
277 };
278 guard_lp_optimal(result, problem)
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::problem::ConstraintType;
285 use crate::sparse::CscMatrix;
286
287 fn make_trivial_lp() -> LpProblem {
288 let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
290 LpProblem::new_general(
291 vec![1.0],
292 a,
293 vec![5.0],
294 vec![ConstraintType::Le],
295 vec![(0.0, f64::INFINITY)],
296 None,
297 )
298 .unwrap()
299 }
300
301 #[test]
304 fn guard_lp_optimal_catches_corrupt_result() {
305 let lp = make_trivial_lp();
306 let corrupt = SolverResult {
307 status: SolveStatus::Optimal,
308 objective: 1e12,
309 solution: vec![1e12],
310 dual_solution: vec![0.0],
311 reduced_costs: vec![0.0],
312 slack: vec![0.0],
313 ..Default::default()
314 };
315 let guarded = guard_lp_optimal(corrupt, &lp);
316 assert_eq!(
317 guarded.status,
318 SolveStatus::SuboptimalSolution,
319 "guard must demote false-Optimal with |Ax-b| >> tol to SuboptimalSolution"
320 );
321 }
322
323 #[test]
325 fn guard_lp_optimal_passthrough_non_optimal() {
326 let lp = make_trivial_lp();
327 for status in [SolveStatus::Infeasible, SolveStatus::Timeout, SolveStatus::NumericalError] {
328 let r = SolverResult { status: status.clone(), ..Default::default() };
329 let out = guard_lp_optimal(r, &lp);
330 assert_eq!(out.status, status, "guard must pass through {status:?}");
331 }
332 }
333
334 fn make_non_reducible_lp() -> LpProblem {
339 let a = CscMatrix::from_triplets(
340 &[0, 1, 0, 1],
341 &[0, 0, 1, 1],
342 &[2.0, 1.0, 1.0, 2.0],
343 2, 2,
344 ).unwrap();
345 LpProblem::new_general(
346 vec![1.0, 1.0],
347 a,
348 vec![3.0, 3.0],
349 vec![ConstraintType::Ge, ConstraintType::Ge],
350 vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
351 None,
352 ).unwrap()
353 }
354
355 fn make_reducible_lp() -> LpProblem {
358 let a = CscMatrix::from_triplets(
359 &[0, 1, 1],
360 &[0, 0, 1],
361 &[1.0, 1.0, 1.0],
362 2, 2,
363 ).unwrap();
364 LpProblem::new_general(
365 vec![1.0, 1.0],
366 a,
367 vec![2.0, 5.0],
368 vec![ConstraintType::Eq, ConstraintType::Le],
369 vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
370 None,
371 ).unwrap()
372 }
373
374 #[test]
376 fn timing_breakdown_set_when_presolve_does_not_reduce() {
377 let lp = make_non_reducible_lp();
378
379 let pr = crate::presolve::run_presolve(&lp, None)
381 .expect("non-reducible LP must not be Infeasible/Unbounded at presolve");
382 assert!(
383 !pr.was_reduced,
384 "make_non_reducible_lp() must produce an LP presolve cannot reduce (was_reduced must be false)"
385 );
386
387 let opts = SolverOptions { presolve: true, ..SolverOptions::default() };
388 let result = solve_with(&lp, &opts);
389 assert_eq!(result.status, SolveStatus::Optimal);
390 assert!(
391 result.timing_breakdown.is_some(),
392 "timing_breakdown must be Some even when presolve does not reduce the problem"
393 );
394 }
395
396 #[test]
398 fn timing_breakdown_set_when_presolve_reduces() {
399 let lp = make_reducible_lp();
400 let opts = SolverOptions { presolve: true, ..SolverOptions::default() };
401 let result = solve_with(&lp, &opts);
402 assert_eq!(result.status, SolveStatus::Optimal);
403 assert!(
404 result.timing_breakdown.is_some(),
405 "timing_breakdown must be Some when presolve reduces the problem"
406 );
407 let _tb = result.timing_breakdown.unwrap();
410 }
411
412 #[test]
417 fn invalid_options_rejected_at_simplex_entry() {
418 let lp = make_trivial_lp();
419 let cases: &[(&str, SolverOptions)] = &[
420 ("nan primal_tol", SolverOptions { primal_tol: f64::NAN, ..Default::default() }),
421 ("zero primal_tol", SolverOptions { primal_tol: 0.0, ..Default::default() }),
422 ("neg dual_tol", SolverOptions { dual_tol: -1.0, ..Default::default() }),
423 ("inf timeout", SolverOptions { timeout_secs: Some(f64::INFINITY), ..Default::default() }),
424 ("neg timeout", SolverOptions { timeout_secs: Some(-1.0), ..Default::default() }),
425 ("zero threads", SolverOptions { threads: 0, ..Default::default() }),
426 ];
427 for (label, opts) in cases {
428 let result = solve_with(&lp, opts);
429 assert_eq!(
430 result.status,
431 SolveStatus::NumericalError,
432 "simplex::solve_with with {label} must return NumericalError"
433 );
434 }
435 }
436}