1use scirs2_core::ndarray::{Array1, Array2};
17use scirs2_core::random::Distribution;
18use serde::{Deserialize, Serialize};
19use sklears_core::{
20 error::{Result, SklearsError},
21 types::Float,
22};
23use std::collections::HashMap;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ValidationConfig {
28 pub strict_mode: bool,
30 pub numerical_tolerance: Float,
32 pub max_condition_number: Float,
34 pub min_eigenvalue: Float,
36 pub enable_statistical_tests: bool,
38 pub confidence_level: Float,
40 pub validate_performance: bool,
42 pub max_computation_time: Float,
44}
45
46impl Default for ValidationConfig {
47 fn default() -> Self {
48 Self {
49 strict_mode: false,
50 numerical_tolerance: 1e-10,
51 max_condition_number: 1e12,
52 min_eigenvalue: 1e-15,
53 enable_statistical_tests: true,
54 confidence_level: 0.95,
55 validate_performance: false,
56 max_computation_time: 300.0, }
58 }
59}
60
61pub struct ValidationFramework {
63 config: ValidationConfig,
64}
65
66impl ValidationFramework {
67 pub fn new() -> Self {
69 Self {
70 config: ValidationConfig::default(),
71 }
72 }
73
74 pub fn with_config(config: ValidationConfig) -> Self {
76 Self { config }
77 }
78
79 pub fn validate_input_matrix(&self, matrix: &Array2<Float>) -> Result<InputValidationResult> {
81 let mut issues = Vec::new();
82 let (rows, cols) = matrix.dim();
83
84 if rows == 0 || cols == 0 {
86 return Err(SklearsError::InvalidInput(
87 "Matrix cannot be empty".to_string(),
88 ));
89 }
90
91 let mut nan_count = 0;
93 let mut inf_count = 0;
94 let mut finite_count = 0;
95
96 for &value in matrix.iter() {
97 if value.is_nan() {
98 nan_count += 1;
99 } else if value.is_infinite() {
100 inf_count += 1;
101 } else {
102 finite_count += 1;
103 }
104 }
105
106 if nan_count > 0 {
107 if self.config.strict_mode {
108 return Err(SklearsError::InvalidInput(format!(
109 "Matrix contains {} NaN values",
110 nan_count
111 )));
112 } else {
113 issues.push(ValidationIssue::Warning(format!(
114 "Matrix contains {} NaN values",
115 nan_count
116 )));
117 }
118 }
119
120 if inf_count > 0 {
121 if self.config.strict_mode {
122 return Err(SklearsError::InvalidInput(format!(
123 "Matrix contains {} infinite values",
124 inf_count
125 )));
126 } else {
127 issues.push(ValidationIssue::Warning(format!(
128 "Matrix contains {} infinite values",
129 inf_count
130 )));
131 }
132 }
133
134 let matrix_norm = self.compute_frobenius_norm(matrix);
136 let condition_number = self.estimate_condition_number(matrix);
137 let rank_estimate = self.estimate_rank(matrix);
138
139 if condition_number > self.config.max_condition_number {
141 issues.push(ValidationIssue::Warning(format!(
142 "Matrix is ill-conditioned (condition number: {:.2e})",
143 condition_number
144 )));
145 }
146
147 let expected_rank = rows.min(cols);
149 if rank_estimate < expected_rank {
150 issues.push(ValidationIssue::Info(format!(
151 "Matrix appears to be rank deficient (estimated rank: {} / {})",
152 rank_estimate, expected_rank
153 )));
154 }
155
156 Ok(InputValidationResult {
157 shape: (rows, cols),
158 finite_values: finite_count,
159 nan_values: nan_count,
160 infinite_values: inf_count,
161 matrix_norm,
162 condition_number,
163 estimated_rank: rank_estimate,
164 issues,
165 is_valid: nan_count == 0 && inf_count == 0,
166 })
167 }
168
169 pub fn validate_parameters(
171 &self,
172 algorithm: DecompositionAlgorithm,
173 params: &HashMap<String, ParameterValue>,
174 matrix_shape: (usize, usize),
175 ) -> Result<ParameterValidationResult> {
176 let mut issues = Vec::new();
177 let (rows, cols) = matrix_shape;
178
179 match algorithm {
180 DecompositionAlgorithm::PCA => {
181 if let Some(ParameterValue::Integer(n_components)) = params.get("n_components") {
182 let n_comp = *n_components as usize;
183 let max_components = rows.min(cols);
184
185 if n_comp == 0 {
186 issues.push(ValidationIssue::Error(
187 "n_components must be positive".to_string(),
188 ));
189 } else if n_comp > max_components {
190 issues.push(ValidationIssue::Warning(format!(
191 "n_components ({}) exceeds maximum possible ({})",
192 n_comp, max_components
193 )));
194 }
195 }
196 }
197
198 DecompositionAlgorithm::SVD => {
199 if let Some(ParameterValue::Boolean(full_matrices)) = params.get("full_matrices") {
200 if *full_matrices && rows != cols {
201 issues.push(ValidationIssue::Info(
202 "full_matrices=true with non-square matrix may be inefficient"
203 .to_string(),
204 ));
205 }
206 }
207 }
208
209 DecompositionAlgorithm::NMF => {
210 if let Some(ParameterValue::Boolean(check_non_negative)) = params.get("check_input")
212 {
213 if *check_non_negative {
214 issues.push(ValidationIssue::Info(
215 "NMF requires non-negative input matrix".to_string(),
216 ));
217 }
218 }
219
220 if let Some(ParameterValue::Integer(n_components)) = params.get("n_components") {
221 let n_comp = *n_components as usize;
222 if n_comp >= rows.min(cols) {
223 issues.push(ValidationIssue::Warning(
224 "NMF with n_components >= min(rows, cols) may not converge".to_string(),
225 ));
226 }
227 }
228 }
229
230 DecompositionAlgorithm::ICA => {
231 if let Some(ParameterValue::Integer(n_components)) = params.get("n_components") {
232 let n_comp = *n_components as usize;
233 if n_comp > cols {
234 issues.push(ValidationIssue::Error(format!(
235 "ICA n_components ({}) cannot exceed number of features ({})",
236 n_comp, cols
237 )));
238 }
239 }
240
241 if let Some(ParameterValue::Float(tolerance)) = params.get("tol") {
242 if *tolerance <= 0.0 {
243 issues.push(ValidationIssue::Error(
244 "ICA tolerance must be positive".to_string(),
245 ));
246 }
247 }
248 }
249 }
250
251 if let Some(ParameterValue::Integer(max_iter)) = params.get("max_iter") {
253 if *max_iter <= 0 {
254 issues.push(ValidationIssue::Error(
255 "max_iter must be positive".to_string(),
256 ));
257 } else if *max_iter > 10000 {
258 issues.push(ValidationIssue::Warning(
259 "Very high max_iter may indicate convergence problems".to_string(),
260 ));
261 }
262 }
263
264 if let Some(ParameterValue::Float(tolerance)) = params.get("tol") {
265 if *tolerance <= 0.0 {
266 issues.push(ValidationIssue::Error(
267 "Tolerance must be positive".to_string(),
268 ));
269 } else if *tolerance > 1e-3 {
270 issues.push(ValidationIssue::Warning(
271 "Large tolerance may reduce decomposition quality".to_string(),
272 ));
273 }
274 }
275
276 let has_errors = issues
277 .iter()
278 .any(|issue| matches!(issue, ValidationIssue::Error(_)));
279
280 Ok(ParameterValidationResult {
281 algorithm,
282 validated_params: params.clone(),
283 issues,
284 is_valid: !has_errors,
285 })
286 }
287
288 pub fn validate_results(
290 &self,
291 original_matrix: &Array2<Float>,
292 result: &DecompositionResult,
293 ) -> Result<ResultValidationResult> {
294 let mut issues = Vec::new();
295 let (m, n) = original_matrix.dim();
296
297 match result {
298 DecompositionResult::SVD { u, s, vt } => {
299 let (u_rows, u_cols) = u.dim();
301 let (vt_rows, vt_cols) = vt.dim();
302 let s_len = s.len();
303
304 if u_rows != m {
305 issues.push(ValidationIssue::Error(format!(
306 "SVD U matrix rows ({}) don't match input rows ({})",
307 u_rows, m
308 )));
309 }
310
311 if vt_cols != n {
312 issues.push(ValidationIssue::Error(format!(
313 "SVD VT matrix cols ({}) don't match input cols ({})",
314 vt_cols, n
315 )));
316 }
317
318 if u_cols != s_len || vt_rows != s_len {
319 issues.push(ValidationIssue::Error(
320 "SVD dimensions are inconsistent".to_string(),
321 ));
322 }
323
324 let mut prev_s = Float::INFINITY;
326 for &s_val in s.iter() {
327 if s_val < 0.0 {
328 issues.push(ValidationIssue::Error(
329 "Singular values must be non-negative".to_string(),
330 ));
331 break;
332 }
333 if s_val > prev_s + self.config.numerical_tolerance {
334 issues.push(ValidationIssue::Warning(
335 "Singular values are not in descending order".to_string(),
336 ));
337 }
338 prev_s = s_val;
339 }
340
341 let u_orthogonality_error = self.check_orthogonality(u);
343 let vt_orthogonality_error = self.check_orthogonality(&vt.t().to_owned());
344
345 if u_orthogonality_error > self.config.numerical_tolerance * 1000.0 {
346 issues.push(ValidationIssue::Warning(format!(
347 "U matrix orthogonality error: {:.2e}",
348 u_orthogonality_error
349 )));
350 }
351
352 if vt_orthogonality_error > self.config.numerical_tolerance * 1000.0 {
353 issues.push(ValidationIssue::Warning(format!(
354 "V matrix orthogonality error: {:.2e}",
355 vt_orthogonality_error
356 )));
357 }
358
359 let reconstruction = self.reconstruct_from_svd(u, s, vt);
361 let reconstruction_error =
362 self.compute_reconstruction_error(original_matrix, &reconstruction);
363
364 if reconstruction_error > self.config.numerical_tolerance * 1000.0 {
365 issues.push(ValidationIssue::Warning(format!(
366 "High reconstruction error: {:.2e}",
367 reconstruction_error
368 )));
369 }
370 }
371
372 DecompositionResult::PCA {
373 components,
374 eigenvalues,
375 mean,
376 } => {
377 let (n_components, n_features) = components.dim();
378
379 if n_features != n {
380 issues.push(ValidationIssue::Error(format!(
381 "PCA components features ({}) don't match input features ({})",
382 n_features, n
383 )));
384 }
385
386 if eigenvalues.len() != n_components {
387 issues.push(ValidationIssue::Error(
388 "PCA eigenvalues length doesn't match components".to_string(),
389 ));
390 }
391
392 if mean.len() != n {
393 issues.push(ValidationIssue::Error(format!(
394 "PCA mean length ({}) doesn't match input features ({})",
395 mean.len(),
396 n
397 )));
398 }
399
400 let mut prev_eigen = Float::INFINITY;
402 for &eigen_val in eigenvalues.iter() {
403 if eigen_val < 0.0 {
404 issues.push(ValidationIssue::Warning(
405 "Negative eigenvalue found".to_string(),
406 ));
407 }
408 if eigen_val > prev_eigen + self.config.numerical_tolerance {
409 issues.push(ValidationIssue::Warning(
410 "Eigenvalues are not in descending order".to_string(),
411 ));
412 }
413 prev_eigen = eigen_val;
414 }
415 }
416
417 DecompositionResult::NMF { w, h } => {
418 let (w_rows, w_cols) = w.dim();
419 let (h_rows, h_cols) = h.dim();
420
421 if w_rows != m {
422 issues.push(ValidationIssue::Error(format!(
423 "NMF W matrix rows ({}) don't match input rows ({})",
424 w_rows, m
425 )));
426 }
427
428 if h_cols != n {
429 issues.push(ValidationIssue::Error(format!(
430 "NMF H matrix cols ({}) don't match input cols ({})",
431 h_cols, n
432 )));
433 }
434
435 if w_cols != h_rows {
436 issues.push(ValidationIssue::Error(
437 "NMF W and H dimensions are incompatible".to_string(),
438 ));
439 }
440
441 if w.iter().any(|&x| x < 0.0) {
443 issues.push(ValidationIssue::Error(
444 "NMF W matrix contains negative values".to_string(),
445 ));
446 }
447
448 if h.iter().any(|&x| x < 0.0) {
449 issues.push(ValidationIssue::Error(
450 "NMF H matrix contains negative values".to_string(),
451 ));
452 }
453
454 let reconstruction = w.dot(h);
456 let reconstruction_error =
457 self.compute_reconstruction_error(original_matrix, &reconstruction);
458
459 if reconstruction_error > self.config.numerical_tolerance * 10000.0 {
460 issues.push(ValidationIssue::Warning(format!(
461 "High NMF reconstruction error: {:.2e}",
462 reconstruction_error
463 )));
464 }
465 }
466 }
467
468 let has_errors = issues
469 .iter()
470 .any(|issue| matches!(issue, ValidationIssue::Error(_)));
471
472 Ok(ResultValidationResult {
473 reconstruction_error: 0.0, numerical_stability_score: self.compute_stability_score(&issues),
475 orthogonality_score: 1.0, issues,
477 is_valid: !has_errors,
478 })
479 }
480
481 pub fn statistical_validation(
483 &self,
484 original: &Array2<Float>,
485 reconstructed: &Array2<Float>,
486 ) -> Result<StatisticalValidationResult> {
487 if !self.config.enable_statistical_tests {
488 return Ok(StatisticalValidationResult::default());
489 }
490
491 let residuals = original - reconstructed;
492
493 let mse = residuals.mapv(|x| x.powi(2)).mean().unwrap_or(0.0);
495 let mae = residuals.mapv(|x| x.abs()).mean().unwrap_or(0.0);
496 let max_error = residuals
497 .mapv(|x| x.abs())
498 .fold(0.0f64, |acc, &x| acc.max(x));
499
500 let original_mean = original.mean().unwrap_or(0.0);
502 let ss_tot = original.mapv(|x| (x - original_mean).powi(2)).sum();
503 let ss_res = residuals.mapv(|x| x.powi(2)).sum();
504 let r_squared = if ss_tot > 0.0 {
505 1.0 - ss_res / ss_tot
506 } else {
507 0.0
508 };
509
510 let original_var = self.compute_variance(original);
512 let residual_var = self.compute_variance(&residuals);
513 let explained_variance_ratio = if original_var > 0.0 {
514 1.0 - residual_var / original_var
515 } else {
516 0.0
517 };
518
519 let normality_p_value = if let Some(slice) = residuals.as_slice() {
521 self.approximate_normality_test(slice)
522 } else {
523 let flat: Vec<Float> = residuals.iter().copied().collect();
524 self.approximate_normality_test(&flat)
525 };
526
527 let mut hypothesis_tests = Vec::new();
529
530 let reconstruction_test = HypothesisTest {
532 test_name: "Reconstruction Quality".to_string(),
533 statistic: mse.sqrt(),
534 p_value: if mse < self.config.numerical_tolerance {
535 0.9
536 } else {
537 0.1
538 },
539 significant: mse > self.config.numerical_tolerance * 100.0,
540 interpretation: if mse < self.config.numerical_tolerance * 100.0 {
541 "Good reconstruction quality".to_string()
542 } else {
543 "Poor reconstruction quality".to_string()
544 },
545 };
546 hypothesis_tests.push(reconstruction_test);
547
548 Ok(StatisticalValidationResult {
549 mse,
550 mae,
551 max_error,
552 r_squared,
553 explained_variance_ratio,
554 normality_p_value,
555 hypothesis_tests,
556 overall_quality_score: (r_squared + explained_variance_ratio) / 2.0,
557 })
558 }
559
560 pub fn performance_validation(
562 &self,
563 execution_time: std::time::Duration,
564 memory_usage: usize,
565 matrix_size: (usize, usize),
566 ) -> Result<PerformanceValidationResult> {
567 let mut issues = Vec::new();
568 let execution_seconds = execution_time.as_secs_f64();
569
570 if execution_seconds > self.config.max_computation_time {
571 issues.push(ValidationIssue::Warning(format!(
572 "Execution time ({:.2}s) exceeds maximum allowed ({:.2}s)",
573 execution_seconds, self.config.max_computation_time
574 )));
575 }
576
577 let (m, n) = matrix_size;
579 let theoretical_complexity = (m * n * n.min(m)) as f64; let normalized_time = execution_seconds / (theoretical_complexity / 1e9);
581
582 let expected_memory = m * n * std::mem::size_of::<Float>() * 3; let memory_efficiency = expected_memory as f64 / memory_usage as f64;
585
586 if memory_efficiency < 0.5 {
587 issues.push(ValidationIssue::Warning(
588 "High memory usage detected".to_string(),
589 ));
590 }
591
592 Ok(PerformanceValidationResult {
593 execution_time: execution_seconds,
594 memory_usage_bytes: memory_usage,
595 theoretical_complexity,
596 normalized_execution_time: normalized_time,
597 memory_efficiency,
598 performance_score: if normalized_time < 1.0 && memory_efficiency > 0.5 {
599 1.0
600 } else {
601 0.5
602 },
603 issues,
604 })
605 }
606
607 fn compute_frobenius_norm(&self, matrix: &Array2<Float>) -> Float {
609 matrix.mapv(|x| x.powi(2)).sum().sqrt()
610 }
611
612 fn estimate_condition_number(&self, matrix: &Array2<Float>) -> Float {
613 let norm = self.compute_frobenius_norm(matrix);
616 if norm > 0.0 {
617 norm / self.config.numerical_tolerance
618 } else {
619 1.0
620 }
621 }
622
623 fn estimate_rank(&self, matrix: &Array2<Float>) -> usize {
624 let (m, n) = matrix.dim();
627 let max_rank = m.min(n);
628
629 let norm = self.compute_frobenius_norm(matrix);
631 if norm < self.config.numerical_tolerance {
632 0
633 } else {
634 max_rank }
636 }
637
638 fn check_orthogonality(&self, matrix: &Array2<Float>) -> Float {
639 let product = matrix.t().dot(matrix);
640 let identity = Array2::<Float>::eye(product.nrows());
641 let diff = &product - &identity;
642 self.compute_frobenius_norm(&diff)
643 }
644
645 fn reconstruct_from_svd(
646 &self,
647 u: &Array2<Float>,
648 s: &Array1<Float>,
649 vt: &Array2<Float>,
650 ) -> Array2<Float> {
651 let s_diag = Array2::from_diag(s);
652 let us = u.dot(&s_diag);
653 us.dot(vt)
654 }
655
656 fn compute_reconstruction_error(
657 &self,
658 original: &Array2<Float>,
659 reconstructed: &Array2<Float>,
660 ) -> Float {
661 let diff = original - reconstructed;
662 self.compute_frobenius_norm(&diff) / self.compute_frobenius_norm(original)
663 }
664
665 fn compute_stability_score(&self, issues: &[ValidationIssue]) -> Float {
666 let error_count = issues
667 .iter()
668 .filter(|issue| matches!(issue, ValidationIssue::Error(_)))
669 .count();
670 let warning_count = issues
671 .iter()
672 .filter(|issue| matches!(issue, ValidationIssue::Warning(_)))
673 .count();
674
675 if error_count > 0 {
676 0.0
677 } else if warning_count > 2 {
678 0.5
679 } else if warning_count > 0 {
680 0.8
681 } else {
682 1.0
683 }
684 }
685
686 fn compute_variance(&self, matrix: &Array2<Float>) -> Float {
687 let mean = matrix.mean().unwrap_or(0.0);
688 matrix.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0)
689 }
690
691 fn approximate_normality_test(&self, data: &[Float]) -> Float {
692 if data.len() < 3 {
694 return 0.5;
695 }
696
697 let mean = data.iter().sum::<Float>() / data.len() as Float;
698 let variance =
699 data.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / (data.len() - 1) as Float;
700
701 if variance < self.config.numerical_tolerance {
702 0.1 } else {
704 0.7 }
706 }
707}
708
709impl Default for ValidationFramework {
710 fn default() -> Self {
711 Self::new()
712 }
713}
714
715#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
717pub enum DecompositionAlgorithm {
718 PCA,
719 SVD,
720 NMF,
721 ICA,
722}
723
724#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
726pub enum ParameterValue {
727 Integer(i64),
728 Float(Float),
729 Boolean(bool),
730 String(String),
731}
732
733#[derive(Debug, Clone)]
735pub enum ValidationIssue {
736 Error(String),
737 Warning(String),
738 Info(String),
739}
740
741#[derive(Debug, Clone)]
743pub struct InputValidationResult {
744 pub shape: (usize, usize),
745 pub finite_values: usize,
746 pub nan_values: usize,
747 pub infinite_values: usize,
748 pub matrix_norm: Float,
749 pub condition_number: Float,
750 pub estimated_rank: usize,
751 pub issues: Vec<ValidationIssue>,
752 pub is_valid: bool,
753}
754
755#[derive(Debug, Clone)]
757pub struct ParameterValidationResult {
758 pub algorithm: DecompositionAlgorithm,
759 pub validated_params: HashMap<String, ParameterValue>,
760 pub issues: Vec<ValidationIssue>,
761 pub is_valid: bool,
762}
763
764#[derive(Debug, Clone)]
766pub enum DecompositionResult {
767 SVD {
768 u: Array2<Float>,
769 s: Array1<Float>,
770 vt: Array2<Float>,
771 },
772 PCA {
773 components: Array2<Float>,
774 eigenvalues: Array1<Float>,
775 mean: Array1<Float>,
776 },
777 NMF {
778 w: Array2<Float>,
779 h: Array2<Float>,
780 },
781}
782
783#[derive(Debug, Clone)]
785pub struct ResultValidationResult {
786 pub reconstruction_error: Float,
787 pub numerical_stability_score: Float,
788 pub orthogonality_score: Float,
789 pub issues: Vec<ValidationIssue>,
790 pub is_valid: bool,
791}
792
793#[derive(Debug, Clone)]
795pub struct StatisticalValidationResult {
796 pub mse: Float,
797 pub mae: Float,
798 pub max_error: Float,
799 pub r_squared: Float,
800 pub explained_variance_ratio: Float,
801 pub normality_p_value: Float,
802 pub hypothesis_tests: Vec<HypothesisTest>,
803 pub overall_quality_score: Float,
804}
805
806impl Default for StatisticalValidationResult {
807 fn default() -> Self {
808 Self {
809 mse: 0.0,
810 mae: 0.0,
811 max_error: 0.0,
812 r_squared: 1.0,
813 explained_variance_ratio: 1.0,
814 normality_p_value: 0.5,
815 hypothesis_tests: Vec::new(),
816 overall_quality_score: 1.0,
817 }
818 }
819}
820
821#[derive(Debug, Clone)]
823pub struct HypothesisTest {
824 pub test_name: String,
825 pub statistic: Float,
826 pub p_value: Float,
827 pub significant: bool,
828 pub interpretation: String,
829}
830
831#[derive(Debug, Clone)]
833pub struct PerformanceValidationResult {
834 pub execution_time: Float,
835 pub memory_usage_bytes: usize,
836 pub theoretical_complexity: Float,
837 pub normalized_execution_time: Float,
838 pub memory_efficiency: Float,
839 pub performance_score: Float,
840 pub issues: Vec<ValidationIssue>,
841}
842
843#[allow(non_snake_case)]
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn test_validation_config() {
850 let config = ValidationConfig::default();
851 assert!(!config.strict_mode);
852 assert_eq!(config.numerical_tolerance, 1e-10);
853 assert!(config.enable_statistical_tests);
854 assert_eq!(config.confidence_level, 0.95);
855 }
856
857 #[test]
858 fn test_validation_framework_creation() {
859 let framework = ValidationFramework::new();
860 assert_eq!(framework.config.numerical_tolerance, 1e-10);
861
862 let custom_config = ValidationConfig {
863 strict_mode: true,
864 numerical_tolerance: 1e-12,
865 ..ValidationConfig::default()
866 };
867 let custom_framework = ValidationFramework::with_config(custom_config);
868 assert!(custom_framework.config.strict_mode);
869 assert_eq!(custom_framework.config.numerical_tolerance, 1e-12);
870 }
871
872 #[test]
873 fn test_input_validation() {
874 let framework = ValidationFramework::new();
875
876 let valid_matrix =
878 Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
879 .expect("operation should succeed");
880
881 let result = framework
882 .validate_input_matrix(&valid_matrix)
883 .expect("operation should succeed");
884 assert!(result.is_valid);
885 assert_eq!(result.shape, (3, 3));
886 assert_eq!(result.finite_values, 9);
887 assert_eq!(result.nan_values, 0);
888
889 let invalid_matrix = Array2::from_shape_vec((2, 2), vec![1.0, Float::NAN, 3.0, 4.0])
891 .expect("shape and data length should match");
892
893 let result = framework
894 .validate_input_matrix(&invalid_matrix)
895 .expect("operation should succeed");
896 assert!(!result.is_valid);
897 assert_eq!(result.nan_values, 1);
898 }
899
900 #[test]
901 fn test_parameter_validation() {
902 let framework = ValidationFramework::new();
903 let mut params = HashMap::new();
904 params.insert("n_components".to_string(), ParameterValue::Integer(2));
905 params.insert("max_iter".to_string(), ParameterValue::Integer(100));
906
907 let result = framework
908 .validate_parameters(DecompositionAlgorithm::PCA, ¶ms, (10, 5))
909 .expect("operation should succeed");
910
911 assert!(result.is_valid);
912 assert_eq!(result.algorithm, DecompositionAlgorithm::PCA);
913
914 params.insert("n_components".to_string(), ParameterValue::Integer(0));
916 let result = framework
917 .validate_parameters(DecompositionAlgorithm::PCA, ¶ms, (10, 5))
918 .expect("operation should succeed");
919
920 assert!(!result.is_valid);
921 }
922
923 #[test]
924 fn test_statistical_validation() {
925 let framework = ValidationFramework::new();
926
927 let original =
928 Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
929 .expect("operation should succeed");
930
931 let reconstructed =
932 Array2::from_shape_vec((3, 3), vec![1.1, 2.1, 3.1, 3.9, 5.1, 5.9, 7.1, 7.9, 9.1])
933 .expect("operation should succeed");
934
935 let result = framework
936 .statistical_validation(&original, &reconstructed)
937 .expect("operation should succeed");
938 assert!(result.r_squared >= 0.0);
939 assert!(result.r_squared <= 1.0);
940 assert!(result.mse >= 0.0);
941 assert!(!result.hypothesis_tests.is_empty());
942 }
943
944 #[test]
945 fn test_performance_validation() {
946 let framework = ValidationFramework::new();
947
948 let execution_time = std::time::Duration::from_millis(100);
949 let memory_usage = 1024 * 1024; let matrix_size = (100, 100);
951
952 let result = framework
953 .performance_validation(execution_time, memory_usage, matrix_size)
954 .expect("operation should succeed");
955
956 assert!(result.execution_time > 0.0);
957 assert_eq!(result.memory_usage_bytes, memory_usage);
958 assert!(result.performance_score >= 0.0);
959 assert!(result.performance_score <= 1.0);
960 }
961
962 #[test]
963 fn test_validation_issues() {
964 let error_issue = ValidationIssue::Error("Test error".to_string());
965 let warning_issue = ValidationIssue::Warning("Test warning".to_string());
966 let info_issue = ValidationIssue::Info("Test info".to_string());
967
968 assert!(matches!(error_issue, ValidationIssue::Error(_)));
969 assert!(matches!(warning_issue, ValidationIssue::Warning(_)));
970 assert!(matches!(info_issue, ValidationIssue::Info(_)));
971 }
972
973 #[test]
974 fn test_parameter_values() {
975 let int_param = ParameterValue::Integer(42);
976 let float_param = ParameterValue::Float(std::f64::consts::PI);
977 let bool_param = ParameterValue::Boolean(true);
978 let string_param = ParameterValue::String("test".to_string());
979
980 assert_eq!(int_param, ParameterValue::Integer(42));
981 assert_eq!(float_param, ParameterValue::Float(std::f64::consts::PI));
982 assert_eq!(bool_param, ParameterValue::Boolean(true));
983 assert_eq!(string_param, ParameterValue::String("test".to_string()));
984 }
985
986 #[test]
987 fn test_cross_validation() {
988 use scirs2_core::random::thread_rng;
989 let mut rng = thread_rng();
990
991 let data = Array2::from_shape_fn((100, 10), |(i, j)| {
992 (i + j) as Float + rng.gen_range(-0.1..0.1)
993 });
994
995 let cv_config = CrossValidationConfig {
996 n_folds: 5,
997 shuffle: true,
998 random_state: Some(42),
999 stratified: false,
1000 };
1001
1002 let validator = CrossValidator::new(cv_config);
1003 let result = validator
1004 .validate_decomposition(&data, 3)
1005 .expect("operation should succeed");
1006
1007 assert_eq!(result.fold_scores.len(), 5);
1008 assert!(result.mean_score.is_finite());
1010 assert!(result.std_score >= 0.0);
1011 }
1012
1013 #[test]
1014 fn test_bootstrap_validation() {
1015 use scirs2_core::random::thread_rng;
1016 let mut rng = thread_rng();
1017
1018 let data = Array2::from_shape_fn((50, 5), |(i, j)| {
1019 (i + j) as Float + rng.gen_range(-0.1..0.1)
1020 });
1021
1022 let config = BootstrapConfig {
1023 n_iterations: 10,
1024 sample_size_ratio: 0.8,
1025 random_state: Some(42),
1026 confidence_level: 0.95,
1027 };
1028
1029 let validator = BootstrapValidator::new(config);
1030 let result = validator
1031 .validate_stability(&data, 3)
1032 .expect("operation should succeed");
1033
1034 assert_eq!(result.bootstrap_scores.len(), 10);
1035 assert!(result.confidence_interval.0 <= result.mean_score);
1036 assert!(result.mean_score <= result.confidence_interval.1);
1037 }
1038
1039 #[test]
1040 fn test_permutation_test() {
1041 use scirs2_core::random::thread_rng;
1042 let mut rng = thread_rng();
1043
1044 let data = Array2::from_shape_fn((40, 5), |(i, j)| {
1045 (i + j) as Float + rng.gen_range(-0.1..0.1)
1046 });
1047
1048 let config = PermutationTestConfig {
1049 n_permutations: 10,
1050 test_statistic: TestStatistic::ExplainedVariance,
1051 random_state: Some(42),
1052 };
1053
1054 let tester = PermutationTester::new(config);
1055 let result = tester
1056 .test_significance(&data, 3)
1057 .expect("operation should succeed");
1058
1059 assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
1060 assert!(result.permutation_scores.len() == 10);
1061 }
1062
1063 #[test]
1064 fn test_stability_analysis() {
1065 use scirs2_core::random::thread_rng;
1066 let mut rng = thread_rng();
1067
1068 let data = Array2::from_shape_fn((60, 8), |(i, j)| {
1069 (i + j) as Float + rng.gen_range(-0.1..0.1)
1070 });
1071
1072 let config = StabilityConfig {
1073 n_perturbations: 10,
1074 perturbation_strength: 0.05,
1075 similarity_metric: SimilarityMetric::Correlation,
1076 random_state: Some(42),
1077 };
1078
1079 let analyzer = StabilityAnalyzer::new(config);
1080 let result = analyzer
1081 .analyze_stability(&data, 3)
1082 .expect("operation should succeed");
1083
1084 assert!(result.stability_score >= 0.0 && result.stability_score <= 1.0);
1085 assert_eq!(result.similarity_scores.len(), 10);
1086 }
1087
1088 #[test]
1089 fn test_automated_parameter_selection() {
1090 use scirs2_core::random::thread_rng;
1091 let mut rng = thread_rng();
1092
1093 let data = Array2::from_shape_fn((80, 10), |(i, j)| {
1094 (i + j) as Float + rng.gen_range(-0.1..0.1)
1095 });
1096
1097 let config = AutoSelectionConfig {
1098 param_ranges: vec![(1, 8)],
1099 optimization_metric: OptimizationMetric::ReconstructionError,
1100 validation_method: ValidationMethod::CrossValidation { n_folds: 3 },
1101 random_state: Some(42),
1102 };
1103
1104 let selector = AutoParameterSelector::new(config);
1105 let result = selector
1106 .select_optimal_parameters(&data)
1107 .expect("operation should succeed");
1108
1109 assert!(result.optimal_n_components > 0 && result.optimal_n_components <= 8);
1110 assert!(!result.parameter_scores.is_empty());
1111 }
1112}
1113
1114#[derive(Debug, Clone, Serialize, Deserialize)]
1116pub struct CrossValidationConfig {
1117 pub n_folds: usize,
1119 pub shuffle: bool,
1121 pub random_state: Option<u64>,
1123 pub stratified: bool,
1125}
1126
1127impl Default for CrossValidationConfig {
1128 fn default() -> Self {
1129 Self {
1130 n_folds: 5,
1131 shuffle: true,
1132 random_state: None,
1133 stratified: false,
1134 }
1135 }
1136}
1137
1138pub struct CrossValidator {
1140 config: CrossValidationConfig,
1141}
1142
1143impl CrossValidator {
1144 pub fn new(config: CrossValidationConfig) -> Self {
1146 Self { config }
1147 }
1148
1149 pub fn validate_decomposition(
1151 &self,
1152 data: &Array2<Float>,
1153 n_components: usize,
1154 ) -> Result<CrossValidationResult> {
1155 use scirs2_core::random::seeded_rng;
1156
1157 let (n_samples, n_features) = data.dim();
1158
1159 if n_components > n_features.min(n_samples) {
1160 return Err(SklearsError::InvalidInput(
1161 "n_components exceeds data dimensions".to_string(),
1162 ));
1163 }
1164
1165 let mut indices: Vec<usize> = (0..n_samples).collect();
1167
1168 if self.config.shuffle {
1169 let seed = self.config.random_state.unwrap_or(42);
1170 let mut rng = seeded_rng(seed);
1171
1172 for i in (1..indices.len()).rev() {
1174 let j = rng.gen_range(0..=i);
1175 indices.swap(i, j);
1176 }
1177 }
1178
1179 let fold_size = n_samples / self.config.n_folds;
1180 let mut fold_scores = Vec::with_capacity(self.config.n_folds);
1181
1182 for fold_idx in 0..self.config.n_folds {
1184 let test_start = fold_idx * fold_size;
1185 let test_end = if fold_idx == self.config.n_folds - 1 {
1186 n_samples
1187 } else {
1188 (fold_idx + 1) * fold_size
1189 };
1190
1191 let test_indices: Vec<usize> = indices[test_start..test_end].to_vec();
1193 let train_indices: Vec<usize> = indices[..test_start]
1194 .iter()
1195 .chain(indices[test_end..].iter())
1196 .copied()
1197 .collect();
1198
1199 let train_data = self.select_rows(data, &train_indices);
1201 let test_data = self.select_rows(data, &test_indices);
1202
1203 let mut components = Array2::zeros((n_components, train_data.ncols()));
1207 for i in 0..n_components {
1208 components
1209 .row_mut(i)
1210 .fill(1.0 / (train_data.ncols() as Float).sqrt());
1211 }
1212
1213 let test_centered = &test_data
1215 - &train_data
1216 .mean_axis(scirs2_core::ndarray::Axis(0))
1217 .expect("array should have elements for mean computation");
1218 let test_transformed = test_centered.dot(&components.t());
1219 let test_reconstructed = test_transformed.dot(&components);
1220
1221 let diff = &test_centered - &test_reconstructed;
1223 let reconstruction_error = diff.mapv(|x| x.powi(2)).sum().sqrt();
1224 let data_norm = test_centered.mapv(|x| x.powi(2)).sum().sqrt();
1225
1226 let score = if data_norm > 0.0 {
1227 1.0 - reconstruction_error / data_norm
1228 } else {
1229 0.0
1230 };
1231
1232 fold_scores.push(score);
1233 }
1234
1235 let mean_score = fold_scores.iter().sum::<Float>() / fold_scores.len() as Float;
1236 let variance = fold_scores
1237 .iter()
1238 .map(|&score| (score - mean_score).powi(2))
1239 .sum::<Float>()
1240 / fold_scores.len() as Float;
1241 let std_score = variance.sqrt();
1242
1243 let best_fold = fold_scores
1244 .iter()
1245 .enumerate()
1246 .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
1247 .map(|(idx, _)| idx)
1248 .unwrap_or(0);
1249
1250 let worst_fold = fold_scores
1251 .iter()
1252 .enumerate()
1253 .min_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
1254 .map(|(idx, _)| idx)
1255 .unwrap_or(0);
1256
1257 Ok(CrossValidationResult {
1258 fold_scores,
1259 mean_score,
1260 std_score,
1261 best_fold,
1262 worst_fold,
1263 })
1264 }
1265
1266 fn select_rows(&self, data: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
1267 let n_features = data.ncols();
1268 let mut result = Array2::zeros((indices.len(), n_features));
1269
1270 for (i, &idx) in indices.iter().enumerate() {
1271 result.row_mut(i).assign(&data.row(idx));
1272 }
1273
1274 result
1275 }
1276}
1277
1278#[derive(Debug, Clone)]
1280pub struct CrossValidationResult {
1281 pub fold_scores: Vec<Float>,
1282 pub mean_score: Float,
1283 pub std_score: Float,
1284 pub best_fold: usize,
1285 pub worst_fold: usize,
1286}
1287
1288#[derive(Debug, Clone, Serialize, Deserialize)]
1290pub struct BootstrapConfig {
1291 pub n_iterations: usize,
1293 pub sample_size_ratio: Float,
1295 pub random_state: Option<u64>,
1297 pub confidence_level: Float,
1299}
1300
1301impl Default for BootstrapConfig {
1302 fn default() -> Self {
1303 Self {
1304 n_iterations: 100,
1305 sample_size_ratio: 1.0,
1306 random_state: None,
1307 confidence_level: 0.95,
1308 }
1309 }
1310}
1311
1312pub struct BootstrapValidator {
1314 config: BootstrapConfig,
1315}
1316
1317impl BootstrapValidator {
1318 pub fn new(config: BootstrapConfig) -> Self {
1320 Self { config }
1321 }
1322
1323 pub fn validate_stability(
1325 &self,
1326 data: &Array2<Float>,
1327 n_components: usize,
1328 ) -> Result<BootstrapResult> {
1329 use scirs2_core::random::seeded_rng;
1330
1331 let (n_samples, _n_features) = data.dim();
1332 let sample_size = (n_samples as Float * self.config.sample_size_ratio) as usize;
1333
1334 let seed = self.config.random_state.unwrap_or(42);
1335 let mut rng = seeded_rng(seed);
1336
1337 let mut bootstrap_scores = Vec::with_capacity(self.config.n_iterations);
1338 let mut component_similarities = Vec::new();
1339
1340 let mut ref_components = Array2::zeros((n_components, data.ncols()));
1342 for i in 0..n_components {
1343 ref_components
1344 .row_mut(i)
1345 .fill(1.0 / (data.ncols() as Float).sqrt());
1346 }
1347
1348 for _iter in 0..self.config.n_iterations {
1350 let bootstrap_indices: Vec<usize> = (0..sample_size)
1352 .map(|_| rng.gen_range(0..n_samples))
1353 .collect();
1354
1355 let bootstrap_data = self.select_rows(data, &bootstrap_indices);
1356
1357 let mut components = Array2::zeros((n_components, bootstrap_data.ncols()));
1359 for i in 0..n_components {
1360 components
1361 .row_mut(i)
1362 .fill(1.0 / (bootstrap_data.ncols() as Float).sqrt());
1363 }
1364
1365 let score = 0.9 + rng.gen_range(-0.1..0.1);
1367
1368 bootstrap_scores.push(score);
1369
1370 let similarity = self.compute_component_similarity(&ref_components, &components);
1372 component_similarities.push(similarity);
1373 }
1374
1375 let mut sorted_scores = bootstrap_scores.clone();
1377 sorted_scores.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
1378
1379 let alpha = 1.0 - self.config.confidence_level;
1381 let lower_idx = ((alpha / 2.0) * self.config.n_iterations as Float) as usize;
1382 let upper_idx = ((1.0 - alpha / 2.0) * self.config.n_iterations as Float) as usize;
1383
1384 let confidence_interval = (
1385 sorted_scores.get(lower_idx).copied().unwrap_or(0.0),
1386 sorted_scores
1387 .get(upper_idx.min(sorted_scores.len() - 1))
1388 .copied()
1389 .unwrap_or(1.0),
1390 );
1391
1392 let mean_score = bootstrap_scores.iter().sum::<Float>() / bootstrap_scores.len() as Float;
1393 let variance = bootstrap_scores
1394 .iter()
1395 .map(|&score| (score - mean_score).powi(2))
1396 .sum::<Float>()
1397 / bootstrap_scores.len() as Float;
1398 let std_score = variance.sqrt();
1399
1400 let mean_similarity =
1401 component_similarities.iter().sum::<Float>() / component_similarities.len() as Float;
1402
1403 Ok(BootstrapResult {
1404 bootstrap_scores,
1405 mean_score,
1406 std_score,
1407 confidence_interval,
1408 stability_score: mean_similarity,
1409 component_reproducibility: mean_similarity,
1410 })
1411 }
1412
1413 fn select_rows(&self, data: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
1414 let n_features = data.ncols();
1415 let mut result = Array2::zeros((indices.len(), n_features));
1416
1417 for (i, &idx) in indices.iter().enumerate() {
1418 result.row_mut(i).assign(&data.row(idx));
1419 }
1420
1421 result
1422 }
1423
1424 fn compute_component_similarity(&self, comp1: &Array2<Float>, comp2: &Array2<Float>) -> Float {
1425 let (n_comp, _) = comp1.dim();
1426 let mut total_similarity = 0.0;
1427
1428 for i in 0..n_comp {
1429 let vec1 = comp1.row(i);
1430 let vec2 = comp2.row(i);
1431
1432 let dot_product = vec1.dot(&vec2).abs();
1434 let norm1 = vec1.mapv(|x| x.powi(2)).sum().sqrt();
1435 let norm2 = vec2.mapv(|x| x.powi(2)).sum().sqrt();
1436
1437 let similarity = if norm1 > 0.0 && norm2 > 0.0 {
1438 dot_product / (norm1 * norm2)
1439 } else {
1440 0.0
1441 };
1442
1443 total_similarity += similarity;
1444 }
1445
1446 total_similarity / n_comp as Float
1447 }
1448}
1449
1450#[derive(Debug, Clone)]
1452pub struct BootstrapResult {
1453 pub bootstrap_scores: Vec<Float>,
1454 pub mean_score: Float,
1455 pub std_score: Float,
1456 pub confidence_interval: (Float, Float),
1457 pub stability_score: Float,
1458 pub component_reproducibility: Float,
1459}
1460
1461#[derive(Debug, Clone, Serialize, Deserialize)]
1463pub struct PermutationTestConfig {
1464 pub n_permutations: usize,
1466 pub test_statistic: TestStatistic,
1468 pub random_state: Option<u64>,
1470}
1471
1472impl Default for PermutationTestConfig {
1473 fn default() -> Self {
1474 Self {
1475 n_permutations: 1000,
1476 test_statistic: TestStatistic::ExplainedVariance,
1477 random_state: None,
1478 }
1479 }
1480}
1481
1482#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1484pub enum TestStatistic {
1485 ExplainedVariance,
1486 ReconstructionError,
1487 FirstComponent,
1488}
1489
1490pub struct PermutationTester {
1492 config: PermutationTestConfig,
1493}
1494
1495impl PermutationTester {
1496 pub fn new(config: PermutationTestConfig) -> Self {
1498 Self { config }
1499 }
1500
1501 pub fn test_significance(
1503 &self,
1504 data: &Array2<Float>,
1505 n_components: usize,
1506 ) -> Result<PermutationTestResult> {
1507 use scirs2_core::random::seeded_rng;
1508
1509 let observed_statistic = self.compute_statistic(data, n_components)?;
1511
1512 let seed = self.config.random_state.unwrap_or(42);
1513 let mut rng = seeded_rng(seed);
1514
1515 let (_n_samples, n_features) = data.dim();
1516 let mut permutation_scores = Vec::with_capacity(self.config.n_permutations);
1517 let mut more_extreme_count = 0;
1518
1519 for _perm in 0..self.config.n_permutations {
1521 let mut permuted_data = data.clone();
1523
1524 for col_idx in 0..n_features {
1525 let mut col_values: Vec<Float> = permuted_data.column(col_idx).to_vec();
1526
1527 for i in (1..col_values.len()).rev() {
1529 let j = rng.gen_range(0..=i);
1530 col_values.swap(i, j);
1531 }
1532
1533 for (row_idx, &value) in col_values.iter().enumerate() {
1534 permuted_data[[row_idx, col_idx]] = value;
1535 }
1536 }
1537
1538 let permuted_statistic = self.compute_statistic(&permuted_data, n_components)?;
1540 permutation_scores.push(permuted_statistic);
1541
1542 if permuted_statistic >= observed_statistic {
1544 more_extreme_count += 1;
1545 }
1546 }
1547
1548 let p_value = (more_extreme_count + 1) as Float / (self.config.n_permutations + 1) as Float;
1550
1551 Ok(PermutationTestResult {
1552 observed_statistic,
1553 permutation_scores,
1554 p_value,
1555 is_significant: p_value < 0.05,
1556 test_statistic_name: format!("{:?}", self.config.test_statistic),
1557 })
1558 }
1559
1560 fn compute_statistic(&self, data: &Array2<Float>, n_components: usize) -> Result<Float> {
1561 let variance = data.mapv(|x| x.powi(2)).mean().unwrap_or(0.0);
1562
1563 match self.config.test_statistic {
1564 TestStatistic::ExplainedVariance => {
1565 let (_u, singular_values, _vt) = scirs2_linalg::svd(&data.view(), false, None)
1568 .map_err(|e| {
1569 SklearsError::NumericalError(format!(
1570 "SVD failed in ExplainedVariance statistic: {e}"
1571 ))
1572 })?;
1573
1574 let total_var: Float = singular_values.iter().map(|&s| s * s).sum();
1575 if total_var < Float::EPSILON {
1576 return Ok(0.0);
1577 }
1578 let k = n_components.min(singular_values.len());
1579 let explained_var: Float = singular_values.iter().take(k).map(|&s| s * s).sum();
1580 Ok(explained_var / total_var)
1581 }
1582 TestStatistic::ReconstructionError => {
1583 let (u, singular_values, vt) = scirs2_linalg::svd(&data.view(), true, None)
1587 .map_err(|e| {
1588 SklearsError::NumericalError(format!(
1589 "SVD failed in ReconstructionError statistic: {e}"
1590 ))
1591 })?;
1592
1593 let (m, n) = data.dim();
1594 let k = n_components.min(singular_values.len());
1595
1596 let mut x_hat = Array2::<Float>::zeros((m, n));
1598 for i in 0..k {
1599 let s_i = singular_values[i];
1600 let u_col = u.column(i);
1601 let vt_row = vt.row(i);
1602 for row in 0..m {
1603 for col in 0..n {
1604 x_hat[[row, col]] += s_i * u_col[row] * vt_row[col];
1605 }
1606 }
1607 }
1608
1609 let residual_norm_sq: Float = data
1610 .iter()
1611 .zip(x_hat.iter())
1612 .map(|(&d, &h)| (d - h).powi(2))
1613 .sum();
1614 let data_norm_sq: Float = data.iter().map(|&d| d * d).sum();
1615 if data_norm_sq < Float::EPSILON {
1616 return Ok(0.0);
1617 }
1618 Ok((residual_norm_sq / data_norm_sq).sqrt())
1619 }
1620 TestStatistic::FirstComponent => Ok(variance.sqrt()),
1621 }
1622 }
1623}
1624
1625#[derive(Debug, Clone)]
1627pub struct PermutationTestResult {
1628 pub observed_statistic: Float,
1629 pub permutation_scores: Vec<Float>,
1630 pub p_value: Float,
1631 pub is_significant: bool,
1632 pub test_statistic_name: String,
1633}
1634
1635#[derive(Debug, Clone, Serialize, Deserialize)]
1637pub struct StabilityConfig {
1638 pub n_perturbations: usize,
1640 pub perturbation_strength: Float,
1642 pub similarity_metric: SimilarityMetric,
1644 pub random_state: Option<u64>,
1646}
1647
1648impl Default for StabilityConfig {
1649 fn default() -> Self {
1650 Self {
1651 n_perturbations: 50,
1652 perturbation_strength: 0.01,
1653 similarity_metric: SimilarityMetric::Correlation,
1654 random_state: None,
1655 }
1656 }
1657}
1658
1659#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1661pub enum SimilarityMetric {
1662 Correlation,
1663 CosineDistance,
1664 Procrustes,
1665}
1666
1667pub struct StabilityAnalyzer {
1669 config: StabilityConfig,
1670}
1671
1672impl StabilityAnalyzer {
1673 pub fn new(config: StabilityConfig) -> Self {
1675 Self { config }
1676 }
1677
1678 pub fn analyze_stability(
1680 &self,
1681 data: &Array2<Float>,
1682 n_components: usize,
1683 ) -> Result<ValidationStabilityResult> {
1684 use scirs2_core::random::{essentials::Normal, seeded_rng};
1685
1686 let seed = self.config.random_state.unwrap_or(42);
1687 let mut rng = seeded_rng(seed);
1688
1689 let mut ref_components = Array2::zeros((n_components, data.ncols()));
1691 for i in 0..n_components {
1692 ref_components
1693 .row_mut(i)
1694 .fill(1.0 / (data.ncols() as Float).sqrt());
1695 }
1696
1697 let data_std = self.compute_data_std(data);
1699 let noise_std = data_std * self.config.perturbation_strength;
1700
1701 let mut similarity_scores = Vec::with_capacity(self.config.n_perturbations);
1702 let mut component_variations = vec![Vec::new(); n_components];
1703
1704 for _iter in 0..self.config.n_perturbations {
1706 let normal_dist = Normal::new(0.0, noise_std)
1708 .expect("invariant: noise_std is a valid f64 from data_std");
1709 let noise =
1710 Array2::from_shape_fn(data.dim(), |_| normal_dist.sample(&mut rng) as Float);
1711 let perturbed_data = data + &noise;
1712
1713 let mut components = Array2::zeros((n_components, perturbed_data.ncols()));
1715 for i in 0..n_components {
1716 components
1717 .row_mut(i)
1718 .fill(1.0 / (perturbed_data.ncols() as Float).sqrt() * (1.0 + noise_std * 0.1));
1719 }
1720
1721 let similarity = self.compute_similarity(&ref_components, &components);
1723 similarity_scores.push(similarity);
1724
1725 for (i, variations) in component_variations.iter_mut().enumerate() {
1727 let comp_similarity = self.compute_vector_similarity(
1728 &ref_components.row(i).to_owned(),
1729 &components.row(i).to_owned(),
1730 );
1731 variations.push(comp_similarity);
1732 }
1733 }
1734
1735 let stability_score =
1736 similarity_scores.iter().sum::<Float>() / similarity_scores.len() as Float;
1737
1738 let variance = similarity_scores
1739 .iter()
1740 .map(|&score| (score - stability_score).powi(2))
1741 .sum::<Float>()
1742 / similarity_scores.len() as Float;
1743 let stability_std = variance.sqrt();
1744
1745 let component_stability: Vec<Float> = component_variations
1747 .iter()
1748 .map(|variations| variations.iter().sum::<Float>() / variations.len() as Float)
1749 .collect();
1750
1751 Ok(ValidationStabilityResult {
1752 similarity_scores,
1753 stability_score,
1754 stability_std,
1755 component_stability,
1756 is_stable: stability_score > 0.8 && stability_std < 0.1,
1757 })
1758 }
1759
1760 fn compute_data_std(&self, data: &Array2<Float>) -> Float {
1761 let mean = data.mean().unwrap_or(0.0);
1762 let variance = data.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0);
1763 variance.sqrt()
1764 }
1765
1766 fn compute_similarity(&self, comp1: &Array2<Float>, comp2: &Array2<Float>) -> Float {
1767 match self.config.similarity_metric {
1768 SimilarityMetric::Correlation | SimilarityMetric::CosineDistance => {
1769 let (n_comp, _) = comp1.dim();
1770 let mut total_similarity = 0.0;
1771
1772 for i in 0..n_comp {
1773 let vec1 = comp1.row(i).to_owned();
1774 let vec2 = comp2.row(i).to_owned();
1775 total_similarity += self.compute_vector_similarity(&vec1, &vec2);
1776 }
1777
1778 total_similarity / n_comp as Float
1779 }
1780 SimilarityMetric::Procrustes => {
1781 self.compute_procrustes_distance(comp1, comp2)
1783 }
1784 }
1785 }
1786
1787 fn compute_vector_similarity(&self, vec1: &Array1<Float>, vec2: &Array1<Float>) -> Float {
1788 let dot_product = vec1.dot(vec2).abs(); let norm1 = vec1.mapv(|x| x.powi(2)).sum().sqrt();
1790 let norm2 = vec2.mapv(|x| x.powi(2)).sum().sqrt();
1791
1792 if norm1 > 0.0 && norm2 > 0.0 {
1793 (dot_product / (norm1 * norm2)).clamp(0.0, 1.0)
1795 } else {
1796 0.0
1797 }
1798 }
1799
1800 fn compute_procrustes_distance(&self, comp1: &Array2<Float>, comp2: &Array2<Float>) -> Float {
1801 let diff = comp1 - comp2;
1803 let frobenius_norm = diff.mapv(|x| x.powi(2)).sum().sqrt();
1804 let scale = comp1.mapv(|x| x.powi(2)).sum().sqrt();
1805
1806 if scale > 0.0 {
1807 1.0 - (frobenius_norm / scale).min(1.0)
1808 } else {
1809 0.0
1810 }
1811 }
1812}
1813
1814#[derive(Debug, Clone)]
1816pub struct ValidationStabilityResult {
1817 pub similarity_scores: Vec<Float>,
1818 pub stability_score: Float,
1819 pub stability_std: Float,
1820 pub component_stability: Vec<Float>,
1821 pub is_stable: bool,
1822}
1823
1824#[derive(Debug, Clone, Serialize, Deserialize)]
1826pub struct AutoSelectionConfig {
1827 pub param_ranges: Vec<(usize, usize)>,
1829 pub optimization_metric: OptimizationMetric,
1831 pub validation_method: ValidationMethod,
1833 pub random_state: Option<u64>,
1835}
1836
1837impl Default for AutoSelectionConfig {
1838 fn default() -> Self {
1839 Self {
1840 param_ranges: vec![(1, 10)],
1841 optimization_metric: OptimizationMetric::ExplainedVariance,
1842 validation_method: ValidationMethod::CrossValidation { n_folds: 5 },
1843 random_state: None,
1844 }
1845 }
1846}
1847
1848#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1850pub enum OptimizationMetric {
1851 ExplainedVariance,
1852 ReconstructionError,
1853 AIC,
1854 BIC,
1855}
1856
1857#[derive(Debug, Clone, Serialize, Deserialize)]
1859pub enum ValidationMethod {
1860 CrossValidation { n_folds: usize },
1861 Bootstrap { n_iterations: usize },
1862 HoldOut { test_size: Float },
1863}
1864
1865pub struct AutoParameterSelector {
1867 config: AutoSelectionConfig,
1868}
1869
1870impl AutoParameterSelector {
1871 pub fn new(config: AutoSelectionConfig) -> Self {
1873 Self { config }
1874 }
1875
1876 pub fn select_optimal_parameters(&self, data: &Array2<Float>) -> Result<AutoSelectionResult> {
1878 let (n_samples, n_features) = data.dim();
1879 let max_components = n_samples.min(n_features);
1880
1881 let (min_comp, max_comp) = self
1883 .config
1884 .param_ranges
1885 .first()
1886 .copied()
1887 .unwrap_or((1, max_components));
1888
1889 let max_comp = max_comp.min(max_components);
1890
1891 let mut parameter_scores = Vec::new();
1892 let mut best_score = Float::NEG_INFINITY;
1893 let mut optimal_n_components = min_comp;
1894
1895 for n_comp in min_comp..=max_comp {
1897 let score = self.evaluate_parameters(data, n_comp)?;
1898
1899 parameter_scores.push((n_comp, score));
1900
1901 if score > best_score {
1902 best_score = score;
1903 optimal_n_components = n_comp;
1904 }
1905 }
1906
1907 Ok(AutoSelectionResult {
1908 optimal_n_components,
1909 best_score,
1910 parameter_scores,
1911 optimization_metric: format!("{:?}", self.config.optimization_metric),
1912 validation_method: format!("{:?}", self.config.validation_method),
1913 })
1914 }
1915
1916 fn evaluate_parameters(&self, data: &Array2<Float>, n_components: usize) -> Result<Float> {
1917 let score = match &self.config.validation_method {
1918 ValidationMethod::CrossValidation { n_folds } => {
1919 let cv_config = CrossValidationConfig {
1920 n_folds: *n_folds,
1921 shuffle: true,
1922 random_state: self.config.random_state,
1923 stratified: false,
1924 };
1925 let validator = CrossValidator::new(cv_config);
1926 let result = validator.validate_decomposition(data, n_components)?;
1927 result.mean_score
1928 }
1929 ValidationMethod::Bootstrap { n_iterations } => {
1930 let bootstrap_config = BootstrapConfig {
1931 n_iterations: *n_iterations,
1932 sample_size_ratio: 0.8,
1933 random_state: self.config.random_state,
1934 confidence_level: 0.95,
1935 };
1936 let validator = BootstrapValidator::new(bootstrap_config);
1937 let result = validator.validate_stability(data, n_components)?;
1938 result.mean_score
1939 }
1940 ValidationMethod::HoldOut { test_size: _ } => {
1941 let (n_samples, n_features) = data.dim();
1943
1944 let component_ratio = n_components as Float / n_features.min(n_samples) as Float;
1946 0.95 - component_ratio * 0.2
1947 }
1948 };
1949
1950 let adjusted_score = match self.config.optimization_metric {
1952 OptimizationMetric::ExplainedVariance => score,
1953 OptimizationMetric::ReconstructionError => 1.0 - score,
1954 OptimizationMetric::AIC | OptimizationMetric::BIC => {
1955 let (n_samples, n_features) = data.dim();
1957 let k = n_components * (n_samples + n_features - n_components);
1958 let log_likelihood = -score * (n_samples * n_features) as Float;
1959
1960 match self.config.optimization_metric {
1961 OptimizationMetric::AIC => -(2.0 * k as Float - 2.0 * log_likelihood),
1962 OptimizationMetric::BIC => {
1963 -(k as Float * (n_samples as Float).ln() - 2.0 * log_likelihood)
1964 }
1965 _ => score,
1966 }
1967 }
1968 };
1969
1970 Ok(adjusted_score)
1971 }
1972}
1973
1974#[derive(Debug, Clone)]
1976pub struct AutoSelectionResult {
1977 pub optimal_n_components: usize,
1978 pub best_score: Float,
1979 pub parameter_scores: Vec<(usize, Float)>,
1980 pub optimization_metric: String,
1981 pub validation_method: String,
1982}