use crate::model::{Domain, Expected, ModelSpec};
use microlp::{ComparisonOp, Error, Solution, SolutionStatus, SolveOutcome};
const FEAS_ABS: f64 = 1e-6;
const INT_ABS: f64 = 1e-5;
const OBJ_CONSISTENCY: f64 = 1e-6;
pub enum Outcome {
Pass,
Fail(String),
Timeout,
}
pub fn check(
spec: &ModelSpec,
expected: &Expected,
result: Result<SolveOutcome, Error>,
) -> Outcome {
match result {
Ok(SolveOutcome::Interrupted(_)) => Outcome::Timeout,
Ok(SolveOutcome::Solution(sol)) => {
if sol.status() == SolutionStatus::Feasible {
return Outcome::Timeout;
}
let values: Vec<f64> = sol.iter().map(|(_, x)| x).collect();
if values.len() != spec.vars.len() {
return Outcome::Fail(format!(
"solution has {} variables, shadow model has {}",
values.len(),
spec.vars.len()
));
}
if let Err(msg) = validate_solution(spec, &values, sol.objective()) {
return Outcome::Fail(msg);
}
match expected {
Expected::Objective { value, tol, vars } => {
if !tol.matches(sol.objective(), *value) {
return Outcome::Fail(format!(
"expected objective {}, got {} (diff {:.3e})",
value,
sol.objective(),
(sol.objective() - value).abs()
));
}
if let Some(expected_vars) = vars {
for (var, want) in expected_vars {
let got = values[var.idx()];
if (got - want).abs() > 1e-6 + 1e-6 * want.abs() {
return Outcome::Fail(format!(
"unique optimum: expected x{} = {}, got {}",
var.idx(),
want,
got
));
}
}
}
Outcome::Pass
}
Expected::Infeasible => Outcome::Fail(format!(
"expected Infeasible, but solver returned a solution with objective {} \
that satisfies all constraints of the shadow model — either the solver \
or the suite's model of this case is wrong",
sol.objective()
)),
Expected::Unbounded => Outcome::Fail(format!(
"expected Unbounded, got a finite solution with objective {}",
sol.objective()
)),
}
}
Err(err) => match (expected, &err) {
(Expected::Infeasible, Error::Infeasible) => Outcome::Pass,
(Expected::Unbounded, Error::Unbounded) => Outcome::Pass,
(Expected::Objective { value, .. }, _) => {
Outcome::Fail(format!("expected objective {}, got error: {}", value, err))
}
(Expected::Infeasible, _) => {
Outcome::Fail(format!("expected Infeasible, got error: {}", err))
}
(Expected::Unbounded, _) => {
Outcome::Fail(format!("expected Unbounded, got error: {}", err))
}
},
}
}
pub fn require_solution(outcome: SolveOutcome, context: &str) -> Result<Solution, String> {
outcome.into_solution().map_err(|interrupted| {
format!(
"{context}: no incumbent before {:?}",
interrupted.termination_reason()
)
})
}
pub fn validate_incumbent(spec: &ModelSpec, sol: &Solution) -> Result<(), String> {
let values: Vec<f64> = sol.iter().map(|(_, x)| x).collect();
if values.len() != spec.vars.len() {
return Err(format!(
"solution has {} variables, shadow model has {}",
values.len(),
spec.vars.len()
));
}
validate_solution(spec, &values, sol.objective())
}
fn validate_solution(spec: &ModelSpec, values: &[f64], reported_obj: f64) -> Result<(), String> {
for (i, (var, &x)) in spec.vars.iter().zip(values).enumerate() {
if !x.is_finite() {
return Err(format!("x{} is not finite: {}", i, x));
}
let slack = FEAS_ABS * (1.0 + var.min.abs().max(var.max.abs()).min(1e12));
if x < var.min - slack || x > var.max + slack {
return Err(format!(
"x{} = {} violates bounds [{}, {}]",
i, x, var.min, var.max
));
}
if var.domain == Domain::Integer && (x - x.round()).abs() > INT_ABS {
return Err(format!("x{} = {} is not integral", i, x));
}
}
for (ci, c) in spec.constraints.iter().enumerate() {
let lhs: f64 = c.terms.iter().map(|(vi, coeff)| coeff * values[*vi]).sum();
let slack = FEAS_ABS * (1.0 + c.rhs.abs());
let ok = match c.op {
ComparisonOp::Le => lhs <= c.rhs + slack,
ComparisonOp::Ge => lhs >= c.rhs - slack,
ComparisonOp::Eq => (lhs - c.rhs).abs() <= slack,
};
if !ok {
return Err(format!(
"constraint #{} violated: lhs = {}, should be {} {}",
ci,
lhs,
match c.op {
ComparisonOp::Le => "<=",
ComparisonOp::Ge => ">=",
ComparisonOp::Eq => "==",
},
c.rhs
));
}
}
let computed: f64 = spec
.vars
.iter()
.zip(values)
.map(|(v, &x)| v.obj_coeff * x)
.sum();
let slack = OBJ_CONSISTENCY * (1.0 + computed.abs());
if (computed - reported_obj).abs() > slack {
return Err(format!(
"reported objective {} disagrees with recomputed value {} from variable values",
reported_obj, computed
));
}
Ok(())
}