use crate::linalg::amd::{amd_with_deadline, inv_permute_vec, permute_sym_upper, permute_vec};
use crate::sparse::CscMatrix;
use faer::dyn_stack::{MemBuffer, MemStack, StackReq};
use faer::linalg::cholesky::ldlt::factor::{LdltError, LdltRegularization};
use faer::reborrow::*;
use faer::sparse::linalg::cholesky::{
factorize_symbolic_cholesky, CholeskySymbolicParams, LdltRef, SymbolicCholesky,
SymbolicCholeskyRaw, SymmetricOrdering, simplicial, supernodal,
};
use faer::sparse::linalg::SupernodalThreshold;
use faer::sparse::{SparseColMat, SymbolicSparseColMat};
#[cfg(test)]
use faer::sparse::Triplet;
use std::sync::Arc;
use std::time::Instant;
#[non_exhaustive]
#[derive(Debug)]
pub enum LdlError {
SingularOrIndefinite,
DeadlineExceeded,
WouldExceedBudget {
l_nnz: usize,
max_l_nnz: usize,
},
}
const SHIFT_FACTOR: f64 = 10.0;
const DEFAULT_PAR: faer::Par = faer::Par::Seq;
const LDLT_REG_DELTA: f64 = 1e-8;
const LDLT_REG_EPSILON: f64 = 1e-13;
pub struct LdlFactorization {
symbolic: Arc<SymbolicCholesky<usize>>,
l_values: Vec<f64>,
n: usize,
par: faer::Par,
}
impl LdlFactorization {
pub fn solve(&self, rhs: &[f64], sol: &mut [f64]) {
sol.copy_from_slice(rhs);
let mut mem = MemBuffer::new(
self.symbolic.solve_in_place_scratch::<f64>(1, self.par),
);
let stack = MemStack::new(&mut mem);
let ldlt = LdltRef::<'_, usize, f64>::new(&self.symbolic, &self.l_values);
let mut sol_mat =
faer::MatMut::from_column_major_slice_mut(sol, self.n, 1);
ldlt.solve_in_place_with_conj(
faer::Conj::No,
sol_mat.rb_mut(),
self.par,
stack,
);
}
}
pub struct LdlFactorizationAmd {
symbolic: Arc<SymbolicCholesky<usize>>,
l_values: Vec<f64>,
perm: Vec<usize>,
n: usize,
par: faer::Par,
}
impl LdlFactorizationAmd {
pub fn nnz_l(&self) -> usize {
self.symbolic.len_val()
}
pub fn solve(&self, rhs: &[f64], sol: &mut [f64]) {
let n = self.n;
let b_p = permute_vec(rhs, &self.perm);
let mut x_p = b_p;
let mut mem = MemBuffer::new(
self.symbolic.solve_in_place_scratch::<f64>(1, self.par),
);
let stack = MemStack::new(&mut mem);
let ldlt = LdltRef::<'_, usize, f64>::new(&self.symbolic, &self.l_values);
let mut sol_mat =
faer::MatMut::from_column_major_slice_mut(&mut x_p, n, 1);
ldlt.solve_in_place_with_conj(
faer::Conj::No,
sol_mat.rb_mut(),
self.par,
stack,
);
let x = inv_permute_vec(&x_p, &self.perm);
sol.copy_from_slice(&x);
}
}
fn csc_upper_to_faer_upper(mat: &CscMatrix) -> SparseColMat<usize, f64> {
let n = mat.nrows;
let symbolic = SymbolicSparseColMat::new_checked(
n,
n,
mat.col_ptr.clone(),
None,
mat.row_ind.clone(),
);
SparseColMat::new(symbolic, mat.values.clone())
}
pub(crate) fn extract_diagonal_signs(
n: usize,
col_ptr: &[usize],
row_ind: &[usize],
values: &[f64],
) -> Vec<i8> {
let mut signs = vec![1i8; n];
for (j, sign) in signs.iter_mut().enumerate() {
for k in col_ptr[j]..col_ptr[j + 1] {
if row_ind[k] == j {
if values[k] < 0.0 {
*sign = -1;
}
break;
}
}
}
signs
}
fn build_symbolic_hl(
a_upper: &SparseColMat<usize, f64>,
) -> Result<SymbolicCholesky<usize>, LdlError> {
factorize_symbolic_cholesky(
a_upper.symbolic(),
faer::Side::Upper,
SymmetricOrdering::Identity,
CholeskySymbolicParams {
supernodal_flop_ratio_threshold: SupernodalThreshold::AUTO,
..Default::default()
},
)
.map_err(|_| LdlError::SingularOrIndefinite)
}
fn do_numeric_factorize(
mat: &CscMatrix,
signs: Option<&[i8]>,
deadline: Option<Instant>,
max_l_nnz: Option<usize>,
par: faer::Par,
) -> Result<(Arc<SymbolicCholesky<usize>>, Vec<f64>), LdlError> {
do_numeric_factorize_with_cache(mat, signs, deadline, max_l_nnz, None, par)
}
fn do_numeric_factorize_with_cache(
mat: &CscMatrix,
signs: Option<&[i8]>,
deadline: Option<Instant>,
max_l_nnz: Option<usize>,
cached_symbolic: Option<Arc<SymbolicCholesky<usize>>>,
par: faer::Par,
) -> Result<(Arc<SymbolicCholesky<usize>>, Vec<f64>), LdlError> {
let a_upper = csc_upper_to_faer_upper(mat);
let symbolic: Arc<SymbolicCholesky<usize>> = match cached_symbolic {
Some(s) => s,
None => Arc::new(build_symbolic_hl(&a_upper)?),
};
let l_nnz = symbolic.len_val();
if let Some(max) = max_l_nnz {
if l_nnz > max {
return Err(LdlError::WouldExceedBudget { l_nnz, max_l_nnz: max });
}
}
if let Some(d) = deadline {
if Instant::now() >= d {
return Err(LdlError::DeadlineExceeded);
}
}
let regularization = LdltRegularization {
dynamic_regularization_signs: signs,
dynamic_regularization_delta: LDLT_REG_DELTA,
dynamic_regularization_epsilon: LDLT_REG_EPSILON,
};
let mut l_values = vec![0.0f64; l_nnz];
let mut mem = MemBuffer::new(StackReq::any_of(&[
symbolic.factorize_numeric_ldlt_scratch::<f64>(par, Default::default()),
symbolic.solve_in_place_scratch::<f64>(1, par),
]));
let stack = MemStack::new(&mut mem);
symbolic
.factorize_numeric_ldlt(
&mut l_values,
a_upper.rb(),
faer::Side::Upper,
regularization,
par,
stack,
Default::default(),
)
.map_err(|_| LdlError::SingularOrIndefinite)?;
Ok((symbolic, l_values))
}
fn q_to_upper_triangular(q: &CscMatrix) -> CscMatrix {
let n = q.nrows;
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f64> = Vec::new();
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
if row <= col {
rows.push(row);
cols.push(col);
vals.push(q.values[k]);
}
}
}
if rows.is_empty() {
CscMatrix::new(n, n)
} else {
CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap()
}
}
fn upper_tri_with_diag_shift(q_upper: &CscMatrix, shift: f64) -> CscMatrix {
let n = q_upper.nrows;
let mut rows: Vec<usize> = Vec::with_capacity(q_upper.values.len() + n);
let mut cols: Vec<usize> = Vec::with_capacity(q_upper.values.len() + n);
let mut vals: Vec<f64> = Vec::with_capacity(q_upper.values.len() + n);
for col in 0..n {
for k in q_upper.col_ptr[col]..q_upper.col_ptr[col + 1] {
rows.push(q_upper.row_ind[k]);
cols.push(col);
vals.push(q_upper.values[k]);
}
rows.push(col);
cols.push(col);
vals.push(shift);
}
CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap()
}
pub fn is_q_psd_by_cholesky(q: &CscMatrix) -> bool {
is_q_psd_by_cholesky_impl(q, SHIFT_FACTOR, true)
}
#[cfg(test)]
pub(crate) fn is_q_psd_by_cholesky_probe(
q: &CscMatrix,
shift_factor: f64,
zeropivot_conservative: bool,
) -> bool {
is_q_psd_by_cholesky_impl(q, shift_factor, zeropivot_conservative)
}
fn is_q_psd_by_cholesky_impl(
q: &CscMatrix,
shift_factor: f64,
zeropivot_conservative: bool,
) -> bool {
let n = q.nrows;
if n == 0 {
return true;
}
if q.values.iter().all(|&v| v == 0.0) {
return true;
}
let mut all_diag_nonneg = true;
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
if q.row_ind[k] == col && q.values[k] < 0.0 {
all_diag_nonneg = false;
break;
}
}
if !all_diag_nonneg {
break;
}
}
if !all_diag_nonneg {
return false;
}
let q_upper = q_to_upper_triangular(q);
let q_abs_max = q.values.iter().fold(0.0_f64, |a, &v| a.max(v.abs()));
let fp_threshold = q_abs_max * (n as f64) * f64::EPSILON;
let shift = (shift_factor * fp_threshold).max(shift_factor * f64::EPSILON);
let q_shifted = upper_tri_with_diag_shift(&q_upper, shift);
let a_upper = csc_upper_to_faer_upper(&q_shifted);
let symbolic = match build_symbolic_hl(&a_upper) {
Ok(s) => s,
Err(_) => return false,
};
let reg: LdltRegularization<f64> = LdltRegularization::default();
let l_nnz = symbolic.len_val();
let mut l_values = vec![0.0f64; l_nnz];
match symbolic.raw() {
SymbolicCholeskyRaw::Simplicial(simp_sym) => {
let scratch_ldlt = simplicial::factorize_simplicial_numeric_ldlt_scratch::<usize, f64>(n);
let scratch_solve = simp_sym.solve_in_place_scratch::<f64>(1);
let mut mem = MemBuffer::new(StackReq::any_of(&[scratch_ldlt, scratch_solve]));
let stack = MemStack::new(&mut mem);
let result = simplicial::factorize_simplicial_numeric_ldlt::<usize, f64>(
&mut l_values,
a_upper.rb(),
reg,
simp_sym,
stack,
);
let col_ptr = simp_sym.col_ptr();
match result {
Err(LdltError::ZeroPivot { .. }) => !zeropivot_conservative,
Ok(_) => !(0..n).any(|j| l_values[col_ptr[j]] < -fp_threshold),
}
}
SymbolicCholeskyRaw::Supernodal(sn_sym) => {
let scratch = symbolic.factorize_numeric_ldlt_scratch::<f64>(faer::Par::Seq, Default::default());
let mut mem = MemBuffer::new(scratch);
let stack = MemStack::new(&mut mem);
let result = symbolic.factorize_numeric_ldlt(
&mut l_values,
a_upper.rb(),
faer::Side::Upper,
reg,
faer::Par::Seq,
stack,
Default::default(),
);
if matches!(result, Err(LdltError::ZeroPivot { .. })) {
return !zeropivot_conservative;
}
let ldlt_ref = supernodal::SupernodalLdltRef::<usize, f64>::new(sn_sym, &l_values);
let n_sn = sn_sym.n_supernodes();
let sn_begin = sn_sym.supernode_begin();
let sn_end = sn_sym.supernode_end();
let has_negative_d = (0..n_sn).any(|s| {
let s_start = sn_begin[s];
let s_end = sn_end[s];
let s_ncols = s_end - s_start;
let node = ldlt_ref.supernode(s);
let ls = node.val();
(0..s_ncols).any(|j| ls[(j, j)] < -fp_threshold)
});
!has_negative_d
}
}
}
pub fn factorize(mat: &CscMatrix) -> Result<LdlFactorization, LdlError> {
factorize_with_par(mat, DEFAULT_PAR)
}
pub fn factorize_budget(mat: &CscMatrix, max_l_nnz: usize) -> Result<LdlFactorization, LdlError> {
let n = mat.nrows;
let (symbolic, l_values) =
do_numeric_factorize(mat, None, None, Some(max_l_nnz), DEFAULT_PAR)?;
Ok(LdlFactorization { symbolic, l_values, n, par: DEFAULT_PAR })
}
pub fn factorize_with_par(
mat: &CscMatrix,
par: faer::Par,
) -> Result<LdlFactorization, LdlError> {
let n = mat.nrows;
let (symbolic, l_values) = do_numeric_factorize(mat, None, None, None, par)?;
Ok(LdlFactorization { symbolic, l_values, n, par })
}
pub fn factorize_quasidefinite_with_amd(
mat: &CscMatrix,
deadline: Option<Instant>,
) -> Result<LdlFactorizationAmd, LdlError> {
factorize_quasidefinite_with_amd_budget_par(mat, deadline, None, DEFAULT_PAR)
}
pub fn factorize_quasidefinite_with_amd_budget_par(
mat: &CscMatrix,
deadline: Option<Instant>,
max_l_nnz: Option<usize>,
par: faer::Par,
) -> Result<LdlFactorizationAmd, LdlError> {
let n = mat.nrows;
let perm = amd_with_deadline(n, &mat.col_ptr, &mat.row_ind, deadline);
if let Some(d) = deadline {
if Instant::now() >= d {
return Err(LdlError::DeadlineExceeded);
}
}
let _ = n;
factorize_quasidefinite_with_cached_perm_budget_par(mat, &perm, deadline, max_l_nnz, par)
}
pub fn factorize_quasidefinite_with_cached_perm_budget_par(
mat: &CscMatrix,
perm: &[usize],
deadline: Option<Instant>,
max_l_nnz: Option<usize>,
par: faer::Par,
) -> Result<LdlFactorizationAmd, LdlError> {
if let Some(d) = deadline {
if Instant::now() >= d {
return Err(LdlError::DeadlineExceeded);
}
}
let n = mat.nrows;
let (new_col_ptr, new_row_ind, new_values) =
permute_sym_upper(n, &mat.col_ptr, &mat.row_ind, &mat.values, perm);
let perm_mat = CscMatrix {
col_ptr: new_col_ptr,
row_ind: new_row_ind,
values: new_values,
nrows: n,
ncols: n,
};
let signs = extract_diagonal_signs(n, &perm_mat.col_ptr, &perm_mat.row_ind, &perm_mat.values);
let (symbolic, l_values) =
do_numeric_factorize(&perm_mat, Some(&signs), deadline, max_l_nnz, par)?;
Ok(LdlFactorizationAmd { symbolic, l_values, perm: perm.to_vec(), n, par })
}
pub fn factorize_quasidefinite_pre_permuted_cached_par(
pre_permuted_mat: &CscMatrix,
perm: &[usize],
deadline: Option<Instant>,
max_l_nnz: Option<usize>,
cached_symbolic: Option<Arc<SymbolicCholesky<usize>>>,
par: faer::Par,
) -> Result<LdlFactorizationAmd, LdlError> {
if let Some(d) = deadline {
if Instant::now() >= d {
return Err(LdlError::DeadlineExceeded);
}
}
let n = pre_permuted_mat.nrows;
let signs = extract_diagonal_signs(
n,
&pre_permuted_mat.col_ptr,
&pre_permuted_mat.row_ind,
&pre_permuted_mat.values,
);
let (symbolic, l_values) = do_numeric_factorize_with_cache(
pre_permuted_mat, Some(&signs), deadline, max_l_nnz, cached_symbolic, par,
)?;
Ok(LdlFactorizationAmd { symbolic, l_values, perm: perm.to_vec(), n, par })
}
impl LdlFactorizationAmd {
pub fn symbolic_arc(&self) -> Arc<SymbolicCholesky<usize>> {
Arc::clone(&self.symbolic)
}
}
#[cfg(test)]
#[allow(clippy::print_stdout, clippy::print_stderr)]
mod tests {
use super::*;
use crate::sparse::CscMatrix;
use faer::sparse::linalg::cholesky::{simplicial, supernodal};
fn upper_tri_csc(n: usize, entries: &[(usize, usize, f64)]) -> CscMatrix {
let mut cols: Vec<Vec<(usize, f64)>> = vec![vec![]; n];
for &(row, col, val) in entries {
assert!(row <= col, "upper triangle only: row={row} col={col}");
cols[col].push((row, val));
}
for c in cols.iter_mut() {
c.sort_by_key(|&(r, _)| r);
}
let nnz: usize = cols.iter().map(|c| c.len()).sum();
let mut col_ptr = vec![0usize; n + 1];
for j in 0..n {
col_ptr[j + 1] = col_ptr[j] + cols[j].len();
}
let mut row_ind = vec![0usize; nnz];
let mut values = vec![0.0f64; nnz];
for j in 0..n {
let start = col_ptr[j];
for (idx, &(row, val)) in cols[j].iter().enumerate() {
row_ind[start + idx] = row;
values[start + idx] = val;
}
}
CscMatrix { col_ptr, row_ind, values, nrows: n, ncols: n }
}
#[test]
fn test_factorize_pd_3x3_solve() {
let mat = upper_tri_csc(3, &[
(0, 0, 4.0), (0, 1, 1.0),
(1, 1, 3.0), (1, 2, 2.0),
(2, 2, 5.0),
]);
let fac = factorize(&mat).expect("factorize failed");
let b = [1.0f64, 2.0, 3.0];
let mut x = [0.0f64; 3];
fac.solve(&b, &mut x);
let ax0 = 4.0 * x[0] + 1.0 * x[1];
let ax1 = 1.0 * x[0] + 3.0 * x[1] + 2.0 * x[2];
let ax2 = 2.0 * x[1] + 5.0 * x[2];
let eps = 1e-8;
assert!((ax0 - b[0]).abs() < eps, "r[0]={}", (ax0 - b[0]).abs());
assert!((ax1 - b[1]).abs() < eps, "r[1]={}", (ax1 - b[1]).abs());
assert!((ax2 - b[2]).abs() < eps, "r[2]={}", (ax2 - b[2]).abs());
}
#[test]
fn test_factorize_pd_identity() {
let n = 4;
let entries: Vec<(usize, usize, f64)> = (0..n).map(|i| (i, i, 1.0)).collect();
let mat = upper_tri_csc(n, &entries);
let fac = factorize(&mat).expect("factorize failed");
let b = vec![1.0, 2.0, 3.0, 4.0];
let mut x = vec![0.0f64; n];
fac.solve(&b, &mut x);
for i in 0..n {
assert!((x[i] - b[i]).abs() < 1e-10, "x[{i}]={}", x[i]);
}
}
#[test]
fn factorize_budget_rejects_when_l_nnz_exceeds_max() {
let mat = upper_tri_csc(3, &[
(0, 0, 4.0), (0, 1, 1.0),
(1, 1, 3.0), (1, 2, 2.0),
(2, 2, 5.0),
]);
match factorize_budget(&mat, 1) {
Err(LdlError::WouldExceedBudget { l_nnz, max_l_nnz }) => {
assert!(l_nnz > max_l_nnz, "l_nnz={l_nnz} should exceed max={max_l_nnz}");
assert_eq!(max_l_nnz, 1);
}
Err(e) => panic!("expected WouldExceedBudget, got Err({e:?})"),
Ok(_) => panic!("expected WouldExceedBudget, got Ok"),
}
}
#[test]
fn factorize_budget_accepts_within_budget() {
let mat = upper_tri_csc(3, &[
(0, 0, 4.0), (0, 1, 1.0),
(1, 1, 3.0), (1, 2, 2.0),
(2, 2, 5.0),
]);
let fac = factorize_budget(&mat, 1_000_000).expect("within budget must succeed");
let b = [1.0f64, 2.0, 3.0];
let mut x = [0.0f64; 3];
fac.solve(&b, &mut x);
let ax0 = 4.0 * x[0] + 1.0 * x[1];
let ax1 = 1.0 * x[0] + 3.0 * x[1] + 2.0 * x[2];
let ax2 = 2.0 * x[1] + 5.0 * x[2];
assert!((ax0 - b[0]).abs() < 1e-8);
assert!((ax1 - b[1]).abs() < 1e-8);
assert!((ax2 - b[2]).abs() < 1e-8);
}
#[test]
fn test_factorize_with_deadline_ok() {
let mat = upper_tri_csc(2, &[(0, 0, 2.0), (0, 1, 1.0), (1, 1, 3.0)]);
let deadline = Some(Instant::now() + std::time::Duration::from_secs(60));
let fac = factorize_quasidefinite_with_amd(&mat, deadline).expect("should succeed");
let b = [1.0f64, 0.0];
let mut x = [0.0f64; 2];
fac.solve(&b, &mut x);
let ax0 = 2.0 * x[0] + 1.0 * x[1];
let ax1 = 1.0 * x[0] + 3.0 * x[1];
assert!((ax0 - b[0]).abs() < 1e-10, "r[0]={}", (ax0 - b[0]).abs());
assert!((ax1 - b[1]).abs() < 1e-10, "r[1]={}", (ax1 - b[1]).abs());
}
#[test]
fn test_factorize_with_deadline_expired() {
let mat = upper_tri_csc(2, &[(0, 0, 2.0), (1, 1, 3.0)]);
let perm = vec![0usize, 1];
let deadline = Some(Instant::now() - std::time::Duration::from_millis(1));
let result = factorize_quasidefinite_with_cached_perm_budget_par(
&mat, &perm, deadline, None, faer::Par::Seq,
);
assert!(
matches!(result, Err(LdlError::DeadlineExceeded)),
"Expected DeadlineExceeded"
);
}
#[test]
fn test_quasidefinite_2x2_identity_perm() {
let mat = upper_tri_csc(2, &[(0, 0, 3.0), (0, 1, 1.0), (1, 1, -2.0)]);
let perm = vec![0usize, 1]; let fac = factorize_quasidefinite_with_cached_perm_budget_par(
&mat, &perm, None, None, faer::Par::Seq,
).expect("quasidefinite factorize failed");
let b = [1.0f64, 2.0];
let mut x = [0.0f64; 2];
fac.solve(&b, &mut x);
let ax0 = 3.0 * x[0] + 1.0 * x[1];
let ax1 = 1.0 * x[0] - 2.0 * x[1];
let eps = 1e-8;
assert!((ax0 - b[0]).abs() < eps, "r[0]={}", (ax0 - b[0]).abs());
assert!((ax1 - b[1]).abs() < eps, "r[1]={}", (ax1 - b[1]).abs());
}
#[test]
fn test_quasidefinite_with_amd() {
let delta = 1e-4f64;
let mat = upper_tri_csc(5, &[
(0, 0, 1.0 + delta), (1, 1, 2.0 + delta),
(2, 2, -delta), (3, 3, -delta), (4, 4, -delta),
(0, 2, 1.0), (1, 3, 1.0), (0, 4, 1.0), (1, 4, 1.0),
]);
let fac = factorize_quasidefinite_with_amd(&mat, None)
.expect("quasidefinite_with_amd failed");
let b = [1.0f64, 2.0, 0.5, -0.5, 1.0];
let mut x = [0.0f64; 5];
fac.solve(&b, &mut x);
let full: &[(usize, usize, f64)] = &[
(0, 0, 1.0 + delta), (1, 1, 2.0 + delta),
(2, 2, -delta), (3, 3, -delta), (4, 4, -delta),
(0, 2, 1.0), (2, 0, 1.0),
(1, 3, 1.0), (3, 1, 1.0),
(0, 4, 1.0), (4, 0, 1.0),
(1, 4, 1.0), (4, 1, 1.0),
];
let mut r = [0.0f64; 5];
for &(row, col, val) in full {
r[row] += val * x[col];
}
let res: f64 = r.iter().zip(b.iter()).map(|(&ri, &bi)| (ri - bi).powi(2)).sum::<f64>().sqrt();
assert!(res < 1e-8, "residual={res:.3e}");
}
#[test]
fn test_is_q_psd_psd_matrix() {
let q = upper_tri_csc(2, &[(0, 0, 4.0), (0, 1, 1.0), (1, 1, 3.0)]);
assert!(is_q_psd_by_cholesky(&q), "PD matrix should be identified as PSD");
}
#[test]
fn test_is_q_psd_indefinite_matrix() {
let q = upper_tri_csc(2, &[(0, 0, 1.0), (1, 1, -1.0)]);
assert!(!is_q_psd_by_cholesky(&q), "Indefinite matrix should NOT be identified as PSD");
}
#[test]
fn test_is_q_psd_large_offdiag_still_psd() {
let q = upper_tri_csc(2, &[(0, 0, 1.0), (0, 1, 1.1), (1, 1, 2.0)]);
assert!(is_q_psd_by_cholesky(&q),
"PSD matrix with large off-diagonal (Gershgorin false alarm) should be true");
}
#[test]
fn test_is_q_psd_singular_psd() {
let q = upper_tri_csc(2, &[(0, 0, 1.0), (0, 1, 1.0), (1, 1, 1.0)]);
assert!(is_q_psd_by_cholesky(&q), "Singular PSD matrix should be identified as PSD");
}
#[test]
fn test_is_q_psd_offdiag_indefinite() {
let q = upper_tri_csc(2, &[(0, 0, 2.0), (0, 1, 3.0), (1, 1, 2.0)]);
assert!(!is_q_psd_by_cholesky(&q), "Indefinite matrix with off-diag should be false");
}
#[test]
fn test_is_q_psd_zero_matrix() {
let q = upper_tri_csc(3, &[]);
assert!(is_q_psd_by_cholesky(&q), "Zero matrix (LP) should be identified as PSD");
}
#[test]
fn test_is_q_psd_empty_matrix() {
let q = CscMatrix::new(0, 0);
assert!(is_q_psd_by_cholesky(&q), "Empty matrix should be identified as PSD");
}
#[test]
fn test_is_q_psd_zeropivot_masks_negative_d() {
let q = upper_tri_csc(4, &[
(0, 0, 2.0),
(0, 1, 1.0),
(0, 2, -1.0),
]);
assert!(!is_q_psd_by_cholesky(&q),
"Indefinite matrix (ZeroPivot masks earlier negative D) must return false");
}
#[test]
fn test_is_q_psd_zeropivot_all_nonneg_d_is_psd() {
let q = upper_tri_csc(3, &[
(0, 0, 1.0),
(1, 1, 1.0),
]);
assert!(is_q_psd_by_cholesky(&q),
"PSD matrix with zero eigenvalue (ZeroPivot) must return true");
}
#[test]
fn no_op_proof_shift_required_for_rank1_psd() {
let q = upper_tri_csc(2, &[(0, 0, 1.0), (0, 1, 1.0), (1, 1, 1.0)]);
let without_shift = is_q_psd_by_cholesky_probe(&q, 0.0, true);
assert!(
!without_shift,
"shift no-op proof: rank-1 PSD は shift 無しだと ZeroPivot で false 化される \
(= shift がなければ production が誤分類)"
);
let with_shift = is_q_psd_by_cholesky_probe(&q, SHIFT_FACTOR, true);
assert!(
with_shift,
"guard: shift 適用時は rank-1 PSD が正しく PSD と分類されること"
);
}
#[test]
fn no_op_proof_zeropivot_conservative_required_when_shift_absent() {
let q = upper_tri_csc(2, &[(0, 1, 1.0)]);
let old_path = is_q_psd_by_cholesky_probe(&q, 0.0, false);
assert!(
old_path,
"ZeroPivot no-op proof: 旧経路 (shift=0, ZeroPivot=true) は indefinite Q を \
PSD と誤分類する (= 旧 bug の再現)"
);
let new_path_alone = is_q_psd_by_cholesky_probe(&q, 0.0, true);
assert!(
!new_path_alone,
"ZeroPivot conservative 単独で旧 bug を修正できること"
);
}
#[test]
fn no_op_proof_shift_alone_also_catches_zero_diag_bilinear() {
let q = upper_tri_csc(2, &[(0, 1, 1.0)]);
let shift_alone = is_q_psd_by_cholesky_probe(&q, SHIFT_FACTOR, false);
assert!(
!shift_alone,
"shift 単独でも Q=[[0,1],[1,0]] は indefinite と検出される \
(= shift 経路が D[1] negative を露出させる)"
);
let production = is_q_psd_by_cholesky_probe(&q, SHIFT_FACTOR, true);
assert!(!production, "production 経路は indefinite を弾く");
}
#[test]
fn test_nnz_l_reasonable() {
let n = 3usize;
let mat = upper_tri_csc(n, &[
(0, 0, 4.0), (0, 1, 1.0), (1, 1, 3.0), (1, 2, 2.0), (2, 2, 5.0),
]);
let perm = vec![0, 1, 2];
let fac = factorize_quasidefinite_with_cached_perm_budget_par(
&mat, &perm, None, None, faer::Par::Seq,
).expect("factorize failed");
let nnz = fac.nnz_l();
assert!(nnz > 0, "nnz_l should be positive for non-trivial matrix");
}
#[test]
fn diag_banded_supernode_vs_simplicial() {
let n = 2000usize;
let band = 2usize;
let mut lo_triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
for i in 0..n {
lo_triplets.push(Triplet::new(i, i, 4.0));
if i + 1 < n { lo_triplets.push(Triplet::new(i + 1, i, -1.0)); }
if i + 2 < n { lo_triplets.push(Triplet::new(i + 2, i, -0.5)); }
}
let a_lower = SparseColMat::<usize, f64>::try_new_from_triplets(n, n, &lo_triplets)
.expect("build lower failed");
let a_nnz = a_lower.compute_nnz();
let a_upper_sym = a_lower.rb().transpose().symbolic().to_col_major()
.expect("transpose failed");
let mut etree_buf = vec![0isize; n];
let mut col_counts_buf = vec![0usize; n];
{
let mut mem = MemBuffer::new(StackReq::any_of(&[
simplicial::prefactorize_symbolic_cholesky_scratch::<usize>(n, a_nnz),
supernodal::factorize_supernodal_symbolic_cholesky_scratch::<usize>(n),
]));
let stack = MemStack::new(&mut mem);
simplicial::prefactorize_symbolic_cholesky(
&mut etree_buf, &mut col_counts_buf, a_upper_sym.rb(), stack,
);
}
let relax_all: &[(usize, f64)] = &[(usize::MAX, 1.0)];
let relax_default: &[(usize, f64)] = &[(4, 1.0), (16, 0.8), (48, 0.1), (usize::MAX, 0.05)];
for (label, relax) in [
("supernodal(relax=ALL)", relax_all),
("supernodal(relax=DEFAULT)", relax_default),
] {
let mut mem = MemBuffer::new(StackReq::any_of(&[
simplicial::prefactorize_symbolic_cholesky_scratch::<usize>(n, a_nnz),
supernodal::factorize_supernodal_symbolic_cholesky_scratch::<usize>(n),
]));
let stack = MemStack::new(&mut mem);
let mut etree = etree_buf.clone();
let mut col_counts = col_counts_buf.clone();
simplicial::prefactorize_symbolic_cholesky(
&mut etree, &mut col_counts, a_upper_sym.rb(), stack,
);
let mut mem2 = MemBuffer::new(supernodal::factorize_supernodal_symbolic_cholesky_scratch::<usize>(n));
let stack2 = MemStack::new(&mut mem2);
let t0 = Instant::now();
let sym = supernodal::factorize_supernodal_symbolic_cholesky(
a_upper_sym.rb(),
unsafe { simplicial::EliminationTreeRef::from_inner(&etree) },
&col_counts,
stack2,
faer::sparse::linalg::SymbolicSupernodalParams { relax: Some(relax) },
).expect("symbolic failed");
let sym_t = t0.elapsed();
let n_sn = sym.n_supernodes();
let len_val = sym.len_val();
let begin = sym.supernode_begin();
let end = sym.supernode_end();
let sizes: Vec<usize> = (0..n_sn).map(|i| end[i] - begin[i]).collect();
let max_sn = sizes.iter().max().copied().unwrap_or(0);
let avg_sn = sizes.iter().sum::<usize>() as f64 / n_sn as f64;
let regularization = faer::linalg::cholesky::ldlt::factor::LdltRegularization {
dynamic_regularization_signs: None,
dynamic_regularization_delta: LDLT_REG_DELTA,
dynamic_regularization_epsilon: LDLT_REG_EPSILON,
};
let mut mem3 = MemBuffer::new(StackReq::any_of(&[
supernodal::factorize_supernodal_numeric_ldlt_scratch::<usize, f64>(
&sym, faer::Par::Seq, Default::default()),
sym.solve_in_place_scratch::<f64>(n, faer::Par::Seq),
]));
let stack3 = MemStack::new(&mut mem3);
let mut l_values = vec![0.0f64; sym.len_val()];
let t1 = Instant::now();
supernodal::factorize_supernodal_numeric_ldlt::<usize, f64>(
&mut l_values, a_lower.rb(), regularization, &sym,
faer::Par::Seq, stack3, Default::default(),
).expect("numeric failed");
let num_t = t1.elapsed();
println!(
"[{label}] n={n}, band={band}: n_supernodes={n_sn}, len_val={len_val}, \
max_sn={max_sn}, avg_sn={avg_sn:.1}, sym={sym_t:.3?}, num={num_t:.3?}"
);
}
{
let mut etree = etree_buf.clone();
let mut col_counts = col_counts_buf.clone();
let mut mem = MemBuffer::new(StackReq::any_of(&[
simplicial::prefactorize_symbolic_cholesky_scratch::<usize>(n, a_nnz),
simplicial::factorize_simplicial_symbolic_cholesky_scratch::<usize>(n),
]));
let stack = MemStack::new(&mut mem);
simplicial::prefactorize_symbolic_cholesky(
&mut etree, &mut col_counts, a_upper_sym.rb(), stack,
);
let mut mem2 = MemBuffer::new(simplicial::factorize_simplicial_symbolic_cholesky_scratch::<usize>(n));
let stack2 = MemStack::new(&mut mem2);
let t0 = Instant::now();
let sym_s = simplicial::factorize_simplicial_symbolic_cholesky(
a_upper_sym.rb(),
unsafe { simplicial::EliminationTreeRef::from_inner(&etree) },
&col_counts,
stack2,
).expect("simplicial symbolic failed");
let sym_t = t0.elapsed();
let regularization = faer::linalg::cholesky::ldlt::factor::LdltRegularization {
dynamic_regularization_signs: None,
dynamic_regularization_delta: LDLT_REG_DELTA,
dynamic_regularization_epsilon: LDLT_REG_EPSILON,
};
let l_nnz = sym_s.len_val();
let mut l_values = vec![0.0f64; l_nnz];
let mut mem3 = MemBuffer::new(
simplicial::factorize_simplicial_numeric_ldlt_scratch::<usize, f64>(n)
);
let stack3 = MemStack::new(&mut mem3);
let t1 = Instant::now();
simplicial::factorize_simplicial_numeric_ldlt::<usize, f64>(
&mut l_values, a_lower.rb(), regularization, &sym_s, stack3,
).expect("simplicial numeric failed");
let num_t = t1.elapsed();
println!(
"[simplicial] n={n}, band={band}: l_nnz={l_nnz}, sym={sym_t:.3?}, num={num_t:.3?}"
);
}
let _ = band; }
}