use crate::model::{Builder, ModelSpec};
use lp_parser_rs::error::LpParseError;
use lp_parser_rs::lexer::{Lexer, ParseResult, RawConstraint};
use lp_parser_rs::lp::LpProblemParser;
use lp_parser_rs::model::{ComparisonOp as LpOp, Sense, VariableType};
use microlp::{ComparisonOp, OptimizationDirection, Problem, Variable};
use std::collections::HashMap;
const INF_SENTINEL: f64 = 1e30;
type ParsedConstraint = (Vec<(usize, f64)>, ComparisonOp, f64);
pub struct ParsedLp {
pub problem: Problem,
pub spec: ModelSpec,
pub obj_offset: f64,
pub direction: OptimizationDirection,
}
#[derive(Clone)]
struct RawVar {
obj: f64,
lo: Option<f64>,
hi: Option<f64>,
general: bool,
binary: bool,
}
impl RawVar {
fn new() -> RawVar {
RawVar {
obj: 0.0,
lo: None,
hi: None,
general: false,
binary: false,
}
}
}
struct VarTable<'a> {
vars: Vec<RawVar>,
names: Vec<&'a str>,
index: HashMap<&'a str, usize>,
}
impl<'a> VarTable<'a> {
fn new() -> Self {
VarTable {
vars: vec![],
names: vec![],
index: HashMap::new(),
}
}
fn var(&mut self, name: &'a str) -> usize {
if let Some(&idx) = self.index.get(name) {
return idx;
}
self.vars.push(RawVar::new());
self.names.push(name);
self.index.insert(name, self.vars.len() - 1);
self.vars.len() - 1
}
}
pub fn parse(text: &str, relax_integers: bool) -> Result<ParsedLp, String> {
let lexer = Lexer::new(text);
let parsed: ParseResult = LpProblemParser::new()
.parse(lexer)
.map_err(|e| format!("LP parse failed: {}", LpParseError::from(e)))?;
let direction = match parsed.sense {
Sense::Minimize => OptimizationDirection::Minimize,
Sense::Maximize => OptimizationDirection::Maximize,
};
if parsed.objectives.len() != 1 {
return Err(format!(
"expected exactly one objective, found {}",
parsed.objectives.len()
));
}
if !parsed.sos.is_empty() {
return Err("SOS constraints are not representable in microlp".to_string());
}
if !parsed.semi_continuous.is_empty() {
return Err("semi-continuous variables are not representable in microlp".to_string());
}
let mut t = VarTable::new();
let objective = &parsed.objectives[0];
let obj_offset = objective.constant;
for c in &objective.coefficients {
let idx = t.var(c.name);
t.vars[idx].obj += c.value;
}
let mut constraints: Vec<ParsedConstraint> = vec![];
for rc in &parsed.constraints {
match rc {
RawConstraint::Standard {
name,
coefficients,
operator,
rhs,
..
} => {
if coefficients.is_empty() {
return Err(format!(
"constraint {} has no variables on either side",
name
));
}
let mut terms: Vec<(usize, f64)> = vec![];
let mut term_pos: HashMap<usize, usize> = HashMap::new();
for c in coefficients {
let idx = t.var(c.name);
match term_pos.get(&idx) {
Some(&pos) => terms[pos].1 += c.value,
None => {
term_pos.insert(idx, terms.len());
terms.push((idx, c.value));
}
}
}
let op = match operator {
LpOp::LT | LpOp::LTE => ComparisonOp::Le,
LpOp::GT | LpOp::GTE => ComparisonOp::Ge,
LpOp::EQ => ComparisonOp::Eq,
};
constraints.push((terms, op, *rhs));
}
RawConstraint::SOS { name, .. } => {
return Err(format!(
"SOS constraint {} is not representable in microlp",
name
));
}
}
}
for (name, vt) in &parsed.bounds {
let idx = t.var(name);
let v = &mut t.vars[idx];
match vt {
VariableType::Free => {
v.lo = Some(f64::NEG_INFINITY);
v.hi = Some(f64::INFINITY);
}
VariableType::LowerBound(l) => v.lo = Some(desentinel(*l)),
VariableType::UpperBound(u) => v.hi = Some(desentinel(*u)),
VariableType::DoubleBound(l, u) => {
v.lo = Some(desentinel(*l));
v.hi = Some(desentinel(*u));
}
other => {
return Err(format!(
"unexpected variable type {} in Bounds for {}",
other, name
));
}
}
}
for name in parsed.generals.iter().chain(parsed.integers.iter()) {
let idx = t.var(name);
t.vars[idx].general = true;
}
for name in &parsed.binaries {
let idx = t.var(name);
t.vars[idx].binary = true;
}
let mut b = Builder::new(direction);
for v in &t.vars {
let (lo, hi) = resolve_bounds(v);
let integer = (v.binary || v.general) && !relax_integers;
if integer {
b.integer(v.obj, clamp_i32(lo), clamp_i32(hi));
} else {
b.real(v.obj, lo, hi);
}
}
for (terms, op, rhs) in &constraints {
let terms: Vec<(Variable, f64)> = terms.iter().map(|&(vi, x)| (b.vars[vi], x)).collect();
b.constraint(&terms, *op, *rhs);
}
assert_eq!(b.spec.vars.len(), t.vars.len());
Ok(ParsedLp {
problem: b.problem,
spec: b.spec,
obj_offset,
direction,
})
}
fn desentinel(v: f64) -> f64 {
if v >= INF_SENTINEL {
f64::INFINITY
} else if v <= -INF_SENTINEL {
f64::NEG_INFINITY
} else {
v
}
}
fn resolve_bounds(v: &RawVar) -> (f64, f64) {
if v.binary {
return (0.0, 1.0);
}
let lo = v.lo.unwrap_or(0.0);
let hi = v.hi.unwrap_or(f64::INFINITY);
(lo, hi)
}
fn clamp_i32(x: f64) -> i32 {
if x <= i32::MIN as f64 {
i32::MIN
} else if x >= i32::MAX as f64 {
i32::MAX
} else {
x.round() as i32
}
}