use crate::real::{Real, is_exact_zero, is_nan, le, lt};
use crate::{CscMatrix, KktError, Ordering, Pattern, SparseError};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Inertia {
pub positive: usize,
pub negative: usize,
pub zero: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ShiftScale {
#[default]
Equilibrated,
Absolute,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Regularization<T: Real = f64> {
pub primal: T,
pub dual: T,
pub pivot_tol: Option<T>,
pub bump: T,
pub scale: ShiftScale,
}
impl<T: Real> Default for Regularization<T> {
fn default() -> Self {
Self {
primal: T::from_f64(1e-6),
dual: T::from_f64(1e-8),
pivot_tol: Some(T::from_f64(1e-13)),
bump: T::from_f64(2e-7),
scale: ShiftScale::Equilibrated,
}
}
}
impl<T: Real> Regularization<T> {
#[must_use]
pub fn none() -> Self {
Self {
primal: T::ZERO,
dual: T::ZERO,
pivot_tol: None,
bump: T::ZERO,
scale: ShiftScale::Equilibrated,
}
}
#[must_use]
pub fn relative_to(k: &CscMatrix<T>, factor: T) -> Self {
let big = inf_norm(&k.val);
Self {
primal: factor * big,
dual: factor * big,
scale: ShiftScale::Absolute,
..Self::default()
}
}
pub fn is_quasi_definite(&self) -> bool {
lt(T::ZERO, self.primal) && lt(T::ZERO, self.dual)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Refinement<T: Real = f64> {
pub max_iters: usize,
pub target_residual: T,
}
impl<T: Real> Default for Refinement<T> {
fn default() -> Self {
Self {
max_iters: 10,
target_residual: T::from_f64(1e-12),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RefineReport<T: Real = f64> {
pub iters: usize,
pub residual: T,
pub converged: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KktForm {
NormalEquations,
Augmented {
n_primal: usize,
n_dual: usize,
},
}
impl KktForm {
pub fn expected_inertia(self, n: usize) -> Inertia {
match self {
Self::NormalEquations => Inertia {
positive: n,
negative: 0,
zero: 0,
},
Self::Augmented { n_primal, n_dual } => Inertia {
positive: n_dual,
negative: n_primal,
zero: 0,
},
}
}
}
pub trait KktSolver<T: Real> {
type Symbolic;
type Numeric;
fn analyze(
&self,
pattern: &Pattern,
form: KktForm,
ordering: &dyn Ordering,
) -> Result<Self::Symbolic, KktError>;
fn factorize(
&self,
symbolic: &Self::Symbolic,
k: &CscMatrix<T>,
reg: Regularization<T>,
) -> Result<(Self::Numeric, Inertia), KktError>;
fn solve(&self, numeric: &Self::Numeric, x: &mut [T]);
fn solve_refined(
&self,
numeric: &Self::Numeric,
k: &CscMatrix<T>,
b: &[T],
x: &mut [T],
refinement: Refinement<T>,
) -> RefineReport<T> {
let mut work = RefineWorkspace::new();
self.solve_refined_with(numeric, k, b, x, refinement, &mut work)
}
#[allow(clippy::too_many_lines)] fn solve_refined_with(
&self,
numeric: &Self::Numeric,
k: &CscMatrix<T>,
b: &[T],
x: &mut [T],
refinement: Refinement<T>,
work: &mut RefineWorkspace<T>,
) -> RefineReport<T> {
let n = b.len();
assert_eq!(x.len(), n, "solve_refined: x has the wrong length");
assert!(
k.n_rows == n && k.n_cols() == n,
"solve_refined: k has the wrong order"
);
let budget = refinement.max_iters;
work.reserve(n, budget);
let scale = T::ONE + inf_norm(b);
let tol = refinement.target_residual * scale;
x.copy_from_slice(b);
self.solve(numeric, x);
let RefineWorkspace {
r,
cur,
v,
z,
h,
g,
cs,
sn,
y,
} = work;
residual(k, x, b, r);
let first = inf_norm(r) / scale;
let mut best = first;
let mut t = 0;
loop {
if !best.is_finite() || le(best, refinement.target_residual, T::ZERO) || t >= budget {
break;
}
let m = budget - t;
let beta = norm2(r);
if !beta.is_finite() || is_exact_zero(beta) {
break;
}
for (vi, &ri) in v[..n].iter_mut().zip(r.iter()) {
*vi = ri / beta;
}
g.fill(T::ZERO);
g[0] = beta;
let mut steps = 0;
for j in 0..m {
let (zj, vj) = (&mut z[j * n..(j + 1) * n], &v[j * n..(j + 1) * n]);
zj.copy_from_slice(vj);
self.solve(numeric, zj);
t += 1;
steps = j + 1;
let (done, rest) = v.split_at_mut((j + 1) * n);
let w = &mut rest[..n];
sym_lower_matvec(k, &z[j * n..(j + 1) * n], w);
let col = &mut h[j * (m + 1)..(j + 1) * (m + 1)];
for i in 0..=j {
let vi = &done[i * n..(i + 1) * n];
let hij = dot(w, vi);
col[i] = hij;
for (wk, &vk) in w.iter_mut().zip(vi) {
*wk -= hij * vk;
}
}
let hnext = norm2(w);
col[j + 1] = hnext;
if !hnext.is_finite() {
steps = j;
break;
}
for i in 0..j {
let (a, c) = (col[i], col[i + 1]);
col[i] = cs[i] * a + sn[i] * c;
col[i + 1] = cs[i] * c - sn[i] * a;
}
let (a, c) = (col[j], col[j + 1]);
let rr = norm2(&[a, c]);
if is_exact_zero(rr) {
steps = j;
break;
}
cs[j] = a / rr;
sn[j] = c / rr;
col[j] = rr;
col[j + 1] = T::ZERO;
g[j + 1] = -sn[j] * g[j];
g[j] *= cs[j];
if is_exact_zero(hnext) || le(g[j + 1].abs(), tol, T::ZERO) {
break;
}
for wk in w.iter_mut() {
*wk = *wk / hnext;
}
}
if steps == 0 {
break;
}
for i in (0..steps).rev() {
let mut acc = g[i];
for l in i + 1..steps {
acc -= h[l * (m + 1) + i] * y[l];
}
y[i] = acc / h[i * (m + 1) + i];
}
cur.copy_from_slice(x);
for (l, &yl) in y[..steps].iter().enumerate() {
for (c, &zl) in cur.iter_mut().zip(&z[l * n..(l + 1) * n]) {
*c += yl * zl;
}
}
residual(k, cur, b, r);
let res = inf_norm(r) / scale;
if res.is_finite() && lt(res, best) {
x.copy_from_slice(cur);
best = res;
} else {
break;
}
}
debug_assert!(
!best.is_finite() || !first.is_finite() || le(best, first, T::ZERO),
"(S7): the returned residual exceeds the first one"
);
RefineReport {
iters: t,
residual: best,
converged: best.is_finite() && le(best, refinement.target_residual, T::ZERO),
}
}
fn nnz_factor(&self, numeric: &Self::Numeric) -> usize;
fn name(&self) -> &'static str;
}
pub(crate) fn inf_norm<T: Real>(v: &[T]) -> T {
let mut m = T::ZERO;
for &x in v {
if is_nan(x) {
return x;
}
m = m.max(x.abs());
}
m
}
fn norm2<T: Real>(v: &[T]) -> T {
let big = inf_norm(v);
if !big.is_finite() || is_exact_zero(big) {
return big;
}
let mut s = T::ZERO;
for &x in v {
let q = x / big;
s += q * q;
}
big * s.sqrt()
}
fn dot<T: Real>(u: &[T], v: &[T]) -> T {
let mut s = T::ZERO;
for (&a, &b) in u.iter().zip(v) {
s += a * b;
}
s
}
#[derive(Clone, Debug, Default)]
pub struct RefineWorkspace<T: Real = f64> {
r: Vec<T>,
cur: Vec<T>,
v: Vec<T>,
z: Vec<T>,
h: Vec<T>,
g: Vec<T>,
cs: Vec<T>,
sn: Vec<T>,
y: Vec<T>,
}
impl<T: Real> RefineWorkspace<T> {
#[must_use]
pub fn new() -> Self {
Self {
r: Vec::new(),
cur: Vec::new(),
v: Vec::new(),
z: Vec::new(),
h: Vec::new(),
g: Vec::new(),
cs: Vec::new(),
sn: Vec::new(),
y: Vec::new(),
}
}
fn reserve(&mut self, n: usize, m: usize) {
fn fit<T: Real>(v: &mut Vec<T>, len: usize) {
if v.len() != len {
v.resize(len, T::ZERO);
}
}
fit(&mut self.r, n);
fit(&mut self.cur, n);
fit(&mut self.v, (m + 1) * n);
fit(&mut self.z, m * n);
fit(&mut self.h, (m + 1) * m);
fit(&mut self.g, m + 1);
fit(&mut self.cs, m);
fit(&mut self.sn, m);
fit(&mut self.y, m);
}
}
pub(crate) fn sym_lower_matvec<T: Real>(k: &CscMatrix<T>, x: &[T], y: &mut [T]) {
y.fill(T::ZERO);
for (j, &xj) in x.iter().enumerate() {
for p in k.col_ptr[j]..k.col_ptr[j + 1] {
let i = k.row_idx[p] as usize;
if i < j {
continue;
}
let v = k.val[p];
y[i] += v * xj;
if i != j {
y[j] += v * x[i];
}
}
}
}
fn residual<T: Real>(k: &CscMatrix<T>, x: &[T], b: &[T], r: &mut [T]) {
sym_lower_matvec(k, x, r);
for (ri, &bi) in r.iter_mut().zip(b) {
*ri = bi - *ri;
}
}
#[cfg_attr(not(feature = "faer"), allow(dead_code))] pub(crate) fn lower_with_diagonal(pattern: &Pattern, form: KktForm) -> Result<Pattern, KktError> {
pattern.check_invariants()?;
let n = pattern.n_cols();
if pattern.n_rows != n {
return Err(KktError::NotSymmetric);
}
if let KktForm::Augmented { n_primal, n_dual } = form
&& n_primal + n_dual != n
{
return Err(KktError::Structure(SparseError::Dimension {
what: "KKT order (n_primal + n_dual)",
expected: n_primal + n_dual,
found: n,
}));
}
let has_upper = (0..n).any(|j| pattern.col(j).first().is_some_and(|&i| (i as usize) < j));
if has_upper && pattern.transpose() != *pattern {
return Err(KktError::NotSymmetric);
}
let mut col_ptr = Vec::with_capacity(n + 1);
let mut row_idx = Vec::with_capacity(pattern.nnz() / 2 + n);
col_ptr.push(0);
for j in 0..n {
let rows = pattern.col(j);
let start = rows.partition_point(|&i| (i as usize) < j);
let below = &rows[start..];
if below.first() != Some(&crate::perm::idx32(j)) {
row_idx.push(crate::perm::idx32(j));
}
row_idx.extend_from_slice(below);
col_ptr.push(row_idx.len());
}
let l = Pattern {
n_rows: n,
col_ptr,
row_idx,
};
debug_assert_eq!(l.check_invariants(), Ok(()));
Ok(l)
}
#[cfg_attr(not(feature = "faer"), allow(dead_code))] pub(crate) fn check_regularization<T: Real>(reg: &Regularization<T>) -> Result<(), KktError> {
for (what, x) in [
("primal regularization", reg.primal),
("dual regularization", reg.dual),
("bump", reg.bump),
] {
if !x.is_finite() || lt(x, T::ZERO) {
return Err(KktError::Structure(SparseError::InvalidValue {
what,
index: 0,
value: x.to_f64(),
}));
}
}
if let Some(tau) = reg.pivot_tol
&& (!tau.is_finite() || !lt(T::ZERO, tau) || !lt(T::ZERO, reg.bump))
{
return Err(KktError::Structure(SparseError::InvalidValue {
what: "pivot_tol and bump (both must be positive)",
index: 0,
value: tau.to_f64(),
}));
}
Ok(())
}
#[cfg_attr(not(feature = "faer"), allow(dead_code))] pub(crate) fn pivot_signs(form: KktForm, n: usize) -> Vec<i8> {
match form {
KktForm::NormalEquations => vec![1; n],
KktForm::Augmented { n_primal, .. } => {
(0..n).map(|k| if k < n_primal { -1 } else { 1 }).collect()
}
}
}
#[cfg_attr(not(feature = "faer"), allow(dead_code))] pub(crate) fn assemble_regularized<T: Real>(
l: &Pattern,
form: KktForm,
k: &CscMatrix<T>,
reg: &Regularization<T>,
) -> Result<(Vec<T>, Vec<T>), KktError> {
let mut val = assemble(l, k)?;
let s = equilibrate_symmetric(l, &mut val);
for (j, &sj) in s.iter().enumerate() {
let shift = match form {
KktForm::NormalEquations => reg.dual,
KktForm::Augmented { n_primal, .. } => {
if j < n_primal {
-reg.primal
} else {
reg.dual
}
}
};
if !is_exact_zero(shift) {
let p = l.col_ptr[j];
val[p] += match reg.scale {
ShiftScale::Equilibrated => shift,
ShiftScale::Absolute => shift * sj * sj,
};
}
}
Ok((val, s))
}
fn assemble<T: Real>(l: &Pattern, k: &CscMatrix<T>) -> Result<Vec<T>, KktError> {
k.check_invariants()?;
let n = l.n_cols();
if k.n_rows != n || k.n_cols() != n {
return Err(KktError::Structure(SparseError::Dimension {
what: "KKT matrix order (as analyzed)",
expected: n,
found: k.n_rows.max(k.n_cols()),
}));
}
let mut val = vec![T::ZERO; l.nnz()];
for j in 0..n {
let lrows = l.col(j);
let base = l.col_ptr[j];
let mut q = 0;
for p in k.col_ptr[j]..k.col_ptr[j + 1] {
let i = k.row_idx[p];
if (i as usize) < j {
continue;
}
while q < lrows.len() && lrows[q] < i {
q += 1;
}
if q == lrows.len() || lrows[q] != i {
return Err(KktError::Structure(SparseError::Malformed(format!(
"KKT entry ({i}, {j}) is outside the analyzed pattern"
))));
}
if !k.val[p].is_finite() {
return Err(KktError::Structure(SparseError::InvalidValue {
what: "KKT matrix entry (not finite)",
index: p,
value: k.val[p].to_f64(),
}));
}
val[base + q] = k.val[p];
}
debug_assert_eq!(lrows[0] as usize, j);
}
Ok(val)
}
pub(crate) fn count_inertia<T: Real>(
ones: impl Iterator<Item = T>,
twos: impl Iterator<Item = (T, T, T)>,
) -> Inertia {
let mut s = Inertia::default();
let mut one = |d: T| {
if lt(T::ZERO, d) {
s.positive += 1;
} else if lt(d, T::ZERO) {
s.negative += 1;
} else {
s.zero += 1;
}
};
for d in ones {
one(d);
}
for (a, b, c) in twos {
let det = a * c - b * b;
if lt(det, T::ZERO) {
one(T::ONE);
one(-T::ONE);
} else if lt(T::ZERO, det) {
one(a + c);
one(a + c);
} else {
one(T::ZERO);
one(a + c);
}
}
s
}
pub const EQUILIBRATION_PASSES: usize = 20;
#[cfg_attr(not(feature = "faer"), allow(dead_code))] pub(crate) fn equilibrate_symmetric<T: Real>(l: &Pattern, val: &mut [T]) -> Vec<T> {
let n = l.n_cols();
let mut s = vec![T::ONE; n];
let mut rmax = vec![T::ZERO; n];
let mut step = vec![T::ONE; n];
for _ in 0..EQUILIBRATION_PASSES {
rmax.fill(T::ZERO);
for j in 0..n {
let range = l.col_ptr[j]..l.col_ptr[j + 1];
for (&i, &x) in l.row_idx[range.clone()].iter().zip(&val[range]) {
let i = i as usize;
let v = x.abs();
rmax[i] = rmax[i].max(v);
rmax[j] = rmax[j].max(v);
}
}
let mut changed = false;
for i in 0..n {
step[i] = if rmax[i].is_finite() && lt(T::ZERO, rmax[i]) {
pow2_round(rmax[i].sqrt().recip())
} else {
T::ONE
};
changed |= !is_exact_zero(step[i] - T::ONE);
}
if !changed {
break;
}
for j in 0..n {
let range = l.col_ptr[j]..l.col_ptr[j + 1];
for (&i, x) in l.row_idx[range.clone()].iter().zip(&mut val[range]) {
*x = *x * step[i as usize] * step[j];
}
}
for (si, &t) in s.iter_mut().zip(&step) {
*si *= t;
}
}
s
}
fn pow2_round<T: Real>(x: T) -> T {
T::from_f64(x.to_f64().log2().round().exp2())
}
#[cfg_attr(not(feature = "faer"), allow(dead_code))] pub(crate) fn check_inertia(found: Inertia, expected: Inertia) -> Result<Inertia, KktError> {
if found == expected {
Ok(found)
} else {
Err(KktError::WrongInertia { found, expected })
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::{Col, Row};
#[test]
fn lower_with_diagonal_accepts_lower_and_symmetric_patterns() {
let lower = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(0), 1.0), (Row(1), Col(1), 2.0)])
.unwrap();
let l = lower_with_diagonal(&lower.pattern(), KktForm::NormalEquations).unwrap();
assert_eq!(l.col_ptr, vec![0, 2, 3, 4]);
assert_eq!(l.row_idx, vec![0, 2, 1, 2]);
let full = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(0), 1.0), (Row(0), Col(2), 1.0)])
.unwrap();
assert_eq!(
lower_with_diagonal(&full.pattern(), KktForm::NormalEquations).unwrap(),
l.clone()
);
let skew = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(0), 1.0), (Row(0), Col(1), 1.0)])
.unwrap();
assert_eq!(
lower_with_diagonal(&skew.pattern(), KktForm::NormalEquations),
Err(KktError::NotSymmetric)
);
let rect = CscMatrix::<f64>::zeros(2, 3);
assert_eq!(
lower_with_diagonal(&rect.pattern(), KktForm::NormalEquations),
Err(KktError::NotSymmetric)
);
assert!(matches!(
lower_with_diagonal(
&lower.pattern(),
KktForm::Augmented {
n_primal: 1,
n_dual: 1
}
),
Err(KktError::Structure(_))
));
}
#[test]
fn inertia_counts_follow_sylvester() {
let s = count_inertia(
[1.0, -2.0, 0.0].into_iter(),
[(0.0, 1.0, 0.0), (2.0, 1.0, 3.0), (-1.0, 0.5, -1.0)].into_iter(),
);
assert_eq!(
s,
Inertia {
positive: 4,
negative: 4,
zero: 1
}
);
let expected = KktForm::Augmented {
n_primal: 2,
n_dual: 3,
}
.expected_inertia(5);
assert_eq!(expected.positive, 3);
assert_eq!(expected.negative, 2);
assert!(check_inertia(s, expected).is_err());
}
#[test]
fn assembly_adds_the_shifts_and_rejects_foreign_entries() {
let k = CscMatrix::from_triplets(
3,
3,
&[
(Row(0), Col(0), -1.0),
(Row(2), Col(0), 1.0),
(Row(1), Col(1), -2.0),
],
)
.unwrap();
let form = KktForm::Augmented {
n_primal: 2,
n_dual: 1,
};
let l = lower_with_diagonal(&k.pattern(), form).unwrap();
let mut reg = Regularization {
primal: 0.5,
dual: 0.25,
pivot_tol: None,
bump: 0.0,
scale: ShiftScale::Absolute,
};
let unscale = |(v, s): (Vec<f64>, Vec<f64>)| -> Vec<f64> {
let mut u = Vec::new();
for j in 0..l.n_cols() {
for p in l.col_ptr[j]..l.col_ptr[j + 1] {
u.push(v[p] / (s[l.row_idx[p] as usize] * s[j]));
}
}
u
};
let v = assemble_regularized(&l, form, &k, ®).unwrap();
assert!(v.1.iter().all(|&x| x > 0.0 && x.log2().fract() == 0.0));
assert_eq!(unscale(v), vec![-1.5, 1.0, -2.5, 0.25]);
reg.scale = ShiftScale::Equilibrated;
let (v, s) = assemble_regularized(&l, form, &k, ®).unwrap();
let u = unscale((v, s.clone()));
assert_eq!(u[0], -1.0 - 0.5 / (s[0] * s[0]));
assert_eq!(u[1], 1.0);
assert_eq!(u[2], -2.0 - 0.5 / (s[1] * s[1]));
assert_eq!(u[3], 0.25 / (s[2] * s[2]));
let mut nan = k.clone();
nan.val[1] = f64::NAN;
assert!(matches!(
assemble_regularized(&l, form, &nan, ®),
Err(KktError::Structure(_))
));
let other = CscMatrix::from_triplets(3, 3, &[(Row(2), Col(1), 1.0)]).unwrap();
assert!(matches!(
assemble_regularized(&l, form, &other, ®),
Err(KktError::Structure(_))
));
}
#[test]
fn symmetric_matvec_reads_the_lower_triangle_only() {
let k = CscMatrix::from_triplets(
2,
2,
&[
(Row(0), Col(0), 2.0),
(Row(1), Col(0), 3.0),
(Row(0), Col(1), 99.0),
(Row(1), Col(1), 4.0),
],
)
.unwrap();
let mut y = [0.0; 2];
sym_lower_matvec(&k, &[1.0, 1.0], &mut y);
assert_eq!(y, [5.0, 7.0]);
}
}