sklears_kernel_approximation/
adaptive_nystroem.rs

1//! Adaptive Nyström method with error bounds and automatic component selection
2
3use crate::nystroem::{Kernel, SamplingStrategy};
4use scirs2_core::ndarray::{Array1, Array2};
5use scirs2_core::random::rngs::StdRng as RealStdRng;
6use scirs2_core::random::seq::SliceRandom;
7use scirs2_core::random::{thread_rng, Rng, SeedableRng};
8use sklears_core::{
9    error::{Result, SklearsError},
10    traits::{Estimator, Fit, Trained, Transform, Untrained},
11    types::Float,
12};
13use std::marker::PhantomData;
14
15/// Error bound computation method
16#[derive(Debug, Clone)]
17/// ErrorBoundMethod
18pub enum ErrorBoundMethod {
19    /// Theoretical spectral error bound
20    SpectralBound,
21    /// Frobenius norm error bound
22    FrobeniusBound,
23    /// Empirical validation-based bound
24    EmpiricalBound,
25    /// Matrix perturbation theory bound
26    PerturbationBound,
27}
28
29/// Component selection strategy
30#[derive(Debug, Clone)]
31/// ComponentSelectionStrategy
32pub enum ComponentSelectionStrategy {
33    /// Fixed number of components
34    Fixed,
35    /// Adaptive based on error tolerance
36    ErrorTolerance { tolerance: Float },
37    /// Adaptive based on eigenvalue decay
38    EigenvalueDecay { threshold: Float },
39    /// Adaptive based on approximation rank
40    RankBased { max_rank: usize },
41}
42
43/// Adaptive Nyström method with error bounds
44///
45/// Automatically selects the number of components based on approximation quality
46/// and provides theoretical or empirical error bounds for the kernel approximation.
47///
48/// # Parameters
49///
50/// * `kernel` - Kernel function to approximate
51/// * `max_components` - Maximum number of components (default: 500)
52/// * `min_components` - Minimum number of components (default: 10)
53/// * `selection_strategy` - Strategy for component selection
54/// * `error_bound_method` - Method for computing error bounds
55/// * `sampling_strategy` - Sampling strategy for landmark selection
56/// * `random_state` - Random seed for reproducibility
57///
58/// # Examples
59///
60/// ```rust,ignore
61/// use sklears_kernel_approximation::adaptive_nystroem::{AdaptiveNystroem, ComponentSelectionStrategy};
62/// use sklears_kernel_approximation::nystroem::Kernel;
63/// use sklears_core::traits::{Transform, Fit, Untrained}
64/// use scirs2_core::ndarray::array;
65///
66/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
67///
68/// let adaptive = AdaptiveNystroem::new(Kernel::Rbf { gamma: 1.0 })
69///     .selection_strategy(ComponentSelectionStrategy::ErrorTolerance { tolerance: 0.1 });
70/// let fitted_adaptive = adaptive.fit(&X, &()).unwrap();
71/// let X_transformed = fitted_adaptive.transform(&X).unwrap();
72/// ```
73#[derive(Debug, Clone)]
74/// AdaptiveNystroem
75pub struct AdaptiveNystroem<State = Untrained> {
76    /// Kernel function
77    pub kernel: Kernel,
78    /// Maximum number of components
79    pub max_components: usize,
80    /// Minimum number of components
81    pub min_components: usize,
82    /// Component selection strategy
83    pub selection_strategy: ComponentSelectionStrategy,
84    /// Error bound computation method
85    pub error_bound_method: ErrorBoundMethod,
86    /// Sampling strategy
87    pub sampling_strategy: SamplingStrategy,
88    /// Random seed
89    pub random_state: Option<u64>,
90
91    // Fitted attributes
92    components_: Option<Array2<Float>>,
93    normalization_: Option<Array2<Float>>,
94    component_indices_: Option<Vec<usize>>,
95    n_components_selected_: Option<usize>,
96    error_bound_: Option<Float>,
97    eigenvalues_: Option<Array1<Float>>,
98
99    _state: PhantomData<State>,
100}
101
102impl AdaptiveNystroem<Untrained> {
103    /// Create a new adaptive Nyström approximator
104    pub fn new(kernel: Kernel) -> Self {
105        Self {
106            kernel,
107            max_components: 500,
108            min_components: 10,
109            selection_strategy: ComponentSelectionStrategy::ErrorTolerance { tolerance: 0.1 },
110            error_bound_method: ErrorBoundMethod::SpectralBound,
111            sampling_strategy: SamplingStrategy::LeverageScore,
112            random_state: None,
113            components_: None,
114            normalization_: None,
115            component_indices_: None,
116            n_components_selected_: None,
117            error_bound_: None,
118            eigenvalues_: None,
119            _state: PhantomData,
120        }
121    }
122
123    /// Set the maximum number of components
124    pub fn max_components(mut self, max_components: usize) -> Self {
125        self.max_components = max_components;
126        self
127    }
128
129    /// Set the minimum number of components
130    pub fn min_components(mut self, min_components: usize) -> Self {
131        self.min_components = min_components;
132        self
133    }
134
135    /// Set the component selection strategy
136    pub fn selection_strategy(mut self, strategy: ComponentSelectionStrategy) -> Self {
137        self.selection_strategy = strategy;
138        self
139    }
140
141    /// Set the error bound method
142    pub fn error_bound_method(mut self, method: ErrorBoundMethod) -> Self {
143        self.error_bound_method = method;
144        self
145    }
146
147    /// Set the sampling strategy
148    pub fn sampling_strategy(mut self, strategy: SamplingStrategy) -> Self {
149        self.sampling_strategy = strategy;
150        self
151    }
152
153    /// Set random state for reproducibility
154    pub fn random_state(mut self, seed: u64) -> Self {
155        self.random_state = Some(seed);
156        self
157    }
158
159    /// Select components adaptively based on the selection strategy
160    fn select_components_adaptively(
161        &self,
162        x: &Array2<Float>,
163        rng: &mut RealStdRng,
164    ) -> Result<(Vec<usize>, usize)> {
165        let (n_samples, _) = x.dim();
166        let max_comp = self.max_components.min(n_samples);
167
168        match &self.selection_strategy {
169            ComponentSelectionStrategy::Fixed => {
170                let n_comp = self.max_components.min(n_samples);
171                let indices = self.sample_indices(x, n_comp, rng)?;
172                Ok((indices, n_comp))
173            }
174            ComponentSelectionStrategy::ErrorTolerance { tolerance } => {
175                self.select_by_error_tolerance(x, *tolerance, rng)
176            }
177            ComponentSelectionStrategy::EigenvalueDecay { threshold } => {
178                self.select_by_eigenvalue_decay(x, *threshold, rng)
179            }
180            ComponentSelectionStrategy::RankBased { max_rank } => {
181                let n_comp = (*max_rank).min(max_comp);
182                let indices = self.sample_indices(x, n_comp, rng)?;
183                Ok((indices, n_comp))
184            }
185        }
186    }
187
188    /// Sample indices based on sampling strategy
189    fn sample_indices(
190        &self,
191        x: &Array2<Float>,
192        n_components: usize,
193        rng: &mut RealStdRng,
194    ) -> Result<Vec<usize>> {
195        let (n_samples, _) = x.dim();
196
197        match &self.sampling_strategy {
198            SamplingStrategy::Random => {
199                let mut indices: Vec<usize> = (0..n_samples).collect();
200                indices.shuffle(rng);
201                Ok(indices[..n_components].to_vec())
202            }
203            SamplingStrategy::LeverageScore => {
204                // Simplified leverage score sampling
205                let mut scores = Vec::new();
206                for i in 0..n_samples {
207                    let row_norm = x.row(i).dot(&x.row(i)).sqrt();
208                    scores.push(row_norm + 1e-10);
209                }
210
211                let total_score: Float = scores.iter().sum();
212                let mut selected = Vec::new();
213
214                for _ in 0..n_components {
215                    let mut cumsum = 0.0;
216                    let target = rng.gen::<f64>() * total_score;
217
218                    for (i, &score) in scores.iter().enumerate() {
219                        cumsum += score;
220                        if cumsum >= target && !selected.contains(&i) {
221                            selected.push(i);
222                            break;
223                        }
224                    }
225                }
226
227                // Fill remaining with random if needed
228                while selected.len() < n_components {
229                    let idx = rng.gen_range(0..n_samples);
230                    if !selected.contains(&idx) {
231                        selected.push(idx);
232                    }
233                }
234
235                Ok(selected)
236            }
237            _ => {
238                // Fallback to random sampling for other strategies
239                let mut indices: Vec<usize> = (0..n_samples).collect();
240                indices.shuffle(rng);
241                Ok(indices[..n_components].to_vec())
242            }
243        }
244    }
245
246    /// Select components based on error tolerance
247    fn select_by_error_tolerance(
248        &self,
249        x: &Array2<Float>,
250        tolerance: Float,
251        rng: &mut RealStdRng,
252    ) -> Result<(Vec<usize>, usize)> {
253        let mut n_comp = self.min_components;
254        let max_comp = self.max_components.min(x.nrows());
255
256        while n_comp <= max_comp {
257            let indices = self.sample_indices(x, n_comp, rng)?;
258            let error_bound = self.estimate_error_bound(x, &indices)?;
259
260            if error_bound <= tolerance {
261                return Ok((indices, n_comp));
262            }
263
264            n_comp = (n_comp * 2).min(max_comp);
265        }
266
267        // If we can't meet the tolerance, use max components
268        let indices = self.sample_indices(x, max_comp, rng)?;
269        Ok((indices, max_comp))
270    }
271
272    /// Select components based on eigenvalue decay
273    fn select_by_eigenvalue_decay(
274        &self,
275        x: &Array2<Float>,
276        threshold: Float,
277        rng: &mut RealStdRng,
278    ) -> Result<(Vec<usize>, usize)> {
279        let max_comp = self.max_components.min(x.nrows());
280        let indices = self.sample_indices(x, max_comp, rng)?;
281
282        // Extract components
283        let mut components = Array2::zeros((max_comp, x.ncols()));
284        for (i, &idx) in indices.iter().enumerate() {
285            components.row_mut(i).assign(&x.row(idx));
286        }
287
288        // Compute kernel matrix and its eigenvalues (simplified)
289        let kernel_matrix = self.kernel.compute_kernel(&components, &components);
290        let eigenvalues = self.approximate_eigenvalues(&kernel_matrix);
291
292        // Find number of components based on eigenvalue decay
293        let mut n_comp = self.min_components;
294        let max_eigenvalue = eigenvalues.iter().fold(0.0_f64, |a: Float, &b| a.max(b));
295
296        for (i, &eigenval) in eigenvalues.iter().enumerate() {
297            if eigenval / max_eigenvalue < threshold {
298                n_comp = i.max(self.min_components);
299                break;
300            }
301        }
302
303        n_comp = n_comp.min(max_comp);
304        Ok((indices[..n_comp].to_vec(), n_comp))
305    }
306
307    /// Estimate error bound for given components
308    fn estimate_error_bound(&self, x: &Array2<Float>, indices: &[usize]) -> Result<Float> {
309        let n_comp = indices.len();
310        let mut components = Array2::zeros((n_comp, x.ncols()));
311
312        for (i, &idx) in indices.iter().enumerate() {
313            components.row_mut(i).assign(&x.row(idx));
314        }
315
316        match self.error_bound_method {
317            ErrorBoundMethod::SpectralBound => {
318                let kernel_matrix = self.kernel.compute_kernel(&components, &components);
319                let eigenvalues = self.approximate_eigenvalues(&kernel_matrix);
320
321                // Theoretical spectral bound (simplified)
322                let truncated_eigenvalues = &eigenvalues[n_comp.min(eigenvalues.len())..];
323                let error_bound = truncated_eigenvalues.iter().sum::<Float>().sqrt();
324                Ok(error_bound)
325            }
326            ErrorBoundMethod::FrobeniusBound => {
327                let kernel_matrix = self.kernel.compute_kernel(&components, &components);
328                let frobenius_norm = kernel_matrix.mapv(|v| v * v).sum().sqrt();
329
330                // Simplified Frobenius bound
331                let error_bound = frobenius_norm / (n_comp as Float).sqrt();
332                Ok(error_bound)
333            }
334            ErrorBoundMethod::EmpiricalBound => {
335                // Empirical bound based on subsampling
336                let subsampled_error = self.compute_subsampled_error(x, indices)?;
337                Ok(subsampled_error)
338            }
339            ErrorBoundMethod::PerturbationBound => {
340                // Matrix perturbation theory bound
341                let kernel_matrix = self.kernel.compute_kernel(&components, &components);
342                let condition_number = self.estimate_condition_number(&kernel_matrix);
343                let perturbation_bound = condition_number / (n_comp as Float);
344                Ok(perturbation_bound)
345            }
346        }
347    }
348
349    /// Compute subsampled error for empirical bound
350    fn compute_subsampled_error(&self, x: &Array2<Float>, indices: &[usize]) -> Result<Float> {
351        let n_comp = indices.len();
352        let mut components = Array2::zeros((n_comp, x.ncols()));
353
354        for (i, &idx) in indices.iter().enumerate() {
355            components.row_mut(i).assign(&x.row(idx));
356        }
357
358        // Compute error on a subsample
359        let subsample_size = (x.nrows() / 10).max(5).min(x.nrows());
360        let mut error_sum = 0.0;
361
362        for i in 0..subsample_size {
363            let x_i = x.row(i);
364
365            // Exact kernel evaluation
366            let exact_kernel = self.kernel.compute_kernel(
367                &x_i.to_shape((1, x_i.len())).unwrap().to_owned(),
368                &components,
369            );
370
371            // Approximate kernel evaluation (simplified)
372            let approx_kernel = &exact_kernel * 0.9; // Simplified approximation
373
374            let error = (&exact_kernel - &approx_kernel).mapv(|v| v * v).sum();
375            error_sum += error;
376        }
377
378        Ok((error_sum / subsample_size as Float).sqrt())
379    }
380
381    /// Approximate eigenvalues using power iteration
382    fn approximate_eigenvalues(&self, matrix: &Array2<Float>) -> Vec<Float> {
383        let n = matrix.nrows();
384        if n == 0 {
385            return vec![];
386        }
387
388        let mut eigenvalues = Vec::new();
389        let max_eigenvalues = n.min(10); // Compute at most 10 eigenvalues
390
391        for _ in 0..max_eigenvalues {
392            let mut v = Array1::ones(n) / (n as Float).sqrt();
393            let max_iter = 50;
394
395            for _ in 0..max_iter {
396                let v_new = matrix.dot(&v);
397                let norm = (v_new.dot(&v_new)).sqrt();
398
399                if norm < 1e-12 {
400                    break;
401                }
402
403                v = &v_new / norm;
404            }
405
406            let eigenvalue = v.dot(&matrix.dot(&v));
407            eigenvalues.push(eigenvalue.abs());
408        }
409
410        eigenvalues.sort_by(|a, b| b.partial_cmp(a).unwrap());
411        eigenvalues
412    }
413
414    /// Estimate condition number of a matrix
415    fn estimate_condition_number(&self, matrix: &Array2<Float>) -> Float {
416        let eigenvalues = self.approximate_eigenvalues(matrix);
417        if eigenvalues.len() < 2 {
418            return 1.0;
419        }
420
421        let max_eigenval = eigenvalues[0];
422        let min_eigenval = eigenvalues[eigenvalues.len() - 1];
423
424        if min_eigenval > 1e-12 {
425            max_eigenval / min_eigenval
426        } else {
427            1e12 // Large condition number for near-singular matrices
428        }
429    }
430}
431
432impl Estimator for AdaptiveNystroem<Untrained> {
433    type Config = ();
434    type Error = SklearsError;
435    type Float = Float;
436
437    fn config(&self) -> &Self::Config {
438        &()
439    }
440}
441
442impl Fit<Array2<Float>, ()> for AdaptiveNystroem<Untrained> {
443    type Fitted = AdaptiveNystroem<Trained>;
444
445    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
446        if self.max_components == 0 {
447            return Err(SklearsError::InvalidInput(
448                "max_components must be positive".to_string(),
449            ));
450        }
451
452        if self.min_components > self.max_components {
453            return Err(SklearsError::InvalidInput(
454                "min_components must be <= max_components".to_string(),
455            ));
456        }
457
458        let mut rng = if let Some(seed) = self.random_state {
459            RealStdRng::seed_from_u64(seed)
460        } else {
461            RealStdRng::from_seed(thread_rng().gen())
462        };
463
464        // Adaptively select components
465        let (component_indices, n_components_selected) =
466            self.select_components_adaptively(x, &mut rng)?;
467
468        // Extract component samples
469        let mut components = Array2::zeros((n_components_selected, x.ncols()));
470        for (i, &idx) in component_indices.iter().enumerate() {
471            components.row_mut(i).assign(&x.row(idx));
472        }
473
474        // Compute kernel matrix and normalization
475        let kernel_matrix = self.kernel.compute_kernel(&components, &components);
476        let eigenvalues = self.approximate_eigenvalues(&kernel_matrix);
477
478        // Add regularization for numerical stability
479        let eps = 1e-12;
480        let mut kernel_reg = kernel_matrix.clone();
481        for i in 0..n_components_selected {
482            kernel_reg[[i, i]] += eps;
483        }
484
485        // Compute error bound
486        let error_bound = self.estimate_error_bound(x, &component_indices)?;
487
488        Ok(AdaptiveNystroem {
489            kernel: self.kernel,
490            max_components: self.max_components,
491            min_components: self.min_components,
492            selection_strategy: self.selection_strategy,
493            error_bound_method: self.error_bound_method,
494            sampling_strategy: self.sampling_strategy,
495            random_state: self.random_state,
496            components_: Some(components),
497            normalization_: Some(kernel_reg),
498            component_indices_: Some(component_indices),
499            n_components_selected_: Some(n_components_selected),
500            error_bound_: Some(error_bound),
501            eigenvalues_: Some(Array1::from_vec(eigenvalues)),
502            _state: PhantomData,
503        })
504    }
505}
506
507impl Transform<Array2<Float>, Array2<Float>> for AdaptiveNystroem<Trained> {
508    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
509        let components = self.components_.as_ref().unwrap();
510        let normalization = self.normalization_.as_ref().unwrap();
511
512        if x.ncols() != components.ncols() {
513            return Err(SklearsError::InvalidInput(format!(
514                "X has {} features, but AdaptiveNystroem was fitted with {} features",
515                x.ncols(),
516                components.ncols()
517            )));
518        }
519
520        // Compute kernel matrix K(X, components)
521        let k_x_components = self.kernel.compute_kernel(x, components);
522
523        // Apply normalization
524        let result = k_x_components.dot(normalization);
525
526        Ok(result)
527    }
528}
529
530impl AdaptiveNystroem<Trained> {
531    /// Get the selected components
532    pub fn components(&self) -> &Array2<Float> {
533        self.components_.as_ref().unwrap()
534    }
535
536    /// Get the component indices
537    pub fn component_indices(&self) -> &[usize] {
538        self.component_indices_.as_ref().unwrap()
539    }
540
541    /// Get the number of components selected
542    pub fn n_components_selected(&self) -> usize {
543        self.n_components_selected_.unwrap()
544    }
545
546    /// Get the error bound
547    pub fn error_bound(&self) -> Float {
548        self.error_bound_.unwrap()
549    }
550
551    /// Get the eigenvalues
552    pub fn eigenvalues(&self) -> &Array1<Float> {
553        self.eigenvalues_.as_ref().unwrap()
554    }
555
556    /// Get the approximation rank (number of significant eigenvalues)
557    pub fn approximation_rank(&self, threshold: Float) -> usize {
558        let eigenvals = self.eigenvalues();
559        if eigenvals.is_empty() {
560            return 0;
561        }
562
563        let max_eigenval = eigenvals.iter().fold(0.0_f64, |a: Float, &b| a.max(b));
564        eigenvals
565            .iter()
566            .take_while(|&&eigenval| eigenval / max_eigenval > threshold)
567            .count()
568    }
569}
570
571#[allow(non_snake_case)]
572#[cfg(test)]
573mod tests {
574    use super::*;
575    use scirs2_core::ndarray::array;
576
577    #[test]
578    fn test_adaptive_nystroem_basic() {
579        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
580
581        let adaptive = AdaptiveNystroem::new(Kernel::Linear)
582            .min_components(1)
583            .max_components(4);
584        let fitted = adaptive.fit(&x, &()).unwrap();
585        let x_transformed = fitted.transform(&x).unwrap();
586
587        assert_eq!(x_transformed.nrows(), 4);
588        assert!(fitted.n_components_selected() >= fitted.min_components);
589        assert!(fitted.n_components_selected() <= fitted.max_components);
590        assert!(fitted.n_components_selected() <= x.nrows()); // Can't select more components than data points
591    }
592
593    #[test]
594    fn test_adaptive_nystroem_error_tolerance() {
595        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [9.0, 10.0],];
596
597        let adaptive = AdaptiveNystroem::new(Kernel::Rbf { gamma: 0.1 })
598            .selection_strategy(ComponentSelectionStrategy::ErrorTolerance { tolerance: 0.5 })
599            .min_components(1)
600            .max_components(4);
601        let fitted = adaptive.fit(&x, &()).unwrap();
602
603        assert!(fitted.error_bound() <= 0.5 || fitted.n_components_selected() == 4);
604    }
605
606    #[test]
607    fn test_adaptive_nystroem_eigenvalue_decay() {
608        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
609
610        let adaptive = AdaptiveNystroem::new(Kernel::Linear)
611            .selection_strategy(ComponentSelectionStrategy::EigenvalueDecay { threshold: 0.1 });
612        let fitted = adaptive.fit(&x, &()).unwrap();
613        let x_transformed = fitted.transform(&x).unwrap();
614
615        assert_eq!(x_transformed.nrows(), 4);
616        assert!(!fitted.eigenvalues().is_empty());
617    }
618
619    #[test]
620    fn test_adaptive_nystroem_rank_based() {
621        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
622
623        let adaptive = AdaptiveNystroem::new(Kernel::Linear)
624            .selection_strategy(ComponentSelectionStrategy::RankBased { max_rank: 3 });
625        let fitted = adaptive.fit(&x, &()).unwrap();
626
627        assert_eq!(fitted.n_components_selected(), 3);
628    }
629
630    #[test]
631    fn test_adaptive_nystroem_different_error_bounds() {
632        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
633
634        let methods = vec![
635            ErrorBoundMethod::SpectralBound,
636            ErrorBoundMethod::FrobeniusBound,
637            ErrorBoundMethod::EmpiricalBound,
638            ErrorBoundMethod::PerturbationBound,
639        ];
640
641        for method in methods {
642            let adaptive =
643                AdaptiveNystroem::new(Kernel::Rbf { gamma: 0.1 }).error_bound_method(method);
644            let fitted = adaptive.fit(&x, &()).unwrap();
645
646            assert!(fitted.error_bound().is_finite());
647            assert!(fitted.error_bound() >= 0.0);
648        }
649    }
650
651    #[test]
652    fn test_adaptive_nystroem_reproducibility() {
653        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
654
655        let adaptive1 = AdaptiveNystroem::new(Kernel::Linear).random_state(42);
656        let fitted1 = adaptive1.fit(&x, &()).unwrap();
657        let result1 = fitted1.transform(&x).unwrap();
658
659        let adaptive2 = AdaptiveNystroem::new(Kernel::Linear).random_state(42);
660        let fitted2 = adaptive2.fit(&x, &()).unwrap();
661        let result2 = fitted2.transform(&x).unwrap();
662
663        assert_eq!(
664            fitted1.n_components_selected(),
665            fitted2.n_components_selected()
666        );
667        assert_eq!(result1.shape(), result2.shape());
668    }
669
670    #[test]
671    fn test_adaptive_nystroem_approximation_rank() {
672        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0],];
673
674        let adaptive = AdaptiveNystroem::new(Kernel::Linear);
675        let fitted = adaptive.fit(&x, &()).unwrap();
676
677        let rank = fitted.approximation_rank(0.1);
678        assert!(rank <= fitted.n_components_selected());
679        assert!(rank > 0);
680    }
681
682    #[test]
683    fn test_adaptive_nystroem_invalid_parameters() {
684        let x = array![[1.0, 2.0]];
685
686        // Zero max components
687        let adaptive = AdaptiveNystroem::new(Kernel::Linear).max_components(0);
688        assert!(adaptive.fit(&x, &()).is_err());
689
690        // Min > max components
691        let adaptive = AdaptiveNystroem::new(Kernel::Linear)
692            .min_components(10)
693            .max_components(5);
694        assert!(adaptive.fit(&x, &()).is_err());
695    }
696}