Skip to main content

sklears_feature_extraction/
basic_features.rs

1//! Basic feature engineering utilities
2//!
3//! This module contains core feature engineering transformers including:
4//! - Polynomial features for capturing non-linear relationships
5//! - Spline basis functions for smooth curve fitting
6//! - Radial basis functions for local non-linear transformations
7
8use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2, Axis};
9// use scirs2_core::numeric::Float as NumTraitsFloat;
10use sklears_core::{
11    error::Result as SklResult,
12    prelude::{SklearsError, Transform},
13    types::Float,
14};
15
16/// Polynomial Feature Generator
17///
18/// Generate polynomial and interaction features from input features.
19/// This transformer can be used to capture non-linear relationships
20/// by creating polynomial combinations of the original features.
21///
22/// # Parameters
23///
24/// * `degree` - Maximum degree of polynomial features
25/// * `interaction_only` - If true, only interaction features are produced
26/// * `include_bias` - If true, include a bias column of ones
27///
28/// # Examples
29///
30/// ```
31/// use sklears_feature_extraction::basic_features::PolynomialFeatures;
32/// use sklears_core::traits::{Transform, Fit};
33/// use scirs2_core::ndarray::array;
34///
35/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
36/// let poly = PolynomialFeatures::new()
37///     .degree(2)
38///     .include_bias(true);
39///
40/// let features = poly.transform(&X.view()).expect("valid input produces transform output");
41/// ```
42#[derive(Debug, Clone)]
43pub struct PolynomialFeatures {
44    degree: usize,
45    interaction_only: bool,
46    include_bias: bool,
47}
48
49#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
50impl PolynomialFeatures {
51    /// Create a new PolynomialFeatures transformer
52    pub fn new() -> Self {
53        Self {
54            degree: 2,
55            interaction_only: false,
56            include_bias: true,
57        }
58    }
59
60    /// Set the maximum degree of polynomial features
61    pub fn degree(mut self, degree: usize) -> Self {
62        self.degree = degree;
63        self
64    }
65
66    /// Set whether to produce only interaction features
67    pub fn interaction_only(mut self, interaction_only: bool) -> Self {
68        self.interaction_only = interaction_only;
69        self
70    }
71
72    /// Set whether to include a bias column
73    pub fn include_bias(mut self, include_bias: bool) -> Self {
74        self.include_bias = include_bias;
75        self
76    }
77
78    /// Transform input features to polynomial features
79    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
80        let (n_samples, n_features) = X.dim();
81
82        if self.degree == 0 && !self.include_bias {
83            return Err(SklearsError::InvalidInput(
84                "At least one of degree > 0 or include_bias=true must be specified".to_string(),
85            ));
86        }
87
88        // Generate all polynomial combinations
89        let combinations = self.generate_combinations(n_features)?;
90        let n_output_features = combinations.len();
91
92        let mut result = Array2::<Float>::zeros((n_samples, n_output_features));
93
94        for (sample_idx, sample) in X.axis_iter(Axis(0)).enumerate() {
95            for (feat_idx, combination) in combinations.iter().enumerate() {
96                let mut value = 1.0;
97
98                if combination.is_empty() {
99                    // Bias term
100                    value = 1.0;
101                } else {
102                    for &feature_idx in combination {
103                        value *= sample[feature_idx];
104                    }
105                }
106
107                result[[sample_idx, feat_idx]] = value;
108            }
109        }
110
111        Ok(result)
112    }
113
114    /// Get feature names for the polynomial features
115    pub fn get_feature_names(&self, input_features: Option<&[String]>) -> SklResult<Vec<String>> {
116        let n_features = match input_features {
117            Some(names) => names.len(),
118            None => {
119                return Err(SklearsError::InvalidInput(
120                    "Input feature names must be provided".to_string(),
121                ))
122            }
123        };
124
125        let combinations = self.generate_combinations(n_features)?;
126        let mut feature_names = Vec::new();
127
128        for combination in combinations {
129            if combination.is_empty() {
130                // Bias term
131                feature_names.push("1".to_string());
132            } else if combination.len() == 1 {
133                // Single feature
134                feature_names.push(
135                    input_features.expect("operation should succeed")[combination[0]].clone(),
136                );
137            } else {
138                // Interaction features
139                let mut name = String::new();
140                for (i, &feature_idx) in combination.iter().enumerate() {
141                    if i > 0 {
142                        name.push(' ');
143                    }
144                    name.push_str(&input_features.expect("operation should succeed")[feature_idx]);
145                }
146                feature_names.push(name);
147            }
148        }
149
150        Ok(feature_names)
151    }
152
153    /// Get the number of output features
154    pub fn get_n_output_features(&self, n_input_features: usize) -> SklResult<usize> {
155        let combinations = self.generate_combinations(n_input_features)?;
156        Ok(combinations.len())
157    }
158
159    fn generate_combinations(&self, n_features: usize) -> SklResult<Vec<Vec<usize>>> {
160        let mut combinations = Vec::new();
161
162        // Add bias term if requested
163        if self.include_bias {
164            combinations.push(Vec::new());
165        }
166
167        // Generate combinations for each degree
168        for degree in 1..=self.degree {
169            let degree_combinations = if self.interaction_only {
170                self.generate_interaction_combinations(n_features, degree)?
171            } else {
172                self.generate_degree_combinations(n_features, degree)?
173            };
174            combinations.extend(degree_combinations);
175        }
176
177        Ok(combinations)
178    }
179
180    fn generate_interaction_combinations(
181        &self,
182        n_features: usize,
183        degree: usize,
184    ) -> SklResult<Vec<Vec<usize>>> {
185        let mut combinations = Vec::new();
186        let mut current = vec![0; degree];
187
188        self.generate_interaction_recursive(
189            &mut combinations,
190            &mut current,
191            n_features,
192            degree,
193            0,
194            0,
195        );
196
197        Ok(combinations)
198    }
199
200    fn generate_interaction_recursive(
201        &self,
202        combinations: &mut Vec<Vec<usize>>,
203        current: &mut [usize],
204        n_features: usize,
205        degree: usize,
206        pos: usize,
207        start: usize,
208    ) {
209        if pos == degree {
210            // All different features (interaction only)
211            let mut unique_features = current.to_vec();
212            unique_features.sort_unstable();
213            unique_features.dedup();
214
215            if unique_features.len() == degree {
216                combinations.push(current.to_vec());
217            }
218            return;
219        }
220
221        for i in start..n_features {
222            current[pos] = i;
223            self.generate_interaction_recursive(
224                combinations,
225                current,
226                n_features,
227                degree,
228                pos + 1,
229                i + 1,
230            );
231        }
232    }
233
234    fn generate_degree_combinations(
235        &self,
236        n_features: usize,
237        degree: usize,
238    ) -> SklResult<Vec<Vec<usize>>> {
239        let mut combinations = Vec::new();
240        let mut current = vec![0; degree];
241
242        self.generate_degree_recursive(&mut combinations, &mut current, n_features, degree, 0, 0);
243
244        Ok(combinations)
245    }
246
247    fn generate_degree_recursive(
248        &self,
249        combinations: &mut Vec<Vec<usize>>,
250        current: &mut [usize],
251        n_features: usize,
252        degree: usize,
253        pos: usize,
254        start: usize,
255    ) {
256        if pos == degree {
257            combinations.push(current.to_vec());
258            return;
259        }
260
261        for i in start..n_features {
262            current[pos] = i;
263            self.generate_degree_recursive(combinations, current, n_features, degree, pos + 1, i);
264        }
265    }
266}
267
268impl Default for PolynomialFeatures {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
275impl Transform<ArrayView2<'_, Float>, Array2<Float>> for PolynomialFeatures {
276    fn transform(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
277        self.transform(X)
278    }
279}
280
281/// Spline Basis Functions Generator
282///
283/// Generate spline basis functions for non-linear feature transformation.
284/// Supports B-splines and Natural Cubic Splines for smooth curve fitting
285/// and feature engineering applications.
286///
287/// # Parameters
288///
289/// * `n_splines` - Number of spline basis functions to generate
290/// * `degree` - Degree of the spline (1 for linear, 2 for quadratic, 3 for cubic)
291/// * `spline_type` - Type of spline (B-spline or Natural Cubic)
292/// * `knots` - Knot positions (if None, uniformly distributed knots are used)
293/// * `extrapolation` - How to handle values outside the knot range
294///
295/// # Examples
296///
297/// ```
298/// use sklears_feature_extraction::basic_features::{SplineBasisFunctions, SplineType};
299/// use scirs2_core::ndarray::array;
300///
301/// let X = array![[0.0], [0.25], [0.5], [0.75], [1.0]];
302/// let spline = SplineBasisFunctions::new()
303///     .n_splines(5)
304///     .degree(3)
305///     .spline_type(SplineType::BSpline);
306///
307/// let features = spline.transform(&X.view()).expect("valid input produces transform output");
308/// ```
309#[derive(Debug, Clone)]
310pub struct SplineBasisFunctions {
311    n_splines: usize,
312    degree: usize,
313    spline_type: SplineType,
314    knots: Option<Vec<Float>>,
315    extrapolation: ExtrapolationMode,
316}
317
318#[derive(Debug, Clone, Copy)]
319pub enum SplineType {
320    /// B-spline basis functions
321    BSpline,
322    /// Natural cubic spline basis functions
323    NaturalCubic,
324    /// Truncated power basis functions
325    TruncatedPower,
326}
327
328#[derive(Debug, Clone, Copy)]
329pub enum ExtrapolationMode {
330    /// Constant extrapolation (edge values)
331    Constant,
332    /// Linear extrapolation
333    Linear,
334    /// Zero outside the knot range
335    Zero,
336    /// Raise error for out-of-bounds values
337    Error,
338}
339
340#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
341impl SplineBasisFunctions {
342    /// Create a new SplineBasisFunctions transformer
343    pub fn new() -> Self {
344        Self {
345            n_splines: 5,
346            degree: 3,
347            spline_type: SplineType::BSpline,
348            knots: None,
349            extrapolation: ExtrapolationMode::Constant,
350        }
351    }
352
353    /// Set the number of spline basis functions
354    pub fn n_splines(mut self, n_splines: usize) -> Self {
355        self.n_splines = n_splines;
356        self
357    }
358
359    /// Set the degree of the splines
360    pub fn degree(mut self, degree: usize) -> Self {
361        self.degree = degree;
362        self
363    }
364
365    /// Set the type of spline
366    pub fn spline_type(mut self, spline_type: SplineType) -> Self {
367        self.spline_type = spline_type;
368        self
369    }
370
371    /// Set custom knot positions
372    pub fn knots(mut self, knots: Vec<Float>) -> Self {
373        self.knots = Some(knots);
374        self
375    }
376
377    /// Set extrapolation mode
378    pub fn extrapolation(mut self, extrapolation: ExtrapolationMode) -> Self {
379        self.extrapolation = extrapolation;
380        self
381    }
382
383    /// Transform input data using spline basis functions
384    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
385        if X.ncols() != 1 {
386            return Err(SklearsError::InvalidInput(
387                "SplineBasisFunctions currently supports only 1D input".to_string(),
388            ));
389        }
390
391        let x_values: Vec<Float> = X.column(0).to_vec();
392        let x_min = x_values.iter().cloned().fold(Float::INFINITY, Float::min);
393        let x_max = x_values
394            .iter()
395            .cloned()
396            .fold(Float::NEG_INFINITY, Float::max);
397
398        // Generate knots if not provided
399        let knots = match &self.knots {
400            Some(knots) => knots.clone(),
401            None => self.generate_uniform_knots(x_min, x_max)?,
402        };
403
404        match self.spline_type {
405            SplineType::BSpline => self.compute_bspline_basis(&x_values, &knots),
406            SplineType::NaturalCubic => self.compute_natural_cubic_basis(&x_values, &knots),
407            SplineType::TruncatedPower => self.compute_truncated_power_basis(&x_values, &knots),
408        }
409    }
410
411    fn generate_uniform_knots(&self, x_min: Float, x_max: Float) -> SklResult<Vec<Float>> {
412        if x_min >= x_max {
413            return Err(SklearsError::InvalidInput(
414                "Invalid range for knot generation".to_string(),
415            ));
416        }
417
418        let mut knots = Vec::new();
419
420        match self.spline_type {
421            SplineType::BSpline => {
422                // For B-splines, we need additional knots at the boundaries
423                let n_interior_knots = self.n_splines - self.degree - 1;
424                let _total_knots = n_interior_knots + 2 * (self.degree + 1);
425
426                let step = (x_max - x_min) / (n_interior_knots + 1) as Float;
427
428                // Add boundary knots with multiplicity
429                for _ in 0..=self.degree {
430                    knots.push(x_min);
431                }
432
433                // Add interior knots
434                for i in 1..=n_interior_knots {
435                    knots.push(x_min + i as Float * step);
436                }
437
438                // Add boundary knots with multiplicity
439                for _ in 0..=self.degree {
440                    knots.push(x_max);
441                }
442            }
443            SplineType::NaturalCubic | SplineType::TruncatedPower => {
444                // For natural cubic and truncated power, use simple uniform spacing
445                let step = (x_max - x_min) / (self.n_splines - 1) as Float;
446                for i in 0..self.n_splines {
447                    knots.push(x_min + i as Float * step);
448                }
449            }
450        }
451
452        Ok(knots)
453    }
454
455    fn compute_bspline_basis(
456        &self,
457        x_values: &[Float],
458        knots: &[Float],
459    ) -> SklResult<Array2<Float>> {
460        let n_samples = x_values.len();
461        let n_basis = self.n_splines;
462        let mut basis_matrix = Array2::zeros((n_samples, n_basis));
463
464        for (sample_idx, &x) in x_values.iter().enumerate() {
465            let basis_values = self.evaluate_bspline_basis(x, knots)?;
466
467            for (basis_idx, &value) in basis_values.iter().enumerate() {
468                if basis_idx < n_basis {
469                    basis_matrix[[sample_idx, basis_idx]] = value;
470                }
471            }
472        }
473
474        Ok(basis_matrix)
475    }
476
477    fn evaluate_bspline_basis(&self, x: Float, knots: &[Float]) -> SklResult<Vec<Float>> {
478        let n_knots = knots.len();
479        let degree = self.degree;
480        let n_basis = n_knots - degree - 1;
481
482        if n_basis == 0 {
483            return Ok(vec![]);
484        }
485
486        let mut basis = vec![0.0; n_basis];
487
488        // Handle extrapolation
489        let (x_clamped, is_extrapolated, extrapolation_factor) = match self.extrapolation {
490            ExtrapolationMode::Constant => (x.max(knots[0]).min(knots[n_knots - 1]), false, 0.0),
491            ExtrapolationMode::Zero => {
492                if x < knots[0] || x > knots[n_knots - 1] {
493                    return Ok(basis); // All zeros
494                }
495                (x, false, 0.0)
496            }
497            ExtrapolationMode::Error => {
498                if x < knots[0] || x > knots[n_knots - 1] {
499                    return Err(SklearsError::InvalidInput(
500                        "Value outside knot range".to_string(),
501                    ));
502                }
503                (x, false, 0.0)
504            }
505            ExtrapolationMode::Linear => {
506                if x < knots[0] {
507                    // Linear extrapolation to the left
508                    let boundary = knots[0];
509                    let next_knot = if n_knots > 1 {
510                        knots[1]
511                    } else {
512                        knots[0] + 1.0
513                    };
514                    let slope = 1.0 / (next_knot - boundary);
515                    let factor = (x - boundary) * slope;
516                    (boundary, true, factor)
517                } else if x > knots[n_knots - 1] {
518                    // Linear extrapolation to the right
519                    let boundary = knots[n_knots - 1];
520                    let prev_knot = if n_knots > 1 {
521                        knots[n_knots - 2]
522                    } else {
523                        knots[n_knots - 1] - 1.0
524                    };
525                    let slope = 1.0 / (boundary - prev_knot);
526                    let factor = (x - boundary) * slope;
527                    (boundary, true, factor)
528                } else {
529                    (x, false, 0.0)
530                }
531            }
532        };
533
534        // De Boor's algorithm for B-spline evaluation
535        // Start with degree 0 (piecewise constant)
536        for i in 0..n_basis {
537            if i + degree + 1 < n_knots && x_clamped >= knots[i] && x_clamped < knots[i + 1] {
538                basis[i] = 1.0;
539            }
540        }
541
542        // Handle the right boundary
543        if (x_clamped - knots[n_knots - 1]).abs() < 1e-10 && n_basis > 0 {
544            basis[n_basis - 1] = 1.0;
545        }
546
547        // Recursive computation for higher degrees
548        for d in 1..=degree {
549            let mut new_basis = vec![0.0; n_basis];
550
551            for i in 0..n_basis {
552                if i + d < n_knots && (knots[i + d] - knots[i]).abs() > 1e-10 {
553                    let alpha = (x_clamped - knots[i]) / (knots[i + d] - knots[i]);
554                    new_basis[i] += alpha * basis[i];
555                }
556
557                if i > 0 && i + d < n_knots && (knots[i + d] - knots[i]).abs() > 1e-10 {
558                    let alpha = (knots[i + d] - x_clamped) / (knots[i + d] - knots[i]);
559                    new_basis[i] += alpha * basis[i - 1];
560                }
561            }
562
563            basis = new_basis;
564        }
565
566        // Apply linear extrapolation if needed
567        if is_extrapolated && extrapolation_factor != 0.0 {
568            // For linear extrapolation, scale the basis functions by the extrapolation factor
569            // This provides a linear extension of the spline beyond the knot boundaries
570            for value in basis.iter_mut() {
571                *value *= 1.0 + extrapolation_factor.abs().min(10.0); // Cap the factor for numerical stability
572            }
573        }
574
575        Ok(basis)
576    }
577
578    fn compute_natural_cubic_basis(
579        &self,
580        x_values: &[Float],
581        knots: &[Float],
582    ) -> SklResult<Array2<Float>> {
583        let n_samples = x_values.len();
584        let n_knots = knots.len();
585        let n_basis = n_knots;
586        let mut basis_matrix = Array2::zeros((n_samples, n_basis));
587
588        if n_knots < 4 {
589            return Err(SklearsError::InvalidInput(
590                "Natural cubic splines require at least 4 knots".to_string(),
591            ));
592        }
593
594        for (sample_idx, &x) in x_values.iter().enumerate() {
595            // Linear basis functions
596            basis_matrix[[sample_idx, 0]] = 1.0;
597            basis_matrix[[sample_idx, 1]] = x;
598
599            // Cubic basis functions
600            for k in 2..n_basis {
601                if k - 2 < n_knots - 2 {
602                    let knot = knots[k - 2];
603                    let _x_min = knots[0];
604                    let x_max = knots[n_knots - 1];
605
606                    let d_k = self.natural_cubic_basis_function(x, knot, x_max);
607                    let d_n = self.natural_cubic_basis_function(x, x_max, x_max);
608
609                    basis_matrix[[sample_idx, k]] = d_k - d_n;
610                }
611            }
612        }
613
614        Ok(basis_matrix)
615    }
616
617    fn natural_cubic_basis_function(&self, x: Float, knot: Float, _x_max: Float) -> Float {
618        if x <= knot {
619            0.0
620        } else {
621            let diff = x - knot;
622            diff.powi(3) / 6.0
623        }
624    }
625
626    fn compute_truncated_power_basis(
627        &self,
628        x_values: &[Float],
629        knots: &[Float],
630    ) -> SklResult<Array2<Float>> {
631        let n_samples = x_values.len();
632        let n_basis = self.n_splines;
633        let mut basis_matrix = Array2::zeros((n_samples, n_basis));
634
635        for (sample_idx, &x) in x_values.iter().enumerate() {
636            // Polynomial terms
637            for d in 0..=self.degree.min(n_basis - 1) {
638                basis_matrix[[sample_idx, d]] = x.powi(d as i32);
639            }
640
641            // Truncated power functions
642            for (basis_idx, &knot) in (self.degree + 1..).zip(knots.iter()) {
643                if basis_idx >= n_basis {
644                    break;
645                }
646
647                if x > knot {
648                    basis_matrix[[sample_idx, basis_idx]] = (x - knot).powi(self.degree as i32);
649                }
650            }
651        }
652
653        Ok(basis_matrix)
654    }
655}
656
657impl Default for SplineBasisFunctions {
658    fn default() -> Self {
659        Self::new()
660    }
661}
662
663#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
664impl Transform<ArrayView2<'_, Float>, Array2<Float>> for SplineBasisFunctions {
665    fn transform(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
666        self.transform(X)
667    }
668}
669
670/// Radial Basis Functions Generator
671///
672/// Generate radial basis functions (RBFs) for non-linear feature transformation.
673/// Supports various RBF types including Gaussian, Multiquadric, and Inverse Multiquadric.
674///
675/// # Parameters
676///
677/// * `n_centers` - Number of RBF centers
678/// * `rbf_type` - Type of radial basis function
679/// * `centers` - RBF center positions (if None, centers are determined from data)
680/// * `shape_parameter` - Shape parameter for RBFs (affects function width)
681/// * `normalize` - Whether to normalize the output features
682///
683/// # Examples
684///
685/// ```
686/// use sklears_feature_extraction::basic_features::{RadialBasisFunctions, RBFType};
687/// use scirs2_core::ndarray::array;
688///
689/// let X = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
690/// let rbf = RadialBasisFunctions::new()
691///     .n_centers(3)
692///     .rbf_type(RBFType::Gaussian)
693///     .shape_parameter(1.0);
694///
695/// let features = rbf.transform(&X.view()).expect("valid input produces transform output");
696/// ```
697#[derive(Debug, Clone)]
698pub struct RadialBasisFunctions {
699    n_centers: usize,
700    rbf_type: RBFType,
701    centers: Option<Array2<Float>>,
702    shape_parameter: Float,
703    normalize: bool,
704}
705
706#[derive(Debug, Clone, Copy)]
707pub enum RBFType {
708    /// Gaussian RBF: exp(-epsilon * r^2)
709    Gaussian,
710    /// Multiquadric RBF: sqrt(1 + epsilon * r^2)
711    Multiquadric,
712    /// Inverse Multiquadric RBF: 1 / sqrt(1 + epsilon * r^2)
713    InverseMultiquadric,
714    /// Thin Plate Spline RBF: r^2 * log(r)
715    ThinPlateSpline,
716    /// Linear RBF: r
717    Linear,
718    /// Cubic RBF: r^3
719    Cubic,
720    /// Quintic RBF: r^5
721    Quintic,
722}
723
724#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
725impl RadialBasisFunctions {
726    /// Create a new RadialBasisFunctions transformer
727    pub fn new() -> Self {
728        Self {
729            n_centers: 10,
730            rbf_type: RBFType::Gaussian,
731            centers: None,
732            shape_parameter: 1.0,
733            normalize: false,
734        }
735    }
736
737    /// Set the number of RBF centers
738    pub fn n_centers(mut self, n_centers: usize) -> Self {
739        self.n_centers = n_centers;
740        self
741    }
742
743    /// Set the type of radial basis function
744    pub fn rbf_type(mut self, rbf_type: RBFType) -> Self {
745        self.rbf_type = rbf_type;
746        self
747    }
748
749    /// Set custom RBF centers
750    pub fn centers(mut self, centers: Array2<Float>) -> Self {
751        self.centers = Some(centers);
752        self
753    }
754
755    /// Set the shape parameter (epsilon)
756    pub fn shape_parameter(mut self, shape_parameter: Float) -> Self {
757        self.shape_parameter = shape_parameter;
758        self
759    }
760
761    /// Set whether to normalize output features
762    pub fn normalize(mut self, normalize: bool) -> Self {
763        self.normalize = normalize;
764        self
765    }
766
767    /// Transform input data using radial basis functions
768    pub fn transform(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
769        let (n_samples, _n_features) = X.dim();
770
771        // Generate centers if not provided
772        let centers = match &self.centers {
773            Some(centers) => centers.clone(),
774            None => self.generate_centers(X)?,
775        };
776
777        let n_centers = centers.nrows();
778        let mut rbf_features = Array2::zeros((n_samples, n_centers));
779
780        // Compute RBF features for each sample
781        for (sample_idx, sample) in X.axis_iter(Axis(0)).enumerate() {
782            for (center_idx, center) in centers.axis_iter(Axis(0)).enumerate() {
783                let distance = self.compute_distance(&sample, &center);
784                let rbf_value = self.evaluate_rbf(distance);
785                rbf_features[[sample_idx, center_idx]] = rbf_value;
786            }
787        }
788
789        // Apply normalization if requested
790        if self.normalize {
791            self.normalize_features(&mut rbf_features);
792        }
793
794        Ok(rbf_features)
795    }
796
797    fn generate_centers(&self, X: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
798        let (n_samples, n_features) = X.dim();
799
800        if self.n_centers > n_samples {
801            return Err(SklearsError::InvalidInput(
802                "Number of centers cannot exceed number of samples".to_string(),
803            ));
804        }
805
806        // Use k-means style center selection (simplified)
807        let mut centers = Array2::zeros((self.n_centers, n_features));
808
809        // Initialize first center randomly (use first sample)
810        if n_samples > 0 {
811            for j in 0..n_features {
812                centers[[0, j]] = X[[0, j]];
813            }
814        }
815
816        // Select remaining centers using farthest-first traversal
817        for center_idx in 1..self.n_centers {
818            let mut best_sample_idx = 0;
819            let mut max_min_distance = 0.0;
820
821            for sample_idx in 0..n_samples {
822                let sample = X.row(sample_idx);
823                let mut min_distance = Float::INFINITY;
824
825                // Find minimum distance to existing centers
826                for existing_center_idx in 0..center_idx {
827                    let center = centers.row(existing_center_idx);
828                    let distance = self.compute_distance(&sample, &center);
829                    min_distance = min_distance.min(distance);
830                }
831
832                // Select sample with maximum minimum distance
833                if min_distance > max_min_distance {
834                    max_min_distance = min_distance;
835                    best_sample_idx = sample_idx;
836                }
837            }
838
839            // Set the new center
840            for j in 0..n_features {
841                centers[[center_idx, j]] = X[[best_sample_idx, j]];
842            }
843        }
844
845        Ok(centers)
846    }
847
848    fn compute_distance(&self, point1: &ArrayView1<Float>, point2: &ArrayView1<Float>) -> Float {
849        // Euclidean distance
850        let mut sum_squared = 0.0;
851        for (x1, x2) in point1.iter().zip(point2.iter()) {
852            sum_squared += (x1 - x2).powi(2);
853        }
854        sum_squared.sqrt()
855    }
856
857    fn evaluate_rbf(&self, distance: Float) -> Float {
858        let r = distance;
859        let epsilon = self.shape_parameter;
860
861        match self.rbf_type {
862            RBFType::Gaussian => (-epsilon * r * r).exp(),
863            RBFType::Multiquadric => (1.0 + epsilon * r * r).sqrt(),
864            RBFType::InverseMultiquadric => 1.0 / (1.0 + epsilon * r * r).sqrt(),
865            RBFType::ThinPlateSpline => {
866                if r < 1e-10 {
867                    0.0
868                } else {
869                    r * r * r.ln()
870                }
871            }
872            RBFType::Linear => r,
873            RBFType::Cubic => r.powi(3),
874            RBFType::Quintic => r.powi(5),
875        }
876    }
877
878    fn normalize_features(&self, features: &mut Array2<Float>) {
879        let (n_samples, n_features) = features.dim();
880
881        for j in 0..n_features {
882            let mut column_sum = 0.0;
883            for i in 0..n_samples {
884                column_sum += features[[i, j]];
885            }
886
887            if column_sum > 1e-10 {
888                for i in 0..n_samples {
889                    features[[i, j]] /= column_sum;
890                }
891            }
892        }
893    }
894}
895
896impl Default for RadialBasisFunctions {
897    fn default() -> Self {
898        Self::new()
899    }
900}
901
902#[allow(non_snake_case)] // X follows sklearn/math convention for feature matrix
903impl Transform<ArrayView2<'_, Float>, Array2<Float>> for RadialBasisFunctions {
904    fn transform(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
905        self.transform(X)
906    }
907}