pub mod ac;
pub mod bnb;
pub mod dc;
#[cfg(feature = "opf-highs")]
pub mod highs;
pub mod ipm;
#[cfg(feature = "opf-ipopt")]
pub mod ipopt;
pub mod model;
pub mod nlp;
#[derive(Clone, Debug, Default)]
pub struct LinearProgram {
pub n_vars: usize,
pub n_rows: usize,
pub col_lower: Vec<f64>,
pub col_upper: Vec<f64>,
pub col_cost: Vec<f64>,
pub offset: f64,
pub hessian: Option<Vec<(usize, usize, f64)>>,
pub rows: Vec<(usize, usize, f64)>,
pub row_lower: Vec<f64>,
pub row_upper: Vec<f64>,
pub col_integral: Vec<bool>,
}
impl LinearProgram {
pub fn new(n_vars: usize) -> Self {
Self {
n_vars,
n_rows: 0,
col_integral: Vec::new(),
col_lower: vec![f64::NEG_INFINITY; n_vars],
col_upper: vec![f64::INFINITY; n_vars],
col_cost: vec![0.0; n_vars],
offset: 0.0,
hessian: None,
rows: Vec::new(),
row_lower: Vec::new(),
row_upper: Vec::new(),
}
}
pub fn add_row(&mut self, coefficients: &[(usize, f64)], lower: f64, upper: f64) -> usize {
let row = self.n_rows;
for &(col, value) in coefficients {
self.rows.push((row, col, value));
}
self.row_lower.push(lower);
self.row_upper.push(upper);
self.n_rows += 1;
row
}
pub fn set_integral(&mut self, col: usize) {
if self.col_integral.len() < self.n_vars {
self.col_integral.resize(self.n_vars, false);
}
if col < self.n_vars {
self.col_integral[col] = true;
}
}
pub fn set_binary(&mut self, col: usize) {
self.set_integral(col);
if col < self.n_vars {
self.col_lower[col] = 0.0;
self.col_upper[col] = 1.0;
}
}
pub fn is_integral(&self, col: usize) -> bool {
self.col_integral.get(col).copied().unwrap_or(false)
}
pub fn has_integers(&self) -> bool {
self.col_integral.iter().any(|x| *x)
}
pub fn n_integers(&self) -> usize {
self.col_integral.iter().filter(|x| **x).count()
}
pub fn validate(&self) -> Result<(), OpfError> {
let n = self.n_vars;
for (name, len) in [
("col_lower", self.col_lower.len()),
("col_upper", self.col_upper.len()),
("col_cost", self.col_cost.len()),
] {
if len != n {
return Err(OpfError::Shape {
what: name,
expected: n,
got: len,
});
}
}
for (name, len) in [
("row_lower", self.row_lower.len()),
("row_upper", self.row_upper.len()),
] {
if len != self.n_rows {
return Err(OpfError::Shape {
what: name,
expected: self.n_rows,
got: len,
});
}
}
for &(row, col, _) in &self.rows {
if row >= self.n_rows || col >= n {
return Err(OpfError::Index { row, col });
}
}
if let Some(hessian) = &self.hessian {
for &(i, j, _) in hessian {
if i >= n || j >= n {
return Err(OpfError::Index { row: i, col: j });
}
if j > i {
return Err(OpfError::UpperTriangleHessian { row: i, col: j });
}
}
}
for k in 0..n {
if self.col_lower[k] > self.col_upper[k] {
return Err(OpfError::CrossedBounds {
index: k,
kind: "column",
});
}
}
for k in 0..self.n_rows {
if self.row_lower[k] > self.row_upper[k] {
return Err(OpfError::CrossedBounds {
index: k,
kind: "row",
});
}
}
if !self.col_integral.is_empty() && self.col_integral.len() != n {
return Err(OpfError::Shape {
what: "col_integral",
expected: n,
got: self.col_integral.len(),
});
}
if self.has_integers() && self.hessian.is_some() {
return Err(OpfError::IntegerQuadratic);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OptStatus {
Optimal,
Infeasible,
Unbounded,
Other(&'static str),
}
#[derive(Clone, Debug)]
pub struct Solution {
pub status: OptStatus,
pub objective: f64,
pub primal: Vec<f64>,
pub row_activity: Vec<f64>,
pub col_dual: Vec<f64>,
pub row_dual: Vec<f64>,
}
impl Solution {
pub fn failed(status: OptStatus) -> Self {
Self {
status,
objective: f64::NAN,
primal: Vec::new(),
row_activity: Vec::new(),
col_dual: Vec::new(),
row_dual: Vec::new(),
}
}
pub fn is_optimal(&self) -> bool {
self.status == OptStatus::Optimal
}
}
pub trait Solver {
fn solve(&mut self, problem: &LinearProgram) -> Result<Solution, OpfError>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OpfError {
Shape {
what: &'static str,
expected: usize,
got: usize,
},
Index {
row: usize,
col: usize,
},
UpperTriangleHessian {
row: usize,
col: usize,
},
CrossedBounds {
index: usize,
kind: &'static str,
},
IntegerQuadratic,
IntegralityUnsupported {
backend: &'static str,
columns: usize,
},
Backend(String),
}
impl std::fmt::Display for OpfError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Shape {
what,
expected,
got,
} => {
write!(f, "{what} has {got} entries, expected {expected}")
}
Self::Index { row, col } => {
write!(
f,
"entry ({row}, {col}) is outside the problem's dimensions"
)
}
Self::UpperTriangleHessian { row, col } => write!(
f,
"Hessian entry ({row}, {col}) is above the diagonal — supply the lower \
triangle only, or the off-diagonal term is counted twice"
),
Self::CrossedBounds { index, kind } => {
write!(
f,
"{kind} {index} has its lower bound above its upper bound"
)
}
Self::IntegerQuadratic => write!(
f,
"the problem has both integral columns and a Hessian; neither backend \
solves a mixed-integer quadratic program"
),
Self::IntegralityUnsupported { backend, columns } => write!(
f,
"{columns} column(s) are integral and the {backend} backend cannot honour \
that; solving the relaxation would return set-points nobody can act on"
),
Self::Backend(message) => write!(f, "{message}"),
}
}
}
impl std::error::Error for OpfError {}
#[cfg(test)]
mod tests {
use super::*;
fn two_var() -> LinearProgram {
let mut lp = LinearProgram::new(2);
lp.col_lower = vec![0.0, 0.0];
lp.col_upper = vec![10.0, 10.0];
lp.col_cost = vec![1.0, 1.0];
lp
}
#[test]
fn add_row_assigns_consecutive_indices_and_keeps_bounds_aligned() {
let mut lp = two_var();
assert_eq!(lp.add_row(&[(0, 1.0), (1, 1.0)], 1.0, f64::INFINITY), 0);
assert_eq!(lp.add_row(&[(0, 1.0)], f64::NEG_INFINITY, 4.0), 1);
assert_eq!(lp.n_rows, 2);
assert_eq!(lp.row_lower.len(), 2);
assert_eq!(lp.rows.len(), 3);
lp.validate().unwrap();
}
#[test]
fn an_upper_triangle_hessian_entry_is_rejected() {
let mut lp = two_var();
lp.hessian = Some(vec![(0, 1, 2.0)]);
assert_eq!(
lp.validate(),
Err(OpfError::UpperTriangleHessian { row: 0, col: 1 })
);
lp.hessian = Some(vec![(1, 0, 2.0)]);
lp.validate().unwrap();
}
#[test]
fn shape_and_index_mistakes_are_caught() {
let mut lp = two_var();
lp.col_cost.push(1.0);
assert!(matches!(
lp.validate(),
Err(OpfError::Shape {
what: "col_cost",
..
})
));
let mut lp = two_var();
lp.add_row(&[(5, 1.0)], 0.0, 1.0);
assert_eq!(lp.validate(), Err(OpfError::Index { row: 0, col: 5 }));
let mut lp = two_var();
lp.col_lower[1] = 20.0;
assert_eq!(
lp.validate(),
Err(OpfError::CrossedBounds {
index: 1,
kind: "column"
})
);
}
#[test]
fn a_failed_solution_carries_no_stale_numbers() {
let s = Solution::failed(OptStatus::Infeasible);
assert!(!s.is_optimal());
assert!(s.primal.is_empty() && s.row_dual.is_empty());
assert!(s.objective.is_nan());
}
}