use pounce_common::types::{Number, lower_bound_present, upper_bound_present};
pub(crate) const MAX_UNKNOWNS: usize = 600;
pub(crate) const MAX_COMPONENT_VARS: usize = 5000;
pub(crate) struct KktView<'a> {
pub x: &'a [Number],
pub g: &'a [Number],
pub grad_f: &'a [Number],
pub jac: &'a [(usize, usize, Number)],
pub x_l: &'a [Number],
pub x_u: &'a [Number],
pub g_l: &'a [Number],
pub g_u: &'a [Number],
pub derived_l: &'a [bool],
pub derived_u: &'a [bool],
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct Outcome {
pub n_spurious: usize,
pub n_unknowns: usize,
pub residual_before: Number,
pub residual_after: Number,
pub rolled_back: bool,
}
fn on_bound(v: Number, b: Number) -> bool {
(v - b).abs() <= 1e-6 * b.abs().max(1.0)
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Sign {
Free,
NonNeg,
NonPos,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Unknown {
Row(usize),
ZLower(usize),
ZUpper(usize),
}
fn residual(v: &KktView<'_>, lambda: &[Number], z_l: &[Number], z_u: &[Number]) -> Vec<Number> {
let mut r = v.grad_f.to_vec();
for &(i, j, a) in v.jac {
if let (Some(rj), Some(&l)) = (r.get_mut(j), lambda.get(i)) {
*rj += a * l;
}
}
for (j, rj) in r.iter_mut().enumerate() {
*rj += -z_l[j] + z_u[j];
}
r
}
pub(crate) fn reattribute(
v: &KktView<'_>,
lambda: &mut [Number],
z_l: &mut [Number],
z_u: &mut [Number],
) -> Outcome {
let n = v.x.len();
let m = v.g.len();
let mut out = Outcome::default();
if z_l.len() != n || z_u.len() != n || lambda.len() != m || v.grad_f.len() != n {
return out;
}
let declared_active_l = |j: usize| lower_bound_present(v.x_l[j]) && on_bound(v.x[j], v.x_l[j]);
let declared_active_u = |j: usize| upper_bound_present(v.x_u[j]) && on_bound(v.x[j], v.x_u[j]);
let mut spurious: Vec<usize> = Vec::new();
for j in 0..n {
let sl = v.derived_l[j] && z_l[j].is_finite() && z_l[j] != 0.0 && !declared_active_l(j);
let su = v.derived_u[j] && z_u[j].is_finite() && z_u[j] != 0.0 && !declared_active_u(j);
if sl || su {
spurious.push(j);
}
}
if spurious.is_empty() {
return out;
}
let (lam0, zl0, zu0) = (lambda.to_vec(), z_l.to_vec(), z_u.to_vec());
let before = residual(v, lambda, z_l, z_u);
for &j in &spurious {
if v.derived_l[j] && !declared_active_l(j) && z_l[j].is_finite() && z_l[j] != 0.0 {
z_l[j] = 0.0;
out.n_spurious += 1;
}
if v.derived_u[j] && !declared_active_u(j) && z_u[j].is_finite() && z_u[j] != 0.0 {
z_u[j] = 0.0;
out.n_spurious += 1;
}
}
let r = residual(v, lambda, z_l, z_u);
let scale = v.grad_f.iter().fold(
1.0_f64,
|a, &b| if b.is_finite() { a.max(b.abs()) } else { a },
);
let seed_tol = 1e-10 * scale;
let seeds: Vec<usize> = spurious
.iter()
.copied()
.filter(|&j| r[j].abs() > seed_tol)
.collect();
if seeds.is_empty() {
return out;
}
let mut by_col: Vec<Vec<(usize, Number)>> = vec![Vec::new(); n];
let mut by_row: Vec<Vec<(usize, Number)>> = vec![Vec::new(); m];
for &(i, j, a) in v.jac {
if i < m && j < n && a != 0.0 && a.is_finite() {
by_col[j].push((i, a));
by_row[i].push((j, a));
}
}
let row_sign = |i: usize| -> Option<Sign> {
let (gl, gu, gi) = (v.g_l[i], v.g_u[i], v.g[i]);
if !gi.is_finite() {
return None;
}
let lo = lower_bound_present(gl) && on_bound(gi, gl);
let hi = upper_bound_present(gu) && on_bound(gi, gu);
match (lo, hi) {
(true, true) => Some(Sign::Free),
(true, false) => Some(Sign::NonPos),
(false, true) => Some(Sign::NonNeg),
(false, false) => None,
}
};
let mut var_slot = vec![usize::MAX; n];
let mut vars: Vec<usize> = Vec::new();
let mut row_seen = vec![false; m];
let mut unknowns: Vec<(Unknown, Sign)> = Vec::new();
let mut queue = std::collections::VecDeque::new();
for &j in &seeds {
if var_slot[j] == usize::MAX {
var_slot[j] = vars.len();
vars.push(j);
queue.push_back(j);
}
}
let mut over_cap = false;
while let Some(j) = queue.pop_front() {
if declared_active_l(j) {
unknowns.push((Unknown::ZLower(j), Sign::NonNeg));
}
if declared_active_u(j) {
unknowns.push((Unknown::ZUpper(j), Sign::NonNeg));
}
for &(i, _) in &by_col[j] {
if row_seen[i] {
continue;
}
row_seen[i] = true;
let Some(sign) = row_sign(i) else { continue };
unknowns.push((Unknown::Row(i), sign));
for &(k, _) in &by_row[i] {
if var_slot[k] == usize::MAX {
var_slot[k] = vars.len();
vars.push(k);
queue.push_back(k);
}
}
}
if unknowns.len() > MAX_UNKNOWNS || vars.len() > MAX_COMPONENT_VARS {
over_cap = true;
break;
}
}
out.residual_before = vars.iter().fold(0.0_f64, |a, &j| a.max(before[j].abs()));
let restore = |lambda: &mut [Number], z_l: &mut [Number], z_u: &mut [Number]| {
lambda.copy_from_slice(&lam0);
z_l.copy_from_slice(&zl0);
z_u.copy_from_slice(&zu0);
};
if over_cap || unknowns.is_empty() {
restore(lambda, z_l, z_u);
out.rolled_back = true;
out.residual_after = out.residual_before;
out.n_spurious = 0;
return out;
}
out.n_unknowns = unknowns.len();
let nv = vars.len();
let k = unknowns.len();
let mut a = vec![0.0; nv * k];
let mut w0 = vec![0.0; k];
let mut constrained = vec![false; k];
for (c, &(u, sign)) in unknowns.iter().enumerate() {
let s = if sign == Sign::NonPos { -1.0 } else { 1.0 };
constrained[c] = sign != Sign::Free;
match u {
Unknown::Row(i) => {
for &(jj, aij) in &by_row[i] {
let slot = var_slot[jj];
if slot != usize::MAX {
a[slot * k + c] += s * aij;
}
}
w0[c] = s * lambda[i];
}
Unknown::ZLower(j) => {
a[var_slot[j] * k + c] = -1.0;
w0[c] = z_l[j];
}
Unknown::ZUpper(j) => {
a[var_slot[j] * k + c] = 1.0;
w0[c] = z_u[j];
}
}
if constrained[c] && !(w0[c] >= 0.0) {
w0[c] = 0.0;
}
}
let mut b: Vec<Number> = vars.iter().map(|&j| r[j]).collect();
for (c, &(u, sign)) in unknowns.iter().enumerate() {
let s = if sign == Sign::NonPos { -1.0 } else { 1.0 };
let current = match u {
Unknown::Row(i) => s * lambda[i],
Unknown::ZLower(j) => z_l[j],
Unknown::ZUpper(j) => z_u[j],
};
let current = if current.is_finite() { current } else { 0.0 };
for (row, bv) in b.iter_mut().enumerate() {
*bv -= a[row * k + c] * current;
}
}
let w = solve_sign_constrained_ls(&a, &b, nv, k, &constrained, &w0);
let res_after = (0..nv)
.map(|row| {
let mut acc = b[row];
for c in 0..k {
acc += a[row * k + c] * w[c];
}
acc.abs()
})
.fold(0.0_f64, Number::max);
let target = (10.0 * out.residual_before).max(1e-8 * scale);
if !(res_after <= target) {
restore(lambda, z_l, z_u);
out.rolled_back = true;
out.residual_after = out.residual_before;
out.n_spurious = 0;
out.n_unknowns = 0;
return out;
}
for (c, &(u, sign)) in unknowns.iter().enumerate() {
let s = if sign == Sign::NonPos { -1.0 } else { 1.0 };
match u {
Unknown::Row(i) => lambda[i] = s * w[c],
Unknown::ZLower(j) => z_l[j] = w[c],
Unknown::ZUpper(j) => z_u[j] = w[c],
}
}
out.residual_after = res_after;
out
}
fn solve_sign_constrained_ls(
a: &[Number],
b: &[Number],
nrows: usize,
k: usize,
constrained: &[bool],
w0: &[Number],
) -> Vec<Number> {
let mut ata = vec![0.0; k * k];
let mut atb = vec![0.0; k];
for row in 0..nrows {
let ar = &a[row * k..(row + 1) * k];
for p in 0..k {
if ar[p] == 0.0 {
continue;
}
atb[p] += ar[p] * b[row];
for q in 0..k {
ata[p * k + q] += ar[p] * ar[q];
}
}
}
let max_diag = (0..k).fold(0.0_f64, |m, p| m.max(ata[p * k + p]));
let mu = 1e-6 * max_diag.max(1e-300);
let mut h = ata.clone();
for p in 0..k {
h[p * k + p] += mu;
}
let mut w = w0.to_vec();
for _ in 0..200 {
let c: Vec<Number> = (0..k).map(|p| atb[p] - mu * w[p]).collect();
let next = box_qp(&h, &c, k, constrained, &w);
let step = next
.iter()
.zip(&w)
.fold(0.0_f64, |m, (p, q)| m.max((p - q).abs()));
let size = next.iter().fold(1.0_f64, |m, p| m.max(p.abs()));
w = next;
if step <= 1e-15 * size {
break;
}
}
w
}
fn box_qp(
h: &[Number],
c: &[Number],
k: usize,
constrained: &[bool],
start: &[Number],
) -> Vec<Number> {
let mut w: Vec<Number> = start.to_vec();
let mut fixed: Vec<bool> = (0..k).map(|p| constrained[p] && w[p] <= 0.0).collect();
for p in 0..k {
if fixed[p] {
w[p] = 0.0;
}
}
for _ in 0..(10 * k + 50) {
let free: Vec<usize> = (0..k).filter(|&p| !fixed[p]).collect();
let nf = free.len();
let mut cand = vec![0.0; k];
if nf > 0 {
let mut hf = vec![0.0; nf * nf];
let mut rhs = vec![0.0; nf];
for (r, &p) in free.iter().enumerate() {
rhs[r] = -c[p];
for (s, &q) in free.iter().enumerate() {
hf[r * nf + s] = h[p * k + q];
}
}
if !cholesky_solve(&mut hf, &mut rhs, nf) {
return w;
}
for (r, &p) in free.iter().enumerate() {
cand[p] = rhs[r];
}
}
let infeasible = free.iter().any(|&p| constrained[p] && cand[p] < 0.0);
if !infeasible {
w = cand;
let mut worst: Option<(usize, Number)> = None;
for p in 0..k {
if !fixed[p] {
continue;
}
let mut gp = c[p];
for q in 0..k {
gp += h[p * k + q] * w[q];
}
let tol = 1e-14 * (h[p * k + p].abs().max(1e-300)).sqrt() * (1.0 + c[p].abs());
if gp < -tol && worst.is_none_or(|(_, g)| gp < g) {
worst = Some((p, gp));
}
}
match worst {
Some((p, _)) => fixed[p] = false,
None => return w,
}
} else {
let mut alpha = 1.0_f64;
let mut block = None;
for &p in &free {
if constrained[p] && cand[p] < 0.0 {
let t = w[p] / (w[p] - cand[p]);
if t < alpha {
alpha = t;
block = Some(p);
}
}
}
for &p in &free {
w[p] += alpha * (cand[p] - w[p]);
}
if let Some(p) = block {
fixed[p] = true;
w[p] = 0.0;
}
for &p in &free {
if constrained[p] && w[p] <= 0.0 {
fixed[p] = true;
w[p] = 0.0;
}
}
}
}
w
}
fn cholesky_solve(m: &mut [Number], rhs: &mut [Number], n: usize) -> bool {
for j in 0..n {
let mut d = m[j * n + j];
for p in 0..j {
d -= m[j * n + p] * m[j * n + p];
}
if !(d > 0.0) {
return false;
}
let d = d.sqrt();
m[j * n + j] = d;
for i in (j + 1)..n {
let mut s = m[i * n + j];
for p in 0..j {
s -= m[i * n + p] * m[j * n + p];
}
m[i * n + j] = s / d;
}
}
for i in 0..n {
let mut s = rhs[i];
for p in 0..i {
s -= m[i * n + p] * rhs[p];
}
rhs[i] = s / m[i * n + i];
}
for i in (0..n).rev() {
let mut s = rhs[i];
for p in (i + 1)..n {
s -= m[p * n + i] * rhs[p];
}
rhs[i] = s / m[i * n + i];
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_derived_bound_multiplier_goes_back_to_the_row_and_the_declared_bound() {
let jac = [(0, 0, 1.0), (0, 1, -2.0)];
let v = KktView {
x: &[4.0, 2.0],
g: &[0.0],
grad_f: &[8.0, 4.0],
jac: &jac,
x_l: &[4.0, -1e19],
x_u: &[1e19, 1e19],
g_l: &[0.0],
g_u: &[0.0],
derived_l: &[false, true],
derived_u: &[false, false],
};
let (mut lam, mut zl, mut zu) = (vec![-8.0], vec![0.0, 20.0], vec![0.0, 0.0]);
let o = reattribute(&v, &mut lam, &mut zl, &mut zu);
assert!(!o.rolled_back, "{o:?}");
assert_eq!(o.n_spurious, 1);
assert!((lam[0] - 2.0).abs() < 1e-10, "λ = {lam:?}");
assert!((zl[0] - 10.0).abs() < 1e-10, "z_L = {zl:?}");
assert_eq!(zl[1], 0.0);
let r = residual(&v, &lam, &zl, &zu);
assert!(r.iter().all(|x| x.abs() < 1e-10), "{r:?}");
}
#[test]
fn a_declared_active_bound_keeps_its_multiplier() {
let jac = [(0, 0, 1.0), (0, 1, -2.0)];
let v = KktView {
x: &[4.0, 2.0],
g: &[0.0],
grad_f: &[8.0, 4.0],
jac: &jac,
x_l: &[4.0, 2.0],
x_u: &[1e19, 1e19],
g_l: &[0.0],
g_u: &[0.0],
derived_l: &[false, true],
derived_u: &[false, false],
};
let (mut lam, mut zl, mut zu) = (vec![-8.0], vec![0.0, 20.0], vec![0.0, 0.0]);
let o = reattribute(&v, &mut lam, &mut zl, &mut zu);
assert_eq!(o, Outcome::default());
assert_eq!((lam, zl), (vec![-8.0], vec![0.0, 20.0]));
}
#[test]
fn inequality_signs_are_respected() {
let jac = [(0, 0, 1.0), (1, 0, 1.0), (1, 1, 1.0)];
let v = KktView {
x: &[0.5, 1.5],
g: &[0.5, 2.0],
grad_f: &[0.0, -3.0],
jac: &jac,
x_l: &[-1e19, -1e19],
x_u: &[1e19, 1e19],
g_l: &[0.5, -1e19],
g_u: &[0.5, 2.0],
derived_l: &[true, false],
derived_u: &[true, true],
};
let (mut lam, mut zl, mut zu) = (vec![0.0, 0.0], vec![0.0, 0.0], vec![0.0, 3.0]);
let o = reattribute(&v, &mut lam, &mut zl, &mut zu);
assert!(!o.rolled_back, "{o:?}");
assert!(
(lam[0] + 3.0).abs() < 1e-10 && (lam[1] - 3.0).abs() < 1e-10,
"{lam:?}"
);
assert_eq!((zl, zu), (vec![0.0, 0.0], vec![0.0, 0.0]));
}
}