use crate::CooMatrixBuilder;
use crate::csr::CsrMatrix;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TestMatrixError {
InvalidDimension {
param: String,
value: usize,
},
InvalidDensity {
density: String,
},
ConstructionError {
description: String,
},
}
impl core::fmt::Display for TestMatrixError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidDimension { param, value } => {
write!(f, "Invalid dimension for {param}: {value}")
}
Self::InvalidDensity { density } => {
write!(f, "Invalid density: {density} (must be in [0, 1])")
}
Self::ConstructionError { description } => {
write!(f, "Matrix construction error: {description}")
}
}
}
}
impl std::error::Error for TestMatrixError {}
pub fn laplacian_2d(nx: usize, ny: usize) -> Result<CsrMatrix<f64>, TestMatrixError> {
if nx == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "nx".to_string(),
value: 0,
});
}
if ny == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "ny".to_string(),
value: 0,
});
}
let n = nx * ny;
let mut builder = CooMatrixBuilder::new(n, n);
for j in 0..ny {
for i in 0..nx {
let idx = j * nx + i;
builder.add(idx, idx, 4.0);
if i > 0 {
builder.add(idx, idx - 1, -1.0);
}
if i < nx - 1 {
builder.add(idx, idx + 1, -1.0);
}
if j > 0 {
builder.add(idx, idx - nx, -1.0);
}
if j < ny - 1 {
builder.add(idx, idx + nx, -1.0);
}
}
}
Ok(builder.build().to_csr())
}
pub fn laplacian_3d(nx: usize, ny: usize, nz: usize) -> Result<CsrMatrix<f64>, TestMatrixError> {
if nx == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "nx".to_string(),
value: 0,
});
}
if ny == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "ny".to_string(),
value: 0,
});
}
if nz == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "nz".to_string(),
value: 0,
});
}
let n = nx * ny * nz;
let nxy = nx * ny;
let mut builder = CooMatrixBuilder::new(n, n);
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
let idx = k * nxy + j * nx + i;
builder.add(idx, idx, 6.0);
if i > 0 {
builder.add(idx, idx - 1, -1.0);
}
if i < nx - 1 {
builder.add(idx, idx + 1, -1.0);
}
if j > 0 {
builder.add(idx, idx - nx, -1.0);
}
if j < ny - 1 {
builder.add(idx, idx + nx, -1.0);
}
if k > 0 {
builder.add(idx, idx - nxy, -1.0);
}
if k < nz - 1 {
builder.add(idx, idx + nxy, -1.0);
}
}
}
}
Ok(builder.build().to_csr())
}
pub fn tridiagonal(
n: usize,
sub: f64,
diag: f64,
sup: f64,
) -> Result<CsrMatrix<f64>, TestMatrixError> {
if n == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "n".to_string(),
value: 0,
});
}
let mut builder = CooMatrixBuilder::new(n, n);
for i in 0..n {
builder.add(i, i, diag);
if i > 0 {
builder.add(i, i - 1, sub);
}
if i < n - 1 {
builder.add(i, i + 1, sup);
}
}
Ok(builder.build().to_csr())
}
pub fn diagonal(n: usize, value: f64) -> Result<CsrMatrix<f64>, TestMatrixError> {
if n == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "n".to_string(),
value: 0,
});
}
let mut builder = CooMatrixBuilder::new(n, n);
for i in 0..n {
builder.add(i, i, value);
}
Ok(builder.build().to_csr())
}
pub fn arrow_matrix(n: usize) -> Result<CsrMatrix<f64>, TestMatrixError> {
if n == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "n".to_string(),
value: 0,
});
}
if n == 1 {
let mut builder = CooMatrixBuilder::new(1, 1);
builder.add(0, 0, 1.0);
return Ok(builder.build().to_csr());
}
let mut builder = CooMatrixBuilder::new(n, n);
builder.add(0, 0, (n + 1) as f64);
for i in 1..n {
builder.add(0, i, 1.0);
builder.add(i, 0, 1.0);
builder.add(i, i, (n + 1 - i) as f64);
}
Ok(builder.build().to_csr())
}
pub fn random_spd(n: usize, density: f64) -> Result<CsrMatrix<f64>, TestMatrixError> {
if n == 0 {
return Err(TestMatrixError::InvalidDimension {
param: "n".to_string(),
value: 0,
});
}
if !(0.0..=1.0).contains(&density) {
return Err(TestMatrixError::InvalidDensity {
density: format!("{density}"),
});
}
let mut builder = CooMatrixBuilder::new(n, n);
for i in 0..n {
builder.add(i, i, n as f64);
}
if density <= 0.0 || n == 1 {
return Ok(builder.build().to_csr());
}
let max_lt_entries = n * (n - 1) / 2;
let target_lt_nnz = ((max_lt_entries as f64) * density).ceil() as usize;
let mut seed: u64 = 0x517cc1b727220a95;
let mut generated = 0usize;
let max_attempts = max_lt_entries * 3;
for attempt in 0..max_attempts {
if generated >= target_lt_nnz {
break;
}
seed ^= seed.wrapping_shl(13);
seed ^= seed.wrapping_shr(7);
seed ^= seed.wrapping_shl(17);
seed = seed.wrapping_add(attempt as u64);
let row = ((seed >> 16) as usize) % n;
let col = ((seed >> 32) as usize) % n;
if row > col {
let val = 0.1 + 0.9 * ((seed & 0xFF) as f64) / 255.0;
builder.add(row, col, val);
builder.add(col, row, val);
builder.add(row, row, val);
builder.add(col, col, val);
generated += 1;
}
}
let mut coo = builder.build();
coo.sum_duplicates();
Ok(coo.to_csr())
}
pub fn poisson_1d(n: usize) -> Result<CsrMatrix<f64>, TestMatrixError> {
tridiagonal(n, -1.0, 2.0, -1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_laplacian_2d_basic() {
let mat = laplacian_2d(3, 3).expect("Failed to create 2D Laplacian");
assert_eq!(mat.nrows(), 9);
assert_eq!(mat.ncols(), 9);
assert_eq!(mat.nnz(), 33);
}
#[test]
fn test_laplacian_2d_symmetry() {
let mat = laplacian_2d(4, 3).expect("Failed to create 2D Laplacian");
let n = mat.nrows();
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-14,
"Laplacian 2D not symmetric at ({row}, {col}): {val} vs {transpose_val}"
);
}
assert_eq!(n, 12);
}
#[test]
fn test_laplacian_2d_positive_definiteness() {
let mat = laplacian_2d(5, 5).expect("Failed to create 2D Laplacian");
let n = mat.nrows();
for i in 0..n {
let diag = mat.get_or_zero(i, i);
let mut off_diag_sum = 0.0;
for (col, val) in mat.row_iter(i) {
if col != i {
off_diag_sum += val.abs();
}
}
assert!(
diag >= off_diag_sum,
"Row {i}: diagonal {diag} < off-diagonal sum {off_diag_sum}"
);
}
}
#[test]
fn test_laplacian_2d_1x1() {
let mat = laplacian_2d(1, 1).expect("Failed to create 1x1 Laplacian");
assert_eq!(mat.nrows(), 1);
assert_eq!(mat.ncols(), 1);
assert_eq!(mat.nnz(), 1);
assert!((mat.get_or_zero(0, 0) - 4.0).abs() < 1e-14);
}
#[test]
fn test_laplacian_2d_zero_dimension() {
assert!(laplacian_2d(0, 5).is_err());
assert!(laplacian_2d(5, 0).is_err());
}
#[test]
fn test_laplacian_3d_basic() {
let mat = laplacian_3d(3, 3, 3).expect("Failed to create 3D Laplacian");
assert_eq!(mat.nrows(), 27);
assert_eq!(mat.ncols(), 27);
assert_eq!(mat.nnz(), 135);
}
#[test]
fn test_laplacian_3d_symmetry() {
let mat = laplacian_3d(3, 2, 2).expect("Failed to create 3D Laplacian");
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-14,
"Laplacian 3D not symmetric at ({row}, {col}): {val} vs {transpose_val}"
);
}
}
#[test]
fn test_laplacian_3d_diagonal_dominance() {
let mat = laplacian_3d(3, 3, 3).expect("Failed to create 3D Laplacian");
let n = mat.nrows();
for i in 0..n {
let diag = mat.get_or_zero(i, i);
let mut off_diag_sum = 0.0;
for (col, val) in mat.row_iter(i) {
if col != i {
off_diag_sum += val.abs();
}
}
assert!(
diag >= off_diag_sum,
"Row {i}: diagonal {diag} < off-diagonal sum {off_diag_sum}"
);
}
}
#[test]
fn test_laplacian_3d_zero_dimension() {
assert!(laplacian_3d(0, 3, 3).is_err());
assert!(laplacian_3d(3, 0, 3).is_err());
assert!(laplacian_3d(3, 3, 0).is_err());
}
#[test]
fn test_tridiagonal_basic() {
let mat = tridiagonal(5, -1.0, 2.0, -1.0).expect("Failed to create tridiagonal");
assert_eq!(mat.nrows(), 5);
assert_eq!(mat.ncols(), 5);
assert_eq!(mat.nnz(), 13);
}
#[test]
fn test_tridiagonal_values() {
let mat = tridiagonal(4, -1.0, 3.0, -2.0).expect("Failed to create tridiagonal");
for i in 0..4 {
assert!((mat.get_or_zero(i, i) - 3.0).abs() < 1e-14);
}
for i in 1..4 {
assert!((mat.get_or_zero(i, i - 1) - (-1.0)).abs() < 1e-14);
}
for i in 0..3 {
assert!((mat.get_or_zero(i, i + 1) - (-2.0)).abs() < 1e-14);
}
assert!((mat.get_or_zero(0, 2)).abs() < 1e-14);
assert!((mat.get_or_zero(0, 3)).abs() < 1e-14);
assert!((mat.get_or_zero(3, 0)).abs() < 1e-14);
}
#[test]
fn test_tridiagonal_symmetric() {
let mat = tridiagonal(5, -1.0, 4.0, -1.0).expect("Failed to create symmetric tridiagonal");
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-14,
"Symmetric tridiagonal not symmetric at ({row}, {col})"
);
}
}
#[test]
fn test_tridiagonal_size_1() {
let mat = tridiagonal(1, -1.0, 5.0, -1.0).expect("Failed to create 1x1 tridiagonal");
assert_eq!(mat.nrows(), 1);
assert_eq!(mat.nnz(), 1);
assert!((mat.get_or_zero(0, 0) - 5.0).abs() < 1e-14);
}
#[test]
fn test_tridiagonal_zero() {
assert!(tridiagonal(0, -1.0, 2.0, -1.0).is_err());
}
#[test]
fn test_diagonal_basic() {
let mat = diagonal(5, 3.0).expect("Failed to create diagonal");
assert_eq!(mat.nrows(), 5);
assert_eq!(mat.ncols(), 5);
assert_eq!(mat.nnz(), 5);
for i in 0..5 {
assert!((mat.get_or_zero(i, i) - 3.0).abs() < 1e-14);
}
}
#[test]
fn test_diagonal_off_diagonal_zeros() {
let mat = diagonal(4, 2.0).expect("Failed to create diagonal");
for i in 0..4 {
for j in 0..4 {
if i != j {
assert!(
mat.get_or_zero(i, j).abs() < 1e-14,
"Off-diagonal entry ({i}, {j}) is not zero"
);
}
}
}
}
#[test]
fn test_diagonal_symmetry() {
let mat = diagonal(10, 7.5).expect("Failed to create diagonal");
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-14,
"Diagonal matrix not symmetric at ({row}, {col})"
);
}
}
#[test]
fn test_diagonal_zero() {
assert!(diagonal(0, 1.0).is_err());
}
#[test]
fn test_arrow_basic() {
let mat = arrow_matrix(5).expect("Failed to create arrow matrix");
assert_eq!(mat.nrows(), 5);
assert_eq!(mat.ncols(), 5);
assert_eq!(mat.nnz(), 13);
}
#[test]
fn test_arrow_symmetry() {
let mat = arrow_matrix(8).expect("Failed to create arrow matrix");
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-14,
"Arrow matrix not symmetric at ({row}, {col}): {val} vs {transpose_val}"
);
}
}
#[test]
fn test_arrow_structure() {
let n = 5;
let mat = arrow_matrix(n).expect("Failed to create arrow matrix");
let mut first_row_cols = Vec::new();
for (col, _) in mat.row_iter(0) {
first_row_cols.push(col);
}
assert_eq!(first_row_cols.len(), n, "First row should have {n} entries");
for i in 1..n {
let mut row_nnz = 0;
for _ in mat.row_iter(i) {
row_nnz += 1;
}
assert_eq!(
row_nnz, 2,
"Row {i} should have exactly 2 entries, got {row_nnz}"
);
}
}
#[test]
fn test_arrow_diagonal_dominance() {
let mat = arrow_matrix(10).expect("Failed to create arrow matrix");
let n = mat.nrows();
for i in 0..n {
let diag = mat.get_or_zero(i, i);
let mut off_diag_sum = 0.0;
for (col, val) in mat.row_iter(i) {
if col != i {
off_diag_sum += val.abs();
}
}
assert!(
diag >= off_diag_sum,
"Arrow row {i}: diagonal {diag} < off-diagonal sum {off_diag_sum}"
);
}
}
#[test]
fn test_arrow_size_1() {
let mat = arrow_matrix(1).expect("Failed to create 1x1 arrow");
assert_eq!(mat.nrows(), 1);
assert_eq!(mat.nnz(), 1);
}
#[test]
fn test_arrow_zero() {
assert!(arrow_matrix(0).is_err());
}
#[test]
fn test_random_spd_basic() {
let mat = random_spd(10, 0.3).expect("Failed to create random SPD");
assert_eq!(mat.nrows(), 10);
assert_eq!(mat.ncols(), 10);
assert!(mat.nnz() >= 10, "Should have at least n entries (diagonal)");
}
#[test]
fn test_random_spd_symmetry() {
let mat = random_spd(20, 0.2).expect("Failed to create random SPD");
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-10,
"Random SPD not symmetric at ({row}, {col}): {val} vs {transpose_val}"
);
}
}
#[test]
fn test_random_spd_positive_diagonal() {
let mat = random_spd(15, 0.4).expect("Failed to create random SPD");
for i in 0..mat.nrows() {
let diag = mat.get_or_zero(i, i);
assert!(
diag > 0.0,
"Random SPD diagonal at {i} should be positive, got {diag}"
);
}
}
#[test]
fn test_random_spd_zero_density() {
let mat = random_spd(5, 0.0).expect("Failed to create diagonal SPD");
assert_eq!(mat.nnz(), 5, "Zero density should yield diagonal matrix");
}
#[test]
fn test_random_spd_diagonally_dominant() {
for &(n, density) in &[(5usize, 0.1f64), (10, 0.3), (25, 0.5), (30, 0.9)] {
let mat = random_spd(n, density).expect("Failed to create random SPD");
for i in 0..mat.nrows() {
let diag = mat.get_or_zero(i, i);
let mut off_diag_sum = 0.0;
for (col, val) in mat.row_iter(i) {
if col != i {
off_diag_sum += val.abs();
}
}
let margin = diag - off_diag_sum;
assert!(
(margin - n as f64).abs() < 1e-9,
"random_spd({n}, {density}) row {i}: expected dominance margin {n}, got {margin}"
);
}
}
}
#[test]
fn test_random_spd_is_genuinely_positive_definite() {
use crate::linalg::cholesky::SparseCholesky;
for &(n, density) in &[
(1usize, 0.0f64),
(5, 0.0),
(10, 0.3),
(20, 0.2),
(15, 0.4),
(40, 0.6),
] {
let mat = random_spd(n, density).expect("Failed to create random SPD");
let csc = mat.to_csc();
let chol = SparseCholesky::new(&csc);
assert!(
chol.is_ok(),
"random_spd({n}, {density}) is not positive definite: Cholesky failed with {:?}",
chol.err()
);
}
}
#[test]
fn test_random_spd_invalid() {
assert!(random_spd(0, 0.5).is_err());
assert!(random_spd(5, -0.1).is_err());
assert!(random_spd(5, 1.1).is_err());
}
#[test]
fn test_poisson_1d_basic() {
let mat = poisson_1d(5).expect("Failed to create 1D Poisson");
assert_eq!(mat.nrows(), 5);
assert_eq!(mat.ncols(), 5);
assert_eq!(mat.nnz(), 13); }
#[test]
fn test_poisson_1d_values() {
let mat = poisson_1d(4).expect("Failed to create 1D Poisson");
for i in 0..4 {
assert!((mat.get_or_zero(i, i) - 2.0).abs() < 1e-14);
}
for i in 0..3 {
assert!((mat.get_or_zero(i, i + 1) - (-1.0)).abs() < 1e-14);
assert!((mat.get_or_zero(i + 1, i) - (-1.0)).abs() < 1e-14);
}
}
#[test]
fn test_poisson_1d_symmetry() {
let mat = poisson_1d(10).expect("Failed to create 1D Poisson");
for (row, col, &val) in mat.iter() {
let transpose_val = mat.get_or_zero(col, row);
assert!(
(val - transpose_val).abs() < 1e-14,
"Poisson 1D not symmetric at ({row}, {col})"
);
}
}
#[test]
fn test_poisson_1d_spd() {
let mat = poisson_1d(10).expect("Failed to create 1D Poisson");
for i in 0..mat.nrows() {
let diag = mat.get_or_zero(i, i);
let mut off_diag_sum = 0.0;
for (col, val) in mat.row_iter(i) {
if col != i {
off_diag_sum += val.abs();
}
}
assert!(
diag >= off_diag_sum,
"Poisson 1D row {i}: diagonal {diag} < off-diagonal sum {off_diag_sum}"
);
}
}
#[test]
fn test_poisson_1d_zero() {
assert!(poisson_1d(0).is_err());
}
#[test]
fn test_poisson_1d_equals_tridiagonal() {
let poisson = poisson_1d(10).expect("poisson_1d");
let tri = tridiagonal(10, -1.0, 2.0, -1.0).expect("tridiagonal");
assert_eq!(poisson.nnz(), tri.nnz());
assert_eq!(poisson.nrows(), tri.nrows());
for i in 0..10 {
for j in 0..10 {
let pval = poisson.get_or_zero(i, j);
let tval = tri.get_or_zero(i, j);
assert!(
(pval - tval).abs() < 1e-14,
"Poisson != tridiag at ({i}, {j}): {pval} vs {tval}"
);
}
}
}
#[test]
fn test_laplacian_2d_1d_consistency() {
let mat = laplacian_2d(5, 1).expect("laplacian_2d(5,1)");
assert_eq!(mat.nrows(), 5);
assert_eq!(mat.ncols(), 5);
for i in 0..5 {
assert!((mat.get_or_zero(i, i) - 4.0).abs() < 1e-14);
}
}
#[test]
fn test_generators_produce_valid_csr() {
let matrices: Vec<CsrMatrix<f64>> = vec![
laplacian_2d(4, 4).expect("lap2d"),
laplacian_3d(3, 3, 3).expect("lap3d"),
tridiagonal(10, -1.0, 2.0, -1.0).expect("tridiag"),
diagonal(10, 5.0).expect("diag"),
arrow_matrix(10).expect("arrow"),
random_spd(10, 0.3).expect("rspd"),
poisson_1d(10).expect("poisson"),
];
for mat in &matrices {
let row_ptrs = mat.row_ptrs();
for i in 1..row_ptrs.len() {
assert!(row_ptrs[i] >= row_ptrs[i - 1], "Row pointers not monotonic");
}
for &col in mat.col_indices() {
assert!(col < mat.ncols(), "Column index out of bounds");
}
assert_eq!(
mat.nnz(),
row_ptrs[mat.nrows()],
"nnz mismatch with row_ptrs"
);
}
}
}