sklears_gaussian_process/
noise_function_learning.rs

1//! Advanced Noise Function Learning for Heteroscedastic Gaussian Processes
2//!
3//! This module implements sophisticated noise function learning techniques including:
4//! - Automatic noise function selection based on data characteristics
5//! - Hyperparameter optimization for noise function kernels
6//! - Ensemble noise functions that combine multiple noise models
7//! - Adaptive regularization and model selection
8//!
9//! # Mathematical Background
10//!
11//! Advanced noise function learning goes beyond simple parameter updates to include:
12//! 1. **Model Selection**: Choosing the best noise function type using information criteria
13//! 2. **Hyperparameter Optimization**: Optimizing kernel parameters for GP-based noise functions
14//! 3. **Ensemble Methods**: Combining multiple noise functions with learned weights
15//! 4. **Adaptive Regularization**: Adjusting regularization based on data characteristics
16//!
17//! # Example
18//!
19//! ```rust
20//! use sklears_gaussian_process::noise_function_learning::{
21//!     AutomaticNoiseFunctionSelector, EnsembleNoiseFunction
22//! };
23//! use sklears_gaussian_process::kernels::RBF;
24//! use scirs2_core::ndarray::array;
25//!
26//! let x_train = array![[0.0], [1.0], [2.0], [3.0]];
27//! let y_train = array![0.0, 1.0, 2.5, 4.2];
28//!
29//! // Automatic noise function selection
30//! let selector = AutomaticNoiseFunctionSelector::new()
31//!     .add_candidate_constant()
32//!     .add_candidate_linear()
33//!     .add_candidate_polynomial(2)
34//!     .add_candidate_gaussian_process(Box::new(RBF::new(1.0)), 5);
35//!
36//! let best_noise_fn = selector.select_best(&x_train, &y_train).unwrap();
37//! ```
38
39use crate::heteroscedastic::{LearnableNoiseFunction, NoiseFunction};
40use crate::kernels::Kernel;
41use scirs2_core::ndarray::{Array1, Array2, Axis};
42// SciRS2 Policy
43use sklears_core::error::{Result as SklResult, SklearsError};
44
45/// Information criteria for model selection
46#[derive(Debug, Clone, Copy)]
47pub enum InformationCriterion {
48    /// Akaike Information Criterion
49    AIC,
50    /// Bayesian Information Criterion
51    BIC,
52    /// Hannan-Quinn Information Criterion
53    HQC,
54}
55
56/// Result of noise function evaluation
57#[derive(Debug, Clone)]
58pub struct NoiseFunctionEvaluation {
59    pub noise_function: LearnableNoiseFunction,
60    pub log_likelihood: f64,
61    pub aic: f64,
62    pub bic: f64,
63    pub hqc: f64,
64    pub rmse: f64,
65    pub n_parameters: usize,
66}
67
68/// Automatic noise function selector that evaluates multiple candidates
69#[derive(Debug, Clone)]
70pub struct AutomaticNoiseFunctionSelector {
71    candidates: Vec<LearnableNoiseFunction>,
72    criterion: InformationCriterion,
73    max_iter: usize,
74    learning_rate: f64,
75    cross_validation_folds: Option<usize>,
76    regularization_strength: f64,
77}
78
79impl AutomaticNoiseFunctionSelector {
80    /// Create a new automatic noise function selector
81    pub fn new() -> Self {
82        Self {
83            candidates: Vec::new(),
84            criterion: InformationCriterion::BIC,
85            max_iter: 100,
86            learning_rate: 0.01,
87            cross_validation_folds: None,
88            regularization_strength: 1e-6,
89        }
90    }
91
92    /// Set the information criterion for model selection
93    pub fn criterion(mut self, criterion: InformationCriterion) -> Self {
94        self.criterion = criterion;
95        self
96    }
97
98    /// Set maximum iterations for noise function training
99    pub fn max_iter(mut self, max_iter: usize) -> Self {
100        self.max_iter = max_iter;
101        self
102    }
103
104    /// Set learning rate for noise function training
105    pub fn learning_rate(mut self, learning_rate: f64) -> Self {
106        self.learning_rate = learning_rate;
107        self
108    }
109
110    /// Enable cross-validation for model selection
111    pub fn cross_validation_folds(mut self, folds: usize) -> Self {
112        self.cross_validation_folds = Some(folds);
113        self
114    }
115
116    /// Set regularization strength
117    pub fn regularization_strength(mut self, strength: f64) -> Self {
118        self.regularization_strength = strength;
119        self
120    }
121
122    /// Add a constant noise function candidate
123    pub fn add_candidate_constant(mut self) -> Self {
124        self.candidates.push(LearnableNoiseFunction::constant(0.1));
125        self
126    }
127
128    /// Add a linear noise function candidate
129    pub fn add_candidate_linear(mut self) -> Self {
130        self.candidates
131            .push(LearnableNoiseFunction::linear(0.1, 0.01));
132        self
133    }
134
135    /// Add a polynomial noise function candidate
136    pub fn add_candidate_polynomial(mut self, degree: usize) -> Self {
137        let mut coeffs = vec![0.1]; // Constant term
138        for _ in 1..=degree {
139            coeffs.push(0.01); // Higher order terms
140        }
141        self.candidates
142            .push(LearnableNoiseFunction::polynomial(coeffs));
143        self
144    }
145
146    /// Add a Gaussian process noise function candidate
147    pub fn add_candidate_gaussian_process(
148        mut self,
149        kernel: Box<dyn Kernel>,
150        n_inducing: usize,
151    ) -> Self {
152        self.candidates
153            .push(LearnableNoiseFunction::gaussian_process(kernel, n_inducing));
154        self
155    }
156
157    /// Add a neural network noise function candidate
158    pub fn add_candidate_neural_network(
159        mut self,
160        input_dim: usize,
161        hidden_sizes: Vec<usize>,
162    ) -> Self {
163        self.candidates.push(LearnableNoiseFunction::neural_network(
164            input_dim,
165            hidden_sizes,
166        ));
167        self
168    }
169
170    /// Select the best noise function based on the specified criterion
171    pub fn select_best(
172        &self,
173        x: &Array2<f64>,
174        y: &Array1<f64>,
175    ) -> SklResult<LearnableNoiseFunction> {
176        if self.candidates.is_empty() {
177            return Err(SklearsError::InvalidInput(
178                "No candidate noise functions specified".to_string(),
179            ));
180        }
181
182        let mut best_evaluation: Option<NoiseFunctionEvaluation> = None;
183
184        for candidate in &self.candidates {
185            let evaluation = if let Some(folds) = self.cross_validation_folds {
186                self.evaluate_with_cross_validation(candidate.clone(), x, y, folds)?
187            } else {
188                self.evaluate_noise_function(candidate.clone(), x, y)?
189            };
190
191            if let Some(ref best) = best_evaluation {
192                let is_better = match self.criterion {
193                    InformationCriterion::AIC => evaluation.aic < best.aic,
194                    InformationCriterion::BIC => evaluation.bic < best.bic,
195                    InformationCriterion::HQC => evaluation.hqc < best.hqc,
196                };
197
198                if is_better {
199                    best_evaluation = Some(evaluation);
200                }
201            } else {
202                best_evaluation = Some(evaluation);
203            }
204        }
205
206        Ok(best_evaluation.unwrap().noise_function)
207    }
208
209    /// Evaluate a noise function on given data
210    fn evaluate_noise_function(
211        &self,
212        mut noise_fn: LearnableNoiseFunction,
213        x: &Array2<f64>,
214        y: &Array1<f64>,
215    ) -> SklResult<NoiseFunctionEvaluation> {
216        // Train the noise function
217        for _iter in 0..self.max_iter {
218            // Compute residuals (simplified - assume zero mean for this evaluation)
219            let residuals = y.clone();
220            noise_fn.update_parameters(x, &residuals, self.learning_rate)?;
221        }
222
223        // Compute noise predictions
224        let noise_predictions = noise_fn.compute_noise(x)?;
225
226        // Compute residuals and log likelihood
227        let residuals: Array1<f64> = y.iter().map(|&yi| yi * yi).collect(); // Squared residuals
228        let mut log_likelihood = 0.0;
229
230        for i in 0..y.len() {
231            let noise_var = noise_predictions[i].max(1e-12);
232            log_likelihood += -0.5
233                * (residuals[i] / noise_var + noise_var.ln() + (2.0 * std::f64::consts::PI).ln());
234        }
235
236        // Add regularization penalty
237        let params = noise_fn.get_parameters();
238        let regularization_penalty =
239            self.regularization_strength * params.iter().map(|&p| p * p).sum::<f64>();
240        log_likelihood -= regularization_penalty;
241
242        let n_data = y.len() as f64;
243        let n_params = params.len() as f64;
244
245        // Compute information criteria
246        let aic = -2.0 * log_likelihood + 2.0 * n_params;
247        let bic = -2.0 * log_likelihood + n_params * n_data.ln();
248        let hqc = -2.0 * log_likelihood + 2.0 * n_params * n_data.ln().ln();
249
250        // Compute RMSE
251        let mut rmse = 0.0;
252        for i in 0..y.len() {
253            let predicted_noise = noise_predictions[i].sqrt();
254            let actual_abs_residual = y[i].abs();
255            rmse += (predicted_noise - actual_abs_residual).powi(2);
256        }
257        rmse = (rmse / n_data).sqrt();
258
259        Ok(NoiseFunctionEvaluation {
260            noise_function: noise_fn,
261            log_likelihood,
262            aic,
263            bic,
264            hqc,
265            rmse,
266            n_parameters: params.len(),
267        })
268    }
269
270    /// Evaluate a noise function using cross-validation
271    fn evaluate_with_cross_validation(
272        &self,
273        noise_fn: LearnableNoiseFunction,
274        x: &Array2<f64>,
275        y: &Array1<f64>,
276        folds: usize,
277    ) -> SklResult<NoiseFunctionEvaluation> {
278        let n_data = x.nrows();
279        let fold_size = n_data / folds;
280        let mut total_log_likelihood = 0.0;
281        let mut total_rmse = 0.0;
282
283        for fold in 0..folds {
284            let start_idx = fold * fold_size;
285            let end_idx = if fold == folds - 1 {
286                n_data
287            } else {
288                (fold + 1) * fold_size
289            };
290
291            // Create train/test splits
292            let mut train_indices = Vec::new();
293            let mut test_indices = Vec::new();
294
295            for i in 0..n_data {
296                if i >= start_idx && i < end_idx {
297                    test_indices.push(i);
298                } else {
299                    train_indices.push(i);
300                }
301            }
302
303            // Extract train and test data
304            let x_train = x.select(Axis(0), &train_indices);
305            let y_train = y.select(Axis(0), &train_indices);
306            let x_test = x.select(Axis(0), &test_indices);
307            let y_test = y.select(Axis(0), &test_indices);
308
309            // Train noise function on training data
310            let mut fold_noise_fn = noise_fn.clone();
311            for _iter in 0..self.max_iter {
312                let residuals = y_train.clone();
313                fold_noise_fn.update_parameters(&x_train, &residuals, self.learning_rate)?;
314            }
315
316            // Evaluate on test data
317            let test_noise_predictions = fold_noise_fn.compute_noise(&x_test)?;
318
319            for i in 0..y_test.len() {
320                let noise_var = test_noise_predictions[i].max(1e-12);
321                let residual_sq = y_test[i] * y_test[i];
322                total_log_likelihood += -0.5
323                    * (residual_sq / noise_var
324                        + noise_var.ln()
325                        + (2.0 * std::f64::consts::PI).ln());
326
327                let predicted_noise = noise_var.sqrt();
328                let actual_abs_residual = y_test[i].abs();
329                total_rmse += (predicted_noise - actual_abs_residual).powi(2);
330            }
331        }
332
333        // Average across folds
334        total_log_likelihood /= n_data as f64;
335        total_rmse = (total_rmse / n_data as f64).sqrt();
336
337        // Train final model on full data
338        let mut final_noise_fn = noise_fn;
339        for _iter in 0..self.max_iter {
340            let residuals = y.clone();
341            final_noise_fn.update_parameters(x, &residuals, self.learning_rate)?;
342        }
343
344        let params = final_noise_fn.get_parameters();
345        let n_data = y.len() as f64;
346        let n_params = params.len() as f64;
347
348        // Compute information criteria using CV log likelihood
349        let aic = -2.0 * total_log_likelihood + 2.0 * n_params;
350        let bic = -2.0 * total_log_likelihood + n_params * n_data.ln();
351        let hqc = -2.0 * total_log_likelihood + 2.0 * n_params * n_data.ln().ln();
352
353        Ok(NoiseFunctionEvaluation {
354            noise_function: final_noise_fn,
355            log_likelihood: total_log_likelihood,
356            aic,
357            bic,
358            hqc,
359            rmse: total_rmse,
360            n_parameters: params.len(),
361        })
362    }
363
364    /// Get all evaluations for analysis
365    pub fn evaluate_all(
366        &self,
367        x: &Array2<f64>,
368        y: &Array1<f64>,
369    ) -> SklResult<Vec<NoiseFunctionEvaluation>> {
370        let mut evaluations = Vec::new();
371
372        for candidate in &self.candidates {
373            let evaluation = if let Some(folds) = self.cross_validation_folds {
374                self.evaluate_with_cross_validation(candidate.clone(), x, y, folds)?
375            } else {
376                self.evaluate_noise_function(candidate.clone(), x, y)?
377            };
378            evaluations.push(evaluation);
379        }
380
381        Ok(evaluations)
382    }
383}
384
385/// Ensemble noise function that combines multiple noise functions
386#[derive(Debug, Clone)]
387pub struct EnsembleNoiseFunction {
388    noise_functions: Vec<LearnableNoiseFunction>,
389    weights: Array1<f64>,
390    combination_method: CombinationMethod,
391}
392
393/// Method for combining ensemble predictions
394#[derive(Debug, Clone, Copy)]
395pub enum CombinationMethod {
396    /// Weighted average
397    WeightedAverage,
398    /// Weighted geometric mean
399    WeightedGeometricMean,
400    /// Minimum variance combination
401    MinimumVariance,
402}
403
404impl EnsembleNoiseFunction {
405    /// Create a new ensemble noise function
406    pub fn new(noise_functions: Vec<LearnableNoiseFunction>) -> Self {
407        let n_functions = noise_functions.len();
408        let weights = Array1::from_elem(n_functions, 1.0 / n_functions as f64);
409
410        Self {
411            noise_functions,
412            weights,
413            combination_method: CombinationMethod::WeightedAverage,
414        }
415    }
416
417    /// Set the combination method
418    pub fn combination_method(mut self, method: CombinationMethod) -> Self {
419        self.combination_method = method;
420        self
421    }
422
423    /// Set custom weights
424    pub fn weights(mut self, weights: Array1<f64>) -> Self {
425        // Normalize weights
426        let sum = weights.sum();
427        self.weights = weights / sum;
428        self
429    }
430
431    /// Learn optimal weights based on data
432    pub fn learn_weights(
433        &mut self,
434        x: &Array2<f64>,
435        y: &Array1<f64>,
436        max_iter: usize,
437    ) -> SklResult<()> {
438        let n_functions = self.noise_functions.len();
439
440        // Initialize weights uniformly
441        self.weights = Array1::from_elem(n_functions, 1.0 / n_functions as f64);
442
443        let learning_rate = 0.01;
444
445        for _iter in 0..max_iter {
446            // Compute predictions from each noise function
447            let mut predictions = Vec::new();
448            for noise_fn in &self.noise_functions {
449                predictions.push(noise_fn.compute_noise(x)?);
450            }
451
452            // Compute ensemble prediction
453            let ensemble_pred = self.combine_predictions(&predictions)?;
454
455            // Compute residuals (simplified)
456            let target_noise: Array1<f64> = y.iter().map(|&yi| yi * yi).collect();
457
458            // Update weights using gradient descent
459            let mut weight_gradients = Array1::zeros(n_functions);
460
461            for i in 0..x.nrows() {
462                let error = ensemble_pred[i] - target_noise[i];
463
464                for j in 0..n_functions {
465                    let pred_diff = predictions[j][i] - ensemble_pred[i];
466                    weight_gradients[j] += error * pred_diff;
467                }
468            }
469
470            // Normalize gradients
471            weight_gradients /= x.nrows() as f64;
472
473            // Update weights
474            for j in 0..n_functions {
475                self.weights[j] -= learning_rate * weight_gradients[j];
476            }
477
478            // Ensure weights are positive and normalized
479            for weight in self.weights.iter_mut() {
480                *weight = weight.max(1e-8);
481            }
482            let sum = self.weights.sum();
483            self.weights /= sum;
484        }
485
486        Ok(())
487    }
488
489    /// Combine predictions from multiple noise functions
490    fn combine_predictions(&self, predictions: &[Array1<f64>]) -> SklResult<Array1<f64>> {
491        if predictions.is_empty() {
492            return Err(SklearsError::InvalidInput(
493                "No predictions to combine".to_string(),
494            ));
495        }
496
497        let n_points = predictions[0].len();
498        let mut combined = Array1::zeros(n_points);
499
500        match self.combination_method {
501            CombinationMethod::WeightedAverage => {
502                for i in 0..n_points {
503                    for (j, pred) in predictions.iter().enumerate() {
504                        combined[i] += self.weights[j] * pred[i];
505                    }
506                }
507            }
508            CombinationMethod::WeightedGeometricMean => {
509                for i in 0..n_points {
510                    let mut log_sum = 0.0;
511                    for (j, pred) in predictions.iter().enumerate() {
512                        log_sum += self.weights[j] * pred[i].max(1e-12).ln();
513                    }
514                    combined[i] = log_sum.exp();
515                }
516            }
517            CombinationMethod::MinimumVariance => {
518                // Simplified minimum variance combination
519                for i in 0..n_points {
520                    let mut weighted_sum = 0.0;
521                    let mut weight_sum = 0.0;
522
523                    for (_j, pred) in predictions.iter().enumerate() {
524                        let variance = pred[i].max(1e-12);
525                        let inv_var_weight = 1.0 / variance;
526                        weighted_sum += inv_var_weight * pred[i];
527                        weight_sum += inv_var_weight;
528                    }
529
530                    combined[i] = weighted_sum / weight_sum;
531                }
532            }
533        }
534
535        Ok(combined)
536    }
537}
538
539impl NoiseFunction for EnsembleNoiseFunction {
540    fn compute_noise(&self, x: &Array2<f64>) -> SklResult<Array1<f64>> {
541        let mut predictions = Vec::new();
542        for noise_fn in &self.noise_functions {
543            predictions.push(noise_fn.compute_noise(x)?);
544        }
545        self.combine_predictions(&predictions)
546    }
547
548    fn update_parameters(
549        &mut self,
550        x: &Array2<f64>,
551        residuals: &Array1<f64>,
552        learning_rate: f64,
553    ) -> SklResult<()> {
554        // Update each noise function
555        for noise_fn in &mut self.noise_functions {
556            noise_fn.update_parameters(x, residuals, learning_rate)?;
557        }
558
559        // Optionally update weights based on performance
560        let target_noise: Array1<f64> = residuals.iter().map(|&r| r * r).collect();
561        self.learn_weights(x, &target_noise, 10)?;
562
563        Ok(())
564    }
565
566    fn get_parameters(&self) -> Vec<f64> {
567        let mut all_params = Vec::new();
568
569        // Add weights
570        all_params.extend(self.weights.iter().cloned());
571
572        // Add parameters from each noise function
573        for noise_fn in &self.noise_functions {
574            all_params.extend(noise_fn.get_parameters());
575        }
576
577        all_params
578    }
579
580    fn set_parameters(&mut self, params: &[f64]) -> SklResult<()> {
581        let n_functions = self.noise_functions.len();
582
583        if params.len() < n_functions {
584            return Err(SklearsError::InvalidInput(
585                "Not enough parameters for ensemble".to_string(),
586            ));
587        }
588
589        // Set weights
590        for i in 0..n_functions {
591            self.weights[i] = params[i];
592        }
593
594        // Normalize weights
595        let sum = self.weights.sum();
596        self.weights /= sum;
597
598        // Set parameters for individual noise functions
599        let mut param_offset = n_functions;
600        for noise_fn in &mut self.noise_functions {
601            let fn_params = noise_fn.get_parameters();
602            let n_fn_params = fn_params.len();
603
604            if param_offset + n_fn_params > params.len() {
605                return Err(SklearsError::InvalidInput(
606                    "Not enough parameters for all noise functions".to_string(),
607                ));
608            }
609
610            noise_fn.set_parameters(&params[param_offset..param_offset + n_fn_params])?;
611            param_offset += n_fn_params;
612        }
613
614        Ok(())
615    }
616
617    fn clone_box(&self) -> Box<dyn NoiseFunction> {
618        Box::new(self.clone())
619    }
620}
621
622/// Adaptive regularization for noise function parameters
623#[derive(Debug, Clone)]
624pub struct AdaptiveRegularization {
625    base_strength: f64,
626    adaptation_rate: f64,
627    complexity_penalty: f64,
628}
629
630impl AdaptiveRegularization {
631    /// Create a new adaptive regularization scheme
632    pub fn new(base_strength: f64) -> Self {
633        Self {
634            base_strength,
635            adaptation_rate: 0.1,
636            complexity_penalty: 1.0,
637        }
638    }
639
640    /// Set the adaptation rate
641    pub fn adaptation_rate(mut self, rate: f64) -> Self {
642        self.adaptation_rate = rate;
643        self
644    }
645
646    /// Set the complexity penalty factor
647    pub fn complexity_penalty(mut self, penalty: f64) -> Self {
648        self.complexity_penalty = penalty;
649        self
650    }
651
652    /// Compute adaptive regularization strength based on data characteristics
653    pub fn compute_regularization_strength(
654        &self,
655        x: &Array2<f64>,
656        y: &Array1<f64>,
657        n_parameters: usize,
658    ) -> f64 {
659        let n_data = y.len() as f64;
660        let n_features = x.ncols() as f64;
661
662        // Data complexity factor
663        let data_complexity = (n_features / n_data).sqrt();
664
665        // Parameter complexity factor
666        let param_complexity = (n_parameters as f64 / n_data).sqrt();
667
668        // Noise level estimate
669        let y_mean = y.mean().unwrap_or(0.0);
670        let y_var = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum::<f64>() / n_data;
671        let noise_level = y_var.sqrt();
672
673        // Adaptive strength
674        let adaptive_factor = 1.0
675            + self.adaptation_rate * (data_complexity + self.complexity_penalty * param_complexity);
676        let strength = self.base_strength * adaptive_factor / noise_level.max(1e-6);
677
678        strength.max(1e-12).min(1e-1) // Clamp to reasonable range
679    }
680}
681
682#[allow(non_snake_case)]
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    use approx::assert_abs_diff_eq;
688    use scirs2_core::ndarray::array;
689
690    #[test]
691    fn test_automatic_noise_function_selector() {
692        let x = array![[0.0], [1.0], [2.0], [3.0], [4.0]];
693        let y = array![0.1, 0.2, 0.4, 0.8, 1.6]; // Exponential noise pattern
694
695        let selector = AutomaticNoiseFunctionSelector::new()
696            .add_candidate_constant()
697            .add_candidate_linear()
698            .add_candidate_polynomial(2)
699            .criterion(InformationCriterion::BIC)
700            .max_iter(50);
701
702        let best_noise_fn = selector.select_best(&x, &y).unwrap();
703
704        // Should be able to compute noise
705        let noise_pred = best_noise_fn.compute_noise(&x).unwrap();
706        assert_eq!(noise_pred.len(), x.nrows());
707        assert!(noise_pred.iter().all(|&n| n > 0.0));
708    }
709
710    #[test]
711    fn test_ensemble_noise_function() {
712        let x = array![[0.0], [1.0], [2.0], [3.0]];
713        let y = array![0.1, 0.2, 0.3, 0.4];
714
715        let noise_functions = vec![
716            LearnableNoiseFunction::constant(0.1),
717            LearnableNoiseFunction::linear(0.1, 0.05),
718        ];
719
720        let mut ensemble = EnsembleNoiseFunction::new(noise_functions)
721            .combination_method(CombinationMethod::WeightedAverage);
722
723        // Test initial prediction
724        let pred = ensemble.compute_noise(&x).unwrap();
725        assert_eq!(pred.len(), x.nrows());
726
727        // Test weight learning
728        ensemble.learn_weights(&x, &y, 10).unwrap();
729
730        // Weights should be normalized
731        let weight_sum: f64 = ensemble.weights.sum();
732        assert_abs_diff_eq!(weight_sum, 1.0, epsilon = 1e-10);
733    }
734
735    #[test]
736    fn test_information_criteria() {
737        let x = array![[0.0], [1.0], [2.0]];
738        let y = array![0.1, 0.2, 0.3];
739
740        let selector = AutomaticNoiseFunctionSelector::new()
741            .add_candidate_constant()
742            .add_candidate_linear()
743            .max_iter(10);
744
745        let evaluations = selector.evaluate_all(&x, &y).unwrap();
746        assert_eq!(evaluations.len(), 2);
747
748        for eval in &evaluations {
749            assert!(eval.aic.is_finite());
750            assert!(eval.bic.is_finite());
751            assert!(eval.hqc.is_finite());
752            assert!(eval.rmse >= 0.0);
753            assert!(eval.n_parameters > 0);
754        }
755    }
756
757    #[test]
758    fn test_cross_validation_evaluation() {
759        let x = array![[0.0], [1.0], [2.0], [3.0], [4.0], [5.0]];
760        let y = array![0.1, 0.2, 0.3, 0.4, 0.5, 0.6];
761
762        let selector = AutomaticNoiseFunctionSelector::new()
763            .add_candidate_constant()
764            .add_candidate_linear()
765            .cross_validation_folds(3)
766            .max_iter(10);
767
768        let best_noise_fn = selector.select_best(&x, &y).unwrap();
769        let noise_pred = best_noise_fn.compute_noise(&x).unwrap();
770        assert_eq!(noise_pred.len(), x.nrows());
771    }
772
773    #[test]
774    fn test_adaptive_regularization() {
775        let x = array![[0.0], [1.0], [2.0], [3.0]];
776        let y = array![0.1, 0.2, 0.3, 0.4];
777
778        let adaptive_reg = AdaptiveRegularization::new(1e-3)
779            .adaptation_rate(0.1)
780            .complexity_penalty(1.5);
781
782        let reg_strength = adaptive_reg.compute_regularization_strength(&x, &y, 3);
783        assert!(reg_strength > 0.0);
784        assert!(reg_strength < 1.0);
785    }
786
787    #[test]
788    fn test_ensemble_combination_methods() {
789        let x = array![[0.0], [1.0], [2.0]];
790
791        let noise_functions = vec![
792            LearnableNoiseFunction::constant(0.1),
793            LearnableNoiseFunction::constant(0.2),
794        ];
795
796        let methods = [
797            CombinationMethod::WeightedAverage,
798            CombinationMethod::WeightedGeometricMean,
799            CombinationMethod::MinimumVariance,
800        ];
801
802        for method in &methods {
803            let ensemble =
804                EnsembleNoiseFunction::new(noise_functions.clone()).combination_method(*method);
805
806            let pred = ensemble.compute_noise(&x).unwrap();
807            assert_eq!(pred.len(), x.nrows());
808            assert!(pred.iter().all(|&p| p > 0.0));
809        }
810    }
811
812    #[test]
813    fn test_noise_function_parameter_management() {
814        let noise_functions = vec![
815            LearnableNoiseFunction::constant(0.1),
816            LearnableNoiseFunction::linear(0.1, 0.05),
817        ];
818
819        let mut ensemble = EnsembleNoiseFunction::new(noise_functions);
820
821        // Test getting parameters
822        let params = ensemble.get_parameters();
823        assert!(params.len() >= 2); // At least weights
824
825        // Test setting parameters
826        let new_params = vec![0.3, 0.7, 0.15, 0.12, 0.03]; // weights + noise function params
827        let result = ensemble.set_parameters(&new_params);
828        assert!(result.is_ok());
829
830        // Weights should be normalized
831        let weight_sum: f64 = ensemble.weights.sum();
832        assert_abs_diff_eq!(weight_sum, 1.0, epsilon = 1e-10);
833    }
834}