use ferrolearn_core::traits::{Fit, Transform};
use ferrolearn_decomp::{DictTransformAlgorithm, DictionaryLearning};
use ndarray::Array2;
fn probe_x() -> Array2<f64> {
Array2::<f64>::from_shape_vec(
(6, 4),
vec![
1., 2., 3., 0., 4., 5., 6., 1., 7., 8., 9., 2., 2., 1., 0., 3., 0., 3., 4., 1., 3., 0., 1., 2., ],
)
.expect("6x4 literal data is well-formed")
}
#[test]
fn green_components_shape_matches_sklearn() {
let x = probe_x();
let fitted = DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(20)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
assert_eq!(fitted.components().dim(), (3, 4));
}
#[test]
fn green_transform_codes_shape_matches_sklearn() {
let x = probe_x();
let fitted = DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(20)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
let codes = fitted.transform(&x).expect("transform on fit X");
assert_eq!(codes.dim(), (6, 3));
}
#[test]
fn green_omp_respects_nonzero_cap() {
let x = probe_x();
let fitted = DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(20)
.with_transform_algorithm(DictTransformAlgorithm::Omp)
.with_transform_n_nonzero_coefs(2)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
let codes = fitted.transform(&x).expect("OMP transform");
for i in 0..codes.nrows() {
let nnz = codes.row(i).iter().filter(|&&v| v.abs() > 1e-12).count();
assert!(nnz <= 2, "row {i}: OMP produced {nnz} nnz, cap is 2");
}
}
#[test]
fn green_lasso_cd_transform_has_zeros() {
let x = probe_x();
let fitted = DictionaryLearning::new(8)
.with_alpha(2.0)
.with_max_iter(20)
.with_transform_algorithm(DictTransformAlgorithm::LassoCd)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
let codes = fitted.transform(&x).expect("LassoCd transform");
let zeros = codes.iter().filter(|&&v| v.abs() < 1e-12).count();
assert!(zeros > 0, "LassoCd codes should contain exact zeros, got 0");
}
#[test]
fn green_reconstruction_err_finite_and_n_iter_in_range() {
let x = probe_x();
let max_iter = 25;
let fitted = DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(max_iter)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
let err = fitted.reconstruction_err();
assert!(err.is_finite() && err >= 0.0, "reconstruction_err = {err}");
let n = fitted.n_iter();
assert!(
(1..=max_iter).contains(&n),
"n_iter = {n} must be in [1, {max_iter}]"
);
}
#[test]
fn green_atoms_unit_l2_norm() {
let x = probe_x();
let fitted = DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(20)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
let d = fitted.components();
for k in 0..d.nrows() {
let norm: f64 = d.row(k).iter().map(|v| v * v).sum::<f64>().sqrt();
assert!(
(norm - 1.0).abs() < 1e-6,
"atom {k}: L2 norm {norm} (sklearn oracle: 1.0)"
);
}
}
#[test]
fn green_determinism_same_seed() {
let x = probe_x();
let build = || {
DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(20)
.with_random_state(7)
.fit(&x, &())
.expect("fit on well-formed 6x4 X")
};
let a = build();
let b = build();
let da = a.components();
let db = b.components();
assert_eq!(da.dim(), db.dim());
for (x, y) in da.iter().zip(db.iter()) {
assert_eq!(x.to_bits(), y.to_bits(), "components differ across fits");
}
let ca = a.transform(&x).expect("transform a");
let cb = b.transform(&x).expect("transform b");
for (x, y) in ca.iter().zip(cb.iter()) {
assert_eq!(x.to_bits(), y.to_bits(), "transform codes differ");
}
}
#[test]
fn green_err_n_components_zero() {
let x = probe_x();
assert!(DictionaryLearning::new(0).fit(&x, &()).is_err());
}
#[test]
fn green_err_zero_samples() {
let x = Array2::<f64>::zeros((0, 4));
assert!(DictionaryLearning::new(2).fit(&x, &()).is_err());
}
#[test]
fn green_err_zero_features() {
let x = Array2::<f64>::zeros((6, 0));
assert!(DictionaryLearning::new(2).fit(&x, &()).is_err());
}
#[test]
fn green_err_alpha_negative() {
let x = probe_x();
assert!(
DictionaryLearning::new(3)
.with_alpha(-1.0)
.fit(&x, &())
.is_err()
);
}
#[test]
fn green_err_transform_col_mismatch() {
let x = probe_x();
let fitted = DictionaryLearning::new(3)
.with_max_iter(10)
.with_random_state(0)
.fit(&x, &())
.expect("fit on well-formed 6x4 X");
let x_bad = Array2::<f64>::zeros((5, 3)); assert!(fitted.transform(&x_bad).is_err());
}
#[test]
fn carveout_components_not_value_pinned() {
let x = probe_x();
let fitted = DictionaryLearning::new(3)
.with_alpha(1.0)
.with_max_iter(20)
.with_random_state(0)
.fit(&x, &())
.expect("fit on the oracle probe input");
assert!(fitted.components().iter().all(|v| v.is_finite()));
assert_eq!(fitted.components().dim(), (3, 4));
}