use scivex_core::linalg::{self, EigDecomposition, QrDecomposition, SvdDecomposition};
use scivex_core::tensor::Tensor;
fn mat(data: &[f64], rows: usize, cols: usize) -> Tensor<f64> {
Tensor::from_vec(data.to_vec(), vec![rows, cols]).unwrap()
}
fn vec1d(data: &[f64]) -> Tensor<f64> {
Tensor::from_vec(data.to_vec(), vec![data.len()]).unwrap()
}
fn assert_close(actual: &[f64], expected: &[f64], tol: f64, msg: &str) {
assert_eq!(actual.len(), expected.len(), "{msg}: length mismatch");
for (i, (&a, &e)) in actual.iter().zip(expected).enumerate() {
assert!(
(a - e).abs() < tol,
"{msg}: element [{i}] differs: got {a}, expected {e}, diff={}",
(a - e).abs()
);
}
}
fn residual_norm(a: &Tensor<f64>, x: &Tensor<f64>, b: &Tensor<f64>) -> f64 {
let n = b.as_slice().len();
let a_data = a.as_slice();
let x_data = x.as_slice();
let b_data = b.as_slice();
let mut max_err = 0.0_f64;
for i in 0..n {
let mut row_sum = 0.0;
for j in 0..n {
row_sum += a_data[i * n + j] * x_data[j];
}
max_err = max_err.max((row_sum - b_data[i]).abs());
}
max_err
}
#[test]
fn ref_det_3x3_analytical() {
let a = mat(&[6.0, 1.0, 1.0, 4.0, -2.0, 5.0, 2.0, 8.0, 7.0], 3, 3);
let d = linalg::det(&a).unwrap();
assert!((d - (-306.0)).abs() < 1e-10, "det = {d}");
}
#[test]
fn ref_det_4x4_analytical() {
let a = mat(
&[
3.0, 2.0, 0.0, 1.0, 4.0, 0.0, 1.0, 2.0, 3.0, 0.0, 2.0, 1.0, 9.0, 2.0, 3.0, 1.0,
],
4,
4,
);
let d = linalg::det(&a).unwrap();
assert!((d - 24.0).abs() < 1e-10, "det = {d}");
}
#[test]
fn ref_det_5x5_permutation() {
let p = mat(
&[
0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0,
],
5,
5,
);
let d = linalg::det(&p).unwrap();
assert!(
(d.abs() - 1.0).abs() < 1e-10,
"permutation det should be ±1, got {d}"
);
}
#[test]
fn ref_solve_3x3_analytical() {
let a = mat(&[3.0, 1.0, -1.0, 2.0, 4.0, 1.0, -1.0, 2.0, 5.0], 3, 3);
let b = vec1d(&[4.0, 1.0, 1.0]);
let x = linalg::solve(&a, &b).unwrap();
assert_close(x.as_slice(), &[2.0, -1.0, 1.0], 1e-10, "solve 3x3");
}
#[test]
fn ref_solve_4x4_ones() {
let a = mat(
&[
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 2.0, 6.0, 4.0, 8.0, 3.0, 1.0, 1.0, 2.0,
],
4,
4,
);
let b = vec1d(&[10.0, 26.0, 20.0, 7.0]); let x = linalg::solve(&a, &b).unwrap();
assert_close(x.as_slice(), &[1.0, 1.0, 1.0, 1.0], 1e-10, "solve 4x4");
}
#[test]
fn ref_solve_5x5_residual() {
let a = mat(
&[
2.0, 1.0, -1.0, 0.0, 0.0, 1.0, 3.0, 0.0, -1.0, 0.0, -1.0, 0.0, 4.0, 1.0, 1.0, 0.0,
-1.0, 1.0, 5.0, 0.0, 0.0, 0.0, 1.0, 0.0, 3.0,
],
5,
5,
);
let b = vec1d(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let x = linalg::solve(&a, &b).unwrap();
let resid = residual_norm(&a, &x, &b);
assert!(resid < 1e-12, "5x5 solve residual too large: {resid}");
}
#[test]
fn ref_inv_3x3_analytical() {
let a = mat(&[1.0, 2.0, 3.0, 0.0, 1.0, 4.0, 5.0, 6.0, 0.0], 3, 3);
let inv = linalg::inv(&a).unwrap();
let expected = [-24.0, 18.0, 5.0, 20.0, -15.0, -4.0, -5.0, 4.0, 1.0];
assert_close(inv.as_slice(), &expected, 1e-10, "inv 3x3");
}
#[test]
fn ref_inv_times_original_is_identity() {
let a = mat(
&[
2.0, 1.0, 0.0, 3.0, 1.0, 3.0, 1.0, 0.0, 0.0, 1.0, 4.0, 2.0, 3.0, 0.0, 2.0, 5.0,
],
4,
4,
);
let inv = linalg::inv(&a).unwrap();
let product = a.matmul(&inv).unwrap();
let eye = Tensor::<f64>::eye(4);
assert_close(product.as_slice(), eye.as_slice(), 1e-10, "A * A^-1 = I");
}
#[test]
fn ref_svd_diagonal_matrix() {
let a = mat(&[5.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 1.0], 3, 3);
let svd = SvdDecomposition::decompose(&a).unwrap();
let s = svd.singular_values();
assert!((s[0] - 5.0).abs() < 1e-10, "s[0]={}", s[0]);
assert!((s[1] - 3.0).abs() < 1e-10, "s[1]={}", s[1]);
assert!((s[2] - 1.0).abs() < 1e-10, "s[2]={}", s[2]);
}
#[test]
fn ref_svd_reconstruction_3x3() {
let a = mat(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0], 3, 3);
let svd = SvdDecomposition::decompose(&a).unwrap();
let u = svd.u();
let vt = svd.vt();
let s = svd.singular_values();
let mut s_mat_data = vec![0.0_f64; 9];
for i in 0..3 {
s_mat_data[i * 3 + i] = s[i];
}
let s_mat = mat(&s_mat_data, 3, 3);
let us = u.matmul(&s_mat).unwrap();
let reconstructed = us.matmul(&vt).unwrap();
assert_close(
reconstructed.as_slice(),
a.as_slice(),
1e-8,
"SVD reconstruction",
);
}
#[test]
fn ref_svd_rank_deficient() {
let a = mat(&[1.0, 2.0, 2.0, 4.0], 2, 2);
let svd = SvdDecomposition::decompose(&a).unwrap();
let s = svd.singular_values();
assert!((s[0] - 5.0).abs() < 1e-8, "s[0]={}", s[0]);
assert!(s[1].abs() < 1e-8, "s[1] should be ~0, got {}", s[1]);
}
#[test]
fn ref_svd_tall_matrix_reconstruction() {
let a = mat(
&[1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 1.0, 1.0, 1.0],
4,
3,
);
let svd = SvdDecomposition::decompose(&a).unwrap();
let u = svd.u();
let vt = svd.vt();
let s = svd.singular_values();
let mut s_mat_data = vec![0.0_f64; 4 * 3];
for i in 0..3 {
s_mat_data[i * 3 + i] = s[i];
}
let s_mat = mat(&s_mat_data, 4, 3);
let us = u.matmul(&s_mat).unwrap();
let reconstructed = us.matmul(&vt).unwrap();
assert_close(
reconstructed.as_slice(),
a.as_slice(),
1e-8,
"SVD reconstruction 4x3",
);
}
#[test]
fn ref_eigh_2x2_analytical() {
let a = mat(&[2.0, 1.0, 1.0, 3.0], 2, 2);
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let vals = eig.eigenvalues();
let sqrt5 = 5.0_f64.sqrt();
let lambda1 = 2.5 + sqrt5 / 2.0; let lambda2 = 2.5 - sqrt5 / 2.0;
let mut sorted = vals.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!(
(sorted[0] - lambda2).abs() < 1e-10,
"λ_min={}, expected={}",
sorted[0],
lambda2
);
assert!(
(sorted[1] - lambda1).abs() < 1e-10,
"λ_max={}, expected={}",
sorted[1],
lambda1
);
}
#[test]
fn ref_eigh_trace_and_det() {
let a = mat(&[4.0, 2.0, 1.0, 2.0, 5.0, 3.0, 1.0, 3.0, 6.0], 3, 3);
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let vals = eig.eigenvalues();
let trace = 4.0 + 5.0 + 6.0; let sum_eig: f64 = vals.iter().sum();
assert!(
(sum_eig - trace).abs() < 1e-8,
"sum(eigenvalues)={sum_eig}, trace={trace}"
);
let det_from_eig: f64 = vals.iter().product();
let det_from_lu = linalg::det(&a).unwrap();
assert!(
(det_from_eig - det_from_lu).abs() < 1e-6,
"product(eigenvalues)={det_from_eig}, det={det_from_lu}"
);
}
#[test]
fn ref_eigh_reconstruction() {
let a = mat(
&[
5.0, 1.0, 2.0, 0.0, 1.0, 4.0, 1.0, 1.0, 2.0, 1.0, 3.0, 0.0, 0.0, 1.0, 0.0, 2.0,
],
4,
4,
);
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let v = eig.eigenvectors();
let vt = v.transpose().unwrap();
let d_vals = eig.eigenvalues();
let mut d_data = vec![0.0_f64; 16];
for i in 0..4 {
d_data[i * 4 + i] = d_vals[i];
}
let d = mat(&d_data, 4, 4);
let vd = v.matmul(&d).unwrap();
let reconstructed = vd.matmul(&vt).unwrap();
assert_close(
reconstructed.as_slice(),
a.as_slice(),
1e-8,
"Eig reconstruction 4x4",
);
}
#[test]
fn ref_qr_r_diagonal() {
let a = mat(
&[12.0, -51.0, 4.0, 6.0, 167.0, -68.0, -4.0, 24.0, -41.0],
3,
3,
);
let qr = QrDecomposition::decompose(&a).unwrap();
let r = qr.r();
let r_data = r.as_slice();
assert!(
(r_data[0].abs() - 14.0).abs() < 1e-8,
"R[0,0]={}",
r_data[0]
);
assert!(
(r_data[4].abs() - 175.0).abs() < 1e-8,
"R[1,1]={}",
r_data[4]
);
assert!(
(r_data[8].abs() - 35.0).abs() < 1e-8,
"R[2,2]={}",
r_data[8]
);
}
#[test]
fn ref_qr_reconstruction() {
let a = mat(
&[12.0, -51.0, 4.0, 6.0, 167.0, -68.0, -4.0, 24.0, -41.0],
3,
3,
);
let qr = QrDecomposition::decompose(&a).unwrap();
let q = qr.q();
let r = qr.r();
let reconstructed = q.matmul(&r).unwrap();
assert_close(
reconstructed.as_slice(),
a.as_slice(),
1e-10,
"QR reconstruction",
);
}
#[test]
fn ref_qr_orthogonality() {
let a = mat(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0], 3, 3);
let qr = QrDecomposition::decompose(&a).unwrap();
let q = qr.q();
let qt = q.transpose().unwrap();
let qtq = qt.matmul(&q).unwrap();
let eye = Tensor::<f64>::eye(3);
assert_close(qtq.as_slice(), eye.as_slice(), 1e-10, "Q^T Q = I");
}
#[test]
fn ref_fft_4point() {
use scivex_core::fft;
let input = Tensor::from_vec(vec![1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0], vec![4, 2]).unwrap();
let result = fft::fft(&input).unwrap();
let r = result.as_slice();
let expected = [10.0, 0.0, -2.0, 2.0, -2.0, 0.0, -2.0, -2.0];
assert_close(r, &expected, 1e-10, "FFT 4-point");
}
#[test]
fn ref_rfft_impulse() {
use scivex_core::fft;
let input = Tensor::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], vec![8]).unwrap();
let result = fft::rfft(&input).unwrap();
let r = result.as_slice();
assert_eq!(r.len(), 10);
for i in 0..5 {
let re: f64 = r[2 * i];
let im: f64 = r[2 * i + 1];
assert!((re - 1.0).abs() < 1e-10, "real[{i}]={re}");
assert!(im.abs() < 1e-10, "imag[{i}]={im}");
}
}
#[test]
fn ref_dot_product() {
let x = vec1d(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let y = vec1d(&[5.0, 4.0, 3.0, 2.0, 1.0]);
let result = scivex_core::linalg::dot(&x, &y).unwrap();
assert!((result - 35.0).abs() < 1e-14, "dot = {result}");
}
#[test]
fn ref_matmul_2x3_3x2() {
let a = mat(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3);
let b = mat(&[7.0, 8.0, 9.0, 10.0, 11.0, 12.0], 3, 2);
let c = a.matmul(&b).unwrap();
assert_close(c.as_slice(), &[58.0, 64.0, 139.0, 154.0], 1e-10, "matmul");
}
#[test]
fn ref_nrm2() {
let x = vec1d(&[3.0, 4.0]);
let n = scivex_core::linalg::nrm2(&x).unwrap();
assert!((n - 5.0).abs() < 1e-14, "nrm2 = {n}");
}