use crate::opf::{LinearProgram, OpfError, OptStatus, Solution, Solver};
use crate::sparse::RealFactorization;
#[derive(Clone, Copy, Debug)]
pub struct IpmOptions {
pub tolerance: f64,
pub max_iterations: usize,
pub primal_regularization: f64,
pub dual_regularization: f64,
pub refinement_rounds: usize,
pub divergence_threshold: f64,
}
impl Default for IpmOptions {
fn default() -> Self {
Self {
tolerance: 1e-10,
max_iterations: 200,
primal_regularization: 1e-9,
dual_regularization: 1e-9,
refinement_rounds: 2,
divergence_threshold: 1e10,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct IpmSolver {
options: IpmOptions,
iterations: usize,
}
impl IpmSolver {
pub fn new() -> Self {
Self {
options: IpmOptions::default(),
iterations: 0,
}
}
pub fn with_options(options: IpmOptions) -> Self {
Self {
options,
iterations: 0,
}
}
pub fn iterations(&self) -> usize {
self.iterations
}
pub fn options_mut(&mut self) -> &mut IpmOptions {
&mut self.options
}
}
struct Internal {
n_vars: usize,
n_total: usize,
m_eq: usize,
a: Vec<(usize, usize, f64)>,
b: Vec<f64>,
lower: Vec<f64>,
upper: Vec<f64>,
q: Vec<(usize, usize, f64)>,
c: Vec<f64>,
}
impl Internal {
fn build(lp: &LinearProgram) -> Self {
let n = lp.n_vars;
let mut lower = lp.col_lower.clone();
let mut upper = lp.col_upper.clone();
let mut c = lp.col_cost.clone();
let mut a: Vec<(usize, usize, f64)> = lp.rows.clone();
let mut b = Vec::with_capacity(lp.n_rows);
let mut n_total = n;
for r in 0..lp.n_rows {
if lp.row_lower[r] == lp.row_upper[r] {
b.push(lp.row_lower[r]);
} else {
a.push((r, n_total, -1.0));
lower.push(lp.row_lower[r]);
upper.push(lp.row_upper[r]);
c.push(0.0);
n_total += 1;
b.push(0.0);
}
}
let mut m_eq = lp.n_rows;
for k in 0..n {
if lower[k] == upper[k] {
a.push((m_eq, k, 1.0));
b.push(lower[k]);
lower[k] = f64::NEG_INFINITY;
upper[k] = f64::INFINITY;
m_eq += 1;
}
}
Self {
n_vars: n,
n_total,
m_eq,
a,
b,
lower,
upper,
q: lp.hessian.clone().unwrap_or_default(),
c,
}
}
fn q_mul(&self, u: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; self.n_total];
for &(i, j, v) in &self.q {
out[i] += v * u[j];
if i != j {
out[j] += v * u[i];
}
}
out
}
fn a_mul(&self, u: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; self.m_eq];
for &(r, k, v) in &self.a {
out[r] += v * u[k];
}
out
}
fn at_mul(&self, y: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; self.n_total];
for &(r, k, v) in &self.a {
out[k] += v * y[r];
}
out
}
}
fn norm_inf(v: &[f64]) -> f64 {
v.iter().fold(0.0f64, |m, x| m.max(x.abs()))
}
impl Solver for IpmSolver {
fn solve(&mut self, problem: &LinearProgram) -> Result<Solution, OpfError> {
problem.validate()?;
if problem.has_integers() {
return Err(OpfError::IntegralityUnsupported {
backend: "interior-point",
columns: problem.n_integers(),
});
}
self.iterations = 0;
let p = Internal::build(problem);
let n_total = p.n_total;
let m_eq = p.m_eq;
let dim = n_total + m_eq;
let has_lower: Vec<bool> = p.lower.iter().map(|x| x.is_finite()).collect();
let has_upper: Vec<bool> = p.upper.iter().map(|x| x.is_finite()).collect();
let n_bounds =
has_lower.iter().filter(|x| **x).count() + has_upper.iter().filter(|x| **x).count();
let mut u = vec![0.0; n_total];
for k in 0..n_total {
u[k] = match (has_lower[k], has_upper[k]) {
(true, true) => 0.5 * (p.lower[k] + p.upper[k]),
(true, false) => p.lower[k] + 1.0,
(false, true) => p.upper[k] - 1.0,
(false, false) => 0.0,
};
}
let mut y = vec![0.0; m_eq];
let mut z_l: Vec<f64> = has_lower
.iter()
.map(|&h| if h { 1.0 } else { 0.0 })
.collect();
let mut z_u: Vec<f64> = has_upper
.iter()
.map(|&h| if h { 1.0 } else { 0.0 })
.collect();
let b_scale = 1.0 + norm_inf(&p.b);
let c_scale = 1.0 + norm_inf(&p.c);
for iteration in 0..self.options.max_iterations {
self.iterations = iteration + 1;
let g: Vec<f64> = (0..n_total)
.map(|k| if has_lower[k] { u[k] - p.lower[k] } else { 1.0 })
.collect();
let t: Vec<f64> = (0..n_total)
.map(|k| if has_upper[k] { p.upper[k] - u[k] } else { 1.0 })
.collect();
let qu = p.q_mul(&u);
let aty = p.at_mul(&y);
let r_d: Vec<f64> = (0..n_total)
.map(|k| qu[k] + p.c[k] - aty[k] - z_l[k] + z_u[k])
.collect();
let au = p.a_mul(&u);
let r_p: Vec<f64> = (0..m_eq).map(|r| au[r] - p.b[r]).collect();
let complementarity: f64 = (0..n_total)
.map(|k| {
(if has_lower[k] { g[k] * z_l[k] } else { 0.0 })
+ (if has_upper[k] { t[k] * z_u[k] } else { 0.0 })
})
.sum();
let mu = if n_bounds > 0 {
complementarity / n_bounds as f64
} else {
0.0
};
let objective = 0.5 * (0..n_total).map(|k| u[k] * qu[k]).sum::<f64>()
+ (0..n_total).map(|k| p.c[k] * u[k]).sum::<f64>();
let primal_ok = norm_inf(&r_p) / b_scale <= self.options.tolerance;
let dual_ok = norm_inf(&r_d) / c_scale <= self.options.tolerance;
let gap_ok = complementarity / (1.0 + objective.abs()) <= self.options.tolerance;
if primal_ok && dual_ok && gap_ok {
return Ok(finish(problem, &p, &u, &y, objective));
}
if !u.iter().all(|x| x.is_finite()) || !y.iter().all(|x| x.is_finite()) {
return Ok(Solution::failed(diagnose(&u, &y, primal_ok, dual_ok)));
}
if norm_inf(&y) > self.options.divergence_threshold && !primal_ok {
return Ok(Solution::failed(OptStatus::Infeasible));
}
if norm_inf(&u) > self.options.divergence_threshold && !dual_ok {
return Ok(Solution::failed(OptStatus::Unbounded));
}
let mut kkt: Vec<(usize, usize, f64)> =
Vec::with_capacity(p.q.len() * 2 + p.a.len() * 2 + n_total + m_eq);
for &(i, j, v) in &p.q {
kkt.push((i, j, v));
if i != j {
kkt.push((j, i, v));
}
}
for k in 0..n_total {
let d = (if has_lower[k] { z_l[k] / g[k] } else { 0.0 })
+ (if has_upper[k] { z_u[k] / t[k] } else { 0.0 });
kkt.push((k, k, d + self.options.primal_regularization));
}
for &(r, k, v) in &p.a {
kkt.push((n_total + r, k, v));
kkt.push((k, n_total + r, v));
}
for r in 0..m_eq {
kkt.push((n_total + r, n_total + r, -self.options.dual_regularization));
}
let Some(factorization) = RealFactorization::new(dim, &kkt) else {
return Ok(Solution::failed(diagnose(&u, &y, primal_ok, dual_ok)));
};
let solve_step = |rc_l: &[f64], rc_u: &[f64]| -> Option<(Vec<f64>, Vec<f64>)> {
let mut rhs = vec![0.0; dim];
for k in 0..n_total {
let mut rho = -r_d[k];
if has_lower[k] {
rho -= rc_l[k] / g[k];
}
if has_upper[k] {
rho += rc_u[k] / t[k];
}
rhs[k] = rho;
}
for r in 0..m_eq {
rhs[n_total + r] = -r_p[r];
}
let mut d = factorization.solve(&rhs)?;
for _ in 0..self.options.refinement_rounds {
let residual =
kkt_residual(&p, &has_lower, &has_upper, &g, &t, &z_l, &z_u, &d, &rhs);
let correction = factorization.solve(&residual)?;
for i in 0..dim {
d[i] += correction[i];
}
}
let du = d[..n_total].to_vec();
let dy: Vec<f64> = (0..m_eq).map(|r| -d[n_total + r]).collect();
Some((du, dy))
};
let rc_l_aff: Vec<f64> = (0..n_total)
.map(|k| if has_lower[k] { g[k] * z_l[k] } else { 0.0 })
.collect();
let rc_u_aff: Vec<f64> = (0..n_total)
.map(|k| if has_upper[k] { t[k] * z_u[k] } else { 0.0 })
.collect();
let Some((du_aff, _)) = solve_step(&rc_l_aff, &rc_u_aff) else {
return Ok(Solution::failed(diagnose(&u, &y, primal_ok, dual_ok)));
};
let dz_l_aff: Vec<f64> = (0..n_total)
.map(|k| {
if has_lower[k] {
(-rc_l_aff[k] - z_l[k] * du_aff[k]) / g[k]
} else {
0.0
}
})
.collect();
let dz_u_aff: Vec<f64> = (0..n_total)
.map(|k| {
if has_upper[k] {
(-rc_u_aff[k] + z_u[k] * du_aff[k]) / t[k]
} else {
0.0
}
})
.collect();
let (ap_aff, ad_aff) = step_lengths(
n_total, &has_lower, &has_upper, &g, &t, &z_l, &z_u, &du_aff, &dz_l_aff, &dz_u_aff,
1.0,
);
let sigma = if n_bounds > 0 && mu > 0.0 {
let mu_aff: f64 = (0..n_total)
.map(|k| {
let mut s = 0.0;
if has_lower[k] {
s += (g[k] + ap_aff * du_aff[k]) * (z_l[k] + ad_aff * dz_l_aff[k]);
}
if has_upper[k] {
s += (t[k] - ap_aff * du_aff[k]) * (z_u[k] + ad_aff * dz_u_aff[k]);
}
s
})
.sum::<f64>()
/ n_bounds as f64;
(mu_aff / mu).powi(3).clamp(0.0, 1.0)
} else {
0.0
};
let target = sigma * mu;
let rc_l: Vec<f64> = (0..n_total)
.map(|k| {
if has_lower[k] {
g[k] * z_l[k] - target + du_aff[k] * dz_l_aff[k]
} else {
0.0
}
})
.collect();
let rc_u: Vec<f64> = (0..n_total)
.map(|k| {
if has_upper[k] {
t[k] * z_u[k] - target - du_aff[k] * dz_u_aff[k]
} else {
0.0
}
})
.collect();
let Some((du, dy)) = solve_step(&rc_l, &rc_u) else {
return Ok(Solution::failed(diagnose(&u, &y, primal_ok, dual_ok)));
};
let dz_l: Vec<f64> = (0..n_total)
.map(|k| {
if has_lower[k] {
(-rc_l[k] - z_l[k] * du[k]) / g[k]
} else {
0.0
}
})
.collect();
let dz_u: Vec<f64> = (0..n_total)
.map(|k| {
if has_upper[k] {
(-rc_u[k] + z_u[k] * du[k]) / t[k]
} else {
0.0
}
})
.collect();
let (mut alpha_p, mut alpha_d) = step_lengths(
n_total, &has_lower, &has_upper, &g, &t, &z_l, &z_u, &du, &dz_l, &dz_u, 0.995,
);
if !p.q.is_empty() {
let alpha = alpha_p.min(alpha_d);
alpha_p = alpha;
alpha_d = alpha;
}
for k in 0..n_total {
u[k] += alpha_p * du[k];
z_l[k] += alpha_d * dz_l[k];
z_u[k] += alpha_d * dz_u[k];
}
for r in 0..m_eq {
y[r] += alpha_d * dy[r];
}
}
Ok(Solution::failed(OptStatus::Other("iteration limit")))
}
}
fn diagnose(u: &[f64], y: &[f64], primal_ok: bool, dual_ok: bool) -> OptStatus {
let (nu, ny) = (norm_inf(u), norm_inf(y));
if !primal_ok && ny > nu.max(1.0) * 1e3 {
OptStatus::Infeasible
} else if !dual_ok && nu > ny.max(1.0) * 1e3 {
OptStatus::Unbounded
} else {
OptStatus::Other("interior-point method failed to converge")
}
}
#[allow(clippy::too_many_arguments)]
fn kkt_residual(
p: &Internal,
has_lower: &[bool],
has_upper: &[bool],
g: &[f64],
t: &[f64],
z_l: &[f64],
z_u: &[f64],
d: &[f64],
rhs: &[f64],
) -> Vec<f64> {
let n_total = p.n_total;
let m_eq = p.m_eq;
let du = &d[..n_total];
let w = &d[n_total..];
let qdu = p.q_mul(du);
let atw = p.at_mul(w);
let adu = p.a_mul(du);
let mut out = vec![0.0; n_total + m_eq];
for k in 0..n_total {
let barrier = (if has_lower[k] { z_l[k] / g[k] } else { 0.0 })
+ (if has_upper[k] { z_u[k] / t[k] } else { 0.0 });
out[k] = rhs[k] - (qdu[k] + barrier * du[k] + atw[k]);
}
for r in 0..m_eq {
out[n_total + r] = rhs[n_total + r] - adu[r];
}
out
}
#[allow(clippy::too_many_arguments)]
fn step_lengths(
n_total: usize,
has_lower: &[bool],
has_upper: &[bool],
g: &[f64],
t: &[f64],
z_l: &[f64],
z_u: &[f64],
du: &[f64],
dz_l: &[f64],
dz_u: &[f64],
tau: f64,
) -> (f64, f64) {
let mut alpha_p = f64::INFINITY;
let mut alpha_d = f64::INFINITY;
for k in 0..n_total {
if has_lower[k] {
if du[k] < 0.0 {
alpha_p = alpha_p.min(-g[k] / du[k]);
}
if dz_l[k] < 0.0 {
alpha_d = alpha_d.min(-z_l[k] / dz_l[k]);
}
}
if has_upper[k] {
if du[k] > 0.0 {
alpha_p = alpha_p.min(t[k] / du[k]);
}
if dz_u[k] < 0.0 {
alpha_d = alpha_d.min(-z_u[k] / dz_u[k]);
}
}
}
((tau * alpha_p).min(1.0), (tau * alpha_d).min(1.0))
}
fn finish(lp: &LinearProgram, p: &Internal, u: &[f64], y: &[f64], objective: f64) -> Solution {
let n = p.n_vars;
let primal = u[..n].to_vec();
let mut row_activity = vec![0.0; lp.n_rows];
for &(r, k, v) in &lp.rows {
row_activity[r] += v * primal[k];
}
let row_dual = y[..lp.n_rows].to_vec();
let mut col_dual = vec![0.0; n];
for &(i, j, v) in &p.q {
col_dual[i] += v * primal[j];
if i != j {
col_dual[j] += v * primal[i];
}
}
for k in 0..n {
col_dual[k] += lp.col_cost[k];
}
for &(r, k, v) in &lp.rows {
col_dual[k] -= v * row_dual[r];
}
Solution {
status: OptStatus::Optimal,
objective: objective + lp.offset,
primal,
row_activity,
col_dual,
row_dual,
}
}