use std::io::Write;
use oximo_core::{Domain, Model, ObjectiveSense, Sense};
use oximo_expr::{LinearTerms, extract_linear};
use crate::error::IoError;
#[allow(clippy::too_many_lines)]
pub fn write_lp<W: Write>(model: &Model, out: &mut W) -> Result<(), IoError> {
let arena = model.arena();
let vars = model.variables();
let constraints = model.constraints();
let objective = model.try_objective().map_err(|_| IoError::NoObjective)?;
let obj_terms = extract_linear(&arena, objective.expr).ok_or(IoError::Nonlinear)?;
writeln!(out, "\\* OXIMO LP export - model: {} *\\", model.name)?;
let sense_kw = match objective.sense {
ObjectiveSense::Minimize => "Minimize",
ObjectiveSense::Maximize => "Maximize",
};
writeln!(out, "{sense_kw}")?;
write!(out, " obj:")?;
write_linear(out, &obj_terms, &vars)?;
writeln!(out)?;
if obj_terms.constant != 0.0 {
writeln!(out, "\\* objective constant: {} *\\", obj_terms.constant)?;
}
writeln!(out, "Subject To")?;
for c in constraints.iter() {
let t = extract_linear(&arena, c.lhs).ok_or(IoError::Nonlinear)?;
let adjusted_rhs = c.rhs - t.constant;
let op = match c.sense {
Sense::Le => "<=",
Sense::Ge => ">=",
Sense::Eq => "=",
};
write!(out, " {}:", c.name)?;
write_linear(out, &t, &vars)?;
writeln!(out, " {op} {adjusted_rhs}")?;
}
let mut wrote_bounds_header = false;
for v in vars.iter() {
if matches!(v.domain, Domain::Binary) {
continue;
}
if v.lb.is_finite() && (v.lb - v.ub).abs() < f64::EPSILON {
if !wrote_bounds_header {
writeln!(out, "Bounds")?;
wrote_bounds_header = true;
}
writeln!(out, " {} <= {} <= {}", v.lb, v.name, v.ub)?;
continue;
}
let lb_default = v.lb == 0.0;
let ub_default = v.ub == f64::INFINITY;
if lb_default && ub_default {
continue;
}
if !wrote_bounds_header {
writeln!(out, "Bounds")?;
wrote_bounds_header = true;
}
if v.lb == f64::NEG_INFINITY && ub_default {
writeln!(out, " {} free", v.name)?;
} else if v.lb == f64::NEG_INFINITY {
writeln!(out, " -inf <= {} <= {}", v.name, v.ub)?;
} else if ub_default {
writeln!(out, " {} >= {}", v.name, v.lb)?;
} else {
writeln!(out, " {} <= {} <= {}", v.lb, v.name, v.ub)?;
}
}
let general_vars: Vec<&str> = vars
.iter()
.filter(|v| matches!(v.domain, Domain::Integer | Domain::SemiInteger { .. }))
.map(|v| v.name.as_str())
.collect();
if !general_vars.is_empty() {
writeln!(out, "General")?;
writeln!(out, " {}", general_vars.join(" "))?;
}
let binary_vars: Vec<&str> = vars
.iter()
.filter(|v| matches!(v.domain, Domain::Binary))
.map(|v| v.name.as_str())
.collect();
if !binary_vars.is_empty() {
writeln!(out, "Binaries")?;
writeln!(out, " {}", binary_vars.join(" "))?;
}
writeln!(out, "End")?;
Ok(())
}
pub fn to_lp_string(model: &Model) -> Result<String, IoError> {
let mut buf = Vec::new();
write_lp(model, &mut buf)?;
Ok(String::from_utf8(buf).expect("LP writer emits ASCII"))
}
fn write_linear<W: Write>(
out: &mut W,
t: &LinearTerms,
vars: &[oximo_core::Variable],
) -> std::io::Result<()> {
let mut first = true;
for (v, coef) in &t.coeffs {
if *coef == 0.0 {
continue;
}
let name = vars[v.index()].name.as_str();
let (sign, mag) = if *coef < 0.0 { ("-", -coef) } else { ("+", *coef) };
if first {
if sign == "-" {
if (mag - 1.0).abs() < f64::EPSILON {
write!(out, " - {name}")?;
} else {
write!(out, " -{mag} {name}")?;
}
} else if (mag - 1.0).abs() < f64::EPSILON {
write!(out, " {name}")?;
} else {
write!(out, " {mag} {name}")?;
}
first = false;
} else if (mag - 1.0).abs() < f64::EPSILON {
write!(out, " {sign} {name}")?;
} else {
write!(out, " {sign} {mag} {name}")?;
}
}
if first {
write!(out, " 0")?;
}
Ok(())
}