pub mod certificate;
use certificate::{BoundGapCertificate, OptimalCertificate};
use crate::error::SolverError;
use crate::options::WarmStartBasis;
use crate::sparse::CscMatrix;
use std::fmt;
pub(crate) fn is_valid_bound_pair(lb: f64, ub: f64) -> bool {
!lb.is_nan() && !ub.is_nan() && lb <= ub && lb < f64::INFINITY && ub > f64::NEG_INFINITY
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConstraintType {
Le,
Ge,
Eq,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum SolveRoute {
#[default]
Unknown,
LpDirect,
LpForwardedFromQp,
QpIpm,
}
#[derive(Debug, Clone, Default)]
pub struct SolveStats {
pub route: SolveRoute,
pub deadline_triggered: bool,
pub postsolve_krylov_ir_skipped: bool,
pub lp_ipm_path: bool,
pub bounded_eq_ub_path: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum SolveStatus {
Optimal,
LocallyOptimal,
Infeasible,
Unbounded,
MaxIterations,
SuboptimalSolution,
Timeout,
NumericalError,
NonConvex(String),
NonconvexLocal,
NonconvexGlobal,
NotSupported(String),
}
impl fmt::Display for SolveStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SolveStatus::Optimal => write!(f, "Optimal"),
SolveStatus::LocallyOptimal => write!(f, "LocallyOptimal"),
SolveStatus::Infeasible => write!(f, "Infeasible"),
SolveStatus::Unbounded => write!(f, "Unbounded"),
SolveStatus::MaxIterations => write!(f, "MaxIterations"),
SolveStatus::SuboptimalSolution => write!(f, "SuboptimalSolution"),
SolveStatus::Timeout => write!(f, "Timeout"),
SolveStatus::NumericalError => write!(f, "NumericalError"),
SolveStatus::NonConvex(msg) => write!(f, "NonConvex({})", msg),
SolveStatus::NonconvexLocal => write!(f, "NonconvexLocal"),
SolveStatus::NonconvexGlobal => write!(f, "NonconvexGlobal"),
SolveStatus::NotSupported(msg) => write!(f, "NotSupported({})", msg),
}
}
}
#[derive(Debug, Clone)]
pub struct SolverResult {
pub status: SolveStatus,
pub objective: f64,
pub solution: Vec<f64>,
pub dual_solution: Vec<f64>,
pub reduced_costs: Vec<f64>,
pub slack: Vec<f64>,
pub warm_start_basis: Option<WarmStartBasis>,
pub bound_duals: Vec<f64>,
pub iterations: usize,
pub final_residuals: Option<(f64, f64, f64)>,
pub duality_gap_rel: Option<f64>,
pub timing_breakdown: Option<TimingBreakdown>,
pub postsolve_dfeas: Option<f64>,
pub stats: SolveStats,
pub bound_gap_cert: Option<BoundGapCertificate>,
pub opt_cert: Option<OptimalCertificate>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct TimingBreakdown {
pub presolve_us: u64,
pub solve_us: u64,
pub postsolve_us: u64,
pub ipm_factorize_us: u64,
pub ipm_solve_us: u64,
pub ipm_reg_retries: u32,
pub ipm_used_iterative: bool,
pub postsolve_map_us: u64,
pub postsolve_lsq_us: u64,
pub postsolve_recovery_us: u64,
pub postsolve_refine_us: u64,
pub postsolve_krylov_ir_us: u64,
}
impl Default for SolverResult {
fn default() -> Self {
SolverResult {
status: SolveStatus::NumericalError,
objective: 0.0,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
bound_duals: vec![],
iterations: 0,
final_residuals: None,
duality_gap_rel: None,
timing_breakdown: None,
postsolve_dfeas: None,
stats: SolveStats::default(),
bound_gap_cert: None,
opt_cert: None,
}
}
}
impl fmt::Display for SolverResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Status: {}, Objective: {}", self.status, self.objective)
}
}
#[derive(Debug, Clone)]
pub struct LpProblem {
pub c: Vec<f64>,
pub a: CscMatrix,
pub b: Vec<f64>,
pub num_vars: usize,
pub num_constraints: usize,
pub constraint_types: Vec<ConstraintType>,
pub bounds: Vec<(f64, f64)>,
pub name: Option<String>,
pub obj_offset: f64,
}
impl LpProblem {
pub fn new(c: Vec<f64>, a: CscMatrix, b: Vec<f64>) -> Result<Self, SolverError> {
let num_vars = c.len();
let num_constraints = b.len();
let constraint_types = vec![ConstraintType::Le; num_constraints];
let bounds = vec![(0.0, f64::INFINITY); num_vars];
let name = None;
Self::new_general(c, a, b, constraint_types, bounds, name)
}
pub fn new_general(
c: Vec<f64>,
a: CscMatrix,
b: Vec<f64>,
constraint_types: Vec<ConstraintType>,
bounds: Vec<(f64, f64)>,
name: Option<String>,
) -> Result<Self, SolverError> {
if c.len() != a.ncols {
return Err(SolverError::DimensionMismatch {
field: "c",
expected: a.ncols,
got: c.len(),
});
}
if b.len() != a.nrows {
return Err(SolverError::DimensionMismatch {
field: "b",
expected: a.nrows,
got: b.len(),
});
}
if constraint_types.len() != b.len() {
return Err(SolverError::DimensionMismatch {
field: "constraint_types",
expected: b.len(),
got: constraint_types.len(),
});
}
if bounds.len() != c.len() {
return Err(SolverError::DimensionMismatch {
field: "bounds",
expected: c.len(),
got: bounds.len(),
});
}
for (i, &v) in c.iter().enumerate() {
if !v.is_finite() {
return Err(SolverError::NonFiniteCoefficient {
field: "c",
index: i,
});
}
}
for (i, &v) in b.iter().enumerate() {
if !v.is_finite() {
return Err(SolverError::NonFiniteCoefficient {
field: "b",
index: i,
});
}
}
for (i, &v) in a.values.iter().enumerate() {
if !v.is_finite() {
return Err(SolverError::NonFiniteCoefficient {
field: "A",
index: i,
});
}
}
for (i, &(lb, ub)) in bounds.iter().enumerate() {
if !is_valid_bound_pair(lb, ub) {
return Err(SolverError::InvalidBounds { index: i, lb, ub });
}
}
Ok(LpProblem {
num_vars: c.len(),
num_constraints: b.len(),
c,
a,
b,
constraint_types,
bounds,
name,
obj_offset: 0.0,
})
}
}
impl fmt::Display for LpProblem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"LP: min c^T x, {} vars, {} constraints",
self.num_vars, self.num_constraints
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::SolverError;
#[test]
fn test_lp_problem_new_valid() {
let c = vec![1.0, 2.0];
let a = CscMatrix::new(2, 2);
let b = vec![5.0, 6.0];
let lp = LpProblem::new(c, a, b).unwrap();
assert_eq!(lp.num_vars, 2);
assert_eq!(lp.num_constraints, 2);
}
#[test]
fn test_lp_problem_new_invalid_c_dimension() {
let c = vec![1.0, 2.0, 3.0];
let a = CscMatrix::new(2, 2);
let b = vec![5.0, 6.0];
let result = LpProblem::new(c, a, b);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SolverError::DimensionMismatch { field: "c", .. }
));
}
#[test]
fn test_lp_problem_new_invalid_b_dimension() {
let c = vec![1.0, 2.0];
let a = CscMatrix::new(2, 2);
let b = vec![5.0, 6.0, 7.0];
let result = LpProblem::new(c, a, b);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SolverError::DimensionMismatch { field: "b", .. }
));
}
#[test]
fn test_lp_problem_display() {
let c = vec![1.0, 2.0];
let a = CscMatrix::new(2, 2);
let b = vec![5.0, 6.0];
let lp = LpProblem::new(c, a, b).unwrap();
let display = format!("{}", lp);
assert_eq!(display, "LP: min c^T x, 2 vars, 2 constraints");
}
#[test]
fn test_solve_status_display() {
assert_eq!(format!("{}", SolveStatus::Optimal), "Optimal");
assert_eq!(format!("{}", SolveStatus::Infeasible), "Infeasible");
assert_eq!(format!("{}", SolveStatus::Unbounded), "Unbounded");
}
#[test]
fn test_solver_result_display() {
let result = SolverResult {
status: SolveStatus::Optimal,
objective: 42.5,
solution: vec![1.0, 2.0],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
};
let display = format!("{}", result);
assert_eq!(display, "Status: Optimal, Objective: 42.5");
}
#[test]
fn solver_result_default_is_not_success() {
let result = SolverResult::default();
assert_eq!(result.status, SolveStatus::NumericalError);
assert!(result.solution.is_empty());
}
fn make_lp(
c: Vec<f64>,
b: Vec<f64>,
a_vals: Vec<f64>,
bounds: Vec<(f64, f64)>,
) -> Result<LpProblem, SolverError> {
let n = c.len();
let m = b.len();
let a = if a_vals.is_empty() {
CscMatrix::new(m, n)
} else {
let rows = vec![0usize; n];
let cols: Vec<usize> = (0..n).collect();
CscMatrix::from_triplets(&rows, &cols, &a_vals, m, n).unwrap()
};
let ct = vec![ConstraintType::Le; m];
LpProblem::new_general(c, a, b, ct, bounds, None)
}
#[test]
fn lp_valid_accepted() {
let res = make_lp(
vec![1.0, 2.0],
vec![5.0],
vec![1.0, 1.0],
vec![(0.0, f64::INFINITY), (0.0, 10.0)],
);
assert!(res.is_ok());
}
#[test]
fn lp_nan_in_c_rejected() {
let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
for bad in bad_vals {
let res = make_lp(
vec![bad, 1.0],
vec![5.0],
vec![1.0, 1.0],
vec![(0.0, f64::INFINITY); 2],
);
assert!(
matches!(
res,
Err(SolverError::NonFiniteCoefficient { field: "c", .. })
),
"expected NonFiniteCoefficient for c={bad}"
);
}
}
#[test]
fn lp_nan_in_b_rejected() {
let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
for bad in bad_vals {
let res = make_lp(
vec![1.0, 2.0],
vec![bad],
vec![1.0, 1.0],
vec![(0.0, f64::INFINITY); 2],
);
assert!(
matches!(
res,
Err(SolverError::NonFiniteCoefficient { field: "b", .. })
),
"expected NonFiniteCoefficient for b={bad}"
);
}
}
#[test]
fn lp_nan_in_a_rejected() {
let n = 2;
let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
for bad in bad_vals {
let mut a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, n).unwrap();
a.values[0] = bad;
let res = LpProblem::new_general(
vec![1.0, 2.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY); n],
None,
);
assert!(
matches!(
res,
Err(SolverError::NonFiniteCoefficient { field: "A", .. })
),
"expected NonFiniteCoefficient for A val={bad}"
);
}
}
#[test]
fn lp_nan_in_bounds_rejected() {
let cases: Vec<(f64, f64)> = vec![(f64::NAN, 1.0), (0.0, f64::NAN), (f64::NAN, f64::NAN)];
for (lb, ub) in cases {
let res = make_lp(
vec![1.0, 2.0],
vec![5.0],
vec![1.0, 1.0],
vec![(lb, ub), (0.0, f64::INFINITY)],
);
assert!(
matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
"expected InvalidBounds for ({lb},{ub})"
);
}
}
#[test]
fn lp_lb_gt_ub_rejected() {
let cases: Vec<(f64, f64)> = vec![
(5.0, 1.0),
(1.0, 0.0),
(f64::INFINITY, f64::NEG_INFINITY),
(0.1, 0.0),
];
for (lb, ub) in cases {
let res = make_lp(
vec![1.0, 2.0],
vec![5.0],
vec![1.0, 1.0],
vec![(lb, ub), (0.0, f64::INFINITY)],
);
assert!(
matches!(res, Err(SolverError::InvalidBounds { .. })),
"expected InvalidBounds for lb={lb} ub={ub}"
);
}
}
#[test]
fn lp_inf_bounds_accepted() {
let res = make_lp(
vec![1.0, 2.0],
vec![5.0],
vec![1.0, 1.0],
vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
);
assert!(res.is_ok(), "±inf bounds should be valid");
}
#[test]
fn lp_empty_interval_same_inf_rejected() {
let cases: Vec<(f64, f64)> = vec![
(f64::INFINITY, f64::INFINITY),
(f64::NEG_INFINITY, f64::NEG_INFINITY),
];
for (lb, ub) in cases {
let res = make_lp(
vec![1.0, 2.0],
vec![5.0],
vec![1.0, 1.0],
vec![(lb, ub), (0.0, f64::INFINITY)],
);
assert!(
matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
"expected InvalidBounds for ({lb},{ub})"
);
}
}
#[test]
fn lp_valid_bound_variants_accepted() {
let valid_cases: Vec<Vec<(f64, f64)>> = vec![
vec![(f64::NEG_INFINITY, 5.0), (0.0, f64::INFINITY)],
vec![(0.0, f64::INFINITY), (f64::NEG_INFINITY, f64::INFINITY)],
vec![(3.0, 3.0), (0.0, 10.0)],
vec![(0.0, 0.0), (0.0, 0.0)],
];
for bounds in valid_cases {
let res = make_lp(vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0], bounds.clone());
assert!(res.is_ok(), "expected Ok for bounds={bounds:?}");
}
}
#[test]
fn lp_lone_inf_bound_rejected() {
let cases: Vec<(f64, f64)> = vec![
(f64::INFINITY, 5.0),
(f64::INFINITY, f64::INFINITY),
(0.0, f64::NEG_INFINITY),
(f64::NEG_INFINITY, f64::NEG_INFINITY),
];
for (lb, ub) in cases {
let res = make_lp(
vec![1.0, 2.0],
vec![5.0],
vec![1.0, 1.0],
vec![(lb, ub), (0.0, f64::INFINITY)],
);
assert!(
matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
"expected InvalidBounds for ({lb},{ub})"
);
}
}
}