use ferrolearn_core::error::FerroError;
use ferrolearn_core::traits::{Fit, Transform};
use ferrolearn_decomp::SparsePCA;
use ndarray::{Array2, array};
fn fixed_x() -> Array2<f64> {
array![
[1.0, 2.0, 0.0, 3.0, 1.0],
[4.0, 0.0, 5.0, 6.0, 0.0],
[7.0, 8.0, 0.0, 9.0, 2.0],
[0.0, 1.0, 2.0, 0.0, 3.0],
[3.0, 0.0, 4.0, 1.0, 5.0],
[2.0, 6.0, 0.0, 4.0, 1.0],
]
}
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false, ...) guards unreachable Err/Ok arms in this divergence pin"
)]
#[test]
fn divergence_transform_is_ridge_not_projection() {
#[allow(
clippy::excessive_precision,
reason = "hard-coded live sklearn 1.5.2 ridge_regression oracle (R-CHAR-3)"
)]
const SK_RIDGE_U: [[f64; 2]; 6] = [
[-1.2975484318260249, -1.349816587476714],
[0.16293758138327855, 5.194195080631285],
[8.393119431806431, -0.08720686487644684],
[-4.977119644128173, -1.8498773447302455],
[-4.25491777438539, 0.7713337715103278],
[1.9735288371498747, -2.678628055058207],
];
let x = fixed_x();
let fitted = match SparsePCA::<f64>::new(2).with_random_state(0).fit(&x, &()) {
Ok(f) => f,
Err(e) => {
assert!(false, "fit unexpectedly failed: {e:?}");
return;
}
};
let got = match fitted.transform(&x) {
Ok(t) => t,
Err(e) => {
assert!(false, "transform unexpectedly failed: {e:?}");
return;
}
};
assert_eq!(got.dim(), (6, 2), "transform shape");
for i in 0..6 {
for k in 0..2 {
let diff = (got[[i, k]] - SK_RIDGE_U[i][k]).abs();
assert!(
diff < 1e-6,
"transform[{i}][{k}] = {} diverges from sklearn ridge oracle {} (diff {diff:.6}); \
ferrolearn returns the plain projection, omitting (C·Cᵀ + 0.01·I)⁻¹",
got[[i, k]],
SK_RIDGE_U[i][k]
);
}
}
}
#[test]
fn divergence_transform_ridge_formula_differs_from_projection() {
let c = array![[0.8_f64, 0.6, 0.0], [0.0, 0.6, 0.8]];
let x = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let mean = array![4.0_f64, 5.0, 6.0];
let mut xc = x.clone();
for mut row in xc.rows_mut() {
for (v, &m) in row.iter_mut().zip(mean.iter()) {
*v -= m;
}
}
let proj = xc.dot(&c.t());
let cct = c.dot(&c.t());
let alpha = 0.01_f64;
let a = cct[[0, 0]] + alpha;
let b = cct[[0, 1]];
let d = cct[[1, 1]] + alpha;
let det = a * d - b * b;
let inv = array![[d / det, -b / det], [-b / det, a / det]];
let ridge = xc.dot(&c.t()).dot(&inv);
let sk_ridge = array![[-3.065693_f64, -3.065693], [0.0, 0.0], [3.065693, 3.065693]];
for i in 0..3 {
for k in 0..2 {
assert!(
(ridge[[i, k]] - sk_ridge[[i, k]]).abs() < 1e-6,
"closed-form ridge[{i}][{k}]={} != sklearn ridge oracle {}",
ridge[[i, k]],
sk_ridge[[i, k]]
);
}
}
let max_diff = ridge
.iter()
.zip(proj.iter())
.map(|(r, p)| (r - p).abs())
.fold(0.0_f64, f64::max);
assert!(
max_diff > 1.0,
"ridge and plain projection must differ (max diff {max_diff:.6}); \
sklearn transform uses ridge, ferrolearn uses projection"
);
}
#[test]
fn green_components_shape() {
let x = fixed_x();
let fitted = SparsePCA::<f64>::new(2)
.with_random_state(0)
.fit(&x, &())
.expect("fit should succeed on fixed_x");
assert_eq!(fitted.components().dim(), (2, 5));
}
#[test]
fn green_components_have_exact_zeros() {
let x = fixed_x();
let fitted = SparsePCA::<f64>::new(2)
.with_alpha(5.0)
.with_random_state(0)
.fit(&x, &())
.expect("fit should succeed on fixed_x");
let zeros = fitted.components().iter().filter(|v| **v == 0.0).count();
assert!(
zeros > 0,
"L1 penalty should produce at least one exact zero, found {zeros}"
);
}
#[test]
fn green_mean_is_column_means() {
let x = fixed_x();
let (n, p) = x.dim();
let mut expected = vec![0.0_f64; p];
for j in 0..p {
let mut s = 0.0;
for i in 0..n {
s += x[[i, j]];
}
expected[j] = s / n as f64;
}
let fitted = SparsePCA::<f64>::new(2)
.with_random_state(0)
.fit(&x, &())
.expect("fit should succeed on fixed_x");
let mean = fitted.mean();
for j in 0..p {
assert!(
(mean[j] - expected[j]).abs() < 1e-9,
"mean[{j}] = {} != column mean {}",
mean[j],
expected[j]
);
}
}
#[test]
fn green_determinism_same_seed() {
let x = fixed_x();
let f1 = SparsePCA::<f64>::new(2)
.with_random_state(0)
.fit(&x, &())
.expect("fit 1");
let f2 = SparsePCA::<f64>::new(2)
.with_random_state(0)
.fit(&x, &())
.expect("fit 2");
let c1 = f1.components();
let c2 = f2.components();
assert_eq!(c1.dim(), c2.dim());
for (a, b) in c1.iter().zip(c2.iter()) {
assert!(
(a - b).abs() < 1e-12,
"components differ across seeded runs"
);
}
let t1 = f1.transform(&x).expect("transform 1");
let t2 = f2.transform(&x).expect("transform 2");
for (a, b) in t1.iter().zip(t2.iter()) {
assert!(
(a - b).abs() < 1e-12,
"transform differs across seeded runs"
);
}
}
#[test]
fn green_converges_finite() {
let x = fixed_x();
let max_iter = 50;
let fitted = SparsePCA::<f64>::new(2)
.with_max_iter(max_iter)
.with_random_state(0)
.fit(&x, &())
.expect("fit should succeed");
assert!(fitted.n_iter() >= 1);
assert!(fitted.n_iter() <= max_iter, "n_iter exceeds max_iter");
let t = fitted.transform(&x).expect("transform");
assert!(t.iter().all(|v| v.is_finite()), "transform must be finite");
}
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false, ...) marks an unreachable non-error match arm"
)]
#[test]
fn green_n_components_zero_errors() {
let x = array![[1.0_f64, 2.0], [3.0, 4.0]];
match SparsePCA::<f64>::new(0).fit(&x, &()) {
Err(FerroError::InvalidParameter { .. }) => {}
other => assert!(false, "expected InvalidParameter, got {other:?}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false, ...) marks an unreachable non-error match arm"
)]
#[test]
fn green_n_components_too_large_errors() {
let x = array![[1.0_f64, 2.0], [3.0, 4.0], [5.0, 6.0]];
match SparsePCA::<f64>::new(5).fit(&x, &()) {
Err(FerroError::InvalidParameter { .. }) => {}
other => assert!(false, "expected InvalidParameter, got {other:?}"),
}
}
#[allow(
clippy::assertions_on_constants,
reason = "assert!(false, ...) marks an unreachable non-error match arm"
)]
#[test]
fn green_transform_shape_and_mismatch() {
let x = fixed_x();
let fitted = SparsePCA::<f64>::new(2)
.with_random_state(0)
.fit(&x, &())
.expect("fit should succeed");
let t = fitted.transform(&x).expect("transform");
assert_eq!(t.dim(), (6, 2), "transform shape (n_samples, n_components)");
let bad = array![[1.0_f64, 2.0, 3.0]]; match fitted.transform(&bad) {
Err(FerroError::ShapeMismatch { .. }) => {}
other => assert!(false, "expected ShapeMismatch, got {other:?}"),
}
}