Files
rustytorch/crates/specialized/rtx-nmf/tests/tdd_comprehensive_test.rs
T
2026-03-04 00:08:42 +00:00

424 lines
12 KiB
Rust

//! Comprehensive tests for rtx-nmf (Non-negative Matrix Factorization)
//! Tests both the GPU-accelerated NMFDecomposer and CPU-based HonestNMF
use rtx_nmf::*;
use rtx_tensor::{Device, Tensor};
#[cfg(test)]
mod nmf_config_tests {
use super::*;
#[test]
fn test_nmf_config_creation_with_defaults() {
// Test: NMFConfig should initialize with default parameters
let config = NMFConfig::new();
assert_eq!(config.components(), 10);
assert_eq!(config.max_iterations(), 100);
assert_eq!(config.tolerance(), 1e-4);
}
#[test]
fn test_nmf_config_with_custom_parameters() {
// Test: NMFConfig should accept custom parameters
let config = NMFConfig::new()
.with_components(5)
.with_max_iterations(200)
.with_tolerance(1e-6)
.with_epsilon(1e-8)
.with_random_seed(42);
assert_eq!(config.components(), 5);
assert_eq!(config.max_iterations(), 200);
assert_eq!(config.tolerance(), 1e-6);
assert_eq!(config.epsilon(), 1e-8);
}
#[test]
fn test_nmf_config_validation() {
// Test: Config validation should catch invalid parameters
let valid_config = NMFConfig::new().with_components(5);
assert!(valid_config.validate().is_ok());
let invalid_config = NMFConfig::new().with_components(0);
assert!(invalid_config.validate().is_err());
}
}
#[cfg(test)]
mod nmf_decomposer_tests {
use super::*;
#[test]
fn test_basic_nmf_decomposition() -> Result<()> {
// Test: NMFDecomposer should decompose a matrix into W and H
let device = Device::Cuda(0);
// Create a simple non-negative matrix
let data = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
];
let x = Tensor::from_data(data, [4, 3], &device)?;
let config = NMFConfig::new().with_components(2).with_max_iterations(100);
let mut nmf = NMFDecomposer::new(config);
let (w, h) = nmf.fit_transform(&x)?;
// Check dimensions
assert_eq!(w.shape().dims(), &[4, 2]); // n_samples x n_components
assert_eq!(h.shape().dims(), &[2, 3]); // n_components x n_features
// Verify non-negativity
let w_data = w.to_cpu()?;
let h_data = h.to_cpu()?;
for val in w_data {
assert!(val >= 0.0, "W matrix should be non-negative");
}
for val in h_data {
assert!(val >= 0.0, "H matrix should be non-negative");
}
Ok(())
}
#[test]
fn test_nmf_reconstruction() -> Result<()> {
// Test: Reconstruction should approximate original matrix
let device = Device::Cuda(0);
// Create synthetic data
let data: Vec<f32> = (0..50).map(|i| (i as f32 + 1.0) * 0.5).collect();
let x = Tensor::from_data(data, [10, 5], &device)?;
let config = NMFConfig::new().with_components(3).with_max_iterations(100);
let mut nmf = NMFDecomposer::new(config);
let result = nmf.fit_transform_detailed(&x)?;
// Calculate reconstruction
let reconstruction = result.w.matmul(&result.h)?;
// Calculate difference
let diff = x.sub(&reconstruction)?;
let error = diff.matrix_norm("fro")?;
// Error should be reasonably small
assert!(
error < 10.0,
"Reconstruction error should be small, got {}",
error
);
assert!(result.reconstruction_error >= 0.0);
Ok(())
}
#[test]
fn test_nmf_convergence_info() -> Result<()> {
// Test: NMF should provide convergence information
let device = Device::Cuda(0);
let data: Vec<f32> = (0..200).map(|i| (i as f32).abs()).collect();
let x = Tensor::from_data(data, [20, 10], &device)?;
let config = NMFConfig::new()
.with_components(5)
.with_max_iterations(50)
.with_tolerance(1e-4);
let mut nmf = NMFDecomposer::new(config);
let result = nmf.fit_transform_detailed(&x)?;
// Check result fields
assert!(result.iterations > 0 && result.iterations <= 50);
assert!(result.reconstruction_error >= 0.0);
assert!(result.computation_time > 0.0);
Ok(())
}
}
#[cfg(test)]
mod honest_nmf_tests {
use super::*;
use nalgebra::DMatrix;
#[test]
fn test_honest_nmf_basic() -> Result<()> {
// Test: HonestNMF should work with CPU matrices
let config = HonestNMFConfig::new()
.with_components(3)
.with_max_iterations(100)
.with_tolerance(1e-4);
let nmf = HonestNMF::new(config)?;
// Create test matrix
let data = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
17.0, 18.0, 19.0, 20.0,
];
let matrix = DMatrix::from_row_slice(5, 4, &data);
let result = nmf.fit_transform(&matrix)?;
// Check dimensions
assert_eq!(result.w.nrows(), 5);
assert_eq!(result.w.ncols(), 3);
assert_eq!(result.h.nrows(), 3);
assert_eq!(result.h.ncols(), 4);
// Verify non-negativity
for val in result.w.iter() {
assert!(*val >= 0.0, "W should be non-negative");
}
for val in result.h.iter() {
assert!(*val >= 0.0, "H should be non-negative");
}
Ok(())
}
#[test]
fn test_honest_nmf_reconstruction() -> Result<()> {
// Test: HonestNMF reconstruction
let config = HonestNMFConfig::new()
.with_components(2)
.with_max_iterations(50);
let nmf = HonestNMF::new(config)?;
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
let matrix = DMatrix::from_row_slice(3, 3, &data);
let result = nmf.fit_transform(&matrix)?;
let reconstruction = result.reconstruct();
// Check dimensions match
assert_eq!(reconstruction.nrows(), matrix.nrows());
assert_eq!(reconstruction.ncols(), matrix.ncols());
// Error should be finite and non-negative
assert!(result.reconstruction_error.is_finite());
assert!(result.reconstruction_error >= 0.0);
Ok(())
}
#[test]
fn test_honest_nmf_convergence() -> Result<()> {
// Test: HonestNMF convergence tracking
let config = HonestNMFConfig::new()
.with_components(4)
.with_max_iterations(100)
.with_tolerance(1e-4)
.with_random_seed(42);
let nmf = HonestNMF::new(config)?;
let data: Vec<f32> = (0..100).map(|i| (i as f32) * 0.5).collect();
let matrix = DMatrix::from_row_slice(10, 10, &data);
let result = nmf.fit_transform(&matrix)?;
assert!(result.iterations > 0);
assert!(result.iterations <= 100);
assert!(result.computation_time > 0.0);
Ok(())
}
}
#[cfg(test)]
mod initialization_tests {
use super::*;
#[test]
fn test_random_initialization() -> Result<()> {
// Test: Random initialization strategy
let device = Device::Cuda(0);
let config = NMFConfig::new()
.with_components(3)
.with_initialization("random")
.with_random_seed(42);
let mut nmf = NMFDecomposer::new(config);
let data: Vec<f32> = (0..60).map(|i| (i as f32) + 1.0).collect();
let x = Tensor::from_data(data, [10, 6], &device)?;
let (w, h) = nmf.fit_transform(&x)?;
// Should produce valid matrices
assert_eq!(w.shape().dims(), &[10, 3]);
assert_eq!(h.shape().dims(), &[3, 6]);
Ok(())
}
}
#[cfg(test)]
mod gpu_acceleration_tests {
use super::*;
#[test]
fn test_gpu_vs_cpu_mode() -> Result<()> {
// Test: Both GPU and CPU modes should work
let device = Device::Cuda(0);
let data: Vec<f32> = (0..30).map(|i| (i as f32) + 0.5).collect();
let x = Tensor::from_data(data, [6, 5], &device)?;
// GPU mode (default)
let config_gpu = NMFConfig::new().with_components(2).with_max_iterations(50);
let mut nmf_gpu = NMFDecomposer::new(config_gpu);
let result_gpu = nmf_gpu.fit_transform_detailed(&x)?;
// CPU mode
let config_cpu = NMFConfig::new()
.with_components(2)
.with_max_iterations(50)
.without_gpu();
let mut nmf_cpu = NMFDecomposer::new(config_cpu);
let result_cpu = nmf_cpu.fit_transform_detailed(&x)?;
// Both should produce valid results
assert!(result_gpu.reconstruction_error >= 0.0);
assert!(result_cpu.reconstruction_error >= 0.0);
Ok(())
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_end_to_end_nmf_pipeline() -> Result<()> {
// Test: Complete NMF pipeline
let device = Device::Cuda(0);
let n_samples = 50;
let n_features = 20;
let n_components = 5;
// Generate synthetic data
let data: Vec<f32> = (0..(n_samples * n_features))
.map(|i| ((i % 17) as f32) * 0.7 + 0.1)
.collect();
let x = Tensor::from_data(data, [n_samples, n_features], &device)?;
// Build and train NMF model
let config = NMFConfig::new()
.with_components(n_components)
.with_max_iterations(100)
.with_tolerance(1e-4)
.with_random_seed(42);
let mut nmf = NMFDecomposer::new(config);
// Fit the model
let result = nmf.fit_transform_detailed(&x)?;
// Verify dimensions
assert_eq!(result.w.shape().dims(), &[n_samples, n_components]);
assert_eq!(result.h.shape().dims(), &[n_components, n_features]);
// Reconstruct and check quality
let reconstruction = result.reconstruct()?;
assert_eq!(reconstruction.shape().dims(), &[n_samples, n_features]);
// Check convergence
assert!(result.iterations > 0);
assert!(result.reconstruction_error >= 0.0);
assert!(result.computation_time > 0.0);
Ok(())
}
#[test]
fn test_nmf_with_various_sizes() -> Result<()> {
// Test: NMF should handle various matrix sizes
let device = Device::Cuda(0);
let test_cases = vec![
(5, 3, 2), // small
(10, 8, 3), // medium
(20, 15, 5), // larger
];
for (m, n, k) in test_cases {
let data: Vec<f32> = (0..(m * n)).map(|i| (i as f32) * 0.1 + 0.5).collect();
let x = Tensor::from_data(data, [m, n], &device)?;
let config = NMFConfig::new().with_components(k).with_max_iterations(50);
let mut nmf = NMFDecomposer::new(config);
let (w, h) = nmf.fit_transform(&x)?;
assert_eq!(w.shape().dims(), &[m, k]);
assert_eq!(h.shape().dims(), &[k, n]);
}
Ok(())
}
}
#[cfg(test)]
mod error_handling_tests {
use super::*;
#[test]
fn test_invalid_components() {
// Test: Should reject invalid number of components
let config = NMFConfig::new().with_components(0);
assert!(config.validate().is_err());
}
#[test]
fn test_components_larger_than_matrix() -> Result<()> {
// Test: Should handle components >= min(m, n)
let device = Device::Cuda(0);
let data: Vec<f32> = (0..12).map(|i| (i as f32) + 1.0).collect();
let x = Tensor::from_data(data, [4, 3], &device)?;
// Components >= min(4, 3) = 3 should fail
let config = NMFConfig::new().with_components(3).with_max_iterations(10);
let mut nmf = NMFDecomposer::new(config);
let result = nmf.fit_transform(&x);
assert!(result.is_err());
Ok(())
}
}
#[cfg(test)]
mod demo_tests {
use super::*;
#[test]
fn test_nmf_demo_creation() {
// Test: NMFDemo should be creatable
let device = Device::Cuda(0);
let _demo = NMFDemo::new(device);
// Just verify it compiles and constructs
}
#[test]
fn test_honest_nmf_demo() -> Result<()> {
// Test: HonestNMFDemo basic functionality
let _demo = HonestNMFDemo::new()?;
// Verify construction
Ok(())
}
}