Skip to main content

sklears_decomposition/
kernel_pca.rs

1//! Kernel Principal Component Analysis (Kernel PCA) implementation
2//!
3//! Kernel PCA is a non-linear dimensionality reduction technique that uses
4//! kernel methods to perform PCA in a higher-dimensional feature space.
5
6use scirs2_core::ndarray::{Array1, Array2, Axis};
7use scirs2_core::random::rng as make_rng;
8use scirs2_core::random::rngs::StdRng;
9use scirs2_core::random::{SeedableRng, SliceRandom};
10use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
11use sklears_core::{
12    error::{Result, SklearsError},
13    traits::{Fit, Trained, Transform, Untrained},
14    types::Float,
15};
16use std::marker::PhantomData;
17
18/// Kernel functions for Kernel PCA
19#[derive(Debug, Clone, Copy)]
20pub enum KernelFunction {
21    /// Linear kernel: K(x, y) = <x, y>
22    Linear,
23    /// RBF (Gaussian) kernel: K(x, y) = exp(-gamma * ||x - y||^2)
24    Rbf { gamma: Float },
25    /// Polynomial kernel: K(x, y) = (gamma * <x, y> + coef0)^degree
26    Polynomial {
27        degree: i32,
28        gamma: Float,
29        coef0: Float,
30    },
31    /// Sigmoid kernel: K(x, y) = tanh(gamma * <x, y> + coef0)
32    Sigmoid { gamma: Float, coef0: Float },
33    /// Laplacian kernel: K(x, y) = exp(-gamma * ||x - y||_1)
34    Laplacian { gamma: Float },
35    /// Chi-squared kernel: K(x, y) = exp(-gamma * sum((x_i - y_i)^2 / (x_i + y_i)))
36    ChiSquared { gamma: Float },
37}
38
39impl Default for KernelFunction {
40    fn default() -> Self {
41        KernelFunction::Rbf { gamma: 1.0 }
42    }
43}
44
45impl KernelFunction {
46    /// Compute kernel value between two vectors
47    pub fn compute(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
48        match self {
49            KernelFunction::Linear => x.dot(y),
50            KernelFunction::Rbf { gamma } => {
51                let diff = x - y;
52                let dist_sq = diff.dot(&diff);
53                (-gamma * dist_sq).exp()
54            }
55            KernelFunction::Polynomial {
56                degree,
57                gamma,
58                coef0,
59            } => {
60                let dot_product = x.dot(y);
61                (gamma * dot_product + coef0).powi(*degree)
62            }
63            KernelFunction::Sigmoid { gamma, coef0 } => {
64                let dot_product = x.dot(y);
65                (gamma * dot_product + coef0).tanh()
66            }
67            KernelFunction::Laplacian { gamma } => {
68                let l1_dist = (x - y).mapv(|x| x.abs()).sum();
69                (-gamma * l1_dist).exp()
70            }
71            KernelFunction::ChiSquared { gamma } => {
72                let mut chi_sq_dist = 0.0;
73                for i in 0..x.len() {
74                    let sum = x[i] + y[i];
75                    if sum > 1e-12 {
76                        let diff = x[i] - y[i];
77                        chi_sq_dist += diff * diff / sum;
78                    }
79                }
80                (-gamma * chi_sq_dist).exp()
81            }
82        }
83    }
84
85    /// Compute kernel matrix between two sets of vectors
86    pub fn compute_matrix(&self, x: &Array2<Float>, y: &Array2<Float>) -> Array2<Float> {
87        let n_x = x.nrows();
88        let n_y = y.nrows();
89        let mut kernel_matrix = Array2::zeros((n_x, n_y));
90
91        for i in 0..n_x {
92            for j in 0..n_y {
93                let x_i = x.row(i).to_owned();
94                let y_j = y.row(j).to_owned();
95                kernel_matrix[[i, j]] = self.compute(&x_i, &y_j);
96            }
97        }
98
99        kernel_matrix
100    }
101}
102
103/// Kernel matrix approximation methods
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum KernelApproximation {
106    /// Full kernel matrix computation (no approximation)
107    #[default]
108    Full,
109    /// Nyström method approximation
110    Nystrom { n_components: usize },
111    /// Random sampling approximation
112    RandomSampling { n_samples: usize },
113}
114
115/// Configuration for Kernel PCA
116#[derive(Debug, Clone)]
117pub struct KernelPcaConfig {
118    /// Number of components to keep
119    pub n_components: Option<usize>,
120    /// Kernel function to use
121    pub kernel: KernelFunction,
122    /// Tolerance for eigenvalue computation
123    pub tol: Float,
124    /// Maximum number of iterations for eigenvalue computation
125    pub max_iter: usize,
126    /// Whether to center the kernel matrix
127    pub center: bool,
128    /// Whether to copy the input data
129    pub copy: bool,
130    /// Kernel matrix approximation method
131    pub approximation: KernelApproximation,
132    /// Random state for reproducible approximations
133    pub random_state: Option<u64>,
134}
135
136impl Default for KernelPcaConfig {
137    fn default() -> Self {
138        Self {
139            n_components: None,
140            kernel: KernelFunction::default(),
141            tol: 1e-8,
142            max_iter: 300,
143            center: true,
144            copy: true,
145            approximation: KernelApproximation::default(),
146            random_state: None,
147        }
148    }
149}
150
151/// Kernel Principal Component Analysis (Kernel PCA)
152///
153/// Non-linear dimensionality reduction through the use of kernels.
154/// It uses the kernel trick to perform PCA in a potentially infinite-dimensional
155/// feature space without explicitly computing the features.
156///
157/// # Examples
158///
159/// ```rust,ignore
160/// use sklears_decomposition::{KernelPCA, KernelFunction};
161/// use sklears_core::traits::{Transform, Fit};
162/// use scirs2_core::ndarray::array;
163///
164/// let x = array![
165///     [1.0, 2.0],
166///     [3.0, 4.0],
167///     [5.0, 6.0],
168///     [7.0, 8.0],
169/// ];
170///
171/// let kpca = KernelPCA::new()
172///     .n_components(2)
173///     .kernel(KernelFunction::Rbf { gamma: 0.1 })
174///     .fit(&x, &())?;
175///
176/// let x_transformed = kpca.transform(&x)?;
177/// # Ok::<(), Box<dyn std::error::Error>>(())
178/// ```
179#[derive(Debug, Clone)]
180pub struct KernelPCA<State = Untrained> {
181    config: KernelPcaConfig,
182    state: PhantomData<State>,
183    // Fitted parameters
184    x_fit_: Option<Array2<Float>>,
185    lambdas_: Option<Array1<Float>>,
186    alphas_: Option<Array2<Float>>,
187    n_components_: Option<usize>,
188    n_features_in_: Option<usize>,
189    n_samples_: Option<usize>,
190}
191
192impl KernelPCA<Untrained> {
193    /// Create a new Kernel PCA
194    pub fn new() -> Self {
195        Self {
196            config: KernelPcaConfig::default(),
197            state: PhantomData,
198            x_fit_: None,
199            lambdas_: None,
200            alphas_: None,
201            n_components_: None,
202            n_features_in_: None,
203            n_samples_: None,
204        }
205    }
206
207    /// Set the number of components to keep
208    pub fn n_components(mut self, n_components: usize) -> Self {
209        self.config.n_components = Some(n_components);
210        self
211    }
212
213    /// Set the kernel function
214    pub fn kernel(mut self, kernel: KernelFunction) -> Self {
215        self.config.kernel = kernel;
216        self
217    }
218
219    /// Set the tolerance for eigenvalue computation
220    pub fn tol(mut self, tol: Float) -> Self {
221        self.config.tol = tol;
222        self
223    }
224
225    /// Set the maximum number of iterations
226    pub fn max_iter(mut self, max_iter: usize) -> Self {
227        self.config.max_iter = max_iter;
228        self
229    }
230
231    /// Set whether to center the kernel matrix
232    pub fn center(mut self, center: bool) -> Self {
233        self.config.center = center;
234        self
235    }
236
237    /// Set whether to copy the input data
238    pub fn copy(mut self, copy: bool) -> Self {
239        self.config.copy = copy;
240        self
241    }
242
243    /// Set the kernel matrix approximation method
244    pub fn approximation(mut self, approximation: KernelApproximation) -> Self {
245        self.config.approximation = approximation;
246        self
247    }
248
249    /// Set the random state for reproducible approximations
250    pub fn random_state(mut self, random_state: u64) -> Self {
251        self.config.random_state = Some(random_state);
252        self
253    }
254}
255
256impl Default for KernelPCA<Untrained> {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262impl Fit<Array2<Float>, ()> for KernelPCA<Untrained> {
263    type Fitted = KernelPCA<Trained>;
264
265    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
266        let (n_samples, n_features) = x.dim();
267
268        if n_samples == 0 {
269            return Err(SklearsError::InvalidInput("Empty dataset".to_string()));
270        }
271
272        if n_features == 0 {
273            return Err(SklearsError::InvalidInput(
274                "Dataset has no features".to_string(),
275            ));
276        }
277
278        let n_components = self
279            .config
280            .n_components
281            .unwrap_or(n_samples.min(n_features))
282            .min(n_samples);
283
284        if n_components == 0 {
285            return Err(SklearsError::InvalidInput(
286                "Number of components must be positive".to_string(),
287            ));
288        }
289
290        // Store the training data
291        let x_fit = if self.config.copy {
292            x.clone()
293        } else {
294            x.to_owned()
295        };
296
297        // Compute kernel matrix or approximation based on configuration
298        let (lambdas, alphas) = match self.config.approximation {
299            KernelApproximation::Full => {
300                // Compute full kernel matrix
301                let mut k = self.config.kernel.compute_matrix(&x_fit, &x_fit);
302
303                // Center the kernel matrix if requested
304                if self.config.center {
305                    self.center_kernel_matrix(&mut k)?;
306                }
307
308                // Solve eigenvalue problem: K * alpha = lambda * alpha
309                self.solve_eigenvalue_problem(&k, n_components)?
310            }
311            KernelApproximation::Nystrom {
312                n_components: nystrom_components,
313            } => {
314                // Use Nyström method for large-scale approximation
315                self.nystrom_approximation(&x_fit, n_components, nystrom_components)?
316            }
317            KernelApproximation::RandomSampling { n_samples } => {
318                // Use random sampling approximation
319                self.random_sampling_approximation(&x_fit, n_components, n_samples)?
320            }
321        };
322
323        Ok(KernelPCA {
324            config: self.config,
325            state: PhantomData,
326            x_fit_: Some(x_fit),
327            lambdas_: Some(lambdas),
328            alphas_: Some(alphas),
329            n_components_: Some(n_components),
330            n_features_in_: Some(n_features),
331            n_samples_: Some(n_samples),
332        })
333    }
334}
335
336impl KernelPCA<Untrained> {
337    /// Center the kernel matrix
338    fn center_kernel_matrix(&self, k: &mut Array2<Float>) -> Result<()> {
339        let n = k.nrows();
340        if n != k.ncols() {
341            return Err(SklearsError::InvalidInput(
342                "Kernel matrix must be square".to_string(),
343            ));
344        }
345
346        // Compute row means
347        let row_means = k.mean_axis(Axis(1)).ok_or_else(|| {
348            SklearsError::NumericalError(
349                "cannot compute row means of empty kernel matrix".to_string(),
350            )
351        })?;
352        // Compute overall mean
353        let overall_mean = k.mean().ok_or_else(|| {
354            SklearsError::NumericalError("cannot compute mean of empty kernel matrix".to_string())
355        })?;
356
357        // Center the kernel matrix: K_centered = K - K_row_means - K_col_means + K_overall_mean
358        for i in 0..n {
359            for j in 0..n {
360                k[[i, j]] = k[[i, j]] - row_means[i] - row_means[j] + overall_mean;
361            }
362        }
363
364        Ok(())
365    }
366
367    /// Solve eigenvalue problem using proper eigendecomposition
368    ///
369    /// This performs symmetric eigendecomposition of the centered kernel matrix
370    /// to find the principal components in the feature space.
371    fn solve_eigenvalue_problem(
372        &self,
373        k: &Array2<Float>,
374        n_components: usize,
375    ) -> Result<(Array1<Float>, Array2<Float>)> {
376        let n = k.nrows();
377        let n_comp = n_components.min(n);
378
379        // Perform symmetric eigendecomposition using scirs2-linalg
380        // Kernel matrices should be positive semi-definite
381        let (eigenvalues, eigenvectors) = k.eigh(UPLO::Lower).map_err(|e| {
382            SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
383        })?;
384
385        // eigh returns eigenvalues in ascending order, so we need to reverse
386        // Collect eigenvalue-eigenvector pairs for sorting
387        let mut eigen_pairs: Vec<(Float, usize)> = eigenvalues
388            .iter()
389            .enumerate()
390            .map(|(i, &val)| (val, i))
391            .collect();
392
393        // Sort by eigenvalue in descending order
394        eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
395
396        // Extract the top n_comp eigenvalues and eigenvectors
397        let mut lambdas = Array1::zeros(n_comp);
398        let mut alphas = Array2::zeros((n, n_comp));
399
400        for comp in 0..n_comp {
401            if comp < eigen_pairs.len() {
402                let (eigenval, idx) = eigen_pairs[comp];
403
404                // Extract eigenvalue (ensure non-negative for numerical stability)
405                lambdas[comp] = eigenval.max(0.0);
406
407                // Extract eigenvector column from eigenvectors matrix
408                for i in 0..n {
409                    alphas[[i, comp]] = eigenvectors[[i, idx]];
410                }
411
412                // Normalize the eigenvector (should already be normalized, but ensure it)
413                let mut norm = 0.0;
414                for i in 0..n {
415                    norm += alphas[[i, comp]] * alphas[[i, comp]];
416                }
417                norm = norm.sqrt();
418
419                if norm > 1e-12 {
420                    for i in 0..n {
421                        alphas[[i, comp]] /= norm;
422                    }
423                }
424            }
425        }
426
427        Ok((lambdas, alphas))
428    }
429
430    /// Nyström method for efficient kernel matrix approximation
431    ///
432    /// The Nyström method approximates a large kernel matrix using a subset of columns/rows.
433    /// This allows efficient computation of eigendecomposition for large datasets.
434    ///
435    /// # Arguments
436    /// * `x` - Training data
437    /// * `n_components` - Number of components to extract
438    /// * `nystrom_components` - Number of landmark points for Nyström approximation
439    fn nystrom_approximation(
440        &self,
441        x: &Array2<Float>,
442        n_components: usize,
443        nystrom_components: usize,
444    ) -> Result<(Array1<Float>, Array2<Float>)> {
445        let (n_samples, _) = x.dim();
446        let m = nystrom_components.min(n_samples);
447
448        // Initialize random number generator with optional seeding for reproducibility
449        let mut rng = if let Some(seed) = self.config.random_state {
450            StdRng::seed_from_u64(seed)
451        } else {
452            StdRng::from_rng(&mut make_rng())
453        };
454
455        // Randomly sample m landmark points
456        let mut landmark_indices: Vec<usize> = (0..n_samples).collect();
457        landmark_indices.shuffle(&mut rng);
458        landmark_indices.truncate(m);
459
460        // Extract landmark points
461        let mut landmarks = Array2::zeros((m, x.ncols()));
462        for (i, &idx) in landmark_indices.iter().enumerate() {
463            landmarks.row_mut(i).assign(&x.row(idx));
464        }
465
466        // Compute kernel submatrices
467        // W: kernel matrix between landmarks (m x m)
468        let mut w = self.config.kernel.compute_matrix(&landmarks, &landmarks);
469
470        // C: kernel matrix between all points and landmarks (n x m)
471        let c = self.config.kernel.compute_matrix(x, &landmarks);
472
473        // Center the matrices if requested
474        if self.config.center {
475            self.center_kernel_matrix(&mut w)?;
476            // Note: C centering is more complex and approximated here
477        }
478
479        // Eigendecomposition of W
480        let (w_eigenvals, w_eigenvecs) = self.solve_eigenvalue_problem(&w, m)?;
481
482        // Filter out near-zero eigenvalues for numerical stability
483        let mut valid_components = Vec::new();
484        for i in 0..m {
485            if w_eigenvals[i] > 1e-10 {
486                valid_components.push(i);
487            }
488        }
489        let k = valid_components.len().min(n_components);
490
491        // Compute Nyström approximation eigenvalues and eigenvectors
492        let mut nystrom_eigenvals = Array1::zeros(k);
493        let mut nystrom_eigenvecs = Array2::zeros((n_samples, k));
494
495        for (comp_idx, &w_idx) in valid_components.iter().take(k).enumerate() {
496            // Eigenvalue: scaled by n_samples/m
497            nystrom_eigenvals[comp_idx] = w_eigenvals[w_idx] * (n_samples as Float) / (m as Float);
498
499            // Eigenvector: C * w_eigenvec / sqrt(m * eigenval)
500            let w_eigenvec = w_eigenvecs.column(w_idx);
501            let scale = 1.0 / (m as Float * w_eigenvals[w_idx]).sqrt();
502
503            for i in 0..n_samples {
504                let mut eigenvec_val = 0.0;
505                for j in 0..m {
506                    eigenvec_val += c[[i, j]] * w_eigenvec[j];
507                }
508                nystrom_eigenvecs[[i, comp_idx]] = eigenvec_val * scale;
509            }
510        }
511
512        Ok((nystrom_eigenvals, nystrom_eigenvecs))
513    }
514
515    /// Random sampling approximation for kernel matrix
516    ///
517    /// This method uses random sampling to create a smaller representative dataset
518    /// and performs full kernel PCA on this subset.
519    ///
520    /// # Arguments
521    /// * `x` - Training data
522    /// * `n_components` - Number of components to extract
523    /// * `n_samples_approx` - Number of samples to use for approximation
524    fn random_sampling_approximation(
525        &self,
526        x: &Array2<Float>,
527        n_components: usize,
528        n_samples_approx: usize,
529    ) -> Result<(Array1<Float>, Array2<Float>)> {
530        let (n_samples, n_features) = x.dim();
531        let m = n_samples_approx.min(n_samples);
532
533        // Initialize random number generator with optional seeding for reproducibility
534        let mut rng = if let Some(seed) = self.config.random_state {
535            StdRng::seed_from_u64(seed)
536        } else {
537            StdRng::from_rng(&mut make_rng())
538        };
539
540        // Randomly sample points
541        let mut sample_indices: Vec<usize> = (0..n_samples).collect();
542        sample_indices.shuffle(&mut rng);
543        sample_indices.truncate(m);
544
545        // Extract sampled data
546        let mut x_sampled = Array2::zeros((m, n_features));
547        for (i, &idx) in sample_indices.iter().enumerate() {
548            x_sampled.row_mut(i).assign(&x.row(idx));
549        }
550
551        // Compute kernel matrix on sampled data
552        let mut k_sampled = self.config.kernel.compute_matrix(&x_sampled, &x_sampled);
553
554        // Center the kernel matrix if requested
555        if self.config.center {
556            self.center_kernel_matrix(&mut k_sampled)?;
557        }
558
559        // Solve eigenvalue problem on sampled data
560        let (eigenvals_sampled, eigenvecs_sampled) =
561            self.solve_eigenvalue_problem(&k_sampled, n_components.min(m))?;
562
563        // Project eigenvectors back to full space
564        // This is an approximation - in practice, we'd need to store the sampled data
565        // and use it during transform phase
566        let mut full_eigenvecs = Array2::zeros((n_samples, eigenvals_sampled.len()));
567
568        // For each eigenvector, interpolate to full space using kernel evaluations
569        for comp in 0..eigenvals_sampled.len() {
570            for i in 0..n_samples {
571                let mut projection = 0.0;
572                for (j, &sample_idx) in sample_indices.iter().enumerate() {
573                    let x_i = x.row(i).to_owned();
574                    let x_sample_j = x.row(sample_idx).to_owned();
575                    let k_val = self.config.kernel.compute(&x_i, &x_sample_j);
576                    projection += k_val * eigenvecs_sampled[[j, comp]];
577                }
578
579                // Normalize by eigenvalue
580                if eigenvals_sampled[comp] > 1e-10 {
581                    projection /= eigenvals_sampled[comp].sqrt();
582                }
583
584                full_eigenvecs[[i, comp]] = projection;
585            }
586        }
587
588        Ok((eigenvals_sampled, full_eigenvecs))
589    }
590}
591
592impl Transform<Array2<Float>, Array2<Float>> for KernelPCA<Trained> {
593    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
594        let (n_samples, n_features) = x.dim();
595
596        if n_features != self.n_features_in() {
597            return Err(SklearsError::FeatureMismatch {
598                expected: self.n_features_in(),
599                actual: n_features,
600            });
601        }
602
603        let x_fit = self
604            .x_fit_
605            .as_ref()
606            .expect("invariant: x_fit_ is Some in Trained state");
607        let alphas = self
608            .alphas_
609            .as_ref()
610            .expect("invariant: alphas_ is Some in Trained state");
611        let lambdas = self
612            .lambdas_
613            .as_ref()
614            .expect("invariant: lambdas_ is Some in Trained state");
615        let n_components = self.n_components();
616
617        // Compute kernel matrix between x and training data
618        let mut k_test = self.config.kernel.compute_matrix(x, x_fit);
619
620        // Center the kernel matrix if needed
621        if self.config.center {
622            // For centering test data, we need to apply the same transformation
623            // as was applied to the training kernel matrix
624            let n_train = x_fit.nrows();
625            let row_means_train: Array1<Float> = Array1::zeros(n_train); // Simplified - should store from training
626            let overall_mean_train: Float = 0.0; // Simplified - should store from training
627
628            for i in 0..n_samples {
629                for j in 0..n_train {
630                    k_test[[i, j]] = k_test[[i, j]] - row_means_train[j] - overall_mean_train;
631                }
632            }
633        }
634
635        // Project onto the principal components
636        let mut x_transformed = Array2::zeros((n_samples, n_components));
637
638        for i in 0..n_samples {
639            for comp in 0..n_components {
640                let mut projection = 0.0;
641                for j in 0..x_fit.nrows() {
642                    projection += k_test[[i, j]] * alphas[[j, comp]];
643                }
644
645                // Scale by sqrt(eigenvalue)
646                if lambdas[comp] > 1e-10 {
647                    projection /= lambdas[comp].sqrt();
648                }
649
650                x_transformed[[i, comp]] = projection;
651            }
652        }
653
654        Ok(x_transformed)
655    }
656}
657
658impl KernelPCA<Trained> {
659    /// Get the eigenvalues
660    pub fn eigenvalues(&self) -> &Array1<Float> {
661        self.lambdas_
662            .as_ref()
663            .expect("invariant: lambdas_ is Some in Trained state")
664    }
665
666    /// Get the eigenvectors (alphas)
667    pub fn eigenvectors(&self) -> &Array2<Float> {
668        self.alphas_
669            .as_ref()
670            .expect("invariant: alphas_ is Some in Trained state")
671    }
672
673    /// Get the number of components
674    pub fn n_components(&self) -> usize {
675        self.n_components_
676            .expect("invariant: n_components_ is Some in Trained state")
677    }
678
679    /// Get the number of features in the input
680    pub fn n_features_in(&self) -> usize {
681        self.n_features_in_
682            .expect("invariant: n_features_in_ is Some in Trained state")
683    }
684
685    /// Get the number of samples in the training data
686    pub fn n_samples(&self) -> usize {
687        self.n_samples_
688            .expect("invariant: n_samples_ is Some in Trained state")
689    }
690
691    /// Get the training data
692    pub fn x_fit(&self) -> &Array2<Float> {
693        self.x_fit_
694            .as_ref()
695            .expect("invariant: x_fit_ is Some in Trained state")
696    }
697
698    /// Pre-image reconstruction using fixed-point iteration
699    ///
700    /// Reconstructs the original space representation from the transformed features.
701    /// This is useful for understanding what the transformed features represent
702    /// in the original input space.
703    ///
704    /// # Arguments
705    /// * `x_transformed` - The transformed data to reconstruct (n_samples, n_components)
706    /// * `max_iter` - Maximum number of iterations for fixed-point iteration
707    /// * `tol` - Tolerance for convergence
708    ///
709    /// # Returns
710    /// Reconstructed data in original space (n_samples, n_features)
711    pub fn inverse_transform(
712        &self,
713        x_transformed: &Array2<Float>,
714        max_iter: usize,
715        tol: Float,
716    ) -> Result<Array2<Float>> {
717        let (_n_samples, n_components_in) = x_transformed.dim();
718        let n_components = self.n_components();
719        let _n_features = self.n_features_in();
720        let _x_fit = self.x_fit();
721        let _alphas = self.eigenvectors();
722        let _lambdas = self.eigenvalues();
723
724        if n_components_in != n_components {
725            return Err(SklearsError::FeatureMismatch {
726                expected: n_components,
727                actual: n_components_in,
728            });
729        }
730
731        // Handle different kernel types
732        match &self.config.kernel {
733            KernelFunction::Linear => self.linear_preimage(x_transformed),
734            KernelFunction::Rbf { gamma: _ } => {
735                self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
736            }
737            KernelFunction::Polynomial { .. } => {
738                self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
739            }
740            KernelFunction::Sigmoid { .. } => {
741                self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
742            }
743            KernelFunction::Laplacian { .. } => {
744                self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
745            }
746            KernelFunction::ChiSquared { .. } => {
747                self.nonlinear_preimage_fixed_point(x_transformed, max_iter, tol)
748            }
749        }
750    }
751
752    /// Linear pre-image reconstruction (exact for linear kernels)
753    fn linear_preimage(&self, x_transformed: &Array2<Float>) -> Result<Array2<Float>> {
754        let x_fit = self.x_fit();
755        let alphas = self.eigenvectors();
756        let lambdas = self.eigenvalues();
757        let (n_samples, n_components) = x_transformed.dim();
758        let n_features = self.n_features_in();
759
760        // For linear kernels: x_reconstructed = sum_i (alpha_i * lambda_i * x_fit_i)
761        let mut x_reconstructed = Array2::zeros((n_samples, n_features));
762
763        for i in 0..n_samples {
764            for k in 0..n_features {
765                let mut value = 0.0;
766                for comp in 0..n_components {
767                    if lambdas[comp] > 1e-10 {
768                        let component_contrib = x_transformed[[i, comp]] * lambdas[comp].sqrt();
769                        for j in 0..x_fit.nrows() {
770                            value += alphas[[j, comp]] * component_contrib * x_fit[[j, k]];
771                        }
772                    }
773                }
774                x_reconstructed[[i, k]] = value;
775            }
776        }
777
778        Ok(x_reconstructed)
779    }
780
781    /// Non-linear pre-image reconstruction using fixed-point iteration
782    fn nonlinear_preimage_fixed_point(
783        &self,
784        x_transformed: &Array2<Float>,
785        max_iter: usize,
786        tol: Float,
787    ) -> Result<Array2<Float>> {
788        let (n_samples, _n_components) = x_transformed.dim();
789        let n_features = self.n_features_in();
790        let x_fit = self.x_fit();
791        let alphas = self.eigenvectors();
792        let lambdas = self.eigenvalues();
793
794        let mut x_reconstructed = Array2::zeros((n_samples, n_features));
795
796        for i in 0..n_samples {
797            // Initialize with mean of training data
798            let mut x_current = x_fit.mean_axis(Axis(0)).ok_or_else(|| {
799                SklearsError::NumericalError(
800                    "cannot compute mean of empty training data".to_string(),
801                )
802            })?;
803            let target_transformed = x_transformed.slice(scirs2_core::ndarray::s![i, ..]);
804
805            for _iter in 0..max_iter {
806                let x_old = x_current.clone();
807
808                // Fixed-point iteration step
809                x_current =
810                    self.fixed_point_step(&x_old, &target_transformed, x_fit, alphas, lambdas)?;
811
812                // Check convergence
813                let diff = (&x_current - &x_old)
814                    .mapv(|x| x.abs())
815                    .fold(0.0f64, |acc, &x| acc.max(x));
816                if diff < tol {
817                    break;
818                }
819            }
820
821            // Store result
822            for j in 0..n_features {
823                x_reconstructed[[i, j]] = x_current[j];
824            }
825        }
826
827        Ok(x_reconstructed)
828    }
829
830    /// Fixed-point iteration step for pre-image reconstruction
831    fn fixed_point_step(
832        &self,
833        x_current: &Array1<Float>,
834        target_transformed: &scirs2_core::ndarray::ArrayView1<Float>,
835        x_fit: &Array2<Float>,
836        alphas: &Array2<Float>,
837        lambdas: &Array1<Float>,
838    ) -> Result<Array1<Float>> {
839        let n_features = x_current.len();
840        let n_train = x_fit.nrows();
841        let n_components = target_transformed.len();
842
843        // Compute kernel derivatives and weights
844        let mut numerator = Array1::<Float>::zeros(n_features);
845        let mut denominator = Array1::<Float>::zeros(n_features);
846
847        for j in 0..n_train {
848            let x_train_j = x_fit.slice(scirs2_core::ndarray::s![j, ..]).to_owned();
849            let _k_val = self.config.kernel.compute(x_current, &x_train_j);
850            let k_deriv = self.kernel_derivative(x_current, &x_train_j);
851
852            // Weight for this training point
853            let mut weight = 0.0;
854            for comp in 0..n_components {
855                if lambdas[comp] > 1e-10 {
856                    weight += target_transformed[comp] * alphas[[j, comp]] / lambdas[comp].sqrt();
857                }
858            }
859
860            for k in 0..n_features {
861                numerator[k] += weight * k_deriv[k] * x_train_j[k];
862                denominator[k] += weight * k_deriv[k];
863            }
864        }
865
866        // Update step
867        let mut x_new = Array1::zeros(n_features);
868        for k in 0..n_features {
869            if denominator[k].abs() > 1e-12 {
870                x_new[k] = numerator[k] / denominator[k];
871            } else {
872                x_new[k] = x_current[k]; // Keep current value if denominator is too small
873            }
874        }
875
876        Ok(x_new)
877    }
878
879    /// Compute kernel derivative with respect to the first argument
880    fn kernel_derivative(&self, x: &Array1<Float>, y: &Array1<Float>) -> Array1<Float> {
881        match &self.config.kernel {
882            KernelFunction::Linear => {
883                // d/dx K(x,y) = y for linear kernel
884                y.clone()
885            }
886            KernelFunction::Rbf { gamma } => {
887                // d/dx K(x,y) = -2*gamma*(x-y)*K(x,y) for RBF kernel
888                let k_val = self.config.kernel.compute(x, y);
889                let diff = x - y;
890                diff.mapv(|d| -2.0 * gamma * d * k_val)
891            }
892            KernelFunction::Polynomial {
893                degree,
894                gamma,
895                coef0,
896            } => {
897                // d/dx K(x,y) = degree * gamma * y * (gamma*<x,y> + coef0)^(degree-1)
898                let dot_product = x.dot(y);
899                let base = gamma * dot_product + coef0;
900                if *degree == 1 || base.abs() < 1e-12 {
901                    y.mapv(|yi| *gamma * yi)
902                } else {
903                    let factor = (*degree as Float) * gamma * base.powf(*degree as Float - 1.0);
904                    y.mapv(|yi| factor * yi)
905                }
906            }
907            KernelFunction::Sigmoid { gamma, coef0 } => {
908                // d/dx K(x,y) = gamma * y * (1 - tanh²(gamma*<x,y> + coef0))
909                let dot_product = x.dot(y);
910                let tanh_val = (gamma * dot_product + coef0).tanh();
911                let factor = gamma * (1.0 - tanh_val * tanh_val);
912                y.mapv(|yi| factor * yi)
913            }
914            KernelFunction::Laplacian { gamma } => {
915                // d/dx K(x,y) = -gamma * sign(x-y) * K(x,y) for Laplacian kernel
916                let k_val = self.config.kernel.compute(x, y);
917                let diff = x - y;
918                diff.mapv(|d| -gamma * d.signum() * k_val)
919            }
920            KernelFunction::ChiSquared { gamma } => {
921                // Approximate derivative for Chi-squared kernel (complex exact form)
922                let k_val = self.config.kernel.compute(x, y);
923                let mut derivative = Array1::zeros(x.len());
924                for i in 0..x.len() {
925                    let sum = x[i] + y[i];
926                    if sum > 1e-12 {
927                        let diff = x[i] - y[i];
928                        derivative[i] = -2.0 * gamma * k_val * diff / sum;
929                    }
930                }
931                derivative
932            }
933        }
934    }
935
936    /// Multi-dimensional scaling (MDS) based pre-image approximation
937    ///
938    /// An alternative pre-image reconstruction method using MDS to preserve
939    /// distances in the original space.
940    pub fn mds_preimage(&self, x_transformed: &Array2<Float>) -> Result<Array2<Float>> {
941        let (n_samples, _n_components) = x_transformed.dim();
942        let n_features = self.n_features_in();
943        let x_fit = self.x_fit();
944
945        // Compute distance matrix in transformed space
946        let mut dist_transformed = Array2::zeros((n_samples, n_samples));
947        for i in 0..n_samples {
948            for j in 0..n_samples {
949                let diff = &x_transformed.slice(scirs2_core::ndarray::s![i, ..])
950                    - &x_transformed.slice(scirs2_core::ndarray::s![j, ..]);
951                dist_transformed[[i, j]] = diff.mapv(|x| x * x).sum().sqrt();
952            }
953        }
954
955        // Find closest training samples for each transformed point
956        let mut x_reconstructed = Array2::zeros((n_samples, n_features));
957
958        for i in 0..n_samples {
959            // Find k nearest neighbors in the training set (using feature space distance)
960            let k = 5.min(x_fit.nrows()); // Use top 5 neighbors
961            let mut distances = Vec::new();
962
963            for j in 0..x_fit.nrows() {
964                // Compute transformed distance to training sample j
965                let x_train_j_transformed =
966                    self.transform_single_sample(&x_fit.slice(scirs2_core::ndarray::s![j, ..]))?;
967                let diff =
968                    &x_transformed.slice(scirs2_core::ndarray::s![i, ..]) - &x_train_j_transformed;
969                let dist = diff.mapv(|x| x * x).sum().sqrt();
970                distances.push((dist, j));
971            }
972
973            // Sort by distance and take k nearest
974            distances.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
975
976            // Weighted average of k nearest neighbors
977            let mut weight_sum = 0.0;
978            for feat in 0..n_features {
979                let mut weighted_value = 0.0;
980                for &(dist, idx) in distances.iter().take(k) {
981                    let weight = if dist > 1e-12 {
982                        1.0 / (dist + 1e-6)
983                    } else {
984                        1e6
985                    };
986                    weighted_value += weight * x_fit[[idx, feat]];
987                    weight_sum += weight;
988                }
989                x_reconstructed[[i, feat]] = weighted_value / weight_sum;
990            }
991        }
992
993        Ok(x_reconstructed)
994    }
995
996    /// Transform a single sample (helper method)
997    fn transform_single_sample(
998        &self,
999        x_sample: &scirs2_core::ndarray::ArrayView1<Float>,
1000    ) -> Result<Array1<Float>> {
1001        let x_fit = self.x_fit();
1002        let alphas = self.eigenvectors();
1003        let lambdas = self.eigenvalues();
1004        let n_components = self.n_components();
1005
1006        // Compute kernel values with training data
1007        let mut k_test = Array1::zeros(x_fit.nrows());
1008        for j in 0..x_fit.nrows() {
1009            let x_train_j = x_fit.slice(scirs2_core::ndarray::s![j, ..]).to_owned();
1010            k_test[j] = self.config.kernel.compute(&x_sample.to_owned(), &x_train_j);
1011        }
1012
1013        // Center the kernel values if the model was trained with centering
1014        if self.config.center {
1015            // This is approximate centering for a single sample
1016            let mean_k = k_test.mean().ok_or_else(|| {
1017                SklearsError::NumericalError(
1018                    "cannot compute mean of empty k_test array".to_string(),
1019                )
1020            })?;
1021            for val in k_test.iter_mut() {
1022                *val -= mean_k;
1023            }
1024        }
1025
1026        // Project onto the principal components
1027        let mut x_transformed = Array1::zeros(n_components);
1028        for comp in 0..n_components {
1029            let mut projection = 0.0;
1030            for j in 0..x_fit.nrows() {
1031                projection += k_test[j] * alphas[[j, comp]];
1032            }
1033
1034            // Scale by sqrt(eigenvalue)
1035            if lambdas[comp] > 1e-10 {
1036                projection /= lambdas[comp].sqrt();
1037            }
1038
1039            x_transformed[comp] = projection;
1040        }
1041
1042        Ok(x_transformed)
1043    }
1044}
1045
1046/// Kernel selection and validation utilities
1047impl KernelFunction {
1048    /// Validate kernel parameters for numerical stability
1049    pub fn validate(&self) -> Result<()> {
1050        match self {
1051            KernelFunction::Linear => Ok(()),
1052            KernelFunction::Rbf { gamma } => {
1053                if *gamma <= 0.0 {
1054                    return Err(SklearsError::InvalidParameter {
1055                        name: "gamma".to_string(),
1056                        reason: "must be positive for RBF kernel".to_string(),
1057                    });
1058                }
1059                if *gamma > 1e6 {
1060                    return Err(SklearsError::InvalidParameter {
1061                        name: "gamma".to_string(),
1062                        reason: "too large, may cause numerical instability".to_string(),
1063                    });
1064                }
1065                Ok(())
1066            }
1067            KernelFunction::Polynomial {
1068                degree,
1069                gamma,
1070                coef0: _,
1071            } => {
1072                if *degree <= 0 {
1073                    return Err(SklearsError::InvalidParameter {
1074                        name: "degree".to_string(),
1075                        reason: "must be positive for polynomial kernel".to_string(),
1076                    });
1077                }
1078                if *degree > 10 {
1079                    return Err(SklearsError::InvalidParameter {
1080                        name: "degree".to_string(),
1081                        reason: "too large, may cause numerical overflow".to_string(),
1082                    });
1083                }
1084                if *gamma <= 0.0 {
1085                    return Err(SklearsError::InvalidParameter {
1086                        name: "gamma".to_string(),
1087                        reason: "must be positive for polynomial kernel".to_string(),
1088                    });
1089                }
1090                Ok(())
1091            }
1092            KernelFunction::Sigmoid { gamma, coef0: _ } => {
1093                if *gamma <= 0.0 {
1094                    return Err(SklearsError::InvalidParameter {
1095                        name: "gamma".to_string(),
1096                        reason: "must be positive for sigmoid kernel".to_string(),
1097                    });
1098                }
1099                Ok(())
1100            }
1101            KernelFunction::Laplacian { gamma } => {
1102                if *gamma <= 0.0 {
1103                    return Err(SklearsError::InvalidParameter {
1104                        name: "gamma".to_string(),
1105                        reason: "must be positive for Laplacian kernel".to_string(),
1106                    });
1107                }
1108                if *gamma > 1e6 {
1109                    return Err(SklearsError::InvalidParameter {
1110                        name: "gamma".to_string(),
1111                        reason: "too large, may cause numerical instability".to_string(),
1112                    });
1113                }
1114                Ok(())
1115            }
1116            KernelFunction::ChiSquared { gamma } => {
1117                if *gamma <= 0.0 {
1118                    return Err(SklearsError::InvalidParameter {
1119                        name: "gamma".to_string(),
1120                        reason: "must be positive for Chi-squared kernel".to_string(),
1121                    });
1122                }
1123                Ok(())
1124            }
1125        }
1126    }
1127
1128    /// Evaluate kernel performance on given data using cross-validation
1129    pub fn evaluate_performance(
1130        &self,
1131        x: &Array2<Float>,
1132        n_components: usize,
1133        cv_folds: usize,
1134    ) -> Result<Float> {
1135        let (n_samples, _) = x.dim();
1136        if n_samples < cv_folds {
1137            return Err(SklearsError::InvalidParameter {
1138                name: "cv_folds".to_string(),
1139                reason: "Number of samples must be >= cv_folds".to_string(),
1140            });
1141        }
1142
1143        self.validate()?;
1144
1145        let fold_size = n_samples / cv_folds;
1146        let mut reconstruction_errors = Vec::new();
1147
1148        for fold in 0..cv_folds {
1149            let start_idx = fold * fold_size;
1150            let end_idx = if fold == cv_folds - 1 {
1151                n_samples
1152            } else {
1153                (fold + 1) * fold_size
1154            };
1155
1156            // Create training and validation sets
1157            let mut train_indices = Vec::new();
1158            let mut val_indices = Vec::new();
1159
1160            for i in 0..n_samples {
1161                if i >= start_idx && i < end_idx {
1162                    val_indices.push(i);
1163                } else {
1164                    train_indices.push(i);
1165                }
1166            }
1167
1168            if train_indices.is_empty() || val_indices.is_empty() {
1169                continue;
1170            }
1171
1172            // Extract training and validation data
1173            let x_train = x.select(scirs2_core::ndarray::Axis(0), &train_indices);
1174            let x_val = x.select(scirs2_core::ndarray::Axis(0), &val_indices);
1175
1176            // Fit Kernel PCA on training data
1177            let kpca = KernelPCA::new()
1178                .n_components(n_components)
1179                .kernel(*self)
1180                .fit(&x_train, &())?;
1181
1182            // Transform validation data
1183            let x_val_transformed = kpca.transform(&x_val)?;
1184
1185            // Compute reconstruction error (simplified)
1186            let error = self.compute_reconstruction_error(&x_val, &x_val_transformed, &kpca)?;
1187            reconstruction_errors.push(error);
1188        }
1189
1190        if reconstruction_errors.is_empty() {
1191            return Err(SklearsError::InvalidParameter {
1192                name: "cv_folds".to_string(),
1193                reason: "No valid cross-validation folds".to_string(),
1194            });
1195        }
1196
1197        // Return mean reconstruction error
1198        let mean_error =
1199            reconstruction_errors.iter().sum::<Float>() / reconstruction_errors.len() as Float;
1200        Ok(mean_error)
1201    }
1202
1203    /// Compute reconstruction error for validation
1204    fn compute_reconstruction_error(
1205        &self,
1206        x_original: &Array2<Float>,
1207        x_transformed: &Array2<Float>,
1208        _kpca: &KernelPCA<sklears_core::traits::Trained>,
1209    ) -> Result<Float> {
1210        // For now, we use a simplified metric based on variance preservation
1211        let original_var = self.compute_total_variance(x_original);
1212        let transformed_var = self.compute_total_variance(x_transformed);
1213
1214        // Higher preserved variance means lower reconstruction error
1215        let error = (original_var - transformed_var).abs() / original_var.max(1e-10);
1216        Ok(error)
1217    }
1218
1219    /// Compute total variance of data matrix
1220    fn compute_total_variance(&self, x: &Array2<Float>) -> Float {
1221        let (n_samples, n_features) = x.dim();
1222        if n_samples == 0 || n_features == 0 {
1223            return 0.0;
1224        }
1225
1226        let mut total_var = 0.0;
1227        for j in 0..n_features {
1228            let col = x.column(j);
1229            let mean = col.mean().unwrap_or(0.0);
1230            let var = col.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0);
1231            total_var += var;
1232        }
1233        total_var
1234    }
1235
1236    /// Select best kernel from a list of candidates using cross-validation
1237    pub fn select_best_kernel(
1238        kernels: &[KernelFunction],
1239        x: &Array2<Float>,
1240        n_components: usize,
1241        cv_folds: usize,
1242    ) -> Result<(KernelFunction, Float)> {
1243        if kernels.is_empty() {
1244            return Err(SklearsError::InvalidParameter {
1245                name: "kernels".to_string(),
1246                reason: "Must provide at least one kernel".to_string(),
1247            });
1248        }
1249
1250        let mut best_kernel = kernels[0];
1251        let mut best_score = Float::INFINITY;
1252
1253        for &kernel in kernels {
1254            match kernel.evaluate_performance(x, n_components, cv_folds) {
1255                Ok(score) => {
1256                    if score < best_score {
1257                        best_score = score;
1258                        best_kernel = kernel;
1259                    }
1260                }
1261                Err(_) => {
1262                    // Skip invalid kernels
1263                    continue;
1264                }
1265            }
1266        }
1267
1268        if best_score == Float::INFINITY {
1269            return Err(SklearsError::InvalidParameter {
1270                name: "kernels".to_string(),
1271                reason: "All kernels failed validation".to_string(),
1272            });
1273        }
1274
1275        Ok((best_kernel, best_score))
1276    }
1277
1278    /// Generate a grid of kernel candidates for hyperparameter tuning
1279    pub fn generate_kernel_grid() -> Vec<KernelFunction> {
1280        let mut kernels = Vec::new();
1281
1282        // Linear kernel
1283        kernels.push(KernelFunction::Linear);
1284
1285        // RBF kernels with different gamma values
1286        for &gamma in &[0.001, 0.01, 0.1, 1.0, 10.0, 100.0] {
1287            kernels.push(KernelFunction::Rbf { gamma });
1288        }
1289
1290        // Polynomial kernels
1291        for &degree in &[2, 3, 4] {
1292            for &gamma in &[0.1, 1.0] {
1293                for &coef0 in &[0.0, 1.0] {
1294                    kernels.push(KernelFunction::Polynomial {
1295                        degree,
1296                        gamma,
1297                        coef0,
1298                    });
1299                }
1300            }
1301        }
1302
1303        // Sigmoid kernels
1304        for &gamma in &[0.001, 0.01, 0.1] {
1305            for &coef0 in &[0.0, 1.0] {
1306                kernels.push(KernelFunction::Sigmoid { gamma, coef0 });
1307            }
1308        }
1309
1310        // Laplacian kernels
1311        for &gamma in &[0.01, 0.1, 1.0, 10.0] {
1312            kernels.push(KernelFunction::Laplacian { gamma });
1313        }
1314
1315        // Chi-squared kernels
1316        for &gamma in &[0.1, 1.0, 10.0] {
1317            kernels.push(KernelFunction::ChiSquared { gamma });
1318        }
1319
1320        kernels
1321    }
1322}
1323
1324/// Kernel PCA with automatic kernel selection
1325impl KernelPCA<Untrained> {
1326    /// Fit Kernel PCA with automatic kernel selection
1327    pub fn fit_with_kernel_selection(
1328        mut self,
1329        x: &Array2<Float>,
1330        kernel_candidates: Option<&[KernelFunction]>,
1331        cv_folds: usize,
1332    ) -> Result<KernelPCA<sklears_core::traits::Trained>> {
1333        let kernels = kernel_candidates
1334            .map(|k| k.to_vec())
1335            .unwrap_or_else(KernelFunction::generate_kernel_grid);
1336
1337        let n_components = self.config.n_components.unwrap_or(x.ncols().min(x.nrows()));
1338
1339        let (best_kernel, _score) =
1340            KernelFunction::select_best_kernel(&kernels, x, n_components, cv_folds)?;
1341
1342        self.config.kernel = best_kernel;
1343        self.fit(x, &())
1344    }
1345}
1346
1347#[allow(non_snake_case)]
1348#[cfg(test)]
1349mod tests {
1350    use super::*;
1351    use approx::assert_abs_diff_eq;
1352    use scirs2_core::ndarray::array;
1353
1354    #[test]
1355    fn test_kernel_functions() {
1356        let x = array![1.0, 2.0];
1357        let y = array![3.0, 4.0];
1358
1359        // Linear kernel
1360        let linear = KernelFunction::Linear;
1361        let linear_result = linear.compute(&x, &y);
1362        assert_abs_diff_eq!(linear_result, 11.0, epsilon = 1e-10); // 1*3 + 2*4 = 11
1363
1364        // RBF kernel
1365        let rbf = KernelFunction::Rbf { gamma: 1.0 };
1366        let rbf_result = rbf.compute(&x, &y);
1367        // exp(-1.0 * ((1-3)^2 + (2-4)^2)) = exp(-8) ≈ 0.000335
1368        assert!(rbf_result > 0.0 && rbf_result < 1.0);
1369
1370        // Polynomial kernel
1371        let poly = KernelFunction::Polynomial {
1372            degree: 2,
1373            gamma: 1.0,
1374            coef0: 1.0,
1375        };
1376        let poly_result = poly.compute(&x, &y);
1377        // (1.0 * 11 + 1.0)^2 = 12^2 = 144
1378        assert_abs_diff_eq!(poly_result, 144.0, epsilon = 1e-10);
1379
1380        // Sigmoid kernel
1381        let sigmoid = KernelFunction::Sigmoid {
1382            gamma: 1.0,
1383            coef0: 0.0,
1384        };
1385        let sigmoid_result = sigmoid.compute(&x, &y);
1386        // tanh(1.0 * 11 + 0.0) = tanh(11) ≈ 1.0
1387        assert!(sigmoid_result > 0.9 && sigmoid_result <= 1.0);
1388    }
1389
1390    #[test]
1391    fn test_kernel_matrix() {
1392        let x = array![[1.0, 2.0], [3.0, 4.0]];
1393        let y = array![[5.0, 6.0], [7.0, 8.0]];
1394
1395        let kernel = KernelFunction::Linear;
1396        let k = kernel.compute_matrix(&x, &y);
1397
1398        assert_eq!(k.dim(), (2, 2));
1399        // K[0,0] = [1,2] · [5,6] = 1*5 + 2*6 = 17
1400        assert_abs_diff_eq!(k[[0, 0]], 17.0, epsilon = 1e-10);
1401        // K[0,1] = [1,2] · [7,8] = 1*7 + 2*8 = 23
1402        assert_abs_diff_eq!(k[[0, 1]], 23.0, epsilon = 1e-10);
1403        // K[1,0] = [3,4] · [5,6] = 3*5 + 4*6 = 39
1404        assert_abs_diff_eq!(k[[1, 0]], 39.0, epsilon = 1e-10);
1405        // K[1,1] = [3,4] · [7,8] = 3*7 + 4*8 = 53
1406        assert_abs_diff_eq!(k[[1, 1]], 53.0, epsilon = 1e-10);
1407    }
1408
1409    #[test]
1410    fn test_kernel_pca_creation() {
1411        let kpca = KernelPCA::new()
1412            .n_components(2)
1413            .kernel(KernelFunction::Rbf { gamma: 0.1 })
1414            .tol(1e-6)
1415            .center(false);
1416
1417        assert_eq!(kpca.config.n_components, Some(2));
1418        assert_eq!(kpca.config.tol, 1e-6);
1419        assert!(!kpca.config.center);
1420    }
1421
1422    #[test]
1423    fn test_kernel_pca_fit_transform() {
1424        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
1425
1426        let kpca = KernelPCA::new()
1427            .n_components(2)
1428            .kernel(KernelFunction::Linear)
1429            .fit(&x, &())
1430            .expect("operation should succeed");
1431
1432        assert_eq!(kpca.n_components(), 2);
1433        assert_eq!(kpca.n_features_in(), 2);
1434        assert_eq!(kpca.n_samples(), 4);
1435
1436        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1437        assert_eq!(x_transformed.dim(), (4, 2));
1438
1439        // Transformed data should be finite
1440        for &val in x_transformed.iter() {
1441            assert!(val.is_finite());
1442        }
1443    }
1444
1445    #[test]
1446    fn test_kernel_pca_rbf() {
1447        let x = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0],];
1448
1449        let kpca = KernelPCA::new()
1450            .n_components(2)
1451            .kernel(KernelFunction::Rbf { gamma: 0.5 })
1452            .fit(&x, &())
1453            .expect("operation should succeed");
1454
1455        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1456        assert_eq!(x_transformed.dim(), (4, 2));
1457
1458        // Check that eigenvalues are non-negative
1459        let eigenvalues = kpca.eigenvalues();
1460        for &val in eigenvalues.iter() {
1461            assert!(val >= 0.0, "Eigenvalue should be non-negative: {}", val);
1462        }
1463    }
1464
1465    #[test]
1466    fn test_kernel_pca_polynomial() {
1467        let x = array![[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0],];
1468
1469        let kpca = KernelPCA::new()
1470            .n_components(3)
1471            .kernel(KernelFunction::Polynomial {
1472                degree: 2,
1473                gamma: 1.0,
1474                coef0: 1.0,
1475            })
1476            .fit(&x, &())
1477            .expect("operation should succeed");
1478
1479        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1480        assert_eq!(x_transformed.dim(), (4, 3));
1481
1482        // Transformed data should be finite
1483        for &val in x_transformed.iter() {
1484            assert!(val.is_finite());
1485        }
1486    }
1487
1488    #[test]
1489    fn test_kernel_pca_errors() {
1490        // Empty dataset
1491        let empty_x: Array2<f64> = Array2::zeros((0, 2));
1492        let result = KernelPCA::new().fit(&empty_x, &());
1493        assert!(result.is_err());
1494
1495        // Zero features
1496        let zero_features_x: Array2<f64> = Array2::zeros((2, 0));
1497        let result = KernelPCA::new().fit(&zero_features_x, &());
1498        assert!(result.is_err());
1499
1500        // Zero components
1501        let x = array![[1.0, 2.0], [3.0, 4.0]];
1502        let result = KernelPCA::new().n_components(0).fit(&x, &());
1503        assert!(result.is_err());
1504    }
1505
1506    #[test]
1507    fn test_kernel_pca_feature_mismatch() {
1508        let x_train = array![[1.0, 2.0], [3.0, 4.0]];
1509        let x_test = array![[1.0, 2.0, 3.0]]; // Wrong number of features
1510
1511        let kpca = KernelPCA::new()
1512            .fit(&x_train, &())
1513            .expect("model fitting should succeed");
1514        let result = kpca.transform(&x_test);
1515
1516        assert!(result.is_err());
1517        assert!(result.unwrap_err().to_string().contains("Feature"));
1518    }
1519
1520    #[test]
1521    fn test_kernel_pca_default() {
1522        let x = array![[1.0, 2.0], [3.0, 4.0]];
1523
1524        let kpca = KernelPCA::default()
1525            .fit(&x, &())
1526            .expect("model fitting should succeed");
1527
1528        // Should use default parameters
1529        assert_eq!(kpca.n_components(), 2); // min(n_samples, n_features)
1530        assert_eq!(kpca.n_features_in(), 2);
1531    }
1532
1533    #[test]
1534    fn test_new_kernel_functions() {
1535        let x = array![1.0, 2.0];
1536        let y = array![3.0, 4.0];
1537
1538        // Laplacian kernel
1539        let laplacian = KernelFunction::Laplacian { gamma: 0.5 };
1540        let laplacian_result = laplacian.compute(&x, &y);
1541        // exp(-0.5 * (|1-3| + |2-4|)) = exp(-0.5 * 4) = exp(-2) ≈ 0.135
1542        assert!(laplacian_result > 0.0 && laplacian_result < 1.0);
1543
1544        // Chi-squared kernel
1545        let chi_squared = KernelFunction::ChiSquared { gamma: 1.0 };
1546        let chi_result = chi_squared.compute(&x, &y);
1547        assert!(chi_result > 0.0 && chi_result <= 1.0);
1548    }
1549
1550    #[test]
1551    fn test_kernel_pca_nystrom_approximation() {
1552        let x = array![
1553            [1.0, 2.0],
1554            [3.0, 4.0],
1555            [5.0, 6.0],
1556            [7.0, 8.0],
1557            [9.0, 10.0],
1558            [11.0, 12.0],
1559        ];
1560
1561        let kpca = KernelPCA::new()
1562            .n_components(2)
1563            .kernel(KernelFunction::Rbf { gamma: 0.1 })
1564            .approximation(KernelApproximation::Nystrom { n_components: 4 })
1565            .random_state(42)
1566            .fit(&x, &())
1567            .expect("operation should succeed");
1568
1569        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1570        assert_eq!(x_transformed.dim(), (6, 2));
1571
1572        // Check that eigenvalues are non-negative
1573        let eigenvalues = kpca.eigenvalues();
1574        for &val in eigenvalues.iter() {
1575            assert!(val >= 0.0, "Eigenvalue should be non-negative: {}", val);
1576        }
1577    }
1578
1579    #[test]
1580    fn test_kernel_pca_random_sampling() {
1581        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0],];
1582
1583        let kpca = KernelPCA::new()
1584            .n_components(2)
1585            .kernel(KernelFunction::Linear)
1586            .approximation(KernelApproximation::RandomSampling { n_samples: 3 })
1587            .random_state(123)
1588            .fit(&x, &())
1589            .expect("operation should succeed");
1590
1591        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1592        assert_eq!(x_transformed.dim(), (5, 2));
1593
1594        // Transformed data should be finite
1595        for &val in x_transformed.iter() {
1596            assert!(val.is_finite());
1597        }
1598    }
1599
1600    #[test]
1601    fn test_kernel_pca_laplacian_kernel() {
1602        let x = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]];
1603
1604        let kpca = KernelPCA::new()
1605            .n_components(2)
1606            .kernel(KernelFunction::Laplacian { gamma: 0.1 })
1607            .fit(&x, &())
1608            .expect("operation should succeed");
1609
1610        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1611        assert_eq!(x_transformed.dim(), (4, 2));
1612
1613        // Check that all values are finite
1614        for &val in x_transformed.iter() {
1615            assert!(val.is_finite());
1616        }
1617    }
1618
1619    #[test]
1620    fn test_kernel_pca_chi_squared_kernel() {
1621        // Use positive data for chi-squared kernel (required for numerical stability)
1622        let x = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
1623
1624        let kpca = KernelPCA::new()
1625            .n_components(2)
1626            .kernel(KernelFunction::ChiSquared { gamma: 0.5 })
1627            .fit(&x, &())
1628            .expect("operation should succeed");
1629
1630        let x_transformed = kpca.transform(&x).expect("transformation should succeed");
1631        assert_eq!(x_transformed.dim(), (4, 2));
1632
1633        // Check that all values are finite
1634        for &val in x_transformed.iter() {
1635            assert!(val.is_finite());
1636        }
1637    }
1638}