1use std::sync::{Arc, Mutex};
122
123use faer::dyn_stack::{MemBuffer, MemStack};
124use faer::linalg::cholesky::ldlt::factor::{LdltError, LdltRegularization};
125use faer::perm::PermRef;
126use faer::sparse::linalg::SupernodalThreshold;
127use faer::sparse::linalg::cholesky::{
128 CholeskySymbolicParams, IntranodeLbltRef, LdltRef, SymbolicCholesky, SymbolicCholeskyRaw,
129 SymmetricOrdering, factorize_symbolic_cholesky, simplicial, supernodal,
130};
131use faer::sparse::{SparseColMatRef, SymbolicSparseColMatRef};
132use faer::{Conj, MatMut, Par, Side, Spec};
133
134use crate::kkt::{
135 assemble_regularized, check_inertia, check_regularization, count_inertia, lower_with_diagonal,
136 pivot_signs,
137};
138use crate::real::{Real, approx_eq, is_exact_zero};
139use crate::{
140 CscMatrix, Inertia, KktError, KktForm, KktSolver, Ordering, OrderingTarget, Pattern, Perm,
141 Regularization, SparseError,
142};
143
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
180pub struct FaerLdl {
181 pub supernodal: Option<bool>,
185}
186
187#[derive(Debug)]
190pub struct FaerSymbolic {
191 form: KktForm,
192 lower: Pattern,
193 row_idx: Vec<usize>,
194 perm: Perm,
195 faer: Arc<SymbolicCholesky<usize>>,
196 permuted: Permuted,
200}
201
202#[derive(Debug)]
204struct Permuted {
205 col_ptr: Vec<usize>,
206 row_idx: Vec<usize>,
207 map: Vec<usize>,
209}
210
211fn permute_lower(lower: &Pattern, perm: &Perm, upper: bool) -> Permuted {
214 let n = lower.n_cols();
215 let nnz = lower.nnz();
216 let mut entries: Vec<(usize, usize, usize)> = Vec::with_capacity(nnz);
217 for j in 0..n {
218 for (q, &i) in lower.col(j).iter().enumerate() {
219 let (a, b) = (perm.inv[i as usize] as usize, perm.inv[j] as usize);
220 let (hi, lo) = (a.max(b), a.min(b));
221 let (col, row) = if upper { (hi, lo) } else { (lo, hi) };
222 entries.push((col, row, lower.col_ptr[j] + q));
223 }
224 }
225 entries.sort_unstable();
226 let mut col_ptr = vec![0_usize; n + 1];
227 let mut row_idx = Vec::with_capacity(nnz);
228 let mut map = vec![0_usize; nnz];
229 for (pos, &(col, row, p)) in entries.iter().enumerate() {
230 col_ptr[col + 1] += 1;
231 row_idx.push(row);
232 map[p] = pos;
233 }
234 for c in 0..n {
235 col_ptr[c + 1] += col_ptr[c];
236 }
237 Permuted {
238 col_ptr,
239 row_idx,
240 map,
241 }
242}
243
244impl FaerSymbolic {
245 pub fn perm(&self) -> &Perm {
248 &self.perm
249 }
250
251 pub fn is_supernodal(&self) -> bool {
253 matches!(self.faer.raw(), SymbolicCholeskyRaw::Supernodal(_))
254 }
255
256 pub fn nnz_factor(&self) -> usize {
258 self.faer.len_val()
259 }
260
261 pub fn form(&self) -> KktForm {
263 self.form
264 }
265}
266
267#[derive(Debug)]
269pub struct FaerNumeric<T: Real> {
270 faer: Arc<SymbolicCholesky<usize>>,
271 values: Vec<T>,
272 lblt: Option<Lblt<T>>,
275 inertia: Inertia,
276 bumped: usize,
277 scale: Vec<T>,
279 scratch: SolveScratch,
281}
282
283struct SolveScratch(Mutex<MemBuffer>);
286
287impl std::fmt::Debug for SolveScratch {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 f.write_str("SolveScratch")
290 }
291}
292
293#[derive(Debug)]
294struct Lblt<T> {
295 subdiag: Vec<T>,
296 fwd: Vec<usize>,
297 inv: Vec<usize>,
298}
299
300impl<T: Real> FaerNumeric<T> {
301 pub fn inertia(&self) -> Inertia {
303 self.inertia
304 }
305
306 pub fn bumped(&self) -> usize {
308 self.bumped
309 }
310
311 pub fn is_pivoted(&self) -> bool {
314 self.lblt.is_some()
315 }
316}
317
318fn faer_error(e: faer::sparse::FaerError) -> KktError {
319 match e {
320 faer::sparse::FaerError::OutOfMemory => KktError::Backend("faer: out of memory"),
321 faer::sparse::FaerError::IndexOverflow => KktError::Backend("faer: index overflow"),
322 _ => KktError::Backend("faer: symbolic factorization failed"),
323 }
324}
325
326fn pivots(sym: &SymbolicCholesky<usize>, values: &[f64]) -> Vec<f64> {
328 match sym.raw() {
329 SymbolicCholeskyRaw::Simplicial(s) => {
330 let l = s.factor();
331 let col_ptr = l.col_ptr();
332 let rows = l.row_idx();
333 (0..s.nrows())
334 .map(|k| {
335 debug_assert_eq!(rows[col_ptr[k]], k, "the diagonal leads each column");
336 values[col_ptr[k]]
337 })
338 .collect()
339 }
340 SymbolicCholeskyRaw::Supernodal(s) => {
341 let mut d = Vec::with_capacity(s.nrows());
342 for t in 0..s.n_supernodes() {
343 let begin = s.supernode_begin()[t];
344 let ncols = s.supernode_end()[t] - begin;
345 let nrows = s.nnz_per_super()[t] + ncols;
346 let block = &values[s.col_ptr_for_val()[t]..s.col_ptr_for_val()[t + 1]];
347 d.extend((0..ncols).map(|c| block[c * nrows + c]));
348 }
349 d
350 }
351 }
352}
353
354fn first_non_finite(sym: &SymbolicCholesky<usize>, values: &[f64]) -> Option<usize> {
357 let p = values.iter().position(|x| !x.is_finite())?;
358 Some(match sym.raw() {
359 SymbolicCholeskyRaw::Simplicial(s) => {
360 s.col_ptr().partition_point(|&c| c <= p) - 1
362 }
363 SymbolicCholeskyRaw::Supernodal(s) => {
364 let ptr = s.col_ptr_for_val();
365 let t = ptr.partition_point(|&c| c <= p) - 1;
366 let ncols = s.supernode_end()[t] - s.supernode_begin()[t];
367 let nrows = s.nnz_per_super()[t] + ncols;
368 s.supernode_begin()[t] + (p - ptr[t]) / nrows
369 }
370 })
371}
372
373#[allow(clippy::type_complexity)]
376fn factor_lblt(
377 sym: &SymbolicCholesky<usize>,
378 a: SparseColMatRef<'_, usize, f64>,
379 values: &mut [f64],
380) -> Result<(Lblt<f64>, Vec<f64>, Vec<(f64, f64, f64)>), KktError> {
381 let n = sym.nrows();
382 let mut subdiag = vec![0.0_f64; n];
383 let mut fwd = vec![0_usize; n];
384 let mut inv = vec![0_usize; n];
385 let mut buf = MemBuffer::try_new(
386 sym.factorize_numeric_intranode_lblt_scratch::<f64>(Par::Seq, Spec::default()),
387 )
388 .map_err(|_| KktError::Backend("faer: out of memory"))?;
389 sym.factorize_numeric_intranode_lblt(
390 values,
391 &mut subdiag,
392 &mut fwd,
393 &mut inv,
394 a,
395 Side::Lower,
396 Par::Seq,
397 MemStack::new(&mut buf),
398 Spec::default(),
399 );
400 let d = pivots(sym, values);
401 let mut ones = Vec::with_capacity(n);
403 let mut twos = Vec::new();
404 let mut p = 0;
405 while p < n {
406 if p + 1 < n && !is_exact_zero(subdiag[p]) {
407 twos.push((d[p], subdiag[p], d[p + 1]));
408 p += 2;
409 } else {
410 ones.push(d[p]);
411 p += 1;
412 }
413 }
414 Ok((Lblt { subdiag, fwd, inv }, ones, twos))
415}
416
417impl KktSolver<f64> for FaerLdl {
418 type Symbolic = FaerSymbolic;
419 type Numeric = FaerNumeric<f64>;
420
421 fn analyze(
422 &self,
423 pattern: &Pattern,
424 form: KktForm,
425 ordering: &dyn Ordering,
426 ) -> Result<FaerSymbolic, KktError> {
427 let lower = lower_with_diagonal(pattern, form)?;
429 let n = lower.n_cols();
430 let full = lower.symmetric()?;
432 let perm = ordering.order(&full, OrderingTarget::Symmetric)?;
433 if perm.len() != n {
434 return Err(KktError::Structure(SparseError::Dimension {
435 what: "ordering length",
436 expected: n,
437 found: perm.len(),
438 }));
439 }
440 let perm = full.etree_postorder(&perm);
442 let row_idx: Vec<usize> = lower.row_idx.iter().map(|&i| i as usize).collect();
444 let fwd: Vec<usize> = perm.fwd.iter().map(|&i| i as usize).collect();
445 let inv: Vec<usize> = perm.inv.iter().map(|&i| i as usize).collect();
446 let a = SymbolicSparseColMatRef::new_checked(n, n, &lower.col_ptr, None, &row_idx);
447 let threshold = match self.supernodal {
448 None => SupernodalThreshold::AUTO,
449 Some(true) => SupernodalThreshold::FORCE_SUPERNODAL,
450 Some(false) => SupernodalThreshold::FORCE_SIMPLICIAL,
451 };
452 let params = CholeskySymbolicParams {
453 supernodal_flop_ratio_threshold: threshold,
454 ..Default::default()
455 };
456 let faer = factorize_symbolic_cholesky(
457 a,
458 Side::Lower,
459 SymmetricOrdering::Custom(PermRef::new_checked(&fwd, &inv, n)),
460 params,
461 )
462 .map_err(faer_error)?;
463 let upper = matches!(faer.raw(), SymbolicCholeskyRaw::Simplicial(_));
465 let permuted = permute_lower(&lower, &perm, upper);
466 Ok(FaerSymbolic {
467 form,
468 lower,
469 row_idx,
470 perm,
471 faer: Arc::new(faer),
472 permuted,
473 })
474 }
475
476 #[allow(clippy::too_many_lines)] fn factorize(
478 &self,
479 symbolic: &FaerSymbolic,
480 k: &CscMatrix<f64>,
481 reg: Regularization<f64>,
482 ) -> Result<(FaerNumeric<f64>, Inertia), KktError> {
483 let form = symbolic.form;
484 let sym = &*symbolic.faer;
485 let n = symbolic.lower.n_cols();
486 check_regularization(®)?;
487 let (v, scale) = assemble_regularized(&symbolic.lower, form, k, ®)?;
489 let a = SparseColMatRef::new(
490 SymbolicSparseColMatRef::new_checked(
491 n,
492 n,
493 &symbolic.lower.col_ptr,
494 None,
495 &symbolic.row_idx,
496 ),
497 &v,
498 );
499 let mut values = vec![0.0_f64; sym.len_val()];
500 let supernodal = matches!(sym.raw(), SymbolicCholeskyRaw::Supernodal(_));
501 let use_lblt = matches!(form, KktForm::Augmented { .. })
503 && !reg.is_quasi_definite()
504 && reg.pivot_tol.is_none()
505 && supernodal;
506 let (lblt, d, twos, faer_bumps) = if use_lblt {
507 let (lblt, ones, twos) = factor_lblt(sym, a, &mut values)?;
508 (Some(lblt), ones, twos, 0)
509 } else {
510 let signs = pivot_signs(form, n);
514 let fwd = &symbolic.perm.fwd;
515 let signs: Vec<i8> = (0..n).map(|k| signs[fwd[k] as usize]).collect();
516 let pm = &symbolic.permuted;
517 let mut pv = vec![0.0_f64; v.len()];
518 for (&dst, &x) in pm.map.iter().zip(&v) {
519 pv[dst] = x;
520 }
521 let a_perm = SparseColMatRef::new(
522 SymbolicSparseColMatRef::new_checked(n, n, &pm.col_ptr, None, &pm.row_idx),
523 &pv,
524 );
525 let regularization = LdltRegularization {
526 dynamic_regularization_signs: reg.pivot_tol.map(|_| signs.as_slice()),
527 dynamic_regularization_delta: reg.bump,
528 dynamic_regularization_epsilon: reg.pivot_tol.unwrap_or(0.0),
529 };
530 let req = match sym.raw() {
531 SymbolicCholeskyRaw::Simplicial(_) => {
532 simplicial::factorize_simplicial_numeric_ldlt_scratch::<usize, f64>(n)
533 }
534 SymbolicCholeskyRaw::Supernodal(s) => {
535 supernodal::factorize_supernodal_numeric_ldlt_scratch::<usize, f64>(
536 s,
537 Par::Seq,
538 Spec::default(),
539 )
540 }
541 };
542 let mut buf =
543 MemBuffer::try_new(req).map_err(|_| KktError::Backend("faer: out of memory"))?;
544 let stack = MemStack::new(&mut buf);
545 let result = match sym.raw() {
546 SymbolicCholeskyRaw::Simplicial(s) => {
547 simplicial::factorize_simplicial_numeric_ldlt(
548 &mut values,
549 a_perm,
550 regularization,
551 s,
552 stack,
553 )
554 }
555 SymbolicCholeskyRaw::Supernodal(s) => {
556 supernodal::factorize_supernodal_numeric_ldlt(
557 &mut values,
558 a_perm,
559 regularization,
560 s,
561 Par::Seq,
562 stack,
563 Spec::default(),
564 )
565 }
566 };
567 let bumps = match result {
568 Ok(info) => info.dynamic_regularization_count,
569 Err(LdltError::ZeroPivot { index }) => {
570 let k = if supernodal { index } else { index - 1 };
573 return Err(KktError::ZeroPivot {
574 position: symbolic.perm.fwd[k] as usize,
575 });
576 }
577 };
578 let d = pivots(sym, &values);
579 let extra = if supernodal && reg.pivot_tol.is_some() {
583 d.iter()
584 .zip(&signs)
585 .filter(|&(&x, &sg)| sg < 0 && approx_eq(x, -reg.bump, 0.0))
586 .count()
587 } else {
588 0
589 };
590 (None, d, Vec::new(), bumps + extra)
591 };
592 let bad = first_non_finite(sym, &values).or_else(|| {
594 lblt.as_ref()
595 .and_then(|b| b.subdiag.iter().position(|x| !x.is_finite()))
596 });
597 if let Some(k) = bad {
598 return Err(KktError::NotFinite {
599 position: symbolic.perm.fwd[k] as usize,
600 });
601 }
602 let bumped = faer_bumps;
604 let found = count_inertia(d.into_iter(), twos.into_iter());
605 let inertia = check_inertia(found, form.expected_inertia(n))?;
607 debug_assert_eq!(inertia, form.expected_inertia(n), "(S6)");
608 Ok((
609 FaerNumeric {
610 faer: Arc::clone(&symbolic.faer),
611 values,
612 lblt,
613 inertia,
614 bumped,
615 scale,
616 scratch: SolveScratch(Mutex::new(
617 MemBuffer::try_new(sym.solve_in_place_scratch::<f64>(1, Par::Seq))
618 .map_err(|_| KktError::Backend("faer: out of memory"))?,
619 )),
620 },
621 inertia,
622 ))
623 }
624
625 fn solve(&self, numeric: &FaerNumeric<f64>, x: &mut [f64]) {
626 let sym = &*numeric.faer;
627 let n = sym.nrows();
628 assert_eq!(x.len(), n, "solve: x has the wrong length");
629 for (xi, &si) in x.iter_mut().zip(&numeric.scale) {
630 *xi *= si;
631 }
632 let mut held = numeric.scratch.0.try_lock().ok();
635 let mut temp;
636 let stack = if let Some(buf) = held.as_deref_mut() {
637 MemStack::new(buf)
638 } else {
639 temp = MemBuffer::new(sym.solve_in_place_scratch::<f64>(1, Par::Seq));
640 MemStack::new(&mut temp)
641 };
642 let rhs = MatMut::from_column_major_slice_mut(x, n, 1);
643 match &numeric.lblt {
644 None => LdltRef::new(sym, &numeric.values).solve_in_place_with_conj(
645 Conj::No,
646 rhs,
647 Par::Seq,
648 stack,
649 ),
650 Some(b) => IntranodeLbltRef::new(
651 sym,
652 &numeric.values,
653 &b.subdiag,
654 PermRef::new_checked(&b.fwd, &b.inv, n),
655 )
656 .solve_in_place_with_conj(Conj::No, rhs, Par::Seq, stack),
657 }
658 for (xi, &si) in x.iter_mut().zip(&numeric.scale) {
659 *xi *= si;
660 }
661 }
662
663 fn nnz_factor(&self, numeric: &FaerNumeric<f64>) -> usize {
664 numeric.faer.len_val()
665 }
666
667 fn name(&self) -> &'static str {
668 "faer-ldl"
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use crate::real::le;
676 use crate::{Amd, Col, Natural, Refinement, Row, ShiftScale};
677 use proptest::prelude::*;
678
679 struct Rng(u64);
681 impl Rng {
682 fn unit(&mut self) -> f64 {
683 self.0 ^= self.0 << 13;
684 self.0 ^= self.0 >> 7;
685 self.0 ^= self.0 << 17;
686 #[allow(clippy::cast_precision_loss)]
687 let u = (self.0 >> 11) as f64 / (1_u64 << 53) as f64;
688 u
689 }
690 }
691
692 fn kkt(m: usize, n: usize, spread: f64, seed: u64) -> (CscMatrix, CscMatrix, KktForm) {
696 let mut r = Rng(seed | 1);
697 let mut a = Vec::new();
698 for i in 0..m {
699 #[allow(
700 clippy::cast_possible_truncation,
701 clippy::cast_sign_loss,
702 clippy::cast_precision_loss
703 )]
704 let j = (r.unit() * n as f64) as usize % n;
705 a.push((i, j, 1.0 + r.unit()));
706 for jj in 0..n {
707 if jj != j && r.unit() < 0.2 {
708 a.push((i, jj, 2.0 * r.unit() - 1.0));
709 }
710 }
711 }
712 let d: Vec<f64> = (0..n)
713 .map(|_| 10f64.powf(spread * (2.0 * r.unit() - 1.0)))
714 .collect();
715 let mut t = Vec::new();
717 for (j, &dj) in d.iter().enumerate() {
718 t.push((Row::from_usize(j), Col::from_usize(j), -dj));
719 }
720 for &(i, j, v) in &a {
721 t.push((Row::from_usize(n + i), Col::from_usize(j), v));
722 }
723 let aug = CscMatrix::from_triplets(n + m, n + m, &t).unwrap();
724 let mut dense = vec![vec![0.0; m]; m];
726 for &(i, j, v) in &a {
727 for &(p, q, w) in &a {
728 if q == j && p <= i {
729 dense[i][p] += v * d[j] * w;
730 }
731 }
732 }
733 let mut tn = Vec::new();
734 for (i, row) in dense.iter().enumerate() {
735 for (p, &v) in row.iter().enumerate() {
736 if !crate::real::is_exact_zero(v) {
737 tn.push((Row::from_usize(i), Col::from_usize(p), v));
738 }
739 }
740 }
741 let normal = CscMatrix::from_triplets(m, m, &tn).unwrap();
742 (
743 aug,
744 normal,
745 KktForm::Augmented {
746 n_primal: n,
747 n_dual: m,
748 },
749 )
750 }
751
752 fn rhs(n: usize, seed: u64) -> Vec<f64> {
753 let mut r = Rng(seed | 3);
754 (0..n).map(|_| 2.0 * r.unit() - 1.0).collect()
755 }
756
757 fn solve_ok(
761 k: &CscMatrix,
762 form: KktForm,
763 reg: Regularization,
764 supernodal: Option<bool>,
765 b: &[f64],
766 ) -> (Inertia, f64, usize) {
767 let s = FaerLdl { supernodal };
768 let sym = s.analyze(&k.pattern(), form, &Amd::default()).unwrap();
769 let (num, inertia) = s.factorize(&sym, k, reg).unwrap();
770 let mut rhs = vec![0.0; b.len()];
773 crate::kkt::sym_lower_matvec(k, b, &mut rhs);
774 let mut x = vec![0.0; b.len()];
775 let rep = s.solve_refined(&num, k, &rhs, &mut x, Refinement::default());
776 (inertia, rep.residual, num.bumped())
777 }
778
779 #[test]
780 fn augmented_and_normal_systems_solve_with_the_expected_inertia() {
781 for (seed, spread) in [(1, 0.0), (2, 4.0), (3, 8.0)] {
782 let (aug, normal, form) = kkt(12, 30, spread, seed);
783 for sn in [None, Some(true), Some(false)] {
784 let (i, res, _) =
785 solve_ok(&aug, form, Regularization::default(), sn, &rhs(42, seed));
786 assert_eq!(i, form.expected_inertia(42));
787 assert!(le(res, 1e-10, 0.0), "aug seed {seed} sn {sn:?}: {res}");
788 let (i, res, _) = solve_ok(
789 &normal,
790 KktForm::NormalEquations,
791 Regularization::default(),
792 sn,
793 &rhs(12, seed),
794 );
795 assert_eq!(i, KktForm::NormalEquations.expected_inertia(12));
796 assert!(le(res, 1e-10, 0.0), "normal seed {seed} sn {sn:?}: {res}");
797 }
798 }
799 }
800
801 #[test]
802 fn unregularized_augmented_system_takes_the_bunch_kaufman_path() {
803 let (aug, _, form) = kkt(10, 25, 2.0, 9);
804 let s = FaerLdl {
805 supernodal: Some(true),
806 };
807 let sym = s.analyze(&aug.pattern(), form, &Natural).unwrap();
808 let (num, inertia) = s.factorize(&sym, &aug, Regularization::none()).unwrap();
809 assert!(num.is_pivoted());
810 assert_eq!(inertia, form.expected_inertia(35));
811 let b = rhs(35, 9);
812 let mut x = vec![0.0; 35];
813 let rep = s.solve_refined(&num, &aug, &b, &mut x, Refinement::default());
814 assert!(rep.converged, "{rep:?}");
815 }
816
817 #[test]
818 fn s6_errors_zero_pivot_and_wrong_inertia() {
819 let k = CscMatrix::from_triplets(2, 2, &[(Row(0), Col(0), -1.0)]).unwrap();
821 let form = KktForm::Augmented {
822 n_primal: 1,
823 n_dual: 1,
824 };
825 let s = FaerLdl {
826 supernodal: Some(false),
827 };
828 let sym = s.analyze(&k.pattern(), form, &Natural).unwrap();
829 assert_eq!(
830 s.factorize(&sym, &k, Regularization::none()).err(),
831 Some(KktError::ZeroPivot { position: 1 })
832 );
833 let sn = FaerLdl {
835 supernodal: Some(true),
836 };
837 let sym = sn.analyze(&k.pattern(), form, &Natural).unwrap();
838 assert!(matches!(
839 sn.factorize(&sym, &k, Regularization::none()),
840 Err(KktError::WrongInertia { .. })
841 ));
842 let (_, inertia) = s
844 .factorize(
845 &s.analyze(&k.pattern(), form, &Natural).unwrap(),
846 &k,
847 Regularization::default(),
848 )
849 .unwrap();
850 assert_eq!(inertia, form.expected_inertia(2));
851 let ind = CscMatrix::from_triplets(
854 2,
855 2,
856 &[
857 (Row(0), Col(0), 1.0),
858 (Row(1), Col(0), 2.0),
859 (Row(1), Col(1), 1.0),
860 ],
861 )
862 .unwrap();
863 let sym = s
864 .analyze(&ind.pattern(), KktForm::NormalEquations, &Natural)
865 .unwrap();
866 assert_eq!(
867 s.factorize(&sym, &ind, Regularization::none()).err(),
868 Some(KktError::WrongInertia {
869 found: Inertia {
870 positive: 1,
871 negative: 1,
872 zero: 0
873 },
874 expected: Inertia {
875 positive: 2,
876 negative: 0,
877 zero: 0
878 }
879 })
880 );
881 let reg = Regularization {
882 primal: 0.0,
883 dual: 0.0,
884 pivot_tol: Some(1e-13),
885 bump: 1e-8,
886 scale: ShiftScale::Equilibrated,
887 };
888 let (num, inertia) = s.factorize(&sym, &ind, reg).unwrap();
889 assert_eq!(inertia.positive, 2);
890 assert_eq!(num.bumped(), 1);
891 let (aug, _, form) = kkt(3, 5, 0.0, 4);
893 let sym = s.analyze(&aug.pattern(), form, &Natural).unwrap();
894 let (num, _) = s.factorize(&sym, &aug, Regularization::default()).unwrap();
895 assert_eq!(num.bumped(), 0);
896 }
897
898 #[test]
899 fn s10_overflow_is_an_error_and_non_finite_input_is_refused() {
900 let t = |v: f64| {
902 CscMatrix::from_triplets(
903 3,
904 3,
905 &[
906 (Row(0), Col(0), v),
907 (Row(1), Col(0), 1.0),
908 (Row(2), Col(0), 1.0),
909 (Row(1), Col(1), 1.0),
910 (Row(2), Col(1), 1.0),
911 (Row(2), Col(2), 1.0),
912 ],
913 )
914 .unwrap()
915 };
916 let k = t(1e-310);
917 let reg = Regularization {
918 primal: 0.0,
919 dual: 0.0,
920 pivot_tol: Some(1e-300),
921 bump: 1e-300,
922 scale: ShiftScale::Equilibrated,
923 };
924 for sn in [Some(false), Some(true)] {
925 let s = FaerLdl { supernodal: sn };
926 let sym = s
927 .analyze(&k.pattern(), KktForm::NormalEquations, &Natural)
928 .unwrap();
929 assert!(
930 matches!(
931 s.factorize(&sym, &k, reg),
932 Err(KktError::NotFinite { .. } | KktError::ZeroPivot { .. })
933 ),
934 "{sn:?}"
935 );
936 let mut nan = t(1.0);
938 nan.val[0] = f64::NAN;
939 assert!(matches!(
940 s.factorize(&sym, &nan, Regularization::default()),
941 Err(KktError::Structure(_))
942 ));
943 }
944 let s = FaerLdl {
945 supernodal: Some(false),
946 };
947 let sym = s
948 .analyze(&k.pattern(), KktForm::NormalEquations, &Natural)
949 .unwrap();
950 assert!(matches!(
951 s.factorize(&sym, &k, reg),
952 Err(KktError::NotFinite { position: 1 | 2 })
953 ));
954 }
955
956 #[test]
957 fn gmres_refinement_removes_the_shift_on_a_nearly_singular_system() {
958 let a = [
962 [1.0, 2.0, 0.0, 1.0],
963 [1.0, 2.0, 1e-6, 1.0],
964 [0.0, 1.0, 1.0, 3.0],
965 ];
966 let mut t = Vec::new();
967 for i in 0..3 {
968 for p in 0..=i {
969 let v: f64 = (0..4).map(|j| a[i][j] * a[p][j]).sum();
970 t.push((Row::from_usize(i), Col::from_usize(p), v));
971 }
972 }
973 let k = CscMatrix::from_triplets(3, 3, &t).unwrap();
974 let s = FaerLdl::default();
975 let sym = s
976 .analyze(&k.pattern(), KktForm::NormalEquations, &Amd::default())
977 .unwrap();
978 let (num, _) = s.factorize(&sym, &k, Regularization::default()).unwrap();
979 let xs = [1.0, -1.0, 0.5];
980 let mut b = vec![0.0; 3];
981 crate::kkt::sym_lower_matvec(&k, &xs, &mut b);
982 let mut x = vec![0.0; 3];
983 let first = s.solve_refined(
984 &num,
985 &k,
986 &b,
987 &mut x,
988 Refinement {
989 max_iters: 0,
990 target_residual: 0.0,
991 },
992 );
993 let rep = s.solve_refined(&num, &k, &b, &mut x, Refinement::default());
994 assert!(rep.converged && rep.iters <= 4, "{first:?} → {rep:?}");
995 assert!(le(rep.residual, first.residual, 0.0));
996 }
997
998 #[test]
999 fn bumps_are_counted_with_both_signs_and_both_structures() {
1000 let k = CscMatrix::from_triplets(2, 2, &[(Row(1), Col(0), 1.0)]).unwrap();
1004 let form = KktForm::Augmented {
1005 n_primal: 1,
1006 n_dual: 1,
1007 };
1008 let reg = Regularization {
1009 primal: 0.0,
1010 dual: 0.0,
1011 pivot_tol: Some(1e-13),
1012 bump: 1e-8,
1013 scale: ShiftScale::Equilibrated,
1014 };
1015 let z = CscMatrix::from_triplets(
1017 3,
1018 3,
1019 &[
1020 (Row(0), Col(0), 1.0),
1021 (Row(2), Col(0), 1.0),
1022 (Row(2), Col(2), 1.0),
1023 ],
1024 )
1025 .unwrap();
1026 for sn in [Some(false), Some(true)] {
1027 let s = FaerLdl { supernodal: sn };
1028 let sym = s.analyze(&k.pattern(), form, &Natural).unwrap();
1029 let (num, inertia) = s.factorize(&sym, &k, reg).unwrap();
1030 assert_eq!(inertia, form.expected_inertia(2));
1031 assert_eq!(num.bumped(), 1, "{sn:?}");
1032 let sym = s
1033 .analyze(&z.pattern(), KktForm::NormalEquations, &Natural)
1034 .unwrap();
1035 let (num, _) = s.factorize(&sym, &z, reg).unwrap();
1036 assert_eq!(num.bumped(), 2, "{sn:?}"); }
1038 }
1039
1040 #[test]
1041 fn structural_errors() {
1042 let s = FaerLdl::default();
1043 let skew = CscMatrix::from_triplets(
1044 2,
1045 2,
1046 &[
1047 (Row(1), Col(0), 1.0),
1048 (Row(1), Col(1), 1.0),
1049 (Row(0), Col(0), 1.0),
1050 (Row(0), Col(1), 3.0),
1051 ],
1052 )
1053 .unwrap();
1054 assert!(
1056 s.analyze(&skew.pattern(), KktForm::NormalEquations, &Amd::default())
1057 .is_ok()
1058 );
1059 let up = CscMatrix::from_triplets(2, 2, &[(Row(0), Col(1), 1.0)]).unwrap();
1060 assert_eq!(
1061 s.analyze(&up.pattern(), KktForm::NormalEquations, &Amd::default())
1062 .err(),
1063 Some(KktError::NotSymmetric)
1064 );
1065 let sym = s
1066 .analyze(
1067 &CscMatrix::<f64>::zeros(2, 2).pattern(),
1068 KktForm::NormalEquations,
1069 &Amd::default(),
1070 )
1071 .unwrap();
1072 let k = CscMatrix::from_triplets(2, 2, &[(Row(1), Col(0), 1.0), (Row(0), Col(0), 1.0)])
1073 .unwrap();
1074 assert!(matches!(
1075 s.factorize(&sym, &k, Regularization::default()),
1076 Err(KktError::Structure(_))
1077 ));
1078 let k3 = CscMatrix::<f64>::zeros(3, 3);
1079 assert!(matches!(
1080 s.factorize(&sym, &k3, Regularization::default()),
1081 Err(KktError::Structure(_))
1082 ));
1083 let bad = Regularization {
1084 pivot_tol: Some(1e-13),
1085 bump: 0.0,
1086 ..Regularization::default()
1087 };
1088 assert!(matches!(
1089 s.factorize(&sym, &CscMatrix::zeros(2, 2), bad),
1090 Err(KktError::Structure(_))
1091 ));
1092 let (aug, _, form) = kkt(4, 8, 1.0, 5);
1095 let sym = s.analyze(&aug.pattern(), form, &Amd::default()).unwrap();
1096 let mut thin = aug.clone();
1097 thin.row_idx.remove(1);
1098 thin.val.remove(1);
1099 for p in &mut thin.col_ptr[1..] {
1100 *p -= 1;
1101 }
1102 assert!(s.factorize(&sym, &thin, Regularization::default()).is_ok());
1103 }
1104
1105 #[test]
1106 fn s7_refinement_keeps_the_best_iterate() {
1107 let (aug, _, form) = kkt(8, 20, 4.0, 77);
1108 let s = FaerLdl::default();
1109 let sym = s.analyze(&aug.pattern(), form, &Amd::default()).unwrap();
1110 let b = rhs(28, 77);
1111 let far = Regularization {
1114 primal: 10.0,
1115 dual: 10.0,
1116 pivot_tol: None,
1117 bump: 0.0,
1118 scale: ShiftScale::Absolute,
1119 };
1120 let (num, _) = s.factorize(&sym, &aug, far).unwrap();
1121 let mut x = vec![0.0; 28];
1122 let none = s.solve_refined(
1123 &num,
1124 &aug,
1125 &b,
1126 &mut x,
1127 Refinement {
1128 max_iters: 0,
1129 target_residual: 0.0,
1130 },
1131 );
1132 let rep = s.solve_refined(
1133 &num,
1134 &aug,
1135 &b,
1136 &mut x,
1137 Refinement {
1138 max_iters: 50,
1139 target_residual: 0.0,
1140 },
1141 );
1142 assert!(le(rep.residual, none.residual, 0.0));
1143 assert_eq!(none.iters, 0);
1144 assert!(!rep.converged);
1145 }
1146
1147 proptest! {
1148 #![proptest_config(ProptestConfig::with_cases(24))]
1149 #[test]
1152 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>()) {
1153 let n = m + extra;
1154 let spread = [0.0, 4.0, 9.0][regime];
1155 let supernodal = [None, Some(true), Some(false)][sn];
1156 let (aug, normal, form) = kkt(m, n, spread, seed);
1157 let (i, res, _) = solve_ok(&aug, form, Regularization::default(), supernodal, &rhs(n + m, seed));
1158 prop_assert_eq!(i, form.expected_inertia(n + m));
1159 prop_assert!(le(res, 1e-10, 0.0), "augmented: {}", res);
1160 let (i, _, _) = solve_ok(&normal, KktForm::NormalEquations, Regularization::default(), supernodal, &rhs(m, seed));
1161 prop_assert_eq!(i, KktForm::NormalEquations.expected_inertia(m));
1162 }
1163 }
1164}