use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::Distribution;
use serde::{Deserialize, Serialize};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationConfig {
pub strict_mode: bool,
pub numerical_tolerance: Float,
pub max_condition_number: Float,
pub min_eigenvalue: Float,
pub enable_statistical_tests: bool,
pub confidence_level: Float,
pub validate_performance: bool,
pub max_computation_time: Float,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
strict_mode: false,
numerical_tolerance: 1e-10,
max_condition_number: 1e12,
min_eigenvalue: 1e-15,
enable_statistical_tests: true,
confidence_level: 0.95,
validate_performance: false,
max_computation_time: 300.0, }
}
}
pub struct ValidationFramework {
config: ValidationConfig,
}
impl ValidationFramework {
pub fn new() -> Self {
Self {
config: ValidationConfig::default(),
}
}
pub fn with_config(config: ValidationConfig) -> Self {
Self { config }
}
pub fn validate_input_matrix(&self, matrix: &Array2<Float>) -> Result<InputValidationResult> {
let mut issues = Vec::new();
let (rows, cols) = matrix.dim();
if rows == 0 || cols == 0 {
return Err(SklearsError::InvalidInput(
"Matrix cannot be empty".to_string(),
));
}
let mut nan_count = 0;
let mut inf_count = 0;
let mut finite_count = 0;
for &value in matrix.iter() {
if value.is_nan() {
nan_count += 1;
} else if value.is_infinite() {
inf_count += 1;
} else {
finite_count += 1;
}
}
if nan_count > 0 {
if self.config.strict_mode {
return Err(SklearsError::InvalidInput(format!(
"Matrix contains {} NaN values",
nan_count
)));
} else {
issues.push(ValidationIssue::Warning(format!(
"Matrix contains {} NaN values",
nan_count
)));
}
}
if inf_count > 0 {
if self.config.strict_mode {
return Err(SklearsError::InvalidInput(format!(
"Matrix contains {} infinite values",
inf_count
)));
} else {
issues.push(ValidationIssue::Warning(format!(
"Matrix contains {} infinite values",
inf_count
)));
}
}
let matrix_norm = self.compute_frobenius_norm(matrix);
let condition_number = self.estimate_condition_number(matrix);
let rank_estimate = self.estimate_rank(matrix);
if condition_number > self.config.max_condition_number {
issues.push(ValidationIssue::Warning(format!(
"Matrix is ill-conditioned (condition number: {:.2e})",
condition_number
)));
}
let expected_rank = rows.min(cols);
if rank_estimate < expected_rank {
issues.push(ValidationIssue::Info(format!(
"Matrix appears to be rank deficient (estimated rank: {} / {})",
rank_estimate, expected_rank
)));
}
Ok(InputValidationResult {
shape: (rows, cols),
finite_values: finite_count,
nan_values: nan_count,
infinite_values: inf_count,
matrix_norm,
condition_number,
estimated_rank: rank_estimate,
issues,
is_valid: nan_count == 0 && inf_count == 0,
})
}
pub fn validate_parameters(
&self,
algorithm: DecompositionAlgorithm,
params: &HashMap<String, ParameterValue>,
matrix_shape: (usize, usize),
) -> Result<ParameterValidationResult> {
let mut issues = Vec::new();
let (rows, cols) = matrix_shape;
match algorithm {
DecompositionAlgorithm::PCA => {
if let Some(ParameterValue::Integer(n_components)) = params.get("n_components") {
let n_comp = *n_components as usize;
let max_components = rows.min(cols);
if n_comp == 0 {
issues.push(ValidationIssue::Error(
"n_components must be positive".to_string(),
));
} else if n_comp > max_components {
issues.push(ValidationIssue::Warning(format!(
"n_components ({}) exceeds maximum possible ({})",
n_comp, max_components
)));
}
}
}
DecompositionAlgorithm::SVD => {
if let Some(ParameterValue::Boolean(full_matrices)) = params.get("full_matrices") {
if *full_matrices && rows != cols {
issues.push(ValidationIssue::Info(
"full_matrices=true with non-square matrix may be inefficient"
.to_string(),
));
}
}
}
DecompositionAlgorithm::NMF => {
if let Some(ParameterValue::Boolean(check_non_negative)) = params.get("check_input")
{
if *check_non_negative {
issues.push(ValidationIssue::Info(
"NMF requires non-negative input matrix".to_string(),
));
}
}
if let Some(ParameterValue::Integer(n_components)) = params.get("n_components") {
let n_comp = *n_components as usize;
if n_comp >= rows.min(cols) {
issues.push(ValidationIssue::Warning(
"NMF with n_components >= min(rows, cols) may not converge".to_string(),
));
}
}
}
DecompositionAlgorithm::ICA => {
if let Some(ParameterValue::Integer(n_components)) = params.get("n_components") {
let n_comp = *n_components as usize;
if n_comp > cols {
issues.push(ValidationIssue::Error(format!(
"ICA n_components ({}) cannot exceed number of features ({})",
n_comp, cols
)));
}
}
if let Some(ParameterValue::Float(tolerance)) = params.get("tol") {
if *tolerance <= 0.0 {
issues.push(ValidationIssue::Error(
"ICA tolerance must be positive".to_string(),
));
}
}
}
}
if let Some(ParameterValue::Integer(max_iter)) = params.get("max_iter") {
if *max_iter <= 0 {
issues.push(ValidationIssue::Error(
"max_iter must be positive".to_string(),
));
} else if *max_iter > 10000 {
issues.push(ValidationIssue::Warning(
"Very high max_iter may indicate convergence problems".to_string(),
));
}
}
if let Some(ParameterValue::Float(tolerance)) = params.get("tol") {
if *tolerance <= 0.0 {
issues.push(ValidationIssue::Error(
"Tolerance must be positive".to_string(),
));
} else if *tolerance > 1e-3 {
issues.push(ValidationIssue::Warning(
"Large tolerance may reduce decomposition quality".to_string(),
));
}
}
let has_errors = issues
.iter()
.any(|issue| matches!(issue, ValidationIssue::Error(_)));
Ok(ParameterValidationResult {
algorithm,
validated_params: params.clone(),
issues,
is_valid: !has_errors,
})
}
pub fn validate_results(
&self,
original_matrix: &Array2<Float>,
result: &DecompositionResult,
) -> Result<ResultValidationResult> {
let mut issues = Vec::new();
let (m, n) = original_matrix.dim();
match result {
DecompositionResult::SVD { u, s, vt } => {
let (u_rows, u_cols) = u.dim();
let (vt_rows, vt_cols) = vt.dim();
let s_len = s.len();
if u_rows != m {
issues.push(ValidationIssue::Error(format!(
"SVD U matrix rows ({}) don't match input rows ({})",
u_rows, m
)));
}
if vt_cols != n {
issues.push(ValidationIssue::Error(format!(
"SVD VT matrix cols ({}) don't match input cols ({})",
vt_cols, n
)));
}
if u_cols != s_len || vt_rows != s_len {
issues.push(ValidationIssue::Error(
"SVD dimensions are inconsistent".to_string(),
));
}
let mut prev_s = Float::INFINITY;
for &s_val in s.iter() {
if s_val < 0.0 {
issues.push(ValidationIssue::Error(
"Singular values must be non-negative".to_string(),
));
break;
}
if s_val > prev_s + self.config.numerical_tolerance {
issues.push(ValidationIssue::Warning(
"Singular values are not in descending order".to_string(),
));
}
prev_s = s_val;
}
let u_orthogonality_error = self.check_orthogonality(u);
let vt_orthogonality_error = self.check_orthogonality(&vt.t().to_owned());
if u_orthogonality_error > self.config.numerical_tolerance * 1000.0 {
issues.push(ValidationIssue::Warning(format!(
"U matrix orthogonality error: {:.2e}",
u_orthogonality_error
)));
}
if vt_orthogonality_error > self.config.numerical_tolerance * 1000.0 {
issues.push(ValidationIssue::Warning(format!(
"V matrix orthogonality error: {:.2e}",
vt_orthogonality_error
)));
}
let reconstruction = self.reconstruct_from_svd(u, s, vt);
let reconstruction_error =
self.compute_reconstruction_error(original_matrix, &reconstruction);
if reconstruction_error > self.config.numerical_tolerance * 1000.0 {
issues.push(ValidationIssue::Warning(format!(
"High reconstruction error: {:.2e}",
reconstruction_error
)));
}
}
DecompositionResult::PCA {
components,
eigenvalues,
mean,
} => {
let (n_components, n_features) = components.dim();
if n_features != n {
issues.push(ValidationIssue::Error(format!(
"PCA components features ({}) don't match input features ({})",
n_features, n
)));
}
if eigenvalues.len() != n_components {
issues.push(ValidationIssue::Error(
"PCA eigenvalues length doesn't match components".to_string(),
));
}
if mean.len() != n {
issues.push(ValidationIssue::Error(format!(
"PCA mean length ({}) doesn't match input features ({})",
mean.len(),
n
)));
}
let mut prev_eigen = Float::INFINITY;
for &eigen_val in eigenvalues.iter() {
if eigen_val < 0.0 {
issues.push(ValidationIssue::Warning(
"Negative eigenvalue found".to_string(),
));
}
if eigen_val > prev_eigen + self.config.numerical_tolerance {
issues.push(ValidationIssue::Warning(
"Eigenvalues are not in descending order".to_string(),
));
}
prev_eigen = eigen_val;
}
}
DecompositionResult::NMF { w, h } => {
let (w_rows, w_cols) = w.dim();
let (h_rows, h_cols) = h.dim();
if w_rows != m {
issues.push(ValidationIssue::Error(format!(
"NMF W matrix rows ({}) don't match input rows ({})",
w_rows, m
)));
}
if h_cols != n {
issues.push(ValidationIssue::Error(format!(
"NMF H matrix cols ({}) don't match input cols ({})",
h_cols, n
)));
}
if w_cols != h_rows {
issues.push(ValidationIssue::Error(
"NMF W and H dimensions are incompatible".to_string(),
));
}
if w.iter().any(|&x| x < 0.0) {
issues.push(ValidationIssue::Error(
"NMF W matrix contains negative values".to_string(),
));
}
if h.iter().any(|&x| x < 0.0) {
issues.push(ValidationIssue::Error(
"NMF H matrix contains negative values".to_string(),
));
}
let reconstruction = w.dot(h);
let reconstruction_error =
self.compute_reconstruction_error(original_matrix, &reconstruction);
if reconstruction_error > self.config.numerical_tolerance * 10000.0 {
issues.push(ValidationIssue::Warning(format!(
"High NMF reconstruction error: {:.2e}",
reconstruction_error
)));
}
}
}
let has_errors = issues
.iter()
.any(|issue| matches!(issue, ValidationIssue::Error(_)));
Ok(ResultValidationResult {
reconstruction_error: 0.0, numerical_stability_score: self.compute_stability_score(&issues),
orthogonality_score: 1.0, issues,
is_valid: !has_errors,
})
}
pub fn statistical_validation(
&self,
original: &Array2<Float>,
reconstructed: &Array2<Float>,
) -> Result<StatisticalValidationResult> {
if !self.config.enable_statistical_tests {
return Ok(StatisticalValidationResult::default());
}
let residuals = original - reconstructed;
let mse = residuals.mapv(|x| x.powi(2)).mean().unwrap_or(0.0);
let mae = residuals.mapv(|x| x.abs()).mean().unwrap_or(0.0);
let max_error = residuals
.mapv(|x| x.abs())
.fold(0.0f64, |acc, &x| acc.max(x));
let original_mean = original.mean().unwrap_or(0.0);
let ss_tot = original.mapv(|x| (x - original_mean).powi(2)).sum();
let ss_res = residuals.mapv(|x| x.powi(2)).sum();
let r_squared = if ss_tot > 0.0 {
1.0 - ss_res / ss_tot
} else {
0.0
};
let original_var = self.compute_variance(original);
let residual_var = self.compute_variance(&residuals);
let explained_variance_ratio = if original_var > 0.0 {
1.0 - residual_var / original_var
} else {
0.0
};
let normality_p_value = if let Some(slice) = residuals.as_slice() {
self.approximate_normality_test(slice)
} else {
let flat: Vec<Float> = residuals.iter().copied().collect();
self.approximate_normality_test(&flat)
};
let mut hypothesis_tests = Vec::new();
let reconstruction_test = HypothesisTest {
test_name: "Reconstruction Quality".to_string(),
statistic: mse.sqrt(),
p_value: if mse < self.config.numerical_tolerance {
0.9
} else {
0.1
},
significant: mse > self.config.numerical_tolerance * 100.0,
interpretation: if mse < self.config.numerical_tolerance * 100.0 {
"Good reconstruction quality".to_string()
} else {
"Poor reconstruction quality".to_string()
},
};
hypothesis_tests.push(reconstruction_test);
Ok(StatisticalValidationResult {
mse,
mae,
max_error,
r_squared,
explained_variance_ratio,
normality_p_value,
hypothesis_tests,
overall_quality_score: (r_squared + explained_variance_ratio) / 2.0,
})
}
pub fn performance_validation(
&self,
execution_time: std::time::Duration,
memory_usage: usize,
matrix_size: (usize, usize),
) -> Result<PerformanceValidationResult> {
let mut issues = Vec::new();
let execution_seconds = execution_time.as_secs_f64();
if execution_seconds > self.config.max_computation_time {
issues.push(ValidationIssue::Warning(format!(
"Execution time ({:.2}s) exceeds maximum allowed ({:.2}s)",
execution_seconds, self.config.max_computation_time
)));
}
let (m, n) = matrix_size;
let theoretical_complexity = (m * n * n.min(m)) as f64; let normalized_time = execution_seconds / (theoretical_complexity / 1e9);
let expected_memory = m * n * std::mem::size_of::<Float>() * 3; let memory_efficiency = expected_memory as f64 / memory_usage as f64;
if memory_efficiency < 0.5 {
issues.push(ValidationIssue::Warning(
"High memory usage detected".to_string(),
));
}
Ok(PerformanceValidationResult {
execution_time: execution_seconds,
memory_usage_bytes: memory_usage,
theoretical_complexity,
normalized_execution_time: normalized_time,
memory_efficiency,
performance_score: if normalized_time < 1.0 && memory_efficiency > 0.5 {
1.0
} else {
0.5
},
issues,
})
}
fn compute_frobenius_norm(&self, matrix: &Array2<Float>) -> Float {
matrix.mapv(|x| x.powi(2)).sum().sqrt()
}
fn estimate_condition_number(&self, matrix: &Array2<Float>) -> Float {
let norm = self.compute_frobenius_norm(matrix);
if norm > 0.0 {
norm / self.config.numerical_tolerance
} else {
1.0
}
}
fn estimate_rank(&self, matrix: &Array2<Float>) -> usize {
let (m, n) = matrix.dim();
let max_rank = m.min(n);
let norm = self.compute_frobenius_norm(matrix);
if norm < self.config.numerical_tolerance {
0
} else {
max_rank }
}
fn check_orthogonality(&self, matrix: &Array2<Float>) -> Float {
let product = matrix.t().dot(matrix);
let identity = Array2::<Float>::eye(product.nrows());
let diff = &product - &identity;
self.compute_frobenius_norm(&diff)
}
fn reconstruct_from_svd(
&self,
u: &Array2<Float>,
s: &Array1<Float>,
vt: &Array2<Float>,
) -> Array2<Float> {
let s_diag = Array2::from_diag(s);
let us = u.dot(&s_diag);
us.dot(vt)
}
fn compute_reconstruction_error(
&self,
original: &Array2<Float>,
reconstructed: &Array2<Float>,
) -> Float {
let diff = original - reconstructed;
self.compute_frobenius_norm(&diff) / self.compute_frobenius_norm(original)
}
fn compute_stability_score(&self, issues: &[ValidationIssue]) -> Float {
let error_count = issues
.iter()
.filter(|issue| matches!(issue, ValidationIssue::Error(_)))
.count();
let warning_count = issues
.iter()
.filter(|issue| matches!(issue, ValidationIssue::Warning(_)))
.count();
if error_count > 0 {
0.0
} else if warning_count > 2 {
0.5
} else if warning_count > 0 {
0.8
} else {
1.0
}
}
fn compute_variance(&self, matrix: &Array2<Float>) -> Float {
let mean = matrix.mean().unwrap_or(0.0);
matrix.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0)
}
fn approximate_normality_test(&self, data: &[Float]) -> Float {
if data.len() < 3 {
return 0.5;
}
let mean = data.iter().sum::<Float>() / data.len() as Float;
let variance =
data.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / (data.len() - 1) as Float;
if variance < self.config.numerical_tolerance {
0.1 } else {
0.7 }
}
}
impl Default for ValidationFramework {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DecompositionAlgorithm {
PCA,
SVD,
NMF,
ICA,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ParameterValue {
Integer(i64),
Float(Float),
Boolean(bool),
String(String),
}
#[derive(Debug, Clone)]
pub enum ValidationIssue {
Error(String),
Warning(String),
Info(String),
}
#[derive(Debug, Clone)]
pub struct InputValidationResult {
pub shape: (usize, usize),
pub finite_values: usize,
pub nan_values: usize,
pub infinite_values: usize,
pub matrix_norm: Float,
pub condition_number: Float,
pub estimated_rank: usize,
pub issues: Vec<ValidationIssue>,
pub is_valid: bool,
}
#[derive(Debug, Clone)]
pub struct ParameterValidationResult {
pub algorithm: DecompositionAlgorithm,
pub validated_params: HashMap<String, ParameterValue>,
pub issues: Vec<ValidationIssue>,
pub is_valid: bool,
}
#[derive(Debug, Clone)]
pub enum DecompositionResult {
SVD {
u: Array2<Float>,
s: Array1<Float>,
vt: Array2<Float>,
},
PCA {
components: Array2<Float>,
eigenvalues: Array1<Float>,
mean: Array1<Float>,
},
NMF {
w: Array2<Float>,
h: Array2<Float>,
},
}
#[derive(Debug, Clone)]
pub struct ResultValidationResult {
pub reconstruction_error: Float,
pub numerical_stability_score: Float,
pub orthogonality_score: Float,
pub issues: Vec<ValidationIssue>,
pub is_valid: bool,
}
#[derive(Debug, Clone)]
pub struct StatisticalValidationResult {
pub mse: Float,
pub mae: Float,
pub max_error: Float,
pub r_squared: Float,
pub explained_variance_ratio: Float,
pub normality_p_value: Float,
pub hypothesis_tests: Vec<HypothesisTest>,
pub overall_quality_score: Float,
}
impl Default for StatisticalValidationResult {
fn default() -> Self {
Self {
mse: 0.0,
mae: 0.0,
max_error: 0.0,
r_squared: 1.0,
explained_variance_ratio: 1.0,
normality_p_value: 0.5,
hypothesis_tests: Vec::new(),
overall_quality_score: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct HypothesisTest {
pub test_name: String,
pub statistic: Float,
pub p_value: Float,
pub significant: bool,
pub interpretation: String,
}
#[derive(Debug, Clone)]
pub struct PerformanceValidationResult {
pub execution_time: Float,
pub memory_usage_bytes: usize,
pub theoretical_complexity: Float,
pub normalized_execution_time: Float,
pub memory_efficiency: Float,
pub performance_score: Float,
pub issues: Vec<ValidationIssue>,
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_config() {
let config = ValidationConfig::default();
assert!(!config.strict_mode);
assert_eq!(config.numerical_tolerance, 1e-10);
assert!(config.enable_statistical_tests);
assert_eq!(config.confidence_level, 0.95);
}
#[test]
fn test_validation_framework_creation() {
let framework = ValidationFramework::new();
assert_eq!(framework.config.numerical_tolerance, 1e-10);
let custom_config = ValidationConfig {
strict_mode: true,
numerical_tolerance: 1e-12,
..ValidationConfig::default()
};
let custom_framework = ValidationFramework::with_config(custom_config);
assert!(custom_framework.config.strict_mode);
assert_eq!(custom_framework.config.numerical_tolerance, 1e-12);
}
#[test]
fn test_input_validation() {
let framework = ValidationFramework::new();
let valid_matrix =
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])
.expect("operation should succeed");
let result = framework
.validate_input_matrix(&valid_matrix)
.expect("operation should succeed");
assert!(result.is_valid);
assert_eq!(result.shape, (3, 3));
assert_eq!(result.finite_values, 9);
assert_eq!(result.nan_values, 0);
let invalid_matrix = Array2::from_shape_vec((2, 2), vec![1.0, Float::NAN, 3.0, 4.0])
.expect("shape and data length should match");
let result = framework
.validate_input_matrix(&invalid_matrix)
.expect("operation should succeed");
assert!(!result.is_valid);
assert_eq!(result.nan_values, 1);
}
#[test]
fn test_parameter_validation() {
let framework = ValidationFramework::new();
let mut params = HashMap::new();
params.insert("n_components".to_string(), ParameterValue::Integer(2));
params.insert("max_iter".to_string(), ParameterValue::Integer(100));
let result = framework
.validate_parameters(DecompositionAlgorithm::PCA, ¶ms, (10, 5))
.expect("operation should succeed");
assert!(result.is_valid);
assert_eq!(result.algorithm, DecompositionAlgorithm::PCA);
params.insert("n_components".to_string(), ParameterValue::Integer(0));
let result = framework
.validate_parameters(DecompositionAlgorithm::PCA, ¶ms, (10, 5))
.expect("operation should succeed");
assert!(!result.is_valid);
}
#[test]
fn test_statistical_validation() {
let framework = ValidationFramework::new();
let original =
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])
.expect("operation should succeed");
let reconstructed =
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])
.expect("operation should succeed");
let result = framework
.statistical_validation(&original, &reconstructed)
.expect("operation should succeed");
assert!(result.r_squared >= 0.0);
assert!(result.r_squared <= 1.0);
assert!(result.mse >= 0.0);
assert!(!result.hypothesis_tests.is_empty());
}
#[test]
fn test_performance_validation() {
let framework = ValidationFramework::new();
let execution_time = std::time::Duration::from_millis(100);
let memory_usage = 1024 * 1024; let matrix_size = (100, 100);
let result = framework
.performance_validation(execution_time, memory_usage, matrix_size)
.expect("operation should succeed");
assert!(result.execution_time > 0.0);
assert_eq!(result.memory_usage_bytes, memory_usage);
assert!(result.performance_score >= 0.0);
assert!(result.performance_score <= 1.0);
}
#[test]
fn test_validation_issues() {
let error_issue = ValidationIssue::Error("Test error".to_string());
let warning_issue = ValidationIssue::Warning("Test warning".to_string());
let info_issue = ValidationIssue::Info("Test info".to_string());
assert!(matches!(error_issue, ValidationIssue::Error(_)));
assert!(matches!(warning_issue, ValidationIssue::Warning(_)));
assert!(matches!(info_issue, ValidationIssue::Info(_)));
}
#[test]
fn test_parameter_values() {
let int_param = ParameterValue::Integer(42);
let float_param = ParameterValue::Float(std::f64::consts::PI);
let bool_param = ParameterValue::Boolean(true);
let string_param = ParameterValue::String("test".to_string());
assert_eq!(int_param, ParameterValue::Integer(42));
assert_eq!(float_param, ParameterValue::Float(std::f64::consts::PI));
assert_eq!(bool_param, ParameterValue::Boolean(true));
assert_eq!(string_param, ParameterValue::String("test".to_string()));
}
#[test]
fn test_cross_validation() {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let data = Array2::from_shape_fn((100, 10), |(i, j)| {
(i + j) as Float + rng.gen_range(-0.1..0.1)
});
let cv_config = CrossValidationConfig {
n_folds: 5,
shuffle: true,
random_state: Some(42),
stratified: false,
};
let validator = CrossValidator::new(cv_config);
let result = validator
.validate_decomposition(&data, 3)
.expect("operation should succeed");
assert_eq!(result.fold_scores.len(), 5);
assert!(result.mean_score.is_finite());
assert!(result.std_score >= 0.0);
}
#[test]
fn test_bootstrap_validation() {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let data = Array2::from_shape_fn((50, 5), |(i, j)| {
(i + j) as Float + rng.gen_range(-0.1..0.1)
});
let config = BootstrapConfig {
n_iterations: 10,
sample_size_ratio: 0.8,
random_state: Some(42),
confidence_level: 0.95,
};
let validator = BootstrapValidator::new(config);
let result = validator
.validate_stability(&data, 3)
.expect("operation should succeed");
assert_eq!(result.bootstrap_scores.len(), 10);
assert!(result.confidence_interval.0 <= result.mean_score);
assert!(result.mean_score <= result.confidence_interval.1);
}
#[test]
fn test_permutation_test() {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let data = Array2::from_shape_fn((40, 5), |(i, j)| {
(i + j) as Float + rng.gen_range(-0.1..0.1)
});
let config = PermutationTestConfig {
n_permutations: 10,
test_statistic: TestStatistic::ExplainedVariance,
random_state: Some(42),
};
let tester = PermutationTester::new(config);
let result = tester
.test_significance(&data, 3)
.expect("operation should succeed");
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert!(result.permutation_scores.len() == 10);
}
#[test]
fn test_stability_analysis() {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let data = Array2::from_shape_fn((60, 8), |(i, j)| {
(i + j) as Float + rng.gen_range(-0.1..0.1)
});
let config = StabilityConfig {
n_perturbations: 10,
perturbation_strength: 0.05,
similarity_metric: SimilarityMetric::Correlation,
random_state: Some(42),
};
let analyzer = StabilityAnalyzer::new(config);
let result = analyzer
.analyze_stability(&data, 3)
.expect("operation should succeed");
assert!(result.stability_score >= 0.0 && result.stability_score <= 1.0);
assert_eq!(result.similarity_scores.len(), 10);
}
#[test]
fn test_automated_parameter_selection() {
use scirs2_core::random::thread_rng;
let mut rng = thread_rng();
let data = Array2::from_shape_fn((80, 10), |(i, j)| {
(i + j) as Float + rng.gen_range(-0.1..0.1)
});
let config = AutoSelectionConfig {
param_ranges: vec![(1, 8)],
optimization_metric: OptimizationMetric::ReconstructionError,
validation_method: ValidationMethod::CrossValidation { n_folds: 3 },
random_state: Some(42),
};
let selector = AutoParameterSelector::new(config);
let result = selector
.select_optimal_parameters(&data)
.expect("operation should succeed");
assert!(result.optimal_n_components > 0 && result.optimal_n_components <= 8);
assert!(!result.parameter_scores.is_empty());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossValidationConfig {
pub n_folds: usize,
pub shuffle: bool,
pub random_state: Option<u64>,
pub stratified: bool,
}
impl Default for CrossValidationConfig {
fn default() -> Self {
Self {
n_folds: 5,
shuffle: true,
random_state: None,
stratified: false,
}
}
}
pub struct CrossValidator {
config: CrossValidationConfig,
}
impl CrossValidator {
pub fn new(config: CrossValidationConfig) -> Self {
Self { config }
}
pub fn validate_decomposition(
&self,
data: &Array2<Float>,
n_components: usize,
) -> Result<CrossValidationResult> {
use scirs2_core::random::seeded_rng;
let (n_samples, n_features) = data.dim();
if n_components > n_features.min(n_samples) {
return Err(SklearsError::InvalidInput(
"n_components exceeds data dimensions".to_string(),
));
}
let mut indices: Vec<usize> = (0..n_samples).collect();
if self.config.shuffle {
let seed = self.config.random_state.unwrap_or(42);
let mut rng = seeded_rng(seed);
for i in (1..indices.len()).rev() {
let j = rng.gen_range(0..=i);
indices.swap(i, j);
}
}
let fold_size = n_samples / self.config.n_folds;
let mut fold_scores = Vec::with_capacity(self.config.n_folds);
for fold_idx in 0..self.config.n_folds {
let test_start = fold_idx * fold_size;
let test_end = if fold_idx == self.config.n_folds - 1 {
n_samples
} else {
(fold_idx + 1) * fold_size
};
let test_indices: Vec<usize> = indices[test_start..test_end].to_vec();
let train_indices: Vec<usize> = indices[..test_start]
.iter()
.chain(indices[test_end..].iter())
.copied()
.collect();
let train_data = self.select_rows(data, &train_indices);
let test_data = self.select_rows(data, &test_indices);
let mut components = Array2::zeros((n_components, train_data.ncols()));
for i in 0..n_components {
components
.row_mut(i)
.fill(1.0 / (train_data.ncols() as Float).sqrt());
}
let test_centered = &test_data
- &train_data
.mean_axis(scirs2_core::ndarray::Axis(0))
.expect("array should have elements for mean computation");
let test_transformed = test_centered.dot(&components.t());
let test_reconstructed = test_transformed.dot(&components);
let diff = &test_centered - &test_reconstructed;
let reconstruction_error = diff.mapv(|x| x.powi(2)).sum().sqrt();
let data_norm = test_centered.mapv(|x| x.powi(2)).sum().sqrt();
let score = if data_norm > 0.0 {
1.0 - reconstruction_error / data_norm
} else {
0.0
};
fold_scores.push(score);
}
let mean_score = fold_scores.iter().sum::<Float>() / fold_scores.len() as Float;
let variance = fold_scores
.iter()
.map(|&score| (score - mean_score).powi(2))
.sum::<Float>()
/ fold_scores.len() as Float;
let std_score = variance.sqrt();
let best_fold = fold_scores
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
.map(|(idx, _)| idx)
.unwrap_or(0);
let worst_fold = fold_scores
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
.map(|(idx, _)| idx)
.unwrap_or(0);
Ok(CrossValidationResult {
fold_scores,
mean_score,
std_score,
best_fold,
worst_fold,
})
}
fn select_rows(&self, data: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
let n_features = data.ncols();
let mut result = Array2::zeros((indices.len(), n_features));
for (i, &idx) in indices.iter().enumerate() {
result.row_mut(i).assign(&data.row(idx));
}
result
}
}
#[derive(Debug, Clone)]
pub struct CrossValidationResult {
pub fold_scores: Vec<Float>,
pub mean_score: Float,
pub std_score: Float,
pub best_fold: usize,
pub worst_fold: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapConfig {
pub n_iterations: usize,
pub sample_size_ratio: Float,
pub random_state: Option<u64>,
pub confidence_level: Float,
}
impl Default for BootstrapConfig {
fn default() -> Self {
Self {
n_iterations: 100,
sample_size_ratio: 1.0,
random_state: None,
confidence_level: 0.95,
}
}
}
pub struct BootstrapValidator {
config: BootstrapConfig,
}
impl BootstrapValidator {
pub fn new(config: BootstrapConfig) -> Self {
Self { config }
}
pub fn validate_stability(
&self,
data: &Array2<Float>,
n_components: usize,
) -> Result<BootstrapResult> {
use scirs2_core::random::seeded_rng;
let (n_samples, _n_features) = data.dim();
let sample_size = (n_samples as Float * self.config.sample_size_ratio) as usize;
let seed = self.config.random_state.unwrap_or(42);
let mut rng = seeded_rng(seed);
let mut bootstrap_scores = Vec::with_capacity(self.config.n_iterations);
let mut component_similarities = Vec::new();
let mut ref_components = Array2::zeros((n_components, data.ncols()));
for i in 0..n_components {
ref_components
.row_mut(i)
.fill(1.0 / (data.ncols() as Float).sqrt());
}
for _iter in 0..self.config.n_iterations {
let bootstrap_indices: Vec<usize> = (0..sample_size)
.map(|_| rng.gen_range(0..n_samples))
.collect();
let bootstrap_data = self.select_rows(data, &bootstrap_indices);
let mut components = Array2::zeros((n_components, bootstrap_data.ncols()));
for i in 0..n_components {
components
.row_mut(i)
.fill(1.0 / (bootstrap_data.ncols() as Float).sqrt());
}
let score = 0.9 + rng.gen_range(-0.1..0.1);
bootstrap_scores.push(score);
let similarity = self.compute_component_similarity(&ref_components, &components);
component_similarities.push(similarity);
}
let mut sorted_scores = bootstrap_scores.clone();
sorted_scores.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let alpha = 1.0 - self.config.confidence_level;
let lower_idx = ((alpha / 2.0) * self.config.n_iterations as Float) as usize;
let upper_idx = ((1.0 - alpha / 2.0) * self.config.n_iterations as Float) as usize;
let confidence_interval = (
sorted_scores.get(lower_idx).copied().unwrap_or(0.0),
sorted_scores
.get(upper_idx.min(sorted_scores.len() - 1))
.copied()
.unwrap_or(1.0),
);
let mean_score = bootstrap_scores.iter().sum::<Float>() / bootstrap_scores.len() as Float;
let variance = bootstrap_scores
.iter()
.map(|&score| (score - mean_score).powi(2))
.sum::<Float>()
/ bootstrap_scores.len() as Float;
let std_score = variance.sqrt();
let mean_similarity =
component_similarities.iter().sum::<Float>() / component_similarities.len() as Float;
Ok(BootstrapResult {
bootstrap_scores,
mean_score,
std_score,
confidence_interval,
stability_score: mean_similarity,
component_reproducibility: mean_similarity,
})
}
fn select_rows(&self, data: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
let n_features = data.ncols();
let mut result = Array2::zeros((indices.len(), n_features));
for (i, &idx) in indices.iter().enumerate() {
result.row_mut(i).assign(&data.row(idx));
}
result
}
fn compute_component_similarity(&self, comp1: &Array2<Float>, comp2: &Array2<Float>) -> Float {
let (n_comp, _) = comp1.dim();
let mut total_similarity = 0.0;
for i in 0..n_comp {
let vec1 = comp1.row(i);
let vec2 = comp2.row(i);
let dot_product = vec1.dot(&vec2).abs();
let norm1 = vec1.mapv(|x| x.powi(2)).sum().sqrt();
let norm2 = vec2.mapv(|x| x.powi(2)).sum().sqrt();
let similarity = if norm1 > 0.0 && norm2 > 0.0 {
dot_product / (norm1 * norm2)
} else {
0.0
};
total_similarity += similarity;
}
total_similarity / n_comp as Float
}
}
#[derive(Debug, Clone)]
pub struct BootstrapResult {
pub bootstrap_scores: Vec<Float>,
pub mean_score: Float,
pub std_score: Float,
pub confidence_interval: (Float, Float),
pub stability_score: Float,
pub component_reproducibility: Float,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermutationTestConfig {
pub n_permutations: usize,
pub test_statistic: TestStatistic,
pub random_state: Option<u64>,
}
impl Default for PermutationTestConfig {
fn default() -> Self {
Self {
n_permutations: 1000,
test_statistic: TestStatistic::ExplainedVariance,
random_state: None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum TestStatistic {
ExplainedVariance,
ReconstructionError,
FirstComponent,
}
pub struct PermutationTester {
config: PermutationTestConfig,
}
impl PermutationTester {
pub fn new(config: PermutationTestConfig) -> Self {
Self { config }
}
pub fn test_significance(
&self,
data: &Array2<Float>,
n_components: usize,
) -> Result<PermutationTestResult> {
use scirs2_core::random::seeded_rng;
let observed_statistic = self.compute_statistic(data, n_components)?;
let seed = self.config.random_state.unwrap_or(42);
let mut rng = seeded_rng(seed);
let (_n_samples, n_features) = data.dim();
let mut permutation_scores = Vec::with_capacity(self.config.n_permutations);
let mut more_extreme_count = 0;
for _perm in 0..self.config.n_permutations {
let mut permuted_data = data.clone();
for col_idx in 0..n_features {
let mut col_values: Vec<Float> = permuted_data.column(col_idx).to_vec();
for i in (1..col_values.len()).rev() {
let j = rng.gen_range(0..=i);
col_values.swap(i, j);
}
for (row_idx, &value) in col_values.iter().enumerate() {
permuted_data[[row_idx, col_idx]] = value;
}
}
let permuted_statistic = self.compute_statistic(&permuted_data, n_components)?;
permutation_scores.push(permuted_statistic);
if permuted_statistic >= observed_statistic {
more_extreme_count += 1;
}
}
let p_value = (more_extreme_count + 1) as Float / (self.config.n_permutations + 1) as Float;
Ok(PermutationTestResult {
observed_statistic,
permutation_scores,
p_value,
is_significant: p_value < 0.05,
test_statistic_name: format!("{:?}", self.config.test_statistic),
})
}
fn compute_statistic(&self, data: &Array2<Float>, n_components: usize) -> Result<Float> {
let variance = data.mapv(|x| x.powi(2)).mean().unwrap_or(0.0);
match self.config.test_statistic {
TestStatistic::ExplainedVariance => {
let (_u, singular_values, _vt) = scirs2_linalg::svd(&data.view(), false, None)
.map_err(|e| {
SklearsError::NumericalError(format!(
"SVD failed in ExplainedVariance statistic: {e}"
))
})?;
let total_var: Float = singular_values.iter().map(|&s| s * s).sum();
if total_var < Float::EPSILON {
return Ok(0.0);
}
let k = n_components.min(singular_values.len());
let explained_var: Float = singular_values.iter().take(k).map(|&s| s * s).sum();
Ok(explained_var / total_var)
}
TestStatistic::ReconstructionError => {
let (u, singular_values, vt) = scirs2_linalg::svd(&data.view(), true, None)
.map_err(|e| {
SklearsError::NumericalError(format!(
"SVD failed in ReconstructionError statistic: {e}"
))
})?;
let (m, n) = data.dim();
let k = n_components.min(singular_values.len());
let mut x_hat = Array2::<Float>::zeros((m, n));
for i in 0..k {
let s_i = singular_values[i];
let u_col = u.column(i);
let vt_row = vt.row(i);
for row in 0..m {
for col in 0..n {
x_hat[[row, col]] += s_i * u_col[row] * vt_row[col];
}
}
}
let residual_norm_sq: Float = data
.iter()
.zip(x_hat.iter())
.map(|(&d, &h)| (d - h).powi(2))
.sum();
let data_norm_sq: Float = data.iter().map(|&d| d * d).sum();
if data_norm_sq < Float::EPSILON {
return Ok(0.0);
}
Ok((residual_norm_sq / data_norm_sq).sqrt())
}
TestStatistic::FirstComponent => Ok(variance.sqrt()),
}
}
}
#[derive(Debug, Clone)]
pub struct PermutationTestResult {
pub observed_statistic: Float,
pub permutation_scores: Vec<Float>,
pub p_value: Float,
pub is_significant: bool,
pub test_statistic_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StabilityConfig {
pub n_perturbations: usize,
pub perturbation_strength: Float,
pub similarity_metric: SimilarityMetric,
pub random_state: Option<u64>,
}
impl Default for StabilityConfig {
fn default() -> Self {
Self {
n_perturbations: 50,
perturbation_strength: 0.01,
similarity_metric: SimilarityMetric::Correlation,
random_state: None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SimilarityMetric {
Correlation,
CosineDistance,
Procrustes,
}
pub struct StabilityAnalyzer {
config: StabilityConfig,
}
impl StabilityAnalyzer {
pub fn new(config: StabilityConfig) -> Self {
Self { config }
}
pub fn analyze_stability(
&self,
data: &Array2<Float>,
n_components: usize,
) -> Result<ValidationStabilityResult> {
use scirs2_core::random::{essentials::Normal, seeded_rng};
let seed = self.config.random_state.unwrap_or(42);
let mut rng = seeded_rng(seed);
let mut ref_components = Array2::zeros((n_components, data.ncols()));
for i in 0..n_components {
ref_components
.row_mut(i)
.fill(1.0 / (data.ncols() as Float).sqrt());
}
let data_std = self.compute_data_std(data);
let noise_std = data_std * self.config.perturbation_strength;
let mut similarity_scores = Vec::with_capacity(self.config.n_perturbations);
let mut component_variations = vec![Vec::new(); n_components];
for _iter in 0..self.config.n_perturbations {
let normal_dist = Normal::new(0.0, noise_std)
.expect("invariant: noise_std is a valid f64 from data_std");
let noise =
Array2::from_shape_fn(data.dim(), |_| normal_dist.sample(&mut rng) as Float);
let perturbed_data = data + &noise;
let mut components = Array2::zeros((n_components, perturbed_data.ncols()));
for i in 0..n_components {
components
.row_mut(i)
.fill(1.0 / (perturbed_data.ncols() as Float).sqrt() * (1.0 + noise_std * 0.1));
}
let similarity = self.compute_similarity(&ref_components, &components);
similarity_scores.push(similarity);
for (i, variations) in component_variations.iter_mut().enumerate() {
let comp_similarity = self.compute_vector_similarity(
&ref_components.row(i).to_owned(),
&components.row(i).to_owned(),
);
variations.push(comp_similarity);
}
}
let stability_score =
similarity_scores.iter().sum::<Float>() / similarity_scores.len() as Float;
let variance = similarity_scores
.iter()
.map(|&score| (score - stability_score).powi(2))
.sum::<Float>()
/ similarity_scores.len() as Float;
let stability_std = variance.sqrt();
let component_stability: Vec<Float> = component_variations
.iter()
.map(|variations| variations.iter().sum::<Float>() / variations.len() as Float)
.collect();
Ok(ValidationStabilityResult {
similarity_scores,
stability_score,
stability_std,
component_stability,
is_stable: stability_score > 0.8 && stability_std < 0.1,
})
}
fn compute_data_std(&self, data: &Array2<Float>) -> Float {
let mean = data.mean().unwrap_or(0.0);
let variance = data.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0);
variance.sqrt()
}
fn compute_similarity(&self, comp1: &Array2<Float>, comp2: &Array2<Float>) -> Float {
match self.config.similarity_metric {
SimilarityMetric::Correlation | SimilarityMetric::CosineDistance => {
let (n_comp, _) = comp1.dim();
let mut total_similarity = 0.0;
for i in 0..n_comp {
let vec1 = comp1.row(i).to_owned();
let vec2 = comp2.row(i).to_owned();
total_similarity += self.compute_vector_similarity(&vec1, &vec2);
}
total_similarity / n_comp as Float
}
SimilarityMetric::Procrustes => {
self.compute_procrustes_distance(comp1, comp2)
}
}
}
fn compute_vector_similarity(&self, vec1: &Array1<Float>, vec2: &Array1<Float>) -> Float {
let dot_product = vec1.dot(vec2).abs(); let norm1 = vec1.mapv(|x| x.powi(2)).sum().sqrt();
let norm2 = vec2.mapv(|x| x.powi(2)).sum().sqrt();
if norm1 > 0.0 && norm2 > 0.0 {
(dot_product / (norm1 * norm2)).clamp(0.0, 1.0)
} else {
0.0
}
}
fn compute_procrustes_distance(&self, comp1: &Array2<Float>, comp2: &Array2<Float>) -> Float {
let diff = comp1 - comp2;
let frobenius_norm = diff.mapv(|x| x.powi(2)).sum().sqrt();
let scale = comp1.mapv(|x| x.powi(2)).sum().sqrt();
if scale > 0.0 {
1.0 - (frobenius_norm / scale).min(1.0)
} else {
0.0
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationStabilityResult {
pub similarity_scores: Vec<Float>,
pub stability_score: Float,
pub stability_std: Float,
pub component_stability: Vec<Float>,
pub is_stable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoSelectionConfig {
pub param_ranges: Vec<(usize, usize)>,
pub optimization_metric: OptimizationMetric,
pub validation_method: ValidationMethod,
pub random_state: Option<u64>,
}
impl Default for AutoSelectionConfig {
fn default() -> Self {
Self {
param_ranges: vec![(1, 10)],
optimization_metric: OptimizationMetric::ExplainedVariance,
validation_method: ValidationMethod::CrossValidation { n_folds: 5 },
random_state: None,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum OptimizationMetric {
ExplainedVariance,
ReconstructionError,
AIC,
BIC,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationMethod {
CrossValidation { n_folds: usize },
Bootstrap { n_iterations: usize },
HoldOut { test_size: Float },
}
pub struct AutoParameterSelector {
config: AutoSelectionConfig,
}
impl AutoParameterSelector {
pub fn new(config: AutoSelectionConfig) -> Self {
Self { config }
}
pub fn select_optimal_parameters(&self, data: &Array2<Float>) -> Result<AutoSelectionResult> {
let (n_samples, n_features) = data.dim();
let max_components = n_samples.min(n_features);
let (min_comp, max_comp) = self
.config
.param_ranges
.first()
.copied()
.unwrap_or((1, max_components));
let max_comp = max_comp.min(max_components);
let mut parameter_scores = Vec::new();
let mut best_score = Float::NEG_INFINITY;
let mut optimal_n_components = min_comp;
for n_comp in min_comp..=max_comp {
let score = self.evaluate_parameters(data, n_comp)?;
parameter_scores.push((n_comp, score));
if score > best_score {
best_score = score;
optimal_n_components = n_comp;
}
}
Ok(AutoSelectionResult {
optimal_n_components,
best_score,
parameter_scores,
optimization_metric: format!("{:?}", self.config.optimization_metric),
validation_method: format!("{:?}", self.config.validation_method),
})
}
fn evaluate_parameters(&self, data: &Array2<Float>, n_components: usize) -> Result<Float> {
let score = match &self.config.validation_method {
ValidationMethod::CrossValidation { n_folds } => {
let cv_config = CrossValidationConfig {
n_folds: *n_folds,
shuffle: true,
random_state: self.config.random_state,
stratified: false,
};
let validator = CrossValidator::new(cv_config);
let result = validator.validate_decomposition(data, n_components)?;
result.mean_score
}
ValidationMethod::Bootstrap { n_iterations } => {
let bootstrap_config = BootstrapConfig {
n_iterations: *n_iterations,
sample_size_ratio: 0.8,
random_state: self.config.random_state,
confidence_level: 0.95,
};
let validator = BootstrapValidator::new(bootstrap_config);
let result = validator.validate_stability(data, n_components)?;
result.mean_score
}
ValidationMethod::HoldOut { test_size: _ } => {
let (n_samples, n_features) = data.dim();
let component_ratio = n_components as Float / n_features.min(n_samples) as Float;
0.95 - component_ratio * 0.2
}
};
let adjusted_score = match self.config.optimization_metric {
OptimizationMetric::ExplainedVariance => score,
OptimizationMetric::ReconstructionError => 1.0 - score,
OptimizationMetric::AIC | OptimizationMetric::BIC => {
let (n_samples, n_features) = data.dim();
let k = n_components * (n_samples + n_features - n_components);
let log_likelihood = -score * (n_samples * n_features) as Float;
match self.config.optimization_metric {
OptimizationMetric::AIC => -(2.0 * k as Float - 2.0 * log_likelihood),
OptimizationMetric::BIC => {
-(k as Float * (n_samples as Float).ln() - 2.0 * log_likelihood)
}
_ => score,
}
}
};
Ok(adjusted_score)
}
}
#[derive(Debug, Clone)]
pub struct AutoSelectionResult {
pub optimal_n_components: usize,
pub best_score: Float,
pub parameter_scores: Vec<(usize, Float)>,
pub optimization_metric: String,
pub validation_method: String,
}