#![allow(clippy::approx_constant)] use approx::assert_abs_diff_eq; use rtx_preprocessing::{InvertibleTransformer, MinMaxScaler, PreprocessingError, Transformer}; use rtx_tensor::{Device, Tensor}; /// Test fixture for MinMaxScaler tests struct MinMaxScalerTestFixture { simple_data: Tensor, multi_feature_data: Tensor, single_value_data: Tensor, constant_data: Tensor, negative_data: Tensor, large_range_data: Tensor, } impl MinMaxScalerTestFixture { fn new() -> Self { // Simple data: [1, 2, 3, 4, 5] -> should scale to [0, 0.25, 0.5, 0.75, 1.0] let simple_data = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0], &[5, 1], &Device::cpu()).unwrap(); // Multi-feature data let multi_feature_data = Tensor::from_slice( &[1.0, 10.0, 2.0, 20.0, 3.0, 30.0, 4.0, 40.0], &[4, 2], &Device::cpu(), ) .unwrap(); // Single value let single_value_data = Tensor::from_slice(&[5.0], &[1, 1], &Device::cpu()).unwrap(); // Constant data (all same values) let constant_data = Tensor::from_slice(&[3.0, 3.0, 3.0, 3.0], &[4, 1], &Device::cpu()).unwrap(); // Data with negative values: [-2, -1, 0, 1, 2] let negative_data = Tensor::from_slice(&[-2.0, -1.0, 0.0, 1.0, 2.0], &[5, 1], &Device::cpu()).unwrap(); // Large range data: [0, 1000] let large_range_data = Tensor::from_slice(&[0.0, 250.0, 500.0, 750.0, 1000.0], &[5, 1], &Device::cpu()) .unwrap(); Self { simple_data, multi_feature_data, single_value_data, constant_data, negative_data, large_range_data, } } } #[test] fn test_minmax_scaler_creation() { // Default range [0, 1] let scaler = MinMaxScaler::new(); assert!(!scaler.is_fitted()); assert_eq!(scaler.feature_range(), (0.0, 1.0)); // Custom range [-1, 1] let scaler = MinMaxScaler::with_range(-1.0, 1.0); assert!(!scaler.is_fitted()); assert_eq!(scaler.feature_range(), (-1.0, 1.0)); // Custom range [0, 10] let scaler = MinMaxScaler::with_range(0.0, 10.0); assert!(!scaler.is_fitted()); assert_eq!(scaler.feature_range(), (0.0, 10.0)); } #[test] fn test_minmax_scaler_invalid_range() { // Should fail with invalid range (min >= max) let result = std::panic::catch_unwind(|| MinMaxScaler::with_range(1.0, 1.0)); assert!(result.is_err()); let result = std::panic::catch_unwind(|| MinMaxScaler::with_range(2.0, 1.0)); assert!(result.is_err()); } #[test] fn test_minmax_scaler_fit_simple() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Should fit successfully let result = scaler.fit(&fixture.simple_data); assert!(result.is_ok()); assert!(scaler.is_fitted()); // Should have computed correct min and max assert_abs_diff_eq!(scaler.data_min()[0], 1.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_max()[0], 5.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_range()[0], 4.0, epsilon = 1e-5); } #[test] fn test_minmax_scaler_fit_multi_feature() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Should fit successfully on multi-feature data let result = scaler.fit(&fixture.multi_feature_data); assert!(result.is_ok()); assert!(scaler.is_fitted()); // Should have computed correct min/max for each feature assert_eq!(scaler.data_min().len(), 2); assert_eq!(scaler.data_max().len(), 2); assert_eq!(scaler.data_range().len(), 2); // Feature 0: [1, 2, 3, 4] assert_abs_diff_eq!(scaler.data_min()[0], 1.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_max()[0], 4.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_range()[0], 3.0, epsilon = 1e-5); // Feature 1: [10, 20, 30, 40] assert_abs_diff_eq!(scaler.data_min()[1], 10.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_max()[1], 40.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_range()[1], 30.0, epsilon = 1e-5); } #[test] fn test_minmax_scaler_fit_empty_data() { let empty_data = Tensor::zeros(&[0, 1], &Device::cpu()).unwrap(); let mut scaler = MinMaxScaler::new(); // Should fail on empty data let result = scaler.fit(&empty_data); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), PreprocessingError::EmptyDataset )); assert!(!scaler.is_fitted()); } #[test] fn test_minmax_scaler_fit_single_value() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Should handle single value let result = scaler.fit(&fixture.single_value_data); assert!(result.is_ok()); assert!(scaler.is_fitted()); // Min and max should be the same value assert_abs_diff_eq!(scaler.data_min()[0], 5.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_max()[0], 5.0, epsilon = 1e-5); assert_abs_diff_eq!(scaler.data_range()[0], 0.0, epsilon = 1e-5); } #[test] fn test_minmax_scaler_fit_constant_data() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Should handle constant data let result = scaler.fit(&fixture.constant_data); assert!(result.is_ok()); assert!(scaler.is_fitted()); // Range should be zero assert_abs_diff_eq!(scaler.data_range()[0], 0.0, epsilon = 1e-5); } #[test] fn test_minmax_scaler_transform_not_fitted() { let fixture = MinMaxScalerTestFixture::new(); let scaler = MinMaxScaler::new(); // Should fail when not fitted let result = scaler.transform(&fixture.simple_data); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), PreprocessingError::NotFitted)); } #[test] fn test_minmax_scaler_transform_simple() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit first scaler.fit(&fixture.simple_data).unwrap(); // Transform should scale to [0, 1] range let transformed = scaler.transform(&fixture.simple_data).unwrap(); assert_eq!(transformed.shape(), &[5, 1]); let values = transformed .to_cpu() .unwrap() .iter() .copied() .collect::>(); let expected = vec![0.0, 0.25, 0.5, 0.75, 1.0]; for (actual, expected) in values.iter().zip(expected.iter()) { assert_abs_diff_eq!(actual, expected, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_transform_custom_range() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::with_range(-1.0, 1.0); // Fit and transform scaler.fit(&fixture.simple_data).unwrap(); let transformed = scaler.transform(&fixture.simple_data).unwrap(); let values = transformed .to_cpu() .unwrap() .iter() .copied() .collect::>(); let expected = vec![-1.0, -0.5, 0.0, 0.5, 1.0]; for (actual, expected) in values.iter().zip(expected.iter()) { assert_abs_diff_eq!(actual, expected, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_transform_negative_data() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit and transform negative data scaler.fit(&fixture.negative_data).unwrap(); let transformed = scaler.transform(&fixture.negative_data).unwrap(); let values = transformed .to_cpu() .unwrap() .iter() .copied() .collect::>(); let expected = vec![0.0, 0.25, 0.5, 0.75, 1.0]; for (actual, expected) in values.iter().zip(expected.iter()) { assert_abs_diff_eq!(actual, expected, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_transform_constant_data() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit and transform constant data scaler.fit(&fixture.constant_data).unwrap(); let transformed = scaler.transform(&fixture.constant_data).unwrap(); let values = transformed .to_cpu() .unwrap() .iter() .copied() .collect::>(); // All values should be 0.0 (min of range) when data has no variance for value in values { assert_abs_diff_eq!(value, 0.0, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_transform_dimension_mismatch() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit on simple data (1 feature) scaler.fit(&fixture.simple_data).unwrap(); // Try to transform multi-feature data (2 features) let result = scaler.transform(&fixture.multi_feature_data); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), PreprocessingError::DimensionMismatch { expected: 1, actual: 2 } )); } #[test] fn test_minmax_scaler_inverse_transform() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit and transform scaler.fit(&fixture.simple_data).unwrap(); let transformed = scaler.transform(&fixture.simple_data).unwrap(); // Inverse transform should recover original data let recovered = scaler.inverse_transform(&transformed).unwrap(); assert_eq!(recovered.shape(), fixture.simple_data.shape()); let original_values = fixture .simple_data .to_cpu() .unwrap() .iter() .copied() .collect::>(); let recovered_values = recovered .to_cpu() .unwrap() .iter() .copied() .collect::>(); for (orig, rec) in original_values.iter().zip(recovered_values.iter()) { assert_abs_diff_eq!(orig, rec, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_inverse_transform_custom_range() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::with_range(-5.0, 5.0); // Fit and transform scaler.fit(&fixture.simple_data).unwrap(); let transformed = scaler.transform(&fixture.simple_data).unwrap(); // Inverse transform should recover original data let recovered = scaler.inverse_transform(&transformed).unwrap(); let original_values = fixture .simple_data .to_cpu() .unwrap() .iter() .copied() .collect::>(); let recovered_values = recovered .to_cpu() .unwrap() .iter() .copied() .collect::>(); for (orig, rec) in original_values.iter().zip(recovered_values.iter()) { assert_abs_diff_eq!(orig, rec, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_fit_transform() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // fit_transform should be equivalent to fit + transform let result1 = scaler.fit_transform(&fixture.simple_data).unwrap(); let mut scaler2 = MinMaxScaler::new(); scaler2.fit(&fixture.simple_data).unwrap(); let result2 = scaler2.transform(&fixture.simple_data).unwrap(); assert_eq!(result1.shape(), result2.shape()); let values1 = result1 .to_cpu() .unwrap() .iter() .copied() .collect::>(); let values2 = result2 .to_cpu() .unwrap() .iter() .copied() .collect::>(); for (v1, v2) in values1.iter().zip(values2.iter()) { assert_abs_diff_eq!(v1, v2, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_reset() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit scaler scaler.fit(&fixture.simple_data).unwrap(); assert!(scaler.is_fitted()); // Reset should make it unfitted scaler.reset(); assert!(!scaler.is_fitted()); // Should fail to transform after reset let result = scaler.transform(&fixture.simple_data); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), PreprocessingError::NotFitted)); } #[test] fn test_minmax_scaler_out_of_range_data() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Fit on simple data [1, 2, 3, 4, 5] scaler.fit(&fixture.simple_data).unwrap(); // Transform data outside the fitted range let out_of_range_data = Tensor::from_slice(&[0.0, 6.0], &[2, 1], &Device::cpu()).unwrap(); let transformed = scaler.transform(&out_of_range_data).unwrap(); let values = transformed .to_cpu() .unwrap() .iter() .copied() .collect::>(); // Should extrapolate: 0 -> -0.25, 6 -> 1.25 assert_abs_diff_eq!(values[0], -0.25, epsilon = 1e-5); assert_abs_diff_eq!(values[1], 1.25, epsilon = 1e-5); } #[test] fn test_minmax_scaler_large_range_data() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::new(); // Should handle large range data efficiently scaler.fit(&fixture.large_range_data).unwrap(); let transformed = scaler.transform(&fixture.large_range_data).unwrap(); let values = transformed .to_cpu() .unwrap() .iter() .copied() .collect::>(); let expected = vec![0.0, 0.25, 0.5, 0.75, 1.0]; for (actual, expected) in values.iter().zip(expected.iter()) { assert_abs_diff_eq!(actual, expected, epsilon = 1e-5); } } #[test] fn test_minmax_scaler_serialization() { let fixture = MinMaxScalerTestFixture::new(); let mut scaler = MinMaxScaler::with_range(-2.0, 2.0); // Fit scaler scaler.fit(&fixture.simple_data).unwrap(); // Should be serializable and deserializable let serialized = bincode::serialize(&scaler).unwrap(); let deserialized: MinMaxScaler = bincode::deserialize(&serialized).unwrap(); // Should maintain fitted state and parameters assert!(deserialized.is_fitted()); assert_eq!(scaler.feature_range(), deserialized.feature_range()); assert_eq!(scaler.data_min(), deserialized.data_min()); assert_eq!(scaler.data_max(), deserialized.data_max()); assert_eq!(scaler.data_range(), deserialized.data_range()); // Should produce same transform results let original_result = scaler.transform(&fixture.simple_data).unwrap(); let deserialized_result = deserialized.transform(&fixture.simple_data).unwrap(); let orig_values = original_result .to_cpu() .unwrap() .iter() .copied() .collect::>(); let deser_values = deserialized_result .to_cpu() .unwrap() .iter() .copied() .collect::>(); for (orig, deser) in orig_values.iter().zip(deser_values.iter()) { assert_abs_diff_eq!(orig, deser, epsilon = 1e-5); } }