pub mod density;
pub mod distance;
pub mod ensemble;
pub mod error;
pub mod handle;
pub mod isolation;
pub mod metrics;
pub mod ptx_kernels;
pub mod reconstruction;
pub mod statistical;
pub mod svdd;
pub mod prelude {
pub use crate::density::copod::Copod;
pub use crate::density::mahalanobis::MahalanobisDetector;
pub use crate::distance::knn_score::KnnAnomalyScorer;
pub use crate::distance::lof::Lof;
pub use crate::ensemble::ensemble::{AnomalyEnsemble, EnsembleMethod};
pub use crate::error::{AnomalyError, AnomalyResult};
pub use crate::handle::{AnomalyHandle, LcgRng, SmVersion};
pub use crate::isolation::iforest_score::{
IsolationScorer, c_factor, isolation_score_from_path,
};
pub use crate::metrics::anomaly_metrics::{
AnomalyDetectionMetrics, auc_pr, auc_roc_anomaly, compute_detection_metrics,
f1_at_threshold,
};
pub use crate::ptx_kernels::{
copod_ecdf_ptx, ensemble_normalize_ptx, f32_hex, iforest_score_ptx, lof_reach_dist_ptx,
mahal_dist_ptx, recon_score_ptx, svdd_loss_ptx,
};
pub use crate::reconstruction::autoencoder::{AeConfig, AutoencoderAnomaly};
pub use crate::reconstruction::vae_anomaly::VaeAnomaly;
pub use crate::statistical::stats::{MadDetector, ZScoreDetector, percentile_threshold};
pub use crate::svdd::deep_svdd::DeepSvdd;
}
#[cfg(test)]
mod e2e_tests {
use crate::prelude::*;
#[test]
fn e2e_autoencoder_normal_low_score() {
let cfg = AeConfig {
encoder_dims: vec![4, 2],
decoder_dims: vec![2, 4],
};
let mut rng = LcgRng::new(1);
let ae = AutoencoderAnomaly::new(cfg, &mut rng).unwrap();
let train = vec![0.5_f32; 4];
let s_train = ae.score(&train).unwrap();
let mut noise = vec![0.0_f32; 4];
let mut rng2 = LcgRng::new(999);
rng2.fill_normal(&mut noise);
let s_noise = ae.score(&noise).unwrap();
assert!(s_train.is_finite(), "train_score={s_train}");
assert!(s_noise.is_finite(), "noise_score={s_noise}");
}
#[test]
fn e2e_autoencoder_score_finite() {
let cfg = AeConfig {
encoder_dims: vec![8, 4, 2],
decoder_dims: vec![2, 4, 8],
};
let mut rng = LcgRng::new(2);
let ae = AutoencoderAnomaly::new(cfg, &mut rng).unwrap();
let s = ae.score(&[0.3_f32; 8]).unwrap();
assert!(s.is_finite() && s >= 0.0, "s={s}");
}
#[test]
fn e2e_vae_score_finite() {
let mut rng = LcgRng::new(3);
let vae = VaeAnomaly::new(&[8, 4], 2, &[2, 4, 8], &mut rng).unwrap();
let s = vae.anomaly_score(&[0.2_f32; 8], &mut rng).unwrap();
assert!(s.is_finite(), "s={s}");
}
#[test]
fn e2e_deep_svdd_score_increases_for_outlier() {
let mut rng = LcgRng::new(4);
let mut svdd = DeepSvdd::new(&[4, 8, 4], &mut rng).unwrap();
let train = vec![0.1_f32; 4 * 20];
svdd.fit(&train, 20).unwrap();
let close = [0.1_f32, 0.1, 0.1, 0.1];
let far = [100.0_f32, 100.0, 100.0, 100.0];
let s_close = svdd.score(&close).unwrap();
let s_far = svdd.score(&far).unwrap();
assert!(
s_far > s_close,
"far score {s_far} should > close score {s_close}"
);
}
#[test]
fn e2e_lof_trivial_normal_case() {
let n = 20_usize;
let data: Vec<f32> = (0..n).map(|i| i as f32).collect();
let mut lof = Lof::new(3);
lof.fit(&data, n, 1).unwrap();
let s = lof.score(&[10.0_f32]).unwrap();
assert!(s.is_finite(), "lof={s}");
assert!(s > 0.0, "lof > 0");
}
#[test]
fn e2e_copod_known_outlier() {
let n = 30_usize;
let data: Vec<f32> = (0..n).map(|i| i as f32 * 0.1).collect();
let mut copod = Copod::new();
copod.fit(&data, n, 1).unwrap();
let s_normal = copod.score(&[1.5_f32]).unwrap();
let s_outlier = copod.score(&[100.0_f32]).unwrap();
assert!(
s_outlier > s_normal,
"outlier {s_outlier} should > normal {s_normal}"
);
}
#[test]
fn e2e_mahalanobis_known_outlier() {
let data = vec![
1.0_f32, 2.0, 1.1, 1.9, 0.9, 2.1, 1.05, 1.95, 0.95, 2.05, 1.0_f32, 2.0, 1.1, 1.9, 0.9,
2.1, 1.05, 1.95, 0.95, 2.05,
];
let mut det = MahalanobisDetector::new();
det.fit(&data, 10, 2).unwrap();
let s_normal = det.score(&[1.0_f32, 2.0]).unwrap();
let s_outlier = det.score(&[50.0_f32, 100.0]).unwrap();
assert!(
s_outlier > s_normal,
"outlier {s_outlier} > normal {s_normal}"
);
}
#[test]
fn e2e_iforest_score_in_range() {
let mut rng = LcgRng::new(8);
let n = 100_usize;
let data: Vec<f32> = (0..n)
.flat_map(|i| vec![i as f32 * 0.1, i as f32 * 0.05])
.collect();
let mut scorer = IsolationScorer::new(50, &mut rng);
scorer.fit(&data, n, 2, &mut rng).unwrap();
let s = scorer.score(&[5.0_f32, 2.5]).unwrap();
assert!((0.0..=1.0).contains(&s), "s={s}");
}
#[test]
fn e2e_zscore_known_outlier() {
let n = 20_usize;
let data: Vec<f32> = (0..n).map(|i| i as f32 * 0.1).collect();
let mut det = ZScoreDetector::new();
det.fit(&data, n, 1).unwrap();
let s_normal = det.score(&[1.0_f32]).unwrap();
let s_outlier = det.score(&[1000.0_f32]).unwrap();
assert!(
s_outlier > s_normal,
"outlier {s_outlier} > normal {s_normal}"
);
}
#[test]
fn e2e_mad_detector_finite() {
let n = 20_usize;
let data: Vec<f32> = (0..n)
.flat_map(|i| vec![i as f32, (i * 2) as f32])
.collect();
let mut det = MadDetector::new();
det.fit(&data, n, 2).unwrap();
let scores = det.score_batch(&data, n).unwrap();
assert!(scores.iter().all(|s| s.is_finite()), "not all finite");
}
#[test]
fn e2e_ensemble_combine_finite() {
let n_det = 3_usize;
let n = 20_usize;
let mut rng = LcgRng::new(11);
let train_scores: Vec<f32> = (0..n * n_det).map(|_| rng.next_f32()).collect();
let mut ens = AnomalyEnsemble::new(EnsembleMethod::Average, n_det);
ens.fit(&train_scores, n).unwrap();
let test = [0.5_f32, 0.8, 0.3];
let s = ens.combine(&test).unwrap();
assert!(s.is_finite(), "s={s}");
assert!((0.0..=1.0).contains(&s), "s={s} not in [0,1]");
}
#[test]
#[allow(clippy::type_complexity)]
fn e2e_ptx_kernels_all_sm_versions() {
let sm_versions = [75_u32, 80, 86, 90, 100, 120];
let kernel_fns: &[(&str, fn(u32) -> String)] = &[
("svdd_loss_kernel", svdd_loss_ptx),
("recon_score_kernel", recon_score_ptx),
("lof_reach_dist_kernel", lof_reach_dist_ptx),
("copod_ecdf_kernel", copod_ecdf_ptx),
("mahal_dist_kernel", mahal_dist_ptx),
("iforest_score_kernel", iforest_score_ptx),
("ensemble_normalize_kernel", ensemble_normalize_ptx),
];
for sm in sm_versions {
for (kernel_name, gen_fn) in kernel_fns {
let ptx = gen_fn(sm);
assert!(
ptx.contains(&format!("sm_{sm}")),
"PTX for {kernel_name} sm={sm} missing sm target"
);
assert!(
ptx.contains(".version"),
"PTX for {kernel_name} sm={sm} missing .version"
);
assert!(
ptx.contains(".visible .entry"),
"PTX for {kernel_name} sm={sm} missing .visible .entry"
);
assert!(
ptx.contains(kernel_name),
"PTX for {kernel_name} sm={sm} missing kernel name"
);
}
}
assert_eq!(f32_hex(1.0_f32), "0F3F800000");
}
}