Skip to main content

sklears_kernel_approximation/
rbf_sampler.rs

1//! Random Fourier Features for RBF Kernel Approximation
2use scirs2_core::ndarray::{Array1, Array2, Axis};
3use scirs2_core::random::essentials::{Normal as RandNormal, Uniform as RandUniform};
4use scirs2_core::random::rngs::StdRng as RealStdRng;
5use scirs2_core::random::Rng;
6use scirs2_core::random::{thread_rng, SeedableRng};
7use scirs2_core::Cauchy;
8use sklears_core::{
9    error::{Result, SklearsError},
10    prelude::{Fit, Transform},
11    traits::{Estimator, Trained, Untrained},
12    types::Float,
13};
14use std::marker::PhantomData;
15
16/// Random Fourier Features for RBF kernel approximation
17///
18/// Approximates the RBF kernel K(x,y) = exp(-gamma * ||x-y||²) using
19/// random Fourier features (Random Kitchen Sinks).
20///
21/// # Parameters
22///
23/// * `gamma` - RBF kernel parameter (default: 1.0)
24/// * `n_components` - Number of Monte Carlo samples (default: 100)
25/// * `random_state` - Random seed for reproducibility
26///
27/// # Examples
28///
29/// ```rust,ignore
30/// use sklears_kernel_approximation::RBFSampler;
31/// use sklears_core::traits::{Transform, Fit, Untrained}
32/// use scirs2_core::ndarray::array;
33///
34/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
35///
36/// let rbf = RBFSampler::new(100);
37/// let fitted_rbf = rbf.fit(&X, &()).unwrap();
38/// let X_transformed = fitted_rbf.transform(&X).unwrap();
39/// assert_eq!(X_transformed.shape(), &[3, 100]);
40/// ```
41#[derive(Debug, Clone)]
42/// RBFSampler
43pub struct RBFSampler<State = Untrained> {
44    /// RBF kernel parameter
45    pub gamma: Float,
46    /// Number of Monte Carlo samples
47    pub n_components: usize,
48    /// Random seed
49    pub random_state: Option<u64>,
50
51    // Fitted attributes
52    random_weights_: Option<Array2<Float>>,
53    random_offset_: Option<Array1<Float>>,
54
55    _state: PhantomData<State>,
56}
57
58impl RBFSampler<Untrained> {
59    /// Create a new RBF sampler
60    pub fn new(n_components: usize) -> Self {
61        Self {
62            gamma: 1.0,
63            n_components,
64            random_state: None,
65            random_weights_: None,
66            random_offset_: None,
67            _state: PhantomData,
68        }
69    }
70
71    /// Set the gamma parameter
72    pub fn gamma(mut self, gamma: Float) -> Self {
73        self.gamma = gamma;
74        self
75    }
76
77    /// Set random state for reproducibility
78    pub fn random_state(mut self, seed: u64) -> Self {
79        self.random_state = Some(seed);
80        self
81    }
82}
83
84impl Estimator for RBFSampler<Untrained> {
85    type Config = ();
86    type Error = SklearsError;
87    type Float = Float;
88
89    fn config(&self) -> &Self::Config {
90        &()
91    }
92}
93
94impl Fit<Array2<Float>, ()> for RBFSampler<Untrained> {
95    type Fitted = RBFSampler<Trained>;
96
97    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
98        let (_, n_features) = x.dim();
99
100        if self.gamma <= 0.0 {
101            return Err(SklearsError::InvalidInput(
102                "gamma must be positive".to_string(),
103            ));
104        }
105
106        if self.n_components == 0 {
107            return Err(SklearsError::InvalidInput(
108                "n_components must be positive".to_string(),
109            ));
110        }
111
112        let mut rng = if let Some(seed) = self.random_state {
113            RealStdRng::seed_from_u64(seed)
114        } else {
115            RealStdRng::from_seed(thread_rng().gen())
116        };
117
118        // Sample random weights from N(0, 2*gamma)
119        let normal = RandNormal::new(0.0, (2.0 * self.gamma).sqrt()).map_err(|_| {
120            SklearsError::InvalidInput("Failed to create normal distribution".to_string())
121        })?;
122        let mut random_weights = Array2::zeros((n_features, self.n_components));
123        for mut col in random_weights.columns_mut() {
124            for val in col.iter_mut() {
125                *val = rng.sample(normal);
126            }
127        }
128
129        // Sample random offsets from Uniform(0, 2π)
130        let uniform = RandUniform::new(0.0, 2.0 * std::f64::consts::PI).map_err(|_| {
131            SklearsError::InvalidInput("Failed to create uniform distribution".to_string())
132        })?;
133        let mut random_offset = Array1::zeros(self.n_components);
134        for val in random_offset.iter_mut() {
135            *val = rng.sample(uniform);
136        }
137
138        Ok(RBFSampler {
139            gamma: self.gamma,
140            n_components: self.n_components,
141            random_state: self.random_state,
142            random_weights_: Some(random_weights),
143            random_offset_: Some(random_offset),
144            _state: PhantomData,
145        })
146    }
147}
148
149impl Transform<Array2<Float>, Array2<Float>> for RBFSampler<Trained> {
150    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
151        let (_n_samples, n_features) = x.dim();
152        let weights = self
153            .random_weights_
154            .as_ref()
155            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
156        let offset = self
157            .random_offset_
158            .as_ref()
159            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
160
161        if n_features != weights.nrows() {
162            return Err(SklearsError::InvalidInput(format!(
163                "X has {} features, but RBFSampler was fitted with {} features",
164                n_features,
165                weights.nrows()
166            )));
167        }
168
169        // Compute projection: X @ weights + offset
170        let projection = x.dot(weights) + offset.view().insert_axis(Axis(0));
171
172        // Apply cosine and normalize: sqrt(2/n_components) * cos(projection)
173        let normalization = (2.0 / self.n_components as Float).sqrt();
174        let result = projection.mapv(|v| normalization * v.cos());
175
176        Ok(result)
177    }
178}
179
180impl RBFSampler<Trained> {
181    /// Get the random weights
182    pub fn random_weights(&self) -> Result<&Array2<Float>> {
183        self.random_weights_
184            .as_ref()
185            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
186    }
187
188    /// Get the random offset
189    pub fn random_offset(&self) -> Result<&Array1<Float>> {
190        self.random_offset_
191            .as_ref()
192            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
193    }
194}
195
196/// Laplacian kernel approximation using Random Fourier Features
197///
198/// Approximates the Laplacian kernel K(x,y) = exp(-gamma * ||x-y||₁) using
199/// random Fourier features with Cauchy distribution.
200///
201/// # Parameters
202///
203/// * `gamma` - Laplacian kernel parameter (default: 1.0)
204/// * `n_components` - Number of Monte Carlo samples (default: 100)
205/// * `random_state` - Random seed for reproducibility
206///
207/// # Examples
208///
209/// ```rust,ignore
210/// use sklears_kernel_approximation::LaplacianSampler;
211/// use sklears_core::traits::{Transform, Fit, Untrained}
212/// use scirs2_core::ndarray::array;
213///
214/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
215///
216/// let laplacian = LaplacianSampler::new(100);
217/// let fitted_laplacian = laplacian.fit(&X, &()).unwrap();
218/// let X_transformed = fitted_laplacian.transform(&X).unwrap();
219/// assert_eq!(X_transformed.shape(), &[3, 100]);
220/// ```
221#[derive(Debug, Clone)]
222/// LaplacianSampler
223pub struct LaplacianSampler<State = Untrained> {
224    /// Laplacian kernel parameter
225    pub gamma: Float,
226    /// Number of Monte Carlo samples
227    pub n_components: usize,
228    /// Random seed
229    pub random_state: Option<u64>,
230
231    // Fitted attributes
232    random_weights_: Option<Array2<Float>>,
233    random_offset_: Option<Array1<Float>>,
234
235    _state: PhantomData<State>,
236}
237
238impl LaplacianSampler<Untrained> {
239    /// Create a new Laplacian sampler
240    pub fn new(n_components: usize) -> Self {
241        Self {
242            gamma: 1.0,
243            n_components,
244            random_state: None,
245            random_weights_: None,
246            random_offset_: None,
247            _state: PhantomData,
248        }
249    }
250
251    /// Set the gamma parameter
252    pub fn gamma(mut self, gamma: Float) -> Self {
253        self.gamma = gamma;
254        self
255    }
256
257    /// Set random state for reproducibility
258    pub fn random_state(mut self, seed: u64) -> Self {
259        self.random_state = Some(seed);
260        self
261    }
262}
263
264impl Estimator for LaplacianSampler<Untrained> {
265    type Config = ();
266    type Error = SklearsError;
267    type Float = Float;
268
269    fn config(&self) -> &Self::Config {
270        &()
271    }
272}
273
274impl Fit<Array2<Float>, ()> for LaplacianSampler<Untrained> {
275    type Fitted = LaplacianSampler<Trained>;
276
277    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
278        let (_, n_features) = x.dim();
279
280        if self.gamma <= 0.0 {
281            return Err(SklearsError::InvalidInput(
282                "gamma must be positive".to_string(),
283            ));
284        }
285
286        if self.n_components == 0 {
287            return Err(SklearsError::InvalidInput(
288                "n_components must be positive".to_string(),
289            ));
290        }
291
292        let mut rng = if let Some(seed) = self.random_state {
293            RealStdRng::seed_from_u64(seed)
294        } else {
295            RealStdRng::from_seed(thread_rng().gen())
296        };
297
298        // Sample random weights from Cauchy distribution (location=0, scale=gamma)
299        let cauchy = Cauchy::new(0.0, self.gamma).map_err(|_| {
300            SklearsError::InvalidInput("Failed to create Cauchy distribution".to_string())
301        })?;
302        let mut random_weights = Array2::zeros((n_features, self.n_components));
303        for mut col in random_weights.columns_mut() {
304            for val in col.iter_mut() {
305                *val = rng.sample(cauchy);
306            }
307        }
308
309        // Sample random offsets from Uniform(0, 2π)
310        let uniform = RandUniform::new(0.0, 2.0 * std::f64::consts::PI).map_err(|_| {
311            SklearsError::InvalidInput("Failed to create uniform distribution".to_string())
312        })?;
313        let mut random_offset = Array1::zeros(self.n_components);
314        for val in random_offset.iter_mut() {
315            *val = rng.sample(uniform);
316        }
317
318        Ok(LaplacianSampler {
319            gamma: self.gamma,
320            n_components: self.n_components,
321            random_state: self.random_state,
322            random_weights_: Some(random_weights),
323            random_offset_: Some(random_offset),
324            _state: PhantomData,
325        })
326    }
327}
328
329impl Transform<Array2<Float>, Array2<Float>> for LaplacianSampler<Trained> {
330    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
331        let (_n_samples, n_features) = x.dim();
332        let weights = self
333            .random_weights_
334            .as_ref()
335            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
336        let offset = self
337            .random_offset_
338            .as_ref()
339            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
340
341        if n_features != weights.nrows() {
342            return Err(SklearsError::InvalidInput(format!(
343                "X has {} features, but LaplacianSampler was fitted with {} features",
344                n_features,
345                weights.nrows()
346            )));
347        }
348
349        // Compute projection: X @ weights + offset
350        let projection = x.dot(weights) + offset.view().insert_axis(Axis(0));
351
352        // Apply cosine and normalize: sqrt(2/n_components) * cos(projection)
353        let normalization = (2.0 / self.n_components as Float).sqrt();
354        let result = projection.mapv(|v| normalization * v.cos());
355
356        Ok(result)
357    }
358}
359
360impl LaplacianSampler<Trained> {
361    /// Get the random weights
362    pub fn random_weights(&self) -> Result<&Array2<Float>> {
363        self.random_weights_
364            .as_ref()
365            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
366    }
367
368    /// Get the random offset
369    pub fn random_offset(&self) -> Result<&Array1<Float>> {
370        self.random_offset_
371            .as_ref()
372            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
373    }
374}
375
376/// Polynomial kernel approximation using Random Fourier Features
377///
378/// Approximates the polynomial kernel K(x,y) = (gamma * <x,y> + coef0)^degree using
379/// random Fourier features based on the binomial theorem expansion.
380///
381/// # Parameters
382///
383/// * `gamma` - Polynomial kernel parameter (default: 1.0)
384/// * `coef0` - Independent term in polynomial kernel (default: 1.0)
385/// * `degree` - Degree of polynomial kernel (default: 3)
386/// * `n_components` - Number of Monte Carlo samples (default: 100)
387/// * `random_state` - Random seed for reproducibility
388///
389/// # Examples
390///
391/// ```rust,ignore
392/// use sklears_kernel_approximation::rbf_sampler::PolynomialSampler;
393/// use sklears_core::traits::{Transform, Fit, Untrained}
394/// use scirs2_core::ndarray::array;
395///
396/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
397///
398/// let poly = PolynomialSampler::new(100).degree(3).gamma(1.0).coef0(1.0);
399/// let fitted_poly = poly.fit(&X, &()).unwrap();
400/// let X_transformed = fitted_poly.transform(&X).unwrap();
401/// assert_eq!(X_transformed.shape(), &[3, 100]);
402/// ```
403#[derive(Debug, Clone)]
404/// PolynomialSampler
405pub struct PolynomialSampler<State = Untrained> {
406    /// Polynomial kernel parameter
407    pub gamma: Float,
408    /// Independent term in polynomial kernel
409    pub coef0: Float,
410    /// Degree of polynomial kernel
411    pub degree: u32,
412    /// Number of Monte Carlo samples
413    pub n_components: usize,
414    /// Random seed
415    pub random_state: Option<u64>,
416
417    // Fitted attributes
418    random_weights_: Option<Array2<Float>>,
419    random_offset_: Option<Array1<Float>>,
420
421    _state: PhantomData<State>,
422}
423
424impl PolynomialSampler<Untrained> {
425    /// Create a new polynomial sampler
426    pub fn new(n_components: usize) -> Self {
427        Self {
428            gamma: 1.0,
429            coef0: 1.0,
430            degree: 3,
431            n_components,
432            random_state: None,
433            random_weights_: None,
434            random_offset_: None,
435            _state: PhantomData,
436        }
437    }
438
439    /// Set the gamma parameter
440    pub fn gamma(mut self, gamma: Float) -> Self {
441        self.gamma = gamma;
442        self
443    }
444
445    /// Set the coef0 parameter
446    pub fn coef0(mut self, coef0: Float) -> Self {
447        self.coef0 = coef0;
448        self
449    }
450
451    /// Set the degree parameter
452    pub fn degree(mut self, degree: u32) -> Self {
453        self.degree = degree;
454        self
455    }
456
457    /// Set random state for reproducibility
458    pub fn random_state(mut self, seed: u64) -> Self {
459        self.random_state = Some(seed);
460        self
461    }
462}
463
464impl Estimator for PolynomialSampler<Untrained> {
465    type Config = ();
466    type Error = SklearsError;
467    type Float = Float;
468
469    fn config(&self) -> &Self::Config {
470        &()
471    }
472}
473
474impl Fit<Array2<Float>, ()> for PolynomialSampler<Untrained> {
475    type Fitted = PolynomialSampler<Trained>;
476
477    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
478        let (_, n_features) = x.dim();
479
480        if self.gamma <= 0.0 {
481            return Err(SklearsError::InvalidInput(
482                "gamma must be positive".to_string(),
483            ));
484        }
485
486        if self.degree == 0 {
487            return Err(SklearsError::InvalidInput(
488                "degree must be positive".to_string(),
489            ));
490        }
491
492        if self.n_components == 0 {
493            return Err(SklearsError::InvalidInput(
494                "n_components must be positive".to_string(),
495            ));
496        }
497
498        let mut rng = if let Some(seed) = self.random_state {
499            RealStdRng::seed_from_u64(seed)
500        } else {
501            RealStdRng::from_seed(thread_rng().gen())
502        };
503
504        // For polynomial kernels, we use a different approach:
505        // Sample random projections from uniform sphere and scaling factors
506        let normal = RandNormal::new(0.0, 1.0).map_err(|_| {
507            SklearsError::InvalidInput("Failed to create normal distribution".to_string())
508        })?;
509        let mut random_weights = Array2::zeros((n_features, self.n_components));
510
511        for mut col in random_weights.columns_mut() {
512            // Sample from standard normal and normalize to get uniform direction on sphere
513            for val in col.iter_mut() {
514                *val = rng.sample(normal);
515            }
516            let norm = (col.dot(&col) as Float).sqrt();
517            if norm > 1e-12 {
518                col /= norm;
519            }
520
521            // Scale by gamma
522            col *= self.gamma.sqrt();
523        }
524
525        // Sample random offsets from Uniform(0, 2π)
526        let uniform = RandUniform::new(0.0, 2.0 * std::f64::consts::PI).map_err(|_| {
527            SklearsError::InvalidInput("Failed to create uniform distribution".to_string())
528        })?;
529        let mut random_offset = Array1::zeros(self.n_components);
530        for val in random_offset.iter_mut() {
531            *val = rng.sample(uniform);
532        }
533
534        Ok(PolynomialSampler {
535            gamma: self.gamma,
536            coef0: self.coef0,
537            degree: self.degree,
538            n_components: self.n_components,
539            random_state: self.random_state,
540            random_weights_: Some(random_weights),
541            random_offset_: Some(random_offset),
542            _state: PhantomData,
543        })
544    }
545}
546
547impl Transform<Array2<Float>, Array2<Float>> for PolynomialSampler<Trained> {
548    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
549        let (_n_samples, n_features) = x.dim();
550        let weights = self
551            .random_weights_
552            .as_ref()
553            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
554        let offset = self
555            .random_offset_
556            .as_ref()
557            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
558
559        if n_features != weights.nrows() {
560            return Err(SklearsError::InvalidInput(format!(
561                "X has {} features, but PolynomialSampler was fitted with {} features",
562                n_features,
563                weights.nrows()
564            )));
565        }
566
567        // Compute projection: X @ weights + offset
568        let projection = x.dot(weights) + offset.view().insert_axis(Axis(0));
569
570        // For polynomial kernels, we apply: (cos(projection) + coef0)^degree
571        // This approximates the polynomial kernel through trigonometric expansion
572        let normalization = (2.0 / self.n_components as Float).sqrt();
573        let result = projection.mapv(|v| {
574            let cos_val = v.cos() + self.coef0;
575            normalization * cos_val.powf(self.degree as Float)
576        });
577
578        Ok(result)
579    }
580}
581
582impl PolynomialSampler<Trained> {
583    /// Get the random weights
584    pub fn random_weights(&self) -> Result<&Array2<Float>> {
585        self.random_weights_
586            .as_ref()
587            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
588    }
589
590    /// Get the random offset
591    pub fn random_offset(&self) -> Result<&Array1<Float>> {
592        self.random_offset_
593            .as_ref()
594            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
595    }
596
597    /// Get the gamma parameter
598    pub fn gamma(&self) -> Float {
599        self.gamma
600    }
601
602    /// Get the coef0 parameter
603    pub fn coef0(&self) -> Float {
604        self.coef0
605    }
606
607    /// Get the degree parameter
608    pub fn degree(&self) -> u32 {
609        self.degree
610    }
611}
612
613/// Arc-cosine kernel approximation using Random Fourier Features
614///
615/// Approximates the arc-cosine kernel which corresponds to infinite-width neural networks.
616/// The arc-cosine kernel of degree n is defined as:
617/// K_n(x,y) = (1/π) * ||x|| * ||y|| * J_n(θ)
618/// where θ is the angle between x and y.
619///
620/// # Parameters
621///
622/// * `degree` - Degree of the arc-cosine kernel (0, 1, or 2)
623/// * `n_components` - Number of Monte Carlo samples (default: 100)
624/// * `random_state` - Random seed for reproducibility
625///
626/// # Examples
627///
628/// ```rust,ignore
629/// use sklears_kernel_approximation::rbf_sampler::ArcCosineSampler;
630/// use sklears_core::traits::{Transform, Fit, Untrained}
631/// use scirs2_core::ndarray::array;
632///
633/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
634///
635/// let arc_cosine = ArcCosineSampler::new(100).degree(1);
636/// let fitted_arc_cosine = arc_cosine.fit(&X, &()).unwrap();
637/// let X_transformed = fitted_arc_cosine.transform(&X).unwrap();
638/// assert_eq!(X_transformed.shape(), &[3, 100]);
639/// ```
640#[derive(Debug, Clone)]
641/// ArcCosineSampler
642pub struct ArcCosineSampler<State = Untrained> {
643    /// Degree of the arc-cosine kernel
644    pub degree: u32,
645    /// Number of Monte Carlo samples
646    pub n_components: usize,
647    /// Random seed
648    pub random_state: Option<u64>,
649
650    // Fitted attributes
651    random_weights_: Option<Array2<Float>>,
652
653    _state: PhantomData<State>,
654}
655
656impl ArcCosineSampler<Untrained> {
657    /// Create a new arc-cosine sampler
658    pub fn new(n_components: usize) -> Self {
659        Self {
660            degree: 1,
661            n_components,
662            random_state: None,
663            random_weights_: None,
664            _state: PhantomData,
665        }
666    }
667
668    /// Set the degree parameter (0, 1, or 2)
669    pub fn degree(mut self, degree: u32) -> Self {
670        self.degree = degree;
671        self
672    }
673
674    /// Set random state for reproducibility
675    pub fn random_state(mut self, seed: u64) -> Self {
676        self.random_state = Some(seed);
677        self
678    }
679}
680
681impl Estimator for ArcCosineSampler<Untrained> {
682    type Config = ();
683    type Error = SklearsError;
684    type Float = Float;
685
686    fn config(&self) -> &Self::Config {
687        &()
688    }
689}
690
691impl Fit<Array2<Float>, ()> for ArcCosineSampler<Untrained> {
692    type Fitted = ArcCosineSampler<Trained>;
693
694    fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
695        let (_, n_features) = x.dim();
696
697        if self.degree > 2 {
698            return Err(SklearsError::InvalidInput(
699                "degree must be 0, 1, or 2".to_string(),
700            ));
701        }
702
703        if self.n_components == 0 {
704            return Err(SklearsError::InvalidInput(
705                "n_components must be positive".to_string(),
706            ));
707        }
708
709        let mut rng = if let Some(seed) = self.random_state {
710            RealStdRng::seed_from_u64(seed)
711        } else {
712            RealStdRng::from_seed(thread_rng().gen())
713        };
714
715        // Sample random weights from standard normal distribution
716        let normal = RandNormal::new(0.0, 1.0).map_err(|_| {
717            SklearsError::InvalidInput("Failed to create normal distribution".to_string())
718        })?;
719        let mut random_weights = Array2::zeros((n_features, self.n_components));
720
721        for mut col in random_weights.columns_mut() {
722            for val in col.iter_mut() {
723                *val = rng.sample(normal);
724            }
725        }
726
727        Ok(ArcCosineSampler {
728            degree: self.degree,
729            n_components: self.n_components,
730            random_state: self.random_state,
731            random_weights_: Some(random_weights),
732            _state: PhantomData,
733        })
734    }
735}
736
737impl Transform<Array2<Float>, Array2<Float>> for ArcCosineSampler<Trained> {
738    fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
739        let (_n_samples, n_features) = x.dim();
740        let weights = self
741            .random_weights_
742            .as_ref()
743            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))?;
744
745        if n_features != weights.nrows() {
746            return Err(SklearsError::InvalidInput(format!(
747                "X has {} features, but ArcCosineSampler was fitted with {} features",
748                n_features,
749                weights.nrows()
750            )));
751        }
752
753        // Compute projection: X @ weights
754        let projection = x.dot(weights);
755
756        // Apply activation function based on degree
757        let normalization = (2.0 / self.n_components as Float).sqrt();
758        let result = match self.degree {
759            0 => {
760                // ReLU: max(0, x)
761                projection.mapv(|v| normalization * v.max(0.0))
762            }
763            1 => {
764                // Sigmoid-like: x * I(x > 0)
765                projection.mapv(|v| if v > 0.0 { normalization * v } else { 0.0 })
766            }
767            2 => {
768                // Quadratic: x² * I(x > 0)
769                projection.mapv(|v| if v > 0.0 { normalization * v * v } else { 0.0 })
770            }
771            _ => unreachable!("degree validation should prevent this"),
772        };
773
774        Ok(result)
775    }
776}
777
778impl ArcCosineSampler<Trained> {
779    /// Get the random weights
780    pub fn random_weights(&self) -> Result<&Array2<Float>> {
781        self.random_weights_
782            .as_ref()
783            .ok_or_else(|| SklearsError::InvalidInput("Model not fitted".to_string()))
784    }
785
786    /// Get the degree parameter
787    pub fn degree(&self) -> u32 {
788        self.degree
789    }
790}
791
792#[allow(non_snake_case)]
793#[cfg(test)]
794mod tests {
795    use super::*;
796    use scirs2_core::ndarray::array;
797
798    #[test]
799    fn test_rbf_sampler_basic() {
800        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
801
802        let rbf = RBFSampler::new(50).gamma(0.1);
803        let fitted = rbf.fit(&x, &()).unwrap();
804        let x_transformed = fitted.transform(&x).unwrap();
805
806        assert_eq!(x_transformed.shape(), &[3, 50]);
807
808        // Check that values are in reasonable range for cosine function
809        for val in x_transformed.iter() {
810            assert!(val.abs() <= 2.0); // sqrt(2) * 1 is the max possible value
811        }
812    }
813
814    #[test]
815    fn test_rbf_sampler_reproducibility() {
816        let x = array![[1.0, 2.0], [3.0, 4.0],];
817
818        let rbf1 = RBFSampler::new(10).random_state(42);
819        let fitted1 = rbf1.fit(&x, &()).unwrap();
820        let result1 = fitted1.transform(&x).unwrap();
821
822        let rbf2 = RBFSampler::new(10).random_state(42);
823        let fitted2 = rbf2.fit(&x, &()).unwrap();
824        let result2 = fitted2.transform(&x).unwrap();
825
826        // Results should be identical with same random state
827        for (a, b) in result1.iter().zip(result2.iter()) {
828            assert!((a - b).abs() < 1e-10);
829        }
830    }
831
832    #[test]
833    fn test_rbf_sampler_feature_mismatch() {
834        let x_train = array![[1.0, 2.0], [3.0, 4.0],];
835
836        let x_test = array![
837            [1.0, 2.0, 3.0], // Wrong number of features
838        ];
839
840        let rbf = RBFSampler::new(10);
841        let fitted = rbf.fit(&x_train, &()).unwrap();
842        let result = fitted.transform(&x_test);
843
844        assert!(result.is_err());
845    }
846
847    #[test]
848    fn test_rbf_sampler_invalid_gamma() {
849        let x = array![[1.0, 2.0]];
850        let rbf = RBFSampler::new(10).gamma(-1.0);
851        let result = rbf.fit(&x, &());
852        assert!(result.is_err());
853    }
854
855    #[test]
856    fn test_rbf_sampler_zero_components() {
857        let x = array![[1.0, 2.0]];
858        let rbf = RBFSampler::new(0);
859        let result = rbf.fit(&x, &());
860        assert!(result.is_err());
861    }
862
863    #[test]
864    fn test_laplacian_sampler_basic() {
865        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
866
867        let laplacian = LaplacianSampler::new(50).gamma(0.1);
868        let fitted = laplacian.fit(&x, &()).unwrap();
869        let x_transformed = fitted.transform(&x).unwrap();
870
871        assert_eq!(x_transformed.shape(), &[3, 50]);
872
873        // Check that values are in reasonable range for cosine function
874        for val in x_transformed.iter() {
875            assert!(val.abs() <= 2.0); // sqrt(2) * 1 is the max possible value
876        }
877    }
878
879    #[test]
880    fn test_laplacian_sampler_reproducibility() {
881        let x = array![[1.0, 2.0], [3.0, 4.0],];
882
883        let laplacian1 = LaplacianSampler::new(10).random_state(42);
884        let fitted1 = laplacian1.fit(&x, &()).unwrap();
885        let result1 = fitted1.transform(&x).unwrap();
886
887        let laplacian2 = LaplacianSampler::new(10).random_state(42);
888        let fitted2 = laplacian2.fit(&x, &()).unwrap();
889        let result2 = fitted2.transform(&x).unwrap();
890
891        // Results should be identical with same random state
892        for (a, b) in result1.iter().zip(result2.iter()) {
893            assert!((a - b).abs() < 1e-10);
894        }
895    }
896
897    #[test]
898    fn test_laplacian_sampler_feature_mismatch() {
899        let x_train = array![[1.0, 2.0], [3.0, 4.0],];
900
901        let x_test = array![
902            [1.0, 2.0, 3.0], // Wrong number of features
903        ];
904
905        let laplacian = LaplacianSampler::new(10);
906        let fitted = laplacian.fit(&x_train, &()).unwrap();
907        let result = fitted.transform(&x_test);
908
909        assert!(result.is_err());
910    }
911
912    #[test]
913    fn test_laplacian_sampler_invalid_gamma() {
914        let x = array![[1.0, 2.0]];
915        let laplacian = LaplacianSampler::new(10).gamma(-1.0);
916        let result = laplacian.fit(&x, &());
917        assert!(result.is_err());
918    }
919
920    #[test]
921    fn test_laplacian_sampler_zero_components() {
922        let x = array![[1.0, 2.0]];
923        let laplacian = LaplacianSampler::new(0);
924        let result = laplacian.fit(&x, &());
925        assert!(result.is_err());
926    }
927
928    #[test]
929    fn test_polynomial_sampler_basic() {
930        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
931
932        let poly = PolynomialSampler::new(50).degree(3).gamma(1.0).coef0(1.0);
933        let fitted = poly.fit(&x, &()).unwrap();
934        let x_transformed = fitted.transform(&x).unwrap();
935
936        assert_eq!(x_transformed.shape(), &[3, 50]);
937
938        // Check that values are in reasonable range
939        for val in x_transformed.iter() {
940            assert!(val.is_finite());
941        }
942    }
943
944    #[test]
945    fn test_polynomial_sampler_reproducibility() {
946        let x = array![[1.0, 2.0], [3.0, 4.0],];
947
948        let poly1 = PolynomialSampler::new(10).degree(2).random_state(42);
949        let fitted1 = poly1.fit(&x, &()).unwrap();
950        let result1 = fitted1.transform(&x).unwrap();
951
952        let poly2 = PolynomialSampler::new(10).degree(2).random_state(42);
953        let fitted2 = poly2.fit(&x, &()).unwrap();
954        let result2 = fitted2.transform(&x).unwrap();
955
956        // Results should be identical with same random state
957        for (a, b) in result1.iter().zip(result2.iter()) {
958            assert!((a - b).abs() < 1e-10);
959        }
960    }
961
962    #[test]
963    fn test_polynomial_sampler_feature_mismatch() {
964        let x_train = array![[1.0, 2.0], [3.0, 4.0],];
965
966        let x_test = array![
967            [1.0, 2.0, 3.0], // Wrong number of features
968        ];
969
970        let poly = PolynomialSampler::new(10);
971        let fitted = poly.fit(&x_train, &()).unwrap();
972        let result = fitted.transform(&x_test);
973
974        assert!(result.is_err());
975    }
976
977    #[test]
978    fn test_polynomial_sampler_invalid_gamma() {
979        let x = array![[1.0, 2.0]];
980        let poly = PolynomialSampler::new(10).gamma(-1.0);
981        let result = poly.fit(&x, &());
982        assert!(result.is_err());
983    }
984
985    #[test]
986    fn test_polynomial_sampler_zero_degree() {
987        let x = array![[1.0, 2.0]];
988        let poly = PolynomialSampler::new(10).degree(0);
989        let result = poly.fit(&x, &());
990        assert!(result.is_err());
991    }
992
993    #[test]
994    fn test_polynomial_sampler_zero_components() {
995        let x = array![[1.0, 2.0]];
996        let poly = PolynomialSampler::new(0);
997        let result = poly.fit(&x, &());
998        assert!(result.is_err());
999    }
1000
1001    #[test]
1002    fn test_polynomial_sampler_different_degrees() {
1003        let x = array![[1.0, 2.0], [3.0, 4.0],];
1004
1005        // Test degree 1
1006        let poly1 = PolynomialSampler::new(10).degree(1);
1007        let fitted1 = poly1.fit(&x, &()).unwrap();
1008        let result1 = fitted1.transform(&x).unwrap();
1009        assert_eq!(result1.shape(), &[2, 10]);
1010
1011        // Test degree 5
1012        let poly5 = PolynomialSampler::new(10).degree(5);
1013        let fitted5 = poly5.fit(&x, &()).unwrap();
1014        let result5 = fitted5.transform(&x).unwrap();
1015        assert_eq!(result5.shape(), &[2, 10]);
1016    }
1017
1018    #[test]
1019    fn test_arc_cosine_sampler_basic() {
1020        let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0],];
1021
1022        let arc_cosine = ArcCosineSampler::new(50).degree(1);
1023        let fitted = arc_cosine.fit(&x, &()).unwrap();
1024        let x_transformed = fitted.transform(&x).unwrap();
1025
1026        assert_eq!(x_transformed.shape(), &[3, 50]);
1027
1028        // Check that values are non-negative (due to ReLU-like activation)
1029        for val in x_transformed.iter() {
1030            assert!(val >= &0.0);
1031            assert!(val.is_finite());
1032        }
1033    }
1034
1035    #[test]
1036    fn test_arc_cosine_sampler_reproducibility() {
1037        let x = array![[1.0, 2.0], [3.0, 4.0],];
1038
1039        let arc1 = ArcCosineSampler::new(10).degree(1).random_state(42);
1040        let fitted1 = arc1.fit(&x, &()).unwrap();
1041        let result1 = fitted1.transform(&x).unwrap();
1042
1043        let arc2 = ArcCosineSampler::new(10).degree(1).random_state(42);
1044        let fitted2 = arc2.fit(&x, &()).unwrap();
1045        let result2 = fitted2.transform(&x).unwrap();
1046
1047        // Results should be identical with same random state
1048        for (a, b) in result1.iter().zip(result2.iter()) {
1049            assert!((a - b).abs() < 1e-10);
1050        }
1051    }
1052
1053    #[test]
1054    fn test_arc_cosine_sampler_different_degrees() {
1055        let x = array![[1.0, 2.0], [3.0, 4.0],];
1056
1057        // Test degree 0 (ReLU)
1058        let arc0 = ArcCosineSampler::new(10).degree(0);
1059        let fitted0 = arc0.fit(&x, &()).unwrap();
1060        let result0 = fitted0.transform(&x).unwrap();
1061        assert_eq!(result0.shape(), &[2, 10]);
1062
1063        // Test degree 1 (Linear ReLU)
1064        let arc1 = ArcCosineSampler::new(10).degree(1);
1065        let fitted1 = arc1.fit(&x, &()).unwrap();
1066        let result1 = fitted1.transform(&x).unwrap();
1067        assert_eq!(result1.shape(), &[2, 10]);
1068
1069        // Test degree 2 (Quadratic ReLU)
1070        let arc2 = ArcCosineSampler::new(10).degree(2);
1071        let fitted2 = arc2.fit(&x, &()).unwrap();
1072        let result2 = fitted2.transform(&x).unwrap();
1073        assert_eq!(result2.shape(), &[2, 10]);
1074    }
1075
1076    #[test]
1077    fn test_arc_cosine_sampler_feature_mismatch() {
1078        let x_train = array![[1.0, 2.0], [3.0, 4.0],];
1079
1080        let x_test = array![
1081            [1.0, 2.0, 3.0], // Wrong number of features
1082        ];
1083
1084        let arc_cosine = ArcCosineSampler::new(10);
1085        let fitted = arc_cosine.fit(&x_train, &()).unwrap();
1086        let result = fitted.transform(&x_test);
1087
1088        assert!(result.is_err());
1089    }
1090
1091    #[test]
1092    fn test_arc_cosine_sampler_invalid_degree() {
1093        let x = array![[1.0, 2.0]];
1094        let arc_cosine = ArcCosineSampler::new(10).degree(3);
1095        let result = arc_cosine.fit(&x, &());
1096        assert!(result.is_err());
1097    }
1098
1099    #[test]
1100    fn test_arc_cosine_sampler_zero_components() {
1101        let x = array![[1.0, 2.0]];
1102        let arc_cosine = ArcCosineSampler::new(0);
1103        let result = arc_cosine.fit(&x, &());
1104        assert!(result.is_err());
1105    }
1106}