Skip to main content

sklears_feature_extraction/
interaction.rs

1//! Interaction and kernel-based feature generation
2//!
3//! This module provides transformers for generating interaction features
4//! and kernel-based approximations for non-linear feature transformation.
5
6use crate::*;
7// use rayon::prelude::*;
8use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
9use sklears_core::error::Result as SklResult;
10use sklears_core::prelude::{SklearsError, Transform};
11
12/// Radial Basis Functions Generator
13///
14/// Generate radial basis functions (RBFs) for non-linear feature transformation.
15/// Supports various RBF types including Gaussian, Multiquadric, and Inverse Multiquadric.
16///
17/// # Parameters
18///
19/// * `n_centers` - Number of RBF centers
20/// * `rbf_type` - Type of radial basis function
21/// * `centers` - RBF center positions (if None, centers are determined from data)
22/// * `shape_parameter` - Shape parameter for RBFs (affects function width)
23/// * `normalize` - Whether to normalize the output features
24///
25/// # Examples
26///
27/// ```
28/// use sklears_feature_extraction::interaction::{RadialBasisFunctions, RBFType};
29/// use scirs2_core::ndarray::array;
30///
31/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
32/// let rbf = RadialBasisFunctions::new()
33///     .n_centers(3)
34///     .rbf_type(RBFType::Gaussian)
35///     .shape_parameter(1.0);
36///
37/// let features = rbf.transform(&X.view()).expect("valid input produces transform output");
38/// ```
39#[derive(Debug, Clone)]
40pub struct RadialBasisFunctions {
41    n_centers: usize,
42    rbf_type: RBFType,
43    centers: Option<Array2<Float>>,
44    shape_parameter: Float,
45    normalize: bool,
46}
47
48#[derive(Debug, Clone, Copy)]
49pub enum RBFType {
50    /// Gaussian RBF: exp(-epsilon * r^2)
51    Gaussian,
52    /// Multiquadric RBF: sqrt(1 + epsilon * r^2)
53    Multiquadric,
54    /// Inverse Multiquadric RBF: 1 / sqrt(1 + epsilon * r^2)
55    InverseMultiquadric,
56    /// Thin Plate Spline RBF: r^2 * log(r)
57    ThinPlateSpline,
58    /// Linear RBF: r
59    Linear,
60    /// Cubic RBF: r^3
61    Cubic,
62    /// Quintic RBF: r^5
63    Quintic,
64}
65
66#[allow(non_snake_case)] // X/Y/K/A/Av/W_inv/C follow sklearn/math convention for feature and kernel matrices
67impl RadialBasisFunctions {
68    /// Create a new RadialBasisFunctions transformer
69    pub fn new() -> Self {
70        Self {
71            n_centers: 10,
72            rbf_type: RBFType::Gaussian,
73            centers: None,
74            shape_parameter: 1.0,
75            normalize: false,
76        }
77    }
78
79    /// Set the number of RBF centers
80    pub fn n_centers(mut self, n_centers: usize) -> Self {
81        self.n_centers = n_centers;
82        self
83    }
84
85    /// Set the type of radial basis function
86    pub fn rbf_type(mut self, rbf_type: RBFType) -> Self {
87        self.rbf_type = rbf_type;
88        self
89    }
90
91    /// Set custom RBF centers
92    pub fn centers(mut self, centers: Array2<Float>) -> Self {
93        self.centers = Some(centers);
94        self
95    }
96
97    /// Set the shape parameter (epsilon)
98    pub fn shape_parameter(mut self, shape_parameter: Float) -> Self {
99        self.shape_parameter = shape_parameter;
100        self
101    }
102
103    /// Set whether to normalize output features
104    pub fn normalize(mut self, normalize: bool) -> Self {
105        self.normalize = normalize;
106        self
107    }
108
109    /// Transform input data using radial basis functions
110    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
111        let (n_samples, _n_features) = X.dim();
112
113        // Generate centers if not provided
114        let centers = match &self.centers {
115            Some(centers) => centers.clone(),
116            None => self.generate_centers(X)?,
117        };
118
119        let n_centers = centers.nrows();
120        let mut rbf_features = Array2::zeros((n_samples, n_centers));
121
122        // Compute RBF features for each sample
123        for (sample_idx, sample) in X.axis_iter(Axis(0)).enumerate() {
124            for (center_idx, center) in centers.axis_iter(Axis(0)).enumerate() {
125                let distance = self.compute_distance(&sample, &center);
126                let rbf_value = self.evaluate_rbf(distance);
127                rbf_features[[sample_idx, center_idx]] = rbf_value;
128            }
129        }
130
131        // Apply normalization if requested
132        if self.normalize {
133            self.normalize_features(&mut rbf_features);
134        }
135
136        Ok(rbf_features)
137    }
138
139    fn generate_centers(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
140        let (n_samples, n_features) = X.dim();
141
142        if self.n_centers > n_samples {
143            return Err(SklearsError::InvalidInput(
144                "Number of centers cannot exceed number of samples".to_string(),
145            ));
146        }
147
148        // Use k-means style center selection (simplified)
149        let mut centers = Array2::zeros((self.n_centers, n_features));
150
151        // Initialize first center randomly (use first sample)
152        if n_samples > 0 {
153            for j in 0..n_features {
154                centers[[0, j]] = X[[0, j]];
155            }
156        }
157
158        // Select remaining centers using farthest-first traversal
159        for center_idx in 1..self.n_centers {
160            let mut best_sample_idx = 0;
161            let mut max_min_distance = 0.0;
162
163            for sample_idx in 0..n_samples {
164                let sample = X.row(sample_idx);
165                let mut min_distance = Float::INFINITY;
166
167                // Find minimum distance to existing centers
168                for existing_center_idx in 0..center_idx {
169                    let center = centers.row(existing_center_idx);
170                    let distance = self.compute_distance(&sample, &center);
171                    min_distance = min_distance.min(distance);
172                }
173
174                // Select sample with maximum minimum distance
175                if min_distance > max_min_distance {
176                    max_min_distance = min_distance;
177                    best_sample_idx = sample_idx;
178                }
179            }
180
181            // Set the new center
182            for j in 0..n_features {
183                centers[[center_idx, j]] = X[[best_sample_idx, j]];
184            }
185        }
186
187        Ok(centers)
188    }
189
190    fn compute_distance(&self, point1: &ArrayView1<Float>, point2: &ArrayView1<Float>) -> Float {
191        // Euclidean distance
192        let mut sum_squared = 0.0;
193        for (x1, x2) in point1.iter().zip(point2.iter()) {
194            sum_squared += (x1 - x2).powi(2);
195        }
196        sum_squared.sqrt()
197    }
198
199    fn evaluate_rbf(&self, distance: Float) -> Float {
200        let r = distance;
201        let epsilon = self.shape_parameter;
202
203        match self.rbf_type {
204            RBFType::Gaussian => (-epsilon * r * r).exp(),
205            RBFType::Multiquadric => (1.0 + epsilon * r * r).sqrt(),
206            RBFType::InverseMultiquadric => 1.0 / (1.0 + epsilon * r * r).sqrt(),
207            RBFType::ThinPlateSpline => {
208                if r < 1e-10 {
209                    0.0
210                } else {
211                    r * r * r.ln()
212                }
213            }
214            RBFType::Linear => r,
215            RBFType::Cubic => r.powi(3),
216            RBFType::Quintic => r.powi(5),
217        }
218    }
219
220    fn normalize_features(&self, features: &mut Array2<Float>) {
221        let (n_samples, n_features) = features.dim();
222
223        for j in 0..n_features {
224            let mut column_sum = 0.0;
225            for i in 0..n_samples {
226                column_sum += features[[i, j]];
227            }
228
229            if column_sum > 1e-10 {
230                for i in 0..n_samples {
231                    features[[i, j]] /= column_sum;
232                }
233            }
234        }
235    }
236}
237
238impl Default for RadialBasisFunctions {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
245impl Transform<ArrayView2<'_, Float>, Array2<Float>> for RadialBasisFunctions {
246    fn transform(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
247        self.transform(X)
248    }
249}
250
251/// RBF Sampler for Random Fourier Features
252///
253/// Approximates the feature map of an RBF kernel by Monte Carlo approximation
254/// of its Fourier transform. This allows the use of linear algorithms with
255/// kernel-like performance.
256///
257/// # Parameters
258///
259/// * `gamma` - Parameter of the RBF kernel
260/// * `n_components` - Number of Monte Carlo samples (feature dimension)
261/// * `random_state` - Random seed for reproducibility
262///
263/// # Mathematical Background
264///
265/// For an RBF kernel K(x,y) = exp(-gamma||x-y||²), the Fourier transform is:
266/// φ(x) = sqrt(2/n_components) * cos(wx + b)
267/// where w is drawn from N(0, 2*gamma*I) and b is drawn from Uniform(0, 2π)
268///
269/// # Examples
270///
271/// ```
272/// use sklears_feature_extraction::interaction::RBFSampler;
273/// use scirs2_core::ndarray::array;
274///
275/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
276/// let mut rbf_sampler = RBFSampler::new()
277///     .gamma(1.0)
278///     .n_components(100)
279///     .random_state(Some(42));
280///
281/// let features = rbf_sampler.fit_transform(&X.view()).expect("valid input produces fit_transform output");
282/// ```
283#[derive(Debug, Clone)]
284pub struct RBFSampler {
285    gamma: Float,
286    n_components: usize,
287    random_state: Option<u64>,
288    random_weights: Option<Array2<Float>>,
289    random_offset: Option<Array1<Float>>,
290}
291
292#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
293impl RBFSampler {
294    /// Create a new RBFSampler
295    pub fn new() -> Self {
296        Self {
297            gamma: 1.0,
298            n_components: 100,
299            random_state: None,
300            random_weights: None,
301            random_offset: None,
302        }
303    }
304
305    /// Set the gamma parameter
306    pub fn gamma(mut self, gamma: Float) -> Self {
307        self.gamma = gamma;
308        self
309    }
310
311    /// Set the number of components
312    pub fn n_components(mut self, n_components: usize) -> Self {
313        self.n_components = n_components;
314        self
315    }
316
317    /// Set the random state
318    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
319        self.random_state = random_state;
320        self
321    }
322
323    /// Fit the sampler to the data (generates random weights)
324    pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<&mut Self> {
325        let (_, n_features) = X.dim();
326
327        // Generate random weights from N(0, 2*gamma*I)
328        let variance = 2.0 * self.gamma;
329        let mut random_weights = Array2::zeros((self.n_components, n_features));
330        let mut random_offset = Array1::zeros(self.n_components);
331
332        // Simple pseudo-random number generation
333        let mut seed = self.random_state.unwrap_or(42);
334
335        for i in 0..self.n_components {
336            for j in 0..n_features {
337                // Box-Muller transform for normal distribution
338                let u1 = (seed % 1000) as Float / 1000.0;
339                seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
340                let u2 = (seed % 1000) as Float / 1000.0;
341                seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
342
343                let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
344                random_weights[[i, j]] = z * variance.sqrt();
345            }
346
347            // Random offset from Uniform(0, 2π)
348            let u = (seed % 1000) as Float / 1000.0;
349            seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
350            random_offset[i] = 2.0 * std::f64::consts::PI * u;
351        }
352
353        self.random_weights = Some(random_weights);
354        self.random_offset = Some(random_offset);
355
356        Ok(self)
357    }
358
359    /// Transform the data using the fitted random features
360    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
361        let weights = self.random_weights.as_ref().ok_or_else(|| {
362            SklearsError::InvalidInput("RBFSampler must be fitted before transform".to_string())
363        })?;
364        let offset = self.random_offset.as_ref().ok_or_else(|| {
365            SklearsError::InvalidInput("RBFSampler must be fitted before transform".to_string())
366        })?;
367
368        let (n_samples, n_features) = X.dim();
369        if weights.ncols() != n_features {
370            return Err(SklearsError::InvalidInput(
371                "Feature dimension mismatch".to_string(),
372            ));
373        }
374
375        let mut features = Array2::zeros((n_samples, self.n_components));
376        let normalization = (2.0 / self.n_components as Float).sqrt();
377
378        for i in 0..n_samples {
379            let sample = X.row(i);
380            for j in 0..self.n_components {
381                let weight_row = weights.row(j);
382                let projection = sample.dot(&weight_row) + offset[j];
383                features[[i, j]] = normalization * projection.cos();
384            }
385        }
386
387        Ok(features)
388    }
389
390    /// Fit and transform in one step
391    pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
392        self.fit(X)?;
393        self.transform(X)
394    }
395}
396
397impl Default for RBFSampler {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403/// Nyström Method for Kernel Approximation
404///
405/// Approximates kernel maps using a subset of training data as landmarks.
406/// The Nyström method provides a low-rank approximation to the kernel matrix.
407///
408/// # Parameters
409///
410/// * `kernel` - Type of kernel to approximate
411/// * `n_components` - Number of components (rank of approximation)
412/// * `gamma` - Parameter for RBF kernel
413/// * `degree` - Degree for polynomial kernel
414/// * `coef0` - Independent term for polynomial kernel
415///
416/// # Mathematical Background
417///
418/// Given a kernel matrix K, the Nyström method approximates it as:
419/// K ≈ C W^+ C^T
420/// where C is the kernel matrix between all points and landmarks,
421/// and W is the kernel matrix between landmarks only.
422///
423/// # Examples
424///
425/// ```
426/// use sklears_feature_extraction::interaction::{Nystroem, NystromKernel};
427/// use scirs2_core::ndarray::array;
428///
429/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
430/// let nystroem = Nystroem::new()
431///     .kernel(NystromKernel::RBF)
432///     .gamma(1.0)
433///     .n_components(50);
434///
435/// let mut fitted = nystroem;
436/// let features = fitted.fit_transform(&X.view()).expect("valid input produces fit_transform output");
437/// ```
438#[derive(Debug, Clone)]
439pub struct Nystroem {
440    kernel: NystromKernel,
441    n_components: usize,
442    gamma: Float,
443    degree: usize,
444    coef0: Float,
445    random_state: Option<u64>,
446    landmarks: Option<Array2<Float>>,
447    normalization_matrix: Option<Array2<Float>>,
448}
449
450#[derive(Debug, Clone, Copy)]
451pub enum NystromKernel {
452    /// RBF kernel: exp(-gamma * ||x - y||²)
453    RBF,
454    /// Polynomial kernel: (gamma * <x, y> + coef0)^degree
455    Polynomial,
456    /// Linear kernel: <x, y>
457    Linear,
458    /// Sigmoid kernel: tanh(gamma * <x, y> + coef0)
459    Sigmoid,
460}
461
462#[allow(non_snake_case)] // X/Y/K/A/Av/W_inv/C follow sklearn/math convention for feature and kernel matrices
463impl Nystroem {
464    /// Create a new Nyström approximator
465    pub fn new() -> Self {
466        Self {
467            kernel: NystromKernel::RBF,
468            n_components: 100,
469            gamma: 1.0,
470            degree: 3,
471            coef0: 1.0,
472            random_state: None,
473            landmarks: None,
474            normalization_matrix: None,
475        }
476    }
477
478    /// Set the kernel type
479    pub fn kernel(mut self, kernel: NystromKernel) -> Self {
480        self.kernel = kernel;
481        self
482    }
483
484    /// Set the number of components
485    pub fn n_components(mut self, n_components: usize) -> Self {
486        self.n_components = n_components;
487        self
488    }
489
490    /// Set the gamma parameter
491    pub fn gamma(mut self, gamma: Float) -> Self {
492        self.gamma = gamma;
493        self
494    }
495
496    /// Set the degree for polynomial kernel
497    pub fn degree(mut self, degree: usize) -> Self {
498        self.degree = degree;
499        self
500    }
501
502    /// Set the coef0 parameter
503    pub fn coef0(mut self, coef0: Float) -> Self {
504        self.coef0 = coef0;
505        self
506    }
507
508    /// Set the random state
509    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
510        self.random_state = random_state;
511        self
512    }
513
514    /// Fit the Nyström approximation
515    #[allow(non_snake_case)]
516    pub fn fit(&mut self, X: &ArrayView2<Float>) -> SklResult<&mut Self> {
517        let (n_samples, n_features) = X.dim();
518        let n_landmarks = self.n_components.min(n_samples);
519
520        // Select landmark points (random subset)
521        let landmark_indices = self.select_landmarks(n_samples, n_landmarks)?;
522        let mut landmarks = Array2::zeros((n_landmarks, n_features));
523
524        for (i, &idx) in landmark_indices.iter().enumerate() {
525            landmarks.row_mut(i).assign(&X.row(idx));
526        }
527
528        // Compute W matrix (kernel between landmarks)
529        let W = self.compute_kernel_matrix(&landmarks.view(), &landmarks.view())?;
530
531        // Compute pseudo-inverse of W (simplified using regularization)
532        let W_inv = self.compute_regularized_inverse(&W)?;
533
534        self.landmarks = Some(landmarks);
535        self.normalization_matrix = Some(W_inv);
536
537        Ok(self)
538    }
539
540    /// Transform data using the fitted Nyström approximation
541    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
542        let landmarks = self.landmarks.as_ref().ok_or_else(|| {
543            SklearsError::InvalidInput("Nystroem must be fitted before transform".to_string())
544        })?;
545        let W_inv = self.normalization_matrix.as_ref().ok_or_else(|| {
546            SklearsError::InvalidInput("Nystroem must be fitted before transform".to_string())
547        })?;
548
549        // Compute C matrix (kernel between X and landmarks)
550        let C = self.compute_kernel_matrix(X, &landmarks.view())?;
551
552        // Apply Nyström approximation: features = C * W_inv^(1/2)
553        let features = C.dot(W_inv);
554
555        Ok(features)
556    }
557
558    /// Fit and transform in one step
559    pub fn fit_transform(&mut self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
560        self.fit(X)?;
561        self.transform(X)
562    }
563
564    fn select_landmarks(&self, n_samples: usize, n_landmarks: usize) -> SklResult<Vec<usize>> {
565        let mut landmarks = Vec::new();
566        let mut seed = self.random_state.unwrap_or(42);
567
568        // Simple random selection
569        for _ in 0..n_landmarks {
570            let idx = (seed % n_samples as u64) as usize;
571            landmarks.push(idx);
572            seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
573        }
574
575        Ok(landmarks)
576    }
577
578    fn compute_kernel_matrix(
579        &self,
580        X: &ArrayView2<Float>,
581        Y: &ArrayView2<Float>,
582    ) -> SklResult<Array2<Float>> {
583        let (n_x, _) = X.dim();
584        let (n_y, _) = Y.dim();
585        let mut K = Array2::zeros((n_x, n_y));
586
587        for i in 0..n_x {
588            for j in 0..n_y {
589                let x_i = X.row(i);
590                let y_j = Y.row(j);
591                K[[i, j]] = self.kernel_function(&x_i, &y_j);
592            }
593        }
594
595        Ok(K)
596    }
597
598    fn kernel_function(&self, x: &ArrayView1<Float>, y: &ArrayView1<Float>) -> Float {
599        match self.kernel {
600            NystromKernel::RBF => {
601                let mut squared_distance = 0.0;
602                for (xi, yi) in x.iter().zip(y.iter()) {
603                    squared_distance += (xi - yi).powi(2);
604                }
605                (-self.gamma * squared_distance).exp()
606            }
607            NystromKernel::Linear => x.dot(y),
608            NystromKernel::Polynomial => {
609                let dot_product = x.dot(y);
610                (self.gamma * dot_product + self.coef0).powf(self.degree as Float)
611            }
612            NystromKernel::Sigmoid => {
613                let dot_product = x.dot(y);
614                (self.gamma * dot_product + self.coef0).tanh()
615            }
616        }
617    }
618
619    fn compute_regularized_inverse(&self, matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
620        let n = matrix.nrows();
621        if n != matrix.ncols() {
622            return Err(SklearsError::InvalidInput(
623                "Matrix must be square for inversion".to_string(),
624            ));
625        }
626
627        // Add regularization to diagonal
628        let regularization = 1e-12;
629        let mut A = matrix.clone();
630        for i in 0..n {
631            A[[i, i]] += regularization;
632        }
633
634        // Simplified matrix inversion using LU decomposition approximation
635        // In practice, you would use a proper linear algebra library
636        self.approximate_matrix_sqrt(&A)
637    }
638
639    fn approximate_matrix_sqrt(&self, matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
640        let n = matrix.nrows();
641
642        // Simplified eigendecomposition for matrix square root
643        // Using power iteration to find dominant eigenvalues/eigenvectors
644        let max_components = n.min(self.n_components);
645        let mut result = Array2::zeros((n, max_components));
646
647        for comp in 0..max_components {
648            let mut v = Array1::ones(n);
649
650            // Normalize
651            let norm = v.iter().map(|x| x * x).sum::<Float>().sqrt();
652            if norm > 0.0 {
653                v /= norm;
654            }
655
656            // Power iteration
657            for _iter in 0..20 {
658                let mut Av = Array1::zeros(n);
659                for i in 0..n {
660                    for j in 0..n {
661                        Av[i] += matrix[[i, j]] * v[j];
662                    }
663                }
664
665                // Orthogonalize against previous components
666                for prev_comp in 0..comp {
667                    let prev_vec = result.column(prev_comp);
668                    let projection = Av.dot(&prev_vec);
669                    for k in 0..n {
670                        Av[k] -= projection * prev_vec[k];
671                    }
672                }
673
674                let norm = Av.iter().map(|x| x * x).sum::<Float>().sqrt();
675                if norm < 1e-10 {
676                    break;
677                }
678                v = Av / norm;
679            }
680
681            // Compute eigenvalue
682            let mut Av = Array1::zeros(n);
683            for i in 0..n {
684                for j in 0..n {
685                    Av[i] += matrix[[i, j]] * v[j];
686                }
687            }
688            let eigenvalue = v.dot(&Av).max(1e-10);
689
690            // Store sqrt(eigenvalue) * eigenvector
691            let sqrt_eigenvalue = eigenvalue.sqrt();
692            for i in 0..n {
693                result[[i, comp]] = sqrt_eigenvalue * v[i];
694            }
695        }
696
697        Ok(result)
698    }
699}
700
701impl Default for Nystroem {
702    fn default() -> Self {
703        Self::new()
704    }
705}
706
707/// Additive Chi-Squared Sampler
708///
709/// Approximate feature map for additive chi-squared kernel using random sampling.
710/// The additive chi-squared kernel is defined as:
711/// k(x, y) = exp(-gamma * sum_i (x_i - y_i)² / (x_i + y_i))
712///
713/// # Parameters
714///
715/// * `sample_steps` - Number of sampling steps
716/// * `sample_interval` - Interval for sampling
717/// * `n_components` - Number of components to generate
718///
719/// # Examples
720///
721/// ```
722/// use sklears_feature_extraction::interaction::AdditiveChi2Sampler;
723/// use scirs2_core::ndarray::array;
724///
725/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
726/// let sampler = AdditiveChi2Sampler::new()
727///     .sample_steps(2)
728///     .sample_interval(0.5);
729///
730/// let features = sampler.transform(&X.view()).expect("valid input produces transform output");
731/// ```
732#[derive(Debug, Clone)]
733pub struct AdditiveChi2Sampler {
734    sample_steps: usize,
735    sample_interval: Float,
736}
737
738impl AdditiveChi2Sampler {
739    /// Create a new AdditiveChi2Sampler
740    pub fn new() -> Self {
741        Self {
742            sample_steps: 2,
743            sample_interval: 1.0,
744        }
745    }
746
747    /// Set the number of sampling steps
748    pub fn sample_steps(mut self, sample_steps: usize) -> Self {
749        self.sample_steps = sample_steps;
750        self
751    }
752
753    /// Set the sampling interval
754    pub fn sample_interval(mut self, sample_interval: Float) -> Self {
755        self.sample_interval = sample_interval;
756        self
757    }
758
759    /// Transform data using additive chi-squared features
760    #[allow(non_snake_case)]
761    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
762        let (n_samples, n_features) = X.dim();
763        let n_components = 2 * n_features * self.sample_steps;
764        let mut features = Array2::zeros((n_samples, n_components));
765
766        for (sample_idx, sample) in X.axis_iter(Axis(0)).enumerate() {
767            let mut feature_idx = 0;
768
769            for &x_i in sample.iter() {
770                for step in 0..self.sample_steps {
771                    let omega = (step + 1) as Float * self.sample_interval;
772
773                    if x_i > 0.0 {
774                        let sqrt_x = x_i.sqrt();
775                        let cos_val = (2.0 * omega * sqrt_x).cos();
776                        let sin_val = (2.0 * omega * sqrt_x).sin();
777
778                        features[[sample_idx, feature_idx]] =
779                            (2.0 / std::f64::consts::PI).sqrt() * cos_val;
780                        features[[sample_idx, feature_idx + 1]] =
781                            (2.0 / std::f64::consts::PI).sqrt() * sin_val;
782                    }
783
784                    feature_idx += 2;
785                }
786            }
787        }
788
789        Ok(features)
790    }
791
792    /// Get the number of output features
793    pub fn get_n_output_features(&self, n_input_features: usize) -> usize {
794        2 * n_input_features * self.sample_steps
795    }
796}
797
798impl Default for AdditiveChi2Sampler {
799    fn default() -> Self {
800        Self::new()
801    }
802}
803
804#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
805impl Transform<ArrayView2<'_, Float>, Array2<Float>> for AdditiveChi2Sampler {
806    fn transform(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
807        self.transform(X)
808    }
809}