use faer::linalg::matmul::matmul;
use faer::sparse::SparseColMat;
use faer::{Accum, Col, Mat, Par};
use crate::error::SparseError;
use crate::io::reference::{DBlock, Inertia, ReferenceFactorization};
pub fn validate_permutation(perm: &[usize], n: usize) -> Result<(), SparseError> {
if perm.len() != n {
return Err(SparseError::InvalidInput {
reason: format!(
"permutation length ({}) does not match expected size ({})",
perm.len(),
n
),
});
}
let mut seen = vec![false; n];
for (i, &p) in perm.iter().enumerate() {
if p >= n {
return Err(SparseError::InvalidInput {
reason: format!("permutation[{}] = {} is out of bounds (n = {})", i, p, n),
});
}
if seen[p] {
return Err(SparseError::InvalidInput {
reason: format!("permutation has duplicate index {} at position {}", p, i),
});
}
seen[p] = true;
}
Ok(())
}
pub fn dense_matvec(a: &SparseColMat<usize, f64>, x: &Col<f64>) -> Col<f64> {
let a_dense = a.to_dense();
let n = a.nrows();
let x_mat = x.as_mat();
let mut result = Mat::<f64>::zeros(n, 1);
matmul(
result.as_mut(),
Accum::Replace,
a_dense.as_ref(),
x_mat,
1.0,
Par::Seq,
);
Col::from_fn(n, |i| result[(i, 0)])
}
pub fn reconstruction_error(
a: &SparseColMat<usize, f64>,
reference: &ReferenceFactorization,
) -> f64 {
let n = a.nrows();
let a_dense = a.to_dense();
let a_norm = a_dense.norm_l2();
if a_norm == 0.0 {
return 0.0;
}
let perm = &reference.permutation;
let pap = Mat::<f64>::from_fn(n, n, |i, j| a_dense[(perm[i], perm[j])]);
let mut l_dense = Mat::<f64>::identity(n, n);
for entry in &reference.l_entries {
l_dense[(entry.row, entry.col)] = entry.value;
}
let mut d_dense = Mat::<f64>::zeros(n, n);
let mut row_offset = 0;
for block in &reference.d_blocks {
match block {
DBlock::OneByOne { value } => {
d_dense[(row_offset, row_offset)] = *value;
row_offset += 1;
}
DBlock::TwoByTwo { values } => {
d_dense[(row_offset, row_offset)] = values[0][0];
d_dense[(row_offset, row_offset + 1)] = values[0][1];
d_dense[(row_offset + 1, row_offset)] = values[1][0];
d_dense[(row_offset + 1, row_offset + 1)] = values[1][1];
row_offset += 2;
}
}
}
let mut ld = Mat::<f64>::zeros(n, n);
matmul(
ld.as_mut(),
Accum::Replace,
l_dense.as_ref(),
d_dense.as_ref(),
1.0,
Par::Seq,
);
let mut ldlt = Mat::<f64>::zeros(n, n);
matmul(
ldlt.as_mut(),
Accum::Replace,
ld.as_ref(),
l_dense.as_ref().transpose(),
1.0,
Par::Seq,
);
let diff = &pap - &ldlt;
diff.norm_l2() / a_norm
}
pub fn backward_error(a: &SparseColMat<usize, f64>, x: &Col<f64>, b: &Col<f64>) -> f64 {
let n = a.nrows();
let a_dense = a.to_dense();
let x_mat = x.as_mat();
let mut ax = Mat::<f64>::zeros(n, 1);
matmul(
ax.as_mut(),
Accum::Replace,
a_dense.as_ref(),
x_mat,
1.0,
Par::Seq,
);
let r = Col::<f64>::from_fn(n, |i| ax[(i, 0)] - b[i]);
let r_norm = r.norm_l2();
let a_norm = a_dense.norm_l2();
let x_norm = x.norm_l2();
let b_norm = b.norm_l2();
let denom = a_norm * x_norm + b_norm;
if denom == 0.0 {
return 0.0;
}
r_norm / denom
}
pub fn sparse_backward_error(a: &SparseColMat<usize, f64>, x: &Col<f64>, b: &Col<f64>) -> f64 {
let n = a.nrows();
let symbolic = a.symbolic();
let col_ptrs = symbolic.col_ptr();
let row_indices = symbolic.row_idx();
let values = a.val();
let mut ax = vec![0.0f64; n];
for j in 0..n {
for idx in col_ptrs[j]..col_ptrs[j + 1] {
let i = row_indices[idx];
let v = values[idx];
ax[i] += v * x[j];
}
}
let mut r_norm_sq = 0.0;
for k in 0..n {
let r = ax[k] - b[k];
r_norm_sq += r * r;
}
let r_norm = r_norm_sq.sqrt();
let x_norm = x.norm_l2();
let b_norm = b.norm_l2();
let mut a_norm_sq = 0.0;
for j in 0..n {
for &v in &values[col_ptrs[j]..col_ptrs[j + 1]] {
a_norm_sq += v * v;
}
}
let a_norm = a_norm_sq.sqrt();
let denom = a_norm * x_norm + b_norm;
if denom == 0.0 {
return 0.0;
}
r_norm / denom
}
pub fn check_inertia(computed: &Inertia, expected: &Inertia) -> bool {
computed.dimension() == expected.dimension()
&& computed.positive == expected.positive
&& computed.negative == expected.negative
&& computed.zero == expected.zero
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::registry;
#[test]
fn reconstruction_error_arrow_5_pd() {
let test = registry::load_test_matrix("arrow-5-pd")
.expect("registry error")
.expect("matrix should exist");
let refdata = test.reference.expect("reference should exist");
let err = reconstruction_error(&test.matrix, &refdata);
assert!(
err < 1e-12,
"reconstruction error for arrow-5-pd: {:.2e} (expected < 1e-12)",
err
);
}
#[test]
fn reconstruction_error_stress_delayed_pivots() {
let test = registry::load_test_matrix("stress-delayed-pivots")
.expect("registry error")
.expect("matrix should exist");
let refdata = test.reference.expect("reference should exist");
let err = reconstruction_error(&test.matrix, &refdata);
assert!(
err < 1e-12,
"reconstruction error for stress-delayed-pivots: {:.2e} (expected < 1e-12)",
err
);
}
#[test]
fn reconstruction_error_with_perturbed_l() {
let test = registry::load_test_matrix("arrow-5-pd")
.expect("registry error")
.expect("matrix should exist");
let mut refdata = test.reference.expect("reference should exist");
if !refdata.l_entries.is_empty() {
refdata.l_entries[0].value += 10.0;
}
let err = reconstruction_error(&test.matrix, &refdata);
assert!(
err > 0.01,
"perturbed reconstruction error should be large: {:.2e}",
err
);
}
#[test]
fn backward_error_exact_solution() {
let test = registry::load_test_matrix("arrow-5-pd")
.expect("registry error")
.expect("matrix should exist");
let a = &test.matrix;
let n = a.nrows();
let x_exact = Col::<f64>::from_fn(n, |i| (i + 1) as f64);
let b = dense_matvec(a, &x_exact);
let err = backward_error(a, &x_exact, &b);
assert!(
err < 1e-14,
"backward error for exact solution: {:.2e} (expected < 1e-14)",
err
);
}
#[test]
fn backward_error_perturbed_solution() {
let test = registry::load_test_matrix("arrow-5-pd")
.expect("registry error")
.expect("matrix should exist");
let a = &test.matrix;
let n = a.nrows();
let x_exact = Col::<f64>::from_fn(n, |i| (i + 1) as f64);
let b = dense_matvec(a, &x_exact);
let x_perturbed = Col::<f64>::from_fn(n, |i| (i + 1) as f64 + 0.1);
let err = backward_error(a, &x_perturbed, &b);
assert!(
err > 1e-4,
"backward error for perturbed solution should be measurable: {:.2e}",
err
);
}
#[test]
fn check_inertia_matching() {
let a = Inertia {
positive: 5,
negative: 3,
zero: 0,
};
let b = Inertia {
positive: 5,
negative: 3,
zero: 0,
};
assert!(check_inertia(&a, &b));
}
#[test]
fn check_inertia_mismatched() {
let a = Inertia {
positive: 5,
negative: 3,
zero: 0,
};
let b = Inertia {
positive: 4,
negative: 3,
zero: 1,
};
assert!(!check_inertia(&a, &b));
}
#[test]
fn validate_permutation_valid() {
assert!(validate_permutation(&[2, 0, 1], 3).is_ok());
assert!(validate_permutation(&[0], 1).is_ok());
assert!(validate_permutation(&[], 0).is_ok());
}
#[test]
fn validate_permutation_wrong_length() {
let result = validate_permutation(&[0, 1], 3);
assert!(result.is_err());
}
#[test]
fn validate_permutation_out_of_bounds() {
let result = validate_permutation(&[0, 5, 2], 3);
assert!(result.is_err());
}
#[test]
fn validate_permutation_duplicate() {
let result = validate_permutation(&[0, 1, 1], 3);
assert!(result.is_err());
}
#[test]
fn dense_matvec_matches_manual() {
let test = registry::load_test_matrix("arrow-5-pd")
.expect("registry error")
.expect("matrix should exist");
let n = test.matrix.nrows();
let x = Col::<f64>::from_fn(n, |i| (i + 1) as f64);
let b = dense_matvec(&test.matrix, &x);
assert_eq!(b.nrows(), n);
let err = backward_error(&test.matrix, &x, &b);
assert!(err < 1e-14);
}
}