use std::sync::{Arc, Mutex};
use faer::dyn_stack::{MemBuffer, MemStack};
use faer::linalg::cholesky::ldlt::factor::{LdltError, LdltRegularization};
use faer::perm::PermRef;
use faer::sparse::linalg::SupernodalThreshold;
use faer::sparse::linalg::cholesky::{
CholeskySymbolicParams, IntranodeLbltRef, LdltRef, SymbolicCholesky, SymbolicCholeskyRaw,
SymmetricOrdering, factorize_symbolic_cholesky, simplicial, supernodal,
};
use faer::sparse::{SparseColMatRef, SymbolicSparseColMatRef};
use faer::{Conj, MatMut, Par, Side, Spec};
use crate::kkt::{
assemble_regularized, check_inertia, check_regularization, count_inertia, lower_with_diagonal,
pivot_signs,
};
use crate::real::{Real, approx_eq, is_exact_zero};
use crate::{
CscMatrix, Inertia, KktError, KktForm, KktSolver, Ordering, OrderingTarget, Pattern, Perm,
Regularization, SparseError,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FaerLdl {
pub supernodal: Option<bool>,
}
#[derive(Debug)]
pub struct FaerSymbolic {
form: KktForm,
lower: Pattern,
row_idx: Vec<usize>,
perm: Perm,
faer: Arc<SymbolicCholesky<usize>>,
permuted: Permuted,
}
#[derive(Debug)]
struct Permuted {
col_ptr: Vec<usize>,
row_idx: Vec<usize>,
map: Vec<usize>,
}
fn permute_lower(lower: &Pattern, perm: &Perm, upper: bool) -> Permuted {
let n = lower.n_cols();
let nnz = lower.nnz();
let mut entries: Vec<(usize, usize, usize)> = Vec::with_capacity(nnz);
for j in 0..n {
for (q, &i) in lower.col(j).iter().enumerate() {
let (a, b) = (perm.inv[i as usize] as usize, perm.inv[j] as usize);
let (hi, lo) = (a.max(b), a.min(b));
let (col, row) = if upper { (hi, lo) } else { (lo, hi) };
entries.push((col, row, lower.col_ptr[j] + q));
}
}
entries.sort_unstable();
let mut col_ptr = vec![0_usize; n + 1];
let mut row_idx = Vec::with_capacity(nnz);
let mut map = vec![0_usize; nnz];
for (pos, &(col, row, p)) in entries.iter().enumerate() {
col_ptr[col + 1] += 1;
row_idx.push(row);
map[p] = pos;
}
for c in 0..n {
col_ptr[c + 1] += col_ptr[c];
}
Permuted {
col_ptr,
row_idx,
map,
}
}
impl FaerSymbolic {
pub fn perm(&self) -> &Perm {
&self.perm
}
pub fn is_supernodal(&self) -> bool {
matches!(self.faer.raw(), SymbolicCholeskyRaw::Supernodal(_))
}
pub fn nnz_factor(&self) -> usize {
self.faer.len_val()
}
pub fn form(&self) -> KktForm {
self.form
}
}
#[derive(Debug)]
pub struct FaerNumeric<T: Real> {
faer: Arc<SymbolicCholesky<usize>>,
values: Vec<T>,
lblt: Option<Lblt<T>>,
inertia: Inertia,
bumped: usize,
scale: Vec<T>,
scratch: SolveScratch,
}
struct SolveScratch(Mutex<MemBuffer>);
impl std::fmt::Debug for SolveScratch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SolveScratch")
}
}
#[derive(Debug)]
struct Lblt<T> {
subdiag: Vec<T>,
fwd: Vec<usize>,
inv: Vec<usize>,
}
impl<T: Real> FaerNumeric<T> {
pub fn inertia(&self) -> Inertia {
self.inertia
}
pub fn bumped(&self) -> usize {
self.bumped
}
pub fn is_pivoted(&self) -> bool {
self.lblt.is_some()
}
}
fn faer_error(e: faer::sparse::FaerError) -> KktError {
match e {
faer::sparse::FaerError::OutOfMemory => KktError::Backend("faer: out of memory"),
faer::sparse::FaerError::IndexOverflow => KktError::Backend("faer: index overflow"),
_ => KktError::Backend("faer: symbolic factorization failed"),
}
}
fn pivots(sym: &SymbolicCholesky<usize>, values: &[f64]) -> Vec<f64> {
match sym.raw() {
SymbolicCholeskyRaw::Simplicial(s) => {
let l = s.factor();
let col_ptr = l.col_ptr();
let rows = l.row_idx();
(0..s.nrows())
.map(|k| {
debug_assert_eq!(rows[col_ptr[k]], k, "the diagonal leads each column");
values[col_ptr[k]]
})
.collect()
}
SymbolicCholeskyRaw::Supernodal(s) => {
let mut d = Vec::with_capacity(s.nrows());
for t in 0..s.n_supernodes() {
let begin = s.supernode_begin()[t];
let ncols = s.supernode_end()[t] - begin;
let nrows = s.nnz_per_super()[t] + ncols;
let block = &values[s.col_ptr_for_val()[t]..s.col_ptr_for_val()[t + 1]];
d.extend((0..ncols).map(|c| block[c * nrows + c]));
}
d
}
}
}
fn first_non_finite(sym: &SymbolicCholesky<usize>, values: &[f64]) -> Option<usize> {
let p = values.iter().position(|x| !x.is_finite())?;
Some(match sym.raw() {
SymbolicCholeskyRaw::Simplicial(s) => {
s.col_ptr().partition_point(|&c| c <= p) - 1
}
SymbolicCholeskyRaw::Supernodal(s) => {
let ptr = s.col_ptr_for_val();
let t = ptr.partition_point(|&c| c <= p) - 1;
let ncols = s.supernode_end()[t] - s.supernode_begin()[t];
let nrows = s.nnz_per_super()[t] + ncols;
s.supernode_begin()[t] + (p - ptr[t]) / nrows
}
})
}
#[allow(clippy::type_complexity)]
fn factor_lblt(
sym: &SymbolicCholesky<usize>,
a: SparseColMatRef<'_, usize, f64>,
values: &mut [f64],
) -> Result<(Lblt<f64>, Vec<f64>, Vec<(f64, f64, f64)>), KktError> {
let n = sym.nrows();
let mut subdiag = vec![0.0_f64; n];
let mut fwd = vec![0_usize; n];
let mut inv = vec![0_usize; n];
let mut buf = MemBuffer::try_new(
sym.factorize_numeric_intranode_lblt_scratch::<f64>(Par::Seq, Spec::default()),
)
.map_err(|_| KktError::Backend("faer: out of memory"))?;
sym.factorize_numeric_intranode_lblt(
values,
&mut subdiag,
&mut fwd,
&mut inv,
a,
Side::Lower,
Par::Seq,
MemStack::new(&mut buf),
Spec::default(),
);
let d = pivots(sym, values);
let mut ones = Vec::with_capacity(n);
let mut twos = Vec::new();
let mut p = 0;
while p < n {
if p + 1 < n && !is_exact_zero(subdiag[p]) {
twos.push((d[p], subdiag[p], d[p + 1]));
p += 2;
} else {
ones.push(d[p]);
p += 1;
}
}
Ok((Lblt { subdiag, fwd, inv }, ones, twos))
}
impl KktSolver<f64> for FaerLdl {
type Symbolic = FaerSymbolic;
type Numeric = FaerNumeric<f64>;
fn analyze(
&self,
pattern: &Pattern,
form: KktForm,
ordering: &dyn Ordering,
) -> Result<FaerSymbolic, KktError> {
let lower = lower_with_diagonal(pattern, form)?;
let n = lower.n_cols();
let full = lower.symmetric()?;
let perm = ordering.order(&full, OrderingTarget::Symmetric)?;
if perm.len() != n {
return Err(KktError::Structure(SparseError::Dimension {
what: "ordering length",
expected: n,
found: perm.len(),
}));
}
let perm = full.etree_postorder(&perm);
let row_idx: Vec<usize> = lower.row_idx.iter().map(|&i| i as usize).collect();
let fwd: Vec<usize> = perm.fwd.iter().map(|&i| i as usize).collect();
let inv: Vec<usize> = perm.inv.iter().map(|&i| i as usize).collect();
let a = SymbolicSparseColMatRef::new_checked(n, n, &lower.col_ptr, None, &row_idx);
let threshold = match self.supernodal {
None => SupernodalThreshold::AUTO,
Some(true) => SupernodalThreshold::FORCE_SUPERNODAL,
Some(false) => SupernodalThreshold::FORCE_SIMPLICIAL,
};
let params = CholeskySymbolicParams {
supernodal_flop_ratio_threshold: threshold,
..Default::default()
};
let faer = factorize_symbolic_cholesky(
a,
Side::Lower,
SymmetricOrdering::Custom(PermRef::new_checked(&fwd, &inv, n)),
params,
)
.map_err(faer_error)?;
let upper = matches!(faer.raw(), SymbolicCholeskyRaw::Simplicial(_));
let permuted = permute_lower(&lower, &perm, upper);
Ok(FaerSymbolic {
form,
lower,
row_idx,
perm,
faer: Arc::new(faer),
permuted,
})
}
#[allow(clippy::too_many_lines)] fn factorize(
&self,
symbolic: &FaerSymbolic,
k: &CscMatrix<f64>,
reg: Regularization<f64>,
) -> Result<(FaerNumeric<f64>, Inertia), KktError> {
let form = symbolic.form;
let sym = &*symbolic.faer;
let n = symbolic.lower.n_cols();
check_regularization(®)?;
let (v, scale) = assemble_regularized(&symbolic.lower, form, k, ®)?;
let a = SparseColMatRef::new(
SymbolicSparseColMatRef::new_checked(
n,
n,
&symbolic.lower.col_ptr,
None,
&symbolic.row_idx,
),
&v,
);
let mut values = vec![0.0_f64; sym.len_val()];
let supernodal = matches!(sym.raw(), SymbolicCholeskyRaw::Supernodal(_));
let use_lblt = matches!(form, KktForm::Augmented { .. })
&& !reg.is_quasi_definite()
&& reg.pivot_tol.is_none()
&& supernodal;
let (lblt, d, twos, faer_bumps) = if use_lblt {
let (lblt, ones, twos) = factor_lblt(sym, a, &mut values)?;
(Some(lblt), ones, twos, 0)
} else {
let signs = pivot_signs(form, n);
let fwd = &symbolic.perm.fwd;
let signs: Vec<i8> = (0..n).map(|k| signs[fwd[k] as usize]).collect();
let pm = &symbolic.permuted;
let mut pv = vec![0.0_f64; v.len()];
for (&dst, &x) in pm.map.iter().zip(&v) {
pv[dst] = x;
}
let a_perm = SparseColMatRef::new(
SymbolicSparseColMatRef::new_checked(n, n, &pm.col_ptr, None, &pm.row_idx),
&pv,
);
let regularization = LdltRegularization {
dynamic_regularization_signs: reg.pivot_tol.map(|_| signs.as_slice()),
dynamic_regularization_delta: reg.bump,
dynamic_regularization_epsilon: reg.pivot_tol.unwrap_or(0.0),
};
let req = match sym.raw() {
SymbolicCholeskyRaw::Simplicial(_) => {
simplicial::factorize_simplicial_numeric_ldlt_scratch::<usize, f64>(n)
}
SymbolicCholeskyRaw::Supernodal(s) => {
supernodal::factorize_supernodal_numeric_ldlt_scratch::<usize, f64>(
s,
Par::Seq,
Spec::default(),
)
}
};
let mut buf =
MemBuffer::try_new(req).map_err(|_| KktError::Backend("faer: out of memory"))?;
let stack = MemStack::new(&mut buf);
let result = match sym.raw() {
SymbolicCholeskyRaw::Simplicial(s) => {
simplicial::factorize_simplicial_numeric_ldlt(
&mut values,
a_perm,
regularization,
s,
stack,
)
}
SymbolicCholeskyRaw::Supernodal(s) => {
supernodal::factorize_supernodal_numeric_ldlt(
&mut values,
a_perm,
regularization,
s,
Par::Seq,
stack,
Spec::default(),
)
}
};
let bumps = match result {
Ok(info) => info.dynamic_regularization_count,
Err(LdltError::ZeroPivot { index }) => {
let k = if supernodal { index } else { index - 1 };
return Err(KktError::ZeroPivot {
position: symbolic.perm.fwd[k] as usize,
});
}
};
let d = pivots(sym, &values);
let extra = if supernodal && reg.pivot_tol.is_some() {
d.iter()
.zip(&signs)
.filter(|&(&x, &sg)| sg < 0 && approx_eq(x, -reg.bump, 0.0))
.count()
} else {
0
};
(None, d, Vec::new(), bumps + extra)
};
let bad = first_non_finite(sym, &values).or_else(|| {
lblt.as_ref()
.and_then(|b| b.subdiag.iter().position(|x| !x.is_finite()))
});
if let Some(k) = bad {
return Err(KktError::NotFinite {
position: symbolic.perm.fwd[k] as usize,
});
}
let bumped = faer_bumps;
let found = count_inertia(d.into_iter(), twos.into_iter());
let inertia = check_inertia(found, form.expected_inertia(n))?;
debug_assert_eq!(inertia, form.expected_inertia(n), "(S6)");
Ok((
FaerNumeric {
faer: Arc::clone(&symbolic.faer),
values,
lblt,
inertia,
bumped,
scale,
scratch: SolveScratch(Mutex::new(
MemBuffer::try_new(sym.solve_in_place_scratch::<f64>(1, Par::Seq))
.map_err(|_| KktError::Backend("faer: out of memory"))?,
)),
},
inertia,
))
}
fn solve(&self, numeric: &FaerNumeric<f64>, x: &mut [f64]) {
let sym = &*numeric.faer;
let n = sym.nrows();
assert_eq!(x.len(), n, "solve: x has the wrong length");
for (xi, &si) in x.iter_mut().zip(&numeric.scale) {
*xi *= si;
}
let mut held = numeric.scratch.0.try_lock().ok();
let mut temp;
let stack = if let Some(buf) = held.as_deref_mut() {
MemStack::new(buf)
} else {
temp = MemBuffer::new(sym.solve_in_place_scratch::<f64>(1, Par::Seq));
MemStack::new(&mut temp)
};
let rhs = MatMut::from_column_major_slice_mut(x, n, 1);
match &numeric.lblt {
None => LdltRef::new(sym, &numeric.values).solve_in_place_with_conj(
Conj::No,
rhs,
Par::Seq,
stack,
),
Some(b) => IntranodeLbltRef::new(
sym,
&numeric.values,
&b.subdiag,
PermRef::new_checked(&b.fwd, &b.inv, n),
)
.solve_in_place_with_conj(Conj::No, rhs, Par::Seq, stack),
}
for (xi, &si) in x.iter_mut().zip(&numeric.scale) {
*xi *= si;
}
}
fn nnz_factor(&self, numeric: &FaerNumeric<f64>) -> usize {
numeric.faer.len_val()
}
fn name(&self) -> &'static str {
"faer-ldl"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::real::le;
use crate::{Amd, Col, Natural, Refinement, Row, ShiftScale};
use proptest::prelude::*;
struct Rng(u64);
impl Rng {
fn unit(&mut self) -> f64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
#[allow(clippy::cast_precision_loss)]
let u = (self.0 >> 11) as f64 / (1_u64 << 53) as f64;
u
}
}
fn kkt(m: usize, n: usize, spread: f64, seed: u64) -> (CscMatrix, CscMatrix, KktForm) {
let mut r = Rng(seed | 1);
let mut a = Vec::new();
for i in 0..m {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let j = (r.unit() * n as f64) as usize % n;
a.push((i, j, 1.0 + r.unit()));
for jj in 0..n {
if jj != j && r.unit() < 0.2 {
a.push((i, jj, 2.0 * r.unit() - 1.0));
}
}
}
let d: Vec<f64> = (0..n)
.map(|_| 10f64.powf(spread * (2.0 * r.unit() - 1.0)))
.collect();
let mut t = Vec::new();
for (j, &dj) in d.iter().enumerate() {
t.push((Row::from_usize(j), Col::from_usize(j), -dj));
}
for &(i, j, v) in &a {
t.push((Row::from_usize(n + i), Col::from_usize(j), v));
}
let aug = CscMatrix::from_triplets(n + m, n + m, &t).unwrap();
let mut dense = vec![vec![0.0; m]; m];
for &(i, j, v) in &a {
for &(p, q, w) in &a {
if q == j && p <= i {
dense[i][p] += v * d[j] * w;
}
}
}
let mut tn = Vec::new();
for (i, row) in dense.iter().enumerate() {
for (p, &v) in row.iter().enumerate() {
if !crate::real::is_exact_zero(v) {
tn.push((Row::from_usize(i), Col::from_usize(p), v));
}
}
}
let normal = CscMatrix::from_triplets(m, m, &tn).unwrap();
(
aug,
normal,
KktForm::Augmented {
n_primal: n,
n_dual: m,
},
)
}
fn rhs(n: usize, seed: u64) -> Vec<f64> {
let mut r = Rng(seed | 3);
(0..n).map(|_| 2.0 * r.unit() - 1.0).collect()
}
fn solve_ok(
k: &CscMatrix,
form: KktForm,
reg: Regularization,
supernodal: Option<bool>,
b: &[f64],
) -> (Inertia, f64, usize) {
let s = FaerLdl { supernodal };
let sym = s.analyze(&k.pattern(), form, &Amd::default()).unwrap();
let (num, inertia) = s.factorize(&sym, k, reg).unwrap();
let mut rhs = vec![0.0; b.len()];
crate::kkt::sym_lower_matvec(k, b, &mut rhs);
let mut x = vec![0.0; b.len()];
let rep = s.solve_refined(&num, k, &rhs, &mut x, Refinement::default());
(inertia, rep.residual, num.bumped())
}
#[test]
fn augmented_and_normal_systems_solve_with_the_expected_inertia() {
for (seed, spread) in [(1, 0.0), (2, 4.0), (3, 8.0)] {
let (aug, normal, form) = kkt(12, 30, spread, seed);
for sn in [None, Some(true), Some(false)] {
let (i, res, _) =
solve_ok(&aug, form, Regularization::default(), sn, &rhs(42, seed));
assert_eq!(i, form.expected_inertia(42));
assert!(le(res, 1e-10, 0.0), "aug seed {seed} sn {sn:?}: {res}");
let (i, res, _) = solve_ok(
&normal,
KktForm::NormalEquations,
Regularization::default(),
sn,
&rhs(12, seed),
);
assert_eq!(i, KktForm::NormalEquations.expected_inertia(12));
assert!(le(res, 1e-10, 0.0), "normal seed {seed} sn {sn:?}: {res}");
}
}
}
#[test]
fn unregularized_augmented_system_takes_the_bunch_kaufman_path() {
let (aug, _, form) = kkt(10, 25, 2.0, 9);
let s = FaerLdl {
supernodal: Some(true),
};
let sym = s.analyze(&aug.pattern(), form, &Natural).unwrap();
let (num, inertia) = s.factorize(&sym, &aug, Regularization::none()).unwrap();
assert!(num.is_pivoted());
assert_eq!(inertia, form.expected_inertia(35));
let b = rhs(35, 9);
let mut x = vec![0.0; 35];
let rep = s.solve_refined(&num, &aug, &b, &mut x, Refinement::default());
assert!(rep.converged, "{rep:?}");
}
#[test]
fn s6_errors_zero_pivot_and_wrong_inertia() {
let k = CscMatrix::from_triplets(2, 2, &[(Row(0), Col(0), -1.0)]).unwrap();
let form = KktForm::Augmented {
n_primal: 1,
n_dual: 1,
};
let s = FaerLdl {
supernodal: Some(false),
};
let sym = s.analyze(&k.pattern(), form, &Natural).unwrap();
assert_eq!(
s.factorize(&sym, &k, Regularization::none()).err(),
Some(KktError::ZeroPivot { position: 1 })
);
let sn = FaerLdl {
supernodal: Some(true),
};
let sym = sn.analyze(&k.pattern(), form, &Natural).unwrap();
assert!(matches!(
sn.factorize(&sym, &k, Regularization::none()),
Err(KktError::WrongInertia { .. })
));
let (_, inertia) = s
.factorize(
&s.analyze(&k.pattern(), form, &Natural).unwrap(),
&k,
Regularization::default(),
)
.unwrap();
assert_eq!(inertia, form.expected_inertia(2));
let ind = CscMatrix::from_triplets(
2,
2,
&[
(Row(0), Col(0), 1.0),
(Row(1), Col(0), 2.0),
(Row(1), Col(1), 1.0),
],
)
.unwrap();
let sym = s
.analyze(&ind.pattern(), KktForm::NormalEquations, &Natural)
.unwrap();
assert_eq!(
s.factorize(&sym, &ind, Regularization::none()).err(),
Some(KktError::WrongInertia {
found: Inertia {
positive: 1,
negative: 1,
zero: 0
},
expected: Inertia {
positive: 2,
negative: 0,
zero: 0
}
})
);
let reg = Regularization {
primal: 0.0,
dual: 0.0,
pivot_tol: Some(1e-13),
bump: 1e-8,
scale: ShiftScale::Equilibrated,
};
let (num, inertia) = s.factorize(&sym, &ind, reg).unwrap();
assert_eq!(inertia.positive, 2);
assert_eq!(num.bumped(), 1);
let (aug, _, form) = kkt(3, 5, 0.0, 4);
let sym = s.analyze(&aug.pattern(), form, &Natural).unwrap();
let (num, _) = s.factorize(&sym, &aug, Regularization::default()).unwrap();
assert_eq!(num.bumped(), 0);
}
#[test]
fn s10_overflow_is_an_error_and_non_finite_input_is_refused() {
let t = |v: f64| {
CscMatrix::from_triplets(
3,
3,
&[
(Row(0), Col(0), v),
(Row(1), Col(0), 1.0),
(Row(2), Col(0), 1.0),
(Row(1), Col(1), 1.0),
(Row(2), Col(1), 1.0),
(Row(2), Col(2), 1.0),
],
)
.unwrap()
};
let k = t(1e-310);
let reg = Regularization {
primal: 0.0,
dual: 0.0,
pivot_tol: Some(1e-300),
bump: 1e-300,
scale: ShiftScale::Equilibrated,
};
for sn in [Some(false), Some(true)] {
let s = FaerLdl { supernodal: sn };
let sym = s
.analyze(&k.pattern(), KktForm::NormalEquations, &Natural)
.unwrap();
assert!(
matches!(
s.factorize(&sym, &k, reg),
Err(KktError::NotFinite { .. } | KktError::ZeroPivot { .. })
),
"{sn:?}"
);
let mut nan = t(1.0);
nan.val[0] = f64::NAN;
assert!(matches!(
s.factorize(&sym, &nan, Regularization::default()),
Err(KktError::Structure(_))
));
}
let s = FaerLdl {
supernodal: Some(false),
};
let sym = s
.analyze(&k.pattern(), KktForm::NormalEquations, &Natural)
.unwrap();
assert!(matches!(
s.factorize(&sym, &k, reg),
Err(KktError::NotFinite { position: 1 | 2 })
));
}
#[test]
fn gmres_refinement_removes_the_shift_on_a_nearly_singular_system() {
let a = [
[1.0, 2.0, 0.0, 1.0],
[1.0, 2.0, 1e-6, 1.0],
[0.0, 1.0, 1.0, 3.0],
];
let mut t = Vec::new();
for i in 0..3 {
for p in 0..=i {
let v: f64 = (0..4).map(|j| a[i][j] * a[p][j]).sum();
t.push((Row::from_usize(i), Col::from_usize(p), v));
}
}
let k = CscMatrix::from_triplets(3, 3, &t).unwrap();
let s = FaerLdl::default();
let sym = s
.analyze(&k.pattern(), KktForm::NormalEquations, &Amd::default())
.unwrap();
let (num, _) = s.factorize(&sym, &k, Regularization::default()).unwrap();
let xs = [1.0, -1.0, 0.5];
let mut b = vec![0.0; 3];
crate::kkt::sym_lower_matvec(&k, &xs, &mut b);
let mut x = vec![0.0; 3];
let first = s.solve_refined(
&num,
&k,
&b,
&mut x,
Refinement {
max_iters: 0,
target_residual: 0.0,
},
);
let rep = s.solve_refined(&num, &k, &b, &mut x, Refinement::default());
assert!(rep.converged && rep.iters <= 4, "{first:?} → {rep:?}");
assert!(le(rep.residual, first.residual, 0.0));
}
#[test]
fn bumps_are_counted_with_both_signs_and_both_structures() {
let k = CscMatrix::from_triplets(2, 2, &[(Row(1), Col(0), 1.0)]).unwrap();
let form = KktForm::Augmented {
n_primal: 1,
n_dual: 1,
};
let reg = Regularization {
primal: 0.0,
dual: 0.0,
pivot_tol: Some(1e-13),
bump: 1e-8,
scale: ShiftScale::Equilibrated,
};
let z = CscMatrix::from_triplets(
3,
3,
&[
(Row(0), Col(0), 1.0),
(Row(2), Col(0), 1.0),
(Row(2), Col(2), 1.0),
],
)
.unwrap();
for sn in [Some(false), Some(true)] {
let s = FaerLdl { supernodal: sn };
let sym = s.analyze(&k.pattern(), form, &Natural).unwrap();
let (num, inertia) = s.factorize(&sym, &k, reg).unwrap();
assert_eq!(inertia, form.expected_inertia(2));
assert_eq!(num.bumped(), 1, "{sn:?}");
let sym = s
.analyze(&z.pattern(), KktForm::NormalEquations, &Natural)
.unwrap();
let (num, _) = s.factorize(&sym, &z, reg).unwrap();
assert_eq!(num.bumped(), 2, "{sn:?}"); }
}
#[test]
fn structural_errors() {
let s = FaerLdl::default();
let skew = CscMatrix::from_triplets(
2,
2,
&[
(Row(1), Col(0), 1.0),
(Row(1), Col(1), 1.0),
(Row(0), Col(0), 1.0),
(Row(0), Col(1), 3.0),
],
)
.unwrap();
assert!(
s.analyze(&skew.pattern(), KktForm::NormalEquations, &Amd::default())
.is_ok()
);
let up = CscMatrix::from_triplets(2, 2, &[(Row(0), Col(1), 1.0)]).unwrap();
assert_eq!(
s.analyze(&up.pattern(), KktForm::NormalEquations, &Amd::default())
.err(),
Some(KktError::NotSymmetric)
);
let sym = s
.analyze(
&CscMatrix::<f64>::zeros(2, 2).pattern(),
KktForm::NormalEquations,
&Amd::default(),
)
.unwrap();
let k = CscMatrix::from_triplets(2, 2, &[(Row(1), Col(0), 1.0), (Row(0), Col(0), 1.0)])
.unwrap();
assert!(matches!(
s.factorize(&sym, &k, Regularization::default()),
Err(KktError::Structure(_))
));
let k3 = CscMatrix::<f64>::zeros(3, 3);
assert!(matches!(
s.factorize(&sym, &k3, Regularization::default()),
Err(KktError::Structure(_))
));
let bad = Regularization {
pivot_tol: Some(1e-13),
bump: 0.0,
..Regularization::default()
};
assert!(matches!(
s.factorize(&sym, &CscMatrix::zeros(2, 2), bad),
Err(KktError::Structure(_))
));
let (aug, _, form) = kkt(4, 8, 1.0, 5);
let sym = s.analyze(&aug.pattern(), form, &Amd::default()).unwrap();
let mut thin = aug.clone();
thin.row_idx.remove(1);
thin.val.remove(1);
for p in &mut thin.col_ptr[1..] {
*p -= 1;
}
assert!(s.factorize(&sym, &thin, Regularization::default()).is_ok());
}
#[test]
fn s7_refinement_keeps_the_best_iterate() {
let (aug, _, form) = kkt(8, 20, 4.0, 77);
let s = FaerLdl::default();
let sym = s.analyze(&aug.pattern(), form, &Amd::default()).unwrap();
let b = rhs(28, 77);
let far = Regularization {
primal: 10.0,
dual: 10.0,
pivot_tol: None,
bump: 0.0,
scale: ShiftScale::Absolute,
};
let (num, _) = s.factorize(&sym, &aug, far).unwrap();
let mut x = vec![0.0; 28];
let none = s.solve_refined(
&num,
&aug,
&b,
&mut x,
Refinement {
max_iters: 0,
target_residual: 0.0,
},
);
let rep = s.solve_refined(
&num,
&aug,
&b,
&mut x,
Refinement {
max_iters: 50,
target_residual: 0.0,
},
);
assert!(le(rep.residual, none.residual, 0.0));
assert_eq!(none.iters, 0);
assert!(!rep.converged);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(24))]
#[test]
fn random_kkt_systems(m in 1_usize..15, extra in 0_usize..20, regime in 0_usize..3, sn in 0_usize..3, seed in any::<u64>()) {
let n = m + extra;
let spread = [0.0, 4.0, 9.0][regime];
let supernodal = [None, Some(true), Some(false)][sn];
let (aug, normal, form) = kkt(m, n, spread, seed);
let (i, res, _) = solve_ok(&aug, form, Regularization::default(), supernodal, &rhs(n + m, seed));
prop_assert_eq!(i, form.expected_inertia(n + m));
prop_assert!(le(res, 1e-10, 0.0), "augmented: {}", res);
let (i, _, _) = solve_ok(&normal, KktForm::NormalEquations, Regularization::default(), supernodal, &rhs(m, seed));
prop_assert_eq!(i, KktForm::NormalEquations.expected_inertia(m));
}
}
}