use rust_physics_engine::linalg::{
cholesky, eigen_symmetric, eigenvalues_general, lu_decompose, qr_householder, solve, svd,
thomas_solve, Mat3, Matrix, Qr, Svd,
};
use rust_physics_engine::monte_carlo::Rng;
fn random_matrix(rng: &mut Rng, rows: usize, cols: usize) -> Matrix {
let mut m = Matrix::zeros(rows, cols);
for v in m.data.iter_mut() {
*v = rng.next_f64() * 2.0 - 1.0;
}
m
}
fn random_dominant(rng: &mut Rng, n: usize) -> Matrix {
let mut m = random_matrix(rng, n, n);
for i in 0..n {
let v = m.get(i, i) + n as f64 + 1.0;
m.set(i, i, v);
}
m
}
fn matrices_close(a: &Matrix, b: &Matrix, tol: f64) -> bool {
a.rows == b.rows
&& a.cols == b.cols
&& a.data.iter().zip(&b.data).all(|(&x, &y)| (x - y).abs() <= tol)
}
#[test]
fn prop_transpose_of_product() {
let mut rng = Rng::new(1);
for _ in 0..50 {
let a = random_matrix(&mut rng, 4, 3);
let b = random_matrix(&mut rng, 3, 5);
let lhs = a.mul(&b).unwrap().transpose();
let rhs = b.transpose().mul(&a.transpose()).unwrap();
assert!(matrices_close(&lhs, &rhs, 1e-12));
}
}
#[test]
fn prop_identity_is_neutral() {
let mut rng = Rng::new(2);
for _ in 0..50 {
let a = random_matrix(&mut rng, 4, 4);
let ai = a.mul(&Matrix::identity(4)).unwrap();
assert!(matrices_close(&a, &ai, 0.0));
}
}
#[test]
fn prop_lu_solve_residual() {
let mut rng = Rng::new(3);
for _ in 0..50 {
let a = random_dominant(&mut rng, 5);
let b: Vec<f64> = (0..5).map(|_| rng.next_f64() * 10.0 - 5.0).collect();
let x = solve(&a, &b).unwrap();
let back = a.mul_vec(&x).unwrap();
for (got, want) in back.iter().zip(&b) {
assert!((got - want).abs() < 1e-9, "residual too large: {got} vs {want}");
}
}
}
#[test]
fn prop_lu_determinant_matches_mat3() {
let mut rng = Rng::new(4);
for _ in 0..50 {
let m3 = Mat3::from_rows(
[rng.next_f64(), rng.next_f64(), rng.next_f64()],
[rng.next_f64(), rng.next_f64(), rng.next_f64()],
[rng.next_f64() + 2.0, rng.next_f64(), rng.next_f64() + 3.0],
);
let a = Matrix::from_mat3(&m3);
match lu_decompose(&a) {
Ok(f) => {
let expected = m3.determinant();
assert!(
(f.determinant() - expected).abs() < 1e-10 * expected.abs().max(1.0),
"lu det {} vs mat3 det {}",
f.determinant(),
expected
);
}
Err(_) => {
assert!(m3.determinant().abs() < 1e-6);
}
}
}
}
#[test]
fn prop_qr_invariants() {
let mut rng = Rng::new(6);
for trial in 0..50 {
let (m, n) = if trial % 2 == 0 { (5, 3) } else { (4, 4) };
let a = random_matrix(&mut rng, m, n);
let Qr { q, r } = qr_householder(&a);
let qtq = q.transpose().mul(&q).unwrap();
assert!(matrices_close(&qtq, &Matrix::identity(m), 1e-11), "Q not orthogonal");
let qr = q.mul(&r).unwrap();
assert!(matrices_close(&qr, &a, 1e-11), "QR != A");
for i in 1..m {
for j in 0..i.min(n) {
assert!(r.get(i, j).abs() <= 1e-12, "R not upper triangular");
}
}
}
}
#[test]
fn prop_thomas_matches_dense_lu() {
let mut rng = Rng::new(8);
for _ in 0..50 {
let n = 6;
let diag: Vec<f64> = (0..n).map(|_| rng.next_f64() + 4.0).collect();
let sub: Vec<f64> = (0..n - 1).map(|_| rng.next_f64() - 0.5).collect();
let sup: Vec<f64> = (0..n - 1).map(|_| rng.next_f64() - 0.5).collect();
let rhs: Vec<f64> = (0..n).map(|_| rng.next_f64() * 4.0 - 2.0).collect();
let mut dense = Matrix::zeros(n, n);
for i in 0..n {
dense.set(i, i, diag[i]);
if i + 1 < n {
dense.set(i + 1, i, sub[i]);
dense.set(i, i + 1, sup[i]);
}
}
let x_thomas = thomas_solve(&sub, &diag, &sup, &rhs).unwrap();
let x_dense = solve(&dense, &rhs).unwrap();
for (a, b) in x_thomas.iter().zip(&x_dense) {
assert!((a - b).abs() < 1e-10, "thomas {a} vs dense {b}");
}
}
}
#[test]
fn prop_symmetric_eigen_invariants() {
let mut rng = Rng::new(31);
for _ in 0..30 {
let n = 4;
let m = random_matrix(&mut rng, n, n);
let a = m.add(&m.transpose()).unwrap().scale(0.5);
let e = eigen_symmetric(&a, 1e-13, 100).unwrap();
for k in 0..n {
let v: Vec<f64> = (0..n).map(|r| e.vectors.get(r, k)).collect();
let av = a.mul_vec(&v).unwrap();
for r in 0..n {
assert!((av[r] - e.values[k] * v[r]).abs() < 1e-9, "A v != lambda v");
}
}
let trace: f64 = (0..n).map(|i| a.get(i, i)).sum();
let sum: f64 = e.values.iter().sum();
assert!((trace - sum).abs() < 1e-9);
let det = lu_decompose(&a).map(|f| f.determinant()).unwrap_or(0.0);
let prod: f64 = e.values.iter().product();
assert!((det - prod).abs() < 1e-8 * det.abs().max(1.0));
}
}
#[test]
fn prop_general_eigenvalues_trace() {
let mut rng = Rng::new(32);
for _ in 0..30 {
let n = 5;
let a = random_matrix(&mut rng, n, n);
let eig = eigenvalues_general(&a, 60).unwrap();
let trace: f64 = (0..n).map(|i| a.get(i, i)).sum();
let sum_re: f64 = eig.iter().map(|c| c.re).sum();
let sum_im: f64 = eig.iter().map(|c| c.im).sum();
assert!((trace - sum_re).abs() < 1e-8, "trace {trace} vs {sum_re}");
assert!(sum_im.abs() < 1e-8);
}
}
#[test]
fn prop_svd_reconstruction() {
let mut rng = Rng::new(33);
for trial in 0..30 {
let (m, n) = if trial % 2 == 0 { (5, 3) } else { (3, 5) };
let a = random_matrix(&mut rng, m, n);
let Svd { u, sigma, vt } = svd(&a).unwrap();
for w in sigma.windows(2) {
assert!(w[0] >= w[1], "singular values not descending");
}
assert!(sigma.iter().all(|&s| s >= 0.0));
let k = sigma.len();
let mut sig = Matrix::zeros(k, k);
for i in 0..k {
sig.set(i, i, sigma[i]);
}
let back = u.mul(&sig).unwrap().mul(&vt).unwrap();
assert!(matrices_close(&back, &a, 1e-9), "U S Vt != A");
}
}
#[test]
fn prop_cg_residual_below_tol() {
use rust_physics_engine::linalg::{conjugate_gradient, pcg_jacobi, CsrMatrix};
let mut rng = Rng::new(34);
for _ in 0..10 {
let a = CsrMatrix::laplacian_2d(8, 8, 0.25);
let n = 64;
let b: Vec<f64> = (0..n).map(|_| rng.next_f64() * 2.0 - 1.0).collect();
let tol = 1e-10;
let bnorm = b.iter().map(|x| x * x).sum::<f64>().sqrt().max(1.0);
for x in [
conjugate_gradient(&a, &b, &vec![0.0; n], tol, 10_000).unwrap(),
pcg_jacobi(&a, &b, tol, 10_000).unwrap(),
] {
let ax = a.mul_vec(&x);
let res: f64 = ax
.iter()
.zip(&b)
.map(|(axi, bi)| (axi - bi) * (axi - bi))
.sum::<f64>()
.sqrt();
assert!(res <= tol * bnorm * 1.01, "residual {res} above tol");
}
}
}
#[test]
fn prop_cholesky_reconstructs_spd() {
let mut rng = Rng::new(5);
for _ in 0..50 {
let n = 4;
let m = random_matrix(&mut rng, n, n);
let a = m
.mul(&m.transpose())
.unwrap()
.add(&Matrix::identity(n).scale(n as f64))
.unwrap();
let l = cholesky(&a).unwrap();
let llt = l.mul(&l.transpose()).unwrap();
assert!(matrices_close(&llt, &a, 1e-9));
}
}