use std::io::Write;
use oximo_core::{Domain, Model, ModelKind, ObjectiveSense, Sense};
use oximo_expr::{LinearTerms, extract_linear};
use rustc_hash::FxHashSet;
use crate::error::IoError;
#[allow(clippy::too_many_lines)]
pub fn write_lp<W: Write>(model: &Model, out: &mut W) -> Result<(), IoError> {
if model.num_soc_constraints() > 0
|| matches!(model.kind(), ModelKind::SOCP | ModelKind::MISOCP)
{
return Err(IoError::Conic);
}
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)?;
}
let mut used_labels: FxHashSet<String> =
constraints.iter().map(|c| c.name.to_string()).collect();
writeln!(out, "Subject To")?;
for c in constraints.iter() {
let t = extract_linear(&arena, c.lhs).ok_or(IoError::Nonlinear)?;
if let Some((sense, rhs)) = c.as_single() {
let op = match sense {
Sense::Le => "<=",
Sense::Ge => ">=",
Sense::Eq => "=",
};
let adjusted_rhs = rhs - t.constant;
write!(out, " {}:", c.name)?;
write_linear(out, &t, &vars)?;
writeln!(out, " {op} {adjusted_rhs}")?;
} else if c.is_range() {
let lo = c.lower - t.constant;
let hi = c.upper - t.constant;
let lo_label = unique_label(&mut used_labels, &format!("{}_lo", c.name));
write!(out, " {lo_label}:")?;
write_linear(out, &t, &vars)?;
writeln!(out, " >= {lo}")?;
let hi_label = unique_label(&mut used_labels, &format!("{}_hi", c.name));
write!(out, " {hi_label}:")?;
write_linear(out, &t, &vars)?;
writeln!(out, " <= {hi}")?;
} else {
writeln!(out, "\\* skipped free row: {} *\\", c.name)?;
}
}
let mut wrote_bounds_header = false;
for v in vars.iter() {
if matches!(v.domain, Domain::Binary) {
continue;
}
if let Some(thr) = v.domain.semi_threshold() {
if !wrote_bounds_header {
writeln!(out, "Bounds")?;
wrote_bounds_header = true;
}
if v.ub == f64::INFINITY {
writeln!(out, " {} >= {}", v.name, thr)?;
} else {
writeln!(out, " {} <= {} <= {}", thr, v.name, v.ub)?;
}
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(" "))?;
}
let semi_vars: Vec<&str> = vars
.iter()
.filter(|v| v.domain.semi_threshold().is_some())
.map(|v| v.name.as_str())
.collect();
if !semi_vars.is_empty() {
writeln!(out, "Semi-Continuous")?;
writeln!(out, " {}", semi_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 unique_label(used: &mut FxHashSet<String>, base: &str) -> String {
if used.insert(base.to_string()) {
return base.to_string();
}
let mut n = 2;
loop {
let candidate = format!("{base}_{n}");
if used.insert(candidate.clone()) {
return candidate;
}
n += 1;
}
}
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(())
}