use crate::options::SolverOptions;
use crate::problem::{LpProblem, SolveRoute, SolveStatus, SolverResult};
pub fn solve_lp_with(problem: &LpProblem, options: &SolverOptions) -> SolverResult {
let mut result = crate::simplex::solve_with(problem, options);
if matches!(
result.status,
SolveStatus::Optimal | SolveStatus::SuboptimalSolution | SolveStatus::Timeout
) {
result.objective += problem.obj_offset;
}
result.stats.route = SolveRoute::LpDirect;
result.stats.deadline_triggered = matches!(result.status, SolveStatus::Timeout);
result
}
pub(crate) fn solve_lp_forwarded_from_qp(
problem: &LpProblem,
options: &SolverOptions,
) -> SolverResult {
let mut result = crate::simplex::solve_with(problem, options);
if matches!(
result.status,
SolveStatus::Optimal | SolveStatus::SuboptimalSolution | SolveStatus::Timeout
) {
result.objective += problem.obj_offset;
}
result.stats.route = SolveRoute::LpForwardedFromQp;
result.stats.deadline_triggered = matches!(result.status, SolveStatus::Timeout);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::problem::ConstraintType;
use crate::sparse::CscMatrix;
fn make_trivial_lp() -> LpProblem {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
LpProblem::new_general(
vec![1.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap()
}
#[test]
fn test_lp_timeout_incumbent_includes_obj_offset() {
use std::sync::{atomic::AtomicBool, Arc};
let mut lp = make_trivial_lp();
lp.obj_offset = 42.5;
let opts = SolverOptions {
cancel_flag: Some(Arc::new(AtomicBool::new(true))),
presolve: false,
..Default::default()
};
let result = solve_lp_with(&lp, &opts);
assert_eq!(
result.status,
SolveStatus::Timeout,
"cancel_flag=true must produce Timeout"
);
assert!(
result.objective.is_finite(),
"Timeout incumbent must have finite objective (not INFINITY); got {}",
result.objective
);
assert!(
(result.objective - 42.5).abs() < 1e-9,
"Timeout incumbent must include obj_offset 42.5; got {} \
(sentinel: removing Timeout from match yields 0.0 ≠ 42.5)",
result.objective
);
}
#[test]
fn invalid_options_rejected_at_lp_entry() {
let lp = make_trivial_lp();
let cases: &[(&str, SolverOptions)] = &[
(
"nan primal_tol",
SolverOptions {
primal_tol: f64::NAN,
..Default::default()
},
),
(
"inf primal_tol",
SolverOptions {
primal_tol: f64::INFINITY,
..Default::default()
},
),
(
"neg timeout_secs",
SolverOptions {
timeout_secs: Some(-0.5),
..Default::default()
},
),
(
"zero threads",
SolverOptions {
threads: 0,
..Default::default()
},
),
(
"nan dual_tol",
SolverOptions {
dual_tol: f64::NAN,
..Default::default()
},
),
];
for (label, opts) in cases {
let result = solve_lp_with(&lp, opts);
assert_eq!(
result.status,
SolveStatus::NumericalError,
"solve_lp_with with {label} must return NumericalError"
);
}
}
fn cx(lp: &LpProblem, sol: &[f64]) -> f64 {
lp.c.iter().zip(sol).map(|(c, x)| c * x).sum::<f64>() + lp.obj_offset
}
fn solve_lp_no_presolve(lp: &LpProblem) -> SolverResult {
let opts = SolverOptions {
presolve: false,
..Default::default()
};
solve_lp_with(lp, &opts)
}
#[test]
fn bigm_nonzero_lb_objective_double_count() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![1.0],
a,
vec![5.0],
vec![ConstraintType::Ge],
vec![(3.0, f64::INFINITY)],
None,
)
.unwrap();
let res = solve_lp_no_presolve(&lp);
assert_eq!(res.status, SolveStatus::Optimal);
assert!(
(res.solution[0] - 5.0).abs() < 1e-6,
"solution must be x=5; got {}",
res.solution[0]
);
assert!(
(res.objective - cx(&lp, &res.solution)).abs() < 1e-6,
"reported objective {} must equal c·x {} (Big-M path double-counts \
sf.obj_offset = c·lb = 3 ⇒ reports 8 instead of 5)",
res.objective,
cx(&lp, &res.solution)
);
}
#[test]
fn le_only_nonzero_lb_objective_correct() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![1.0],
a,
vec![10.0],
vec![ConstraintType::Le],
vec![(3.0, f64::INFINITY)],
None,
)
.unwrap();
let res = solve_lp_no_presolve(&lp);
assert_eq!(res.status, SolveStatus::Optimal);
assert!(
(res.objective - cx(&lp, &res.solution)).abs() < 1e-6,
"Le-only path: reported {} must equal c·x {}",
res.objective,
cx(&lp, &res.solution)
);
}
#[test]
fn bounded_nonzero_lb_objective_correct() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![1.0],
a,
vec![10.0],
vec![ConstraintType::Le],
vec![(3.0, 8.0)],
None,
)
.unwrap();
let res = solve_lp_no_presolve(&lp);
assert_eq!(res.status, SolveStatus::Optimal);
assert!(
(res.objective - cx(&lp, &res.solution)).abs() < 1e-6,
"bounded path: reported {} must equal c·x {}",
res.objective,
cx(&lp, &res.solution)
);
}
}