use scivex_core::linalg::{
self, CholeskyDecomposition, EigDecomposition, LuDecomposition, QrDecomposition,
SvdDecomposition,
};
use scivex_core::tensor::Tensor;
use scivex_core::{fft, linalg::dot};
const DECOMP_TOL: f64 = 1e-6;
const EXACT_TOL: f64 = 1e-10;
fn assert_approx(actual: f64, expected: f64, tol: f64, msg: &str) {
assert!(
(actual - expected).abs() < tol,
"{msg}: expected {expected}, got {actual} (diff = {})",
(actual - expected).abs()
);
}
fn assert_slice_approx(actual: &[f64], expected: &[f64], tol: f64, msg: &str) {
assert_eq!(
actual.len(),
expected.len(),
"{msg}: length mismatch: {} vs {}",
actual.len(),
expected.len()
);
for (i, (&a, &e)) in actual.iter().zip(expected.iter()).enumerate() {
assert!(
(a - e).abs() < tol,
"{msg}[{i}]: expected {e}, got {a} (diff = {})",
(a - e).abs()
);
}
}
#[test]
fn test_matmul_numpy_reference() {
let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
let b = Tensor::from_vec(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2]).unwrap();
let c = a.matmul(&b).unwrap();
assert_slice_approx(c.as_slice(), &[19.0, 22.0, 43.0, 50.0], EXACT_TOL, "matmul");
}
#[test]
fn test_lu_decomposition_numpy_reference() {
let a = Tensor::from_vec(vec![2.0, 1.0, 6.0, 4.0], vec![2, 2]).unwrap();
let lu = LuDecomposition::decompose(&a).unwrap();
let p = lu.p();
let l = lu.l();
let u = lu.u();
let p_expected = &[0.0, 1.0, 1.0, 0.0];
assert_slice_approx(p.as_slice(), p_expected, DECOMP_TOL, "LU P");
let l_expected = &[1.0, 0.0, 1.0 / 3.0, 1.0];
assert_slice_approx(l.as_slice(), l_expected, DECOMP_TOL, "LU L");
let u_expected = &[6.0, 4.0, 0.0, -1.0 / 3.0];
assert_slice_approx(u.as_slice(), u_expected, DECOMP_TOL, "LU U");
let pa = p.matmul(&a).unwrap();
let lu_prod = l.matmul(&u).unwrap();
assert_slice_approx(pa.as_slice(), lu_prod.as_slice(), DECOMP_TOL, "PA = LU");
}
#[test]
fn test_qr_decomposition_numpy_reference() {
let a = Tensor::from_vec(vec![1.0_f64, -1.0, 1.0, 1.0], vec![2, 2]).unwrap();
let qr = QrDecomposition::decompose(&a).unwrap();
let q = qr.q();
let r = qr.r();
let sqrt2_inv = 1.0 / 2.0_f64.sqrt();
for &val in q.as_slice() {
assert_approx(val.abs(), sqrt2_inv, DECOMP_TOL, "QR |Q| element");
}
let sqrt2 = 2.0_f64.sqrt();
assert_approx(r.as_slice()[0].abs(), sqrt2, DECOMP_TOL, "QR |R[0,0]|");
assert_approx(r.as_slice()[3].abs(), sqrt2, DECOMP_TOL, "QR |R[1,1]|");
assert_approx(r.as_slice()[1].abs(), 0.0, DECOMP_TOL, "QR R[0,1]");
let qr_prod = q.matmul(&r).unwrap();
assert_slice_approx(qr_prod.as_slice(), a.as_slice(), DECOMP_TOL, "A = QR");
let qt = q.transpose().unwrap();
let qtq = qt.matmul(&q).unwrap();
let eye = Tensor::<f64>::eye(2);
assert_slice_approx(qtq.as_slice(), eye.as_slice(), DECOMP_TOL, "Q^T Q = I");
}
#[test]
fn test_cholesky_numpy_reference() {
let a = Tensor::from_vec(vec![4.0, 2.0, 2.0, 3.0], vec![2, 2]).unwrap();
let chol = CholeskyDecomposition::decompose(&a).unwrap();
let l = chol.l();
let sqrt2 = 2.0_f64.sqrt();
let l_expected = &[2.0, 0.0, 1.0, sqrt2];
assert_slice_approx(l.as_slice(), l_expected, DECOMP_TOL, "Cholesky L");
let lt = l.transpose().unwrap();
let prod = l.matmul(<).unwrap();
assert_slice_approx(prod.as_slice(), a.as_slice(), DECOMP_TOL, "L L^T = A");
}
#[test]
fn test_svd_numpy_reference() {
let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![3, 2]).unwrap();
let svd = SvdDecomposition::decompose(&a).unwrap();
let s = svd.singular_values();
assert_approx(s[0], 9.525_518_091_565_107, DECOMP_TOL, "SVD s[0]");
assert_approx(s[1], 0.514_300_580_658_644_6, DECOMP_TOL, "SVD s[1]");
let u = svd.u();
let vt = svd.vt();
let (m, n) = (3, 2);
let mut s_mat = vec![0.0_f64; m * n];
for i in 0..n {
s_mat[i * n + i] = s[i];
}
let s_tensor = Tensor::from_vec(s_mat, vec![m, n]).unwrap();
let us = u.matmul(&s_tensor).unwrap();
let reconstructed = us.matmul(&vt).unwrap();
assert_slice_approx(
reconstructed.as_slice(),
a.as_slice(),
DECOMP_TOL,
"SVD reconstruction",
);
let ut = u.transpose().unwrap();
let utu = ut.matmul(&u).unwrap();
let eye_m = Tensor::<f64>::eye(m);
assert_slice_approx(utu.as_slice(), eye_m.as_slice(), DECOMP_TOL, "U^T U = I");
let v = vt.transpose().unwrap();
let vtv = vt.matmul(&v).unwrap();
let eye_n = Tensor::<f64>::eye(n);
assert_slice_approx(vtv.as_slice(), eye_n.as_slice(), DECOMP_TOL, "V^T V = I");
}
#[test]
fn test_eig_numpy_reference() {
let a = Tensor::from_vec(vec![2.0, 1.0, 1.0, 2.0], vec![2, 2]).unwrap();
let eig = EigDecomposition::decompose_symmetric(&a).unwrap();
let vals = eig.eigenvalues();
assert_approx(vals[0], 3.0, DECOMP_TOL, "eig[0]");
assert_approx(vals[1], 1.0, DECOMP_TOL, "eig[1]");
let v = eig.eigenvectors();
let vt = v.transpose().unwrap();
let mut d_data = vec![0.0_f64; 4];
d_data[0] = vals[0];
d_data[3] = vals[1];
let d = Tensor::from_vec(d_data, vec![2, 2]).unwrap();
let vd = v.matmul(&d).unwrap();
let reconstructed = vd.matmul(&vt).unwrap();
assert_slice_approx(
reconstructed.as_slice(),
a.as_slice(),
DECOMP_TOL,
"Eigen reconstruction",
);
let vtv = vt.matmul(&v).unwrap();
let eye = Tensor::<f64>::eye(2);
assert_slice_approx(vtv.as_slice(), eye.as_slice(), DECOMP_TOL, "V^T V = I");
}
#[test]
fn test_det_numpy_reference() {
let a = Tensor::from_vec(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0],
vec![3, 3],
)
.unwrap();
let det = linalg::det(&a).unwrap();
assert_approx(det, -3.0, EXACT_TOL, "det");
}
#[test]
fn test_det_tensor_method_numpy_reference() {
let a = Tensor::from_vec(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0],
vec![3, 3],
)
.unwrap();
let det = a.det().unwrap();
assert_approx(det, -3.0, EXACT_TOL, "det (method)");
}
#[test]
fn test_dot_numpy_reference() {
let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], vec![5]).unwrap();
let b = Tensor::from_vec(vec![2.0, 3.0, 4.0, 5.0, 6.0], vec![5]).unwrap();
let result = dot(&a, &b).unwrap();
assert_approx(result, 70.0, EXACT_TOL, "dot product");
}
#[test]
fn test_dot_tensor_method_numpy_reference() {
let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0], vec![5]).unwrap();
let b = Tensor::from_vec(vec![2.0, 3.0, 4.0, 5.0, 6.0], vec![5]).unwrap();
let result = a.dot(&b).unwrap();
assert_approx(result, 70.0, EXACT_TOL, "dot product (method)");
}
#[test]
fn test_fft_numpy_reference() {
let input =
Tensor::from_vec(vec![1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0], vec![4, 2]).unwrap();
let spectrum = fft::fft(&input).unwrap();
assert_eq!(spectrum.shape(), &[4, 2]);
let s = spectrum.as_slice();
let expected_re = [0.0, 2.0, 0.0, 2.0];
let expected_im = [0.0, 0.0, 0.0, 0.0];
for i in 0..4 {
assert_approx(
s[i * 2],
expected_re[i],
DECOMP_TOL,
&format!("FFT re[{i}]"),
);
assert_approx(
s[i * 2 + 1],
expected_im[i],
DECOMP_TOL,
&format!("FFT im[{i}]"),
);
}
}
#[test]
fn test_rfft_numpy_reference() {
let signal = Tensor::from_vec(vec![1.0, 0.0, -1.0, 0.0], vec![4]).unwrap();
let spectrum = fft::rfft(&signal).unwrap();
assert_eq!(spectrum.shape(), &[3, 2]);
let s = spectrum.as_slice();
let expected_re = [0.0, 2.0, 0.0];
let expected_im = [0.0, 0.0, 0.0];
for i in 0..3 {
assert_approx(
s[i * 2],
expected_re[i],
DECOMP_TOL,
&format!("RFFT re[{i}]"),
);
assert_approx(
s[i * 2 + 1],
expected_im[i],
DECOMP_TOL,
&format!("RFFT im[{i}]"),
);
}
}
#[test]
fn test_transpose_numpy_reference() {
let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]).unwrap();
let at = a.transpose().unwrap();
assert_eq!(at.shape(), &[3, 2]);
assert_slice_approx(
at.as_slice(),
&[1.0, 4.0, 2.0, 5.0, 3.0, 6.0],
EXACT_TOL,
"transpose",
);
}
#[test]
fn test_matmul_transpose_identity() {
let a = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
let b = Tensor::from_vec(vec![5.0, 6.0, 7.0, 8.0], vec![2, 2]).unwrap();
let ab = a.matmul(&b).unwrap();
let ab_t = ab.transpose().unwrap();
let bt = b.transpose().unwrap();
let at = a.transpose().unwrap();
let bt_at = bt.matmul(&at).unwrap();
assert_slice_approx(
ab_t.as_slice(),
bt_at.as_slice(),
EXACT_TOL,
"(AB)^T = B^T A^T",
);
}
#[test]
fn test_det_inverse_relationship() {
let a = Tensor::from_vec(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0],
vec![3, 3],
)
.unwrap();
let det_a = linalg::det(&a).unwrap();
assert_approx(det_a, -3.0, EXACT_TOL, "det(A)");
let inv_a = linalg::inv(&a).unwrap();
let det_inv_a = linalg::det(&inv_a).unwrap();
assert_approx(det_inv_a, 1.0 / det_a, DECOMP_TOL, "det(A^{-1}) = 1/det(A)");
}
#[test]
fn test_fft_ifft_roundtrip_numpy_reference() {
let input =
Tensor::from_vec(vec![1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0], vec![4, 2]).unwrap();
let spectrum = fft::fft(&input).unwrap();
let recovered = fft::ifft(&spectrum).unwrap();
assert_slice_approx(
recovered.as_slice(),
input.as_slice(),
DECOMP_TOL,
"FFT round-trip",
);
}
#[test]
fn test_solve_numpy_reference() {
let a = Tensor::from_vec(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0],
vec![3, 3],
)
.unwrap();
let b = Tensor::from_vec(vec![1.0, 2.0, 3.0], vec![3]).unwrap();
let x = linalg::solve(&a, &b).unwrap();
assert_approx(x.as_slice()[0], -1.0 / 3.0, DECOMP_TOL, "solve x[0]");
assert_approx(x.as_slice()[1], 2.0 / 3.0, DECOMP_TOL, "solve x[1]");
assert_approx(x.as_slice()[2], 0.0, DECOMP_TOL, "solve x[2]");
let ax = a.matvec(&x).unwrap();
assert_slice_approx(ax.as_slice(), b.as_slice(), DECOMP_TOL, "A * x = b");
}
#[test]
fn test_cholesky_solve_numpy_reference() {
let a = Tensor::from_vec(vec![4.0, 2.0, 2.0, 3.0], vec![2, 2]).unwrap();
let b = Tensor::from_vec(vec![1.0, 2.0], vec![2]).unwrap();
let chol = CholeskyDecomposition::decompose(&a).unwrap();
let x = chol.solve(&b).unwrap();
let ax = a.matvec(&x).unwrap();
assert_slice_approx(ax.as_slice(), b.as_slice(), DECOMP_TOL, "Cholesky A*x = b");
}