use numrs2::array::Array;
use numrs2::linalg_stable::StableDecompositions;
use numrs2::random::distributions_enhanced::nearest_correlation_matrix;
use numrs2::random::random_correlation_matrix;
use numrs2::random::sobol_sequence;
use serial_test::serial;
#[test]
fn test_sobol_dim1_matches_scipy_known_values() {
let samples = sobol_sequence::<f64>(1, 8).expect("test: sobol_sequence should succeed");
let data = samples.to_vec();
let expected = [0.0, 0.5, 0.75, 0.25, 0.375, 0.875, 0.625, 0.125];
assert_eq!(data.len(), expected.len());
for (i, (&got, &want)) in data.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-9,
"point {i}: got {got}, expected {want}"
);
}
}
#[test]
fn test_sobol_dim2_matches_scipy_known_values() {
let samples = sobol_sequence::<f64>(2, 8).expect("test: sobol_sequence should succeed");
let data = samples.to_vec();
#[rustfmt::skip]
let expected: [f64; 16] = [
0.0, 0.0,
0.5, 0.5,
0.75, 0.25,
0.25, 0.75,
0.375, 0.375,
0.875, 0.875,
0.625, 0.125,
0.125, 0.625,
];
assert_eq!(data.len(), expected.len());
for (i, (&got, &want)) in data.iter().zip(expected.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-9,
"entry {i}: got {got}, expected {want}"
);
}
}
#[test]
fn test_sobol_stratification_beyond_old_8dim_table() {
let k = 4usize;
let n = 1usize << k;
for &dim in &[1usize, 5, 9, 17, 33] {
let samples = sobol_sequence::<f64>(dim, n).unwrap_or_else(|e| {
panic!("test: sobol_sequence(dim={dim}, n={n}) should succeed: {e:?}")
});
let data = samples.to_vec();
for d in 0..dim {
let mut bin_hits = vec![0usize; n];
for i in 0..n {
let v = data[i * dim + d];
assert!(
(0.0..1.0).contains(&v),
"dim={dim} coord={d} point={i}: value {v} out of [0,1)"
);
let mut bin = (v * n as f64).floor() as usize;
if bin >= n {
bin = n - 1;
}
bin_hits[bin] += 1;
}
for (bin, &hits) in bin_hits.iter().enumerate() {
assert_eq!(
hits, 1,
"dim={dim} coord={d}: bin {bin} hit {hits} times (expected exactly 1) -- \
stratification violated, points: {data:?}"
);
}
}
}
}
#[test]
fn test_sobol_dim_zero_is_rejected() {
let result = sobol_sequence::<f64>(0, 4);
assert!(result.is_err(), "dim=0 should be rejected");
}
#[test]
fn test_sobol_dim_above_40_is_rejected() {
let result = sobol_sequence::<f64>(41, 4);
assert!(
result.is_err(),
"dim=41 should be rejected (embedded Joe-Kuo table covers dims 1..=40)"
);
}
#[test]
fn test_sobol_dim_40_succeeds() {
let result = sobol_sequence::<f64>(40, 4);
assert!(result.is_ok(), "dim=40 (table boundary) should succeed");
}
#[test]
fn test_sobol_points_in_unit_interval() {
for &dim in &[1usize, 8, 20, 40] {
let samples = sobol_sequence::<f64>(dim, 64).expect("test: sobol_sequence should succeed");
for v in samples.to_vec() {
assert!((0.0..1.0).contains(&v), "dim={dim}: value {v} out of [0,1)");
}
}
}
#[test]
fn test_nearest_correlation_matrix_higham_example() {
#[rustfmt::skip]
let a: Array<f64> = Array::from_vec(vec![
1.0, 1.0, 0.0,
1.0, 1.0, 1.0,
0.0, 1.0, 1.0,
])
.reshape(&[3, 3]);
let result = nearest_correlation_matrix(&a)
.expect("test: nearest_correlation_matrix should converge on the Higham example");
let data = result.to_vec();
for i in 0..3 {
let d = data[i * 3 + i];
assert!((d - 1.0).abs() < 1e-8, "diag[{i}] = {d}, expected 1.0");
}
for i in 0..3 {
for j in 0..3 {
assert!(
(data[i * 3 + j] - data[j * 3 + i]).abs() < 1e-10,
"not symmetric at ({i},{j})"
);
}
}
assert!(
(data[1] - 0.760_689_85).abs() < 1e-4,
"corr[0,1] = {}, expected ~0.76069",
data[1]
);
assert!(
(data[2] - 0.157_298_11).abs() < 1e-4,
"corr[0,2] = {}, expected ~0.15730",
data[2]
);
let min_eig = min_eigenvalue_3x3_symmetric(&data);
assert!(
min_eig >= -1e-10,
"min eigenvalue {min_eig} should be >= -1e-10 (PSD)"
);
}
#[test]
fn test_nearest_correlation_matrix_already_valid_returns_itself() {
#[rustfmt::skip]
let b: Array<f64> = Array::from_vec(vec![
1.0, 0.5, 0.3,
0.5, 1.0, 0.2,
0.3, 0.2, 1.0,
])
.reshape(&[3, 3]);
let result = nearest_correlation_matrix(&b)
.expect("test: nearest_correlation_matrix should succeed on an already-valid matrix");
let data = result.to_vec();
let original = b.to_vec();
for (i, (&got, &want)) in data.iter().zip(original.iter()).enumerate() {
assert!(
(got - want).abs() < 1e-8,
"entry {i}: got {got}, expected ~{want} (already-valid input should be near-fixed)"
);
}
}
#[test]
fn test_nearest_correlation_matrix_strongly_indefinite_input() {
#[rustfmt::skip]
let a: Array<f64> = Array::from_vec(vec![
1.0, 0.9, -0.9, 0.9,
0.9, 1.0, 0.9, -0.9,
-0.9, 0.9, 1.0, 0.9,
0.9, -0.9, 0.9, 1.0,
])
.reshape(&[4, 4]);
let result =
nearest_correlation_matrix(&a).expect("test: nearest_correlation_matrix should converge");
let data = result.to_vec();
let n = 4;
for i in 0..n {
assert!(
(data[i * n + i] - 1.0).abs() < 1e-8,
"diag[{i}] = {}",
data[i * n + i]
);
for j in 0..n {
assert!(
(data[i * n + j] - data[j * n + i]).abs() < 1e-10,
"not symmetric at ({i},{j})"
);
}
}
let result_array = Array::from_vec(data).reshape(&[n, n]);
let (eigenvalues, _) = StableDecompositions::symmetric_eigendecomposition(&result_array)
.expect("test: symmetric_eigendecomposition should succeed on the projected result");
let min_eig = eigenvalues.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(
min_eig >= -1e-8,
"min eigenvalue {min_eig} should be (numerically) >= 0"
);
}
#[test]
fn test_nearest_correlation_matrix_rejects_non_square() {
let a: Array<f64> = Array::from_vec(vec![1.0, 0.5, 0.5, 1.0, 0.2, 0.3]).reshape(&[2, 3]);
let result = nearest_correlation_matrix(&a);
assert!(result.is_err(), "non-square input should be rejected");
}
#[test]
#[serial]
fn test_random_correlation_matrix_converges_and_is_valid() {
for &n in &[2usize, 3, 5, 10, 20, 30, 50] {
let result = random_correlation_matrix::<f64>(n);
let corr = result.unwrap_or_else(|e| {
panic!("random_correlation_matrix(n={n}) should converge, got error: {e:?}")
});
let data = corr.to_vec();
assert_eq!(data.len(), n * n);
for i in 0..n {
assert!(
(data[i * n + i] - 1.0).abs() < 1e-6,
"n={n}: diag[{i}] = {}, expected 1.0",
data[i * n + i]
);
for j in 0..n {
assert!(
(data[i * n + j] - data[j * n + i]).abs() < 1e-8,
"n={n}: not symmetric at ({i},{j})"
);
}
}
let corr_array = Array::from_vec(data).reshape(&[n, n]);
let (eigenvalues, _) = StableDecompositions::symmetric_eigendecomposition(&corr_array)
.unwrap_or_else(|e| {
panic!("n={n}: symmetric_eigendecomposition on the result should succeed: {e:?}")
});
let min_eig = eigenvalues.iter().cloned().fold(f64::INFINITY, f64::min);
assert!(
min_eig >= -1e-6,
"n={n}: min eigenvalue {min_eig} should be (numerically) >= 0"
);
}
}
fn min_eigenvalue_3x3_symmetric(m: &[f64]) -> f64 {
let a = m; let p1 = a[1] * a[1] + a[2] * a[2] + a[5] * a[5];
if p1 < 1e-14 {
return a[0].min(a[4]).min(a[8]);
}
let q = (a[0] + a[4] + a[8]) / 3.0;
let p2 = (a[0] - q).powi(2) + (a[4] - q).powi(2) + (a[8] - q).powi(2) + 2.0 * p1;
let p = (p2 / 6.0).sqrt();
let mut b = [0.0f64; 9];
for i in 0..9 {
b[i] = a[i] / p;
}
b[0] -= q / p;
b[4] -= q / p;
b[8] -= q / p;
let det_b = b[0] * (b[4] * b[8] - b[5] * b[7]) - b[1] * (b[3] * b[8] - b[5] * b[6])
+ b[2] * (b[3] * b[7] - b[4] * b[6]);
let r = (det_b / 2.0).clamp(-1.0, 1.0);
let phi = r.acos() / 3.0;
let eig1 = q + 2.0 * p * phi.cos();
let eig3 = q + 2.0 * p * (phi + 2.0 * std::f64::consts::PI / 3.0).cos();
let eig2 = 3.0 * q - eig1 - eig3;
eig1.min(eig2).min(eig3)
}