use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Solver {
Gurobi,
Xpress,
Scip,
Highs,
Cplex,
Cbc,
Copt,
Optverse,
Mosek,
}
impl Solver {
pub const fn key(self) -> &'static str {
match self {
Solver::Gurobi => "gurobi",
Solver::Xpress => "xpress",
Solver::Scip => "scip",
Solver::Highs => "highs",
Solver::Cplex => "cplex",
Solver::Cbc => "cbc",
Solver::Copt => "copt",
Solver::Optverse => "optverse",
Solver::Mosek => "mosek",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParserInfo {
pub version: String,
pub git_hash: String,
}
impl ParserInfo {
pub fn current() -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
git_hash: env!("MIPLOG_GIT_HASH").to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverLog {
pub solver: Solver,
pub parser: ParserInfo,
pub version: Option<String>,
pub solver_git_hash: Option<String>,
pub problem: Option<String>,
pub termination: Termination,
pub timing: Timing,
pub bounds: Bounds,
pub tree: TreeStats,
pub presolve: PresolveStats,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub cuts: BTreeMap<String, u64>,
#[serde(skip_serializing_if = "ProgressTable::is_empty", default)]
pub progress: ProgressTable,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub other_data: Vec<NamedValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NamedValue {
pub name: String,
pub value: serde_json::Value,
}
impl NamedValue {
pub fn new(name: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProgressTable {
pub time_seconds: Vec<f64>,
pub nodes_explored: Vec<Option<u64>>,
pub primal: Vec<Option<f64>>,
pub dual: Vec<Option<f64>>,
pub gap: Vec<Option<f64>>,
pub depth: Vec<Option<u32>>,
pub lp_iterations: Vec<Option<u64>>,
pub event: Vec<Option<NodeEvent>>,
}
impl ProgressTable {
pub fn len(&self) -> usize {
self.time_seconds.len()
}
pub fn is_empty(&self) -> bool {
self.time_seconds.is_empty()
}
pub fn push(&mut self, row: NodeSnapshot) {
self.time_seconds.push(row.time_seconds);
self.nodes_explored.push(row.nodes_explored);
self.primal.push(row.primal);
self.dual.push(row.dual);
self.gap.push(row.gap);
self.depth.push(row.depth);
self.lp_iterations.push(row.lp_iterations);
self.event.push(row.event);
}
pub fn iter(&self) -> impl Iterator<Item = NodeSnapshot> + '_ {
(0..self.len()).map(move |i| NodeSnapshot {
time_seconds: self.time_seconds[i],
nodes_explored: self.nodes_explored[i],
primal: self.primal[i],
dual: self.dual[i],
gap: self.gap[i],
depth: self.depth[i],
lp_iterations: self.lp_iterations[i],
event: self.event[i].clone(),
})
}
pub fn last_time(&self) -> Option<f64> {
self.time_seconds.last().copied()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NodeSnapshot {
pub time_seconds: f64,
pub nodes_explored: Option<u64>,
pub primal: Option<f64>,
pub dual: Option<f64>,
pub gap: Option<f64>,
pub depth: Option<u32>,
pub lp_iterations: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub event: Option<NodeEvent>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeEvent {
Heuristic,
BranchSolution,
Cutoff,
Other(String),
}
impl SolverLog {
pub fn new(solver: Solver) -> Self {
Self {
solver,
parser: ParserInfo::current(),
version: None,
solver_git_hash: None,
problem: None,
termination: Termination::default(),
timing: Timing::default(),
bounds: Bounds::default(),
tree: TreeStats::default(),
presolve: PresolveStats::default(),
cuts: BTreeMap::new(),
progress: ProgressTable::default(),
other_data: Vec::new(),
}
}
pub fn verify_common(&self) -> Result<(), Vec<&'static str>> {
let mut missing = Vec::new();
if self.termination.status == Status::Unknown {
missing.push("termination.status");
}
if self.timing.wall_seconds.is_none() {
missing.push("timing.wall_seconds");
}
if self.termination.status == Status::Optimal {
if self.bounds.primal.is_none() {
missing.push("bounds.primal");
}
if self.bounds.dual.is_none() {
missing.push("bounds.dual");
}
}
if missing.is_empty() {
Ok(())
} else {
Err(missing)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Optimal,
Infeasible,
Unbounded,
InfeasibleOrUnbounded,
TimeLimit,
MemoryLimit,
OtherLimit,
UserInterrupt,
NumericalError,
#[default]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Termination {
pub status: Status,
pub raw_reason: Option<String>,
}
impl Termination {
pub fn solved_to_completion(&self) -> bool {
matches!(
self.status,
Status::Optimal
| Status::Infeasible
| Status::Unbounded
| Status::InfeasibleOrUnbounded
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Timing {
pub wall_seconds: Option<f64>,
pub cpu_seconds: Option<f64>,
pub reading_seconds: Option<f64>,
pub presolve_seconds: Option<f64>,
pub root_relaxation_seconds: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Bounds {
pub primal: Option<f64>,
pub dual: Option<f64>,
pub gap: Option<f64>,
pub root_dual: Option<f64>,
pub first_primal: Option<f64>,
pub first_primal_time_seconds: Option<f64>,
pub primal_dual_integral: Option<f64>,
}
impl Bounds {
pub fn effective_gap(&self) -> Option<f64> {
if let Some(g) = self.gap {
return Some(g);
}
match (self.primal, self.dual) {
(Some(p), Some(d)) => Some((p - d).abs() / p.abs().max(1e-10)),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TreeStats {
pub nodes_explored: Option<u64>,
pub simplex_iterations: Option<u64>,
pub solutions_found: Option<u64>,
pub max_depth: Option<u32>,
pub restarts: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PresolveStats {
pub rows_before: Option<u64>,
pub cols_before: Option<u64>,
pub nonzeros_before: Option<u64>,
pub rows_after: Option<u64>,
pub cols_after: Option<u64>,
pub nonzeros_after: Option<u64>,
}