Skip to main content

sklears_feature_selection/
type_safe.rs

1//! Type-Safe Feature Selection Framework
2//!
3//! This module provides compile-time guarantees for feature selection operations,
4//! using Rust's advanced type system features including phantom types, const generics,
5//! and zero-cost abstractions to ensure correctness and performance.
6
7use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
8use sklears_core::error::{Result as SklResult, SklearsError};
9use std::marker::PhantomData;
10
11type Result<T> = SklResult<T>;
12
13/// Phantom type markers for selection method types
14pub mod selection_types {
15    /// Marker for filter-based selection methods
16    #[derive(Debug, Clone, Copy, Default)]
17    pub struct Filter;
18
19    /// Marker for wrapper-based selection methods
20    #[derive(Debug, Clone, Copy, Default)]
21    pub struct Wrapper;
22
23    /// Marker for embedded selection methods
24    #[derive(Debug, Clone, Copy, Default)]
25    pub struct Embedded;
26
27    /// Marker for univariate selection methods
28    #[derive(Debug, Clone, Copy, Default)]
29    pub struct Univariate;
30
31    /// Marker for multivariate selection methods
32    #[derive(Debug, Clone, Copy, Default)]
33    pub struct Multivariate;
34
35    /// Marker for supervised selection methods
36    #[derive(Debug, Clone, Copy, Default)]
37    pub struct Supervised;
38
39    /// Marker for unsupervised selection methods
40    #[derive(Debug, Clone, Copy, Default)]
41    pub struct Unsupervised;
42
43    /// Marker for deterministic selection methods
44    #[derive(Debug, Clone, Copy, Default)]
45    pub struct Deterministic;
46
47    /// Marker for stochastic selection methods
48    #[derive(Debug, Clone, Copy, Default)]
49    pub struct Stochastic;
50}
51
52/// Phantom type markers for data states
53pub mod data_states {
54    /// Marker for untrained/unfitted state
55    #[derive(Debug, Clone, Copy, Default)]
56    pub struct Untrained;
57
58    /// Marker for trained/fitted state
59    #[derive(Debug, Clone, Copy, Default)]
60    pub struct Trained;
61
62    /// Marker for validated state
63    #[derive(Debug, Clone, Copy, Default)]
64    pub struct Validated;
65
66    /// Marker for optimized state
67    #[derive(Debug, Clone, Copy, Default)]
68    pub struct Optimized;
69}
70
71/// Type-safe feature index with compile-time bounds checking
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
73pub struct FeatureIndex<const MAX_FEATURES: usize> {
74    index: usize,
75}
76
77impl<const MAX_FEATURES: usize> FeatureIndex<MAX_FEATURES> {
78    /// Create a new feature index with compile-time bounds checking
79    pub const fn new(index: usize) -> Option<Self> {
80        if index < MAX_FEATURES {
81            Some(Self { index })
82        } else {
83            None
84        }
85    }
86
87    /// Create a new feature index without bounds checking (unsafe)
88    ///
89    /// # Safety
90    /// The caller must ensure that `index < MAX_FEATURES`
91    pub const unsafe fn new_unchecked(index: usize) -> Self {
92        Self { index }
93    }
94
95    /// Get the inner index value
96    pub const fn get(self) -> usize {
97        self.index
98    }
99
100    /// Convert to a runtime feature index
101    pub const fn to_runtime(self) -> RuntimeFeatureIndex {
102        RuntimeFeatureIndex::new(self.index)
103    }
104}
105
106/// Runtime feature index for dynamic bounds checking
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub struct RuntimeFeatureIndex {
109    index: usize,
110}
111
112impl RuntimeFeatureIndex {
113    /// Create a new runtime feature index
114    pub const fn new(index: usize) -> Self {
115        Self { index }
116    }
117
118    /// Get the inner index value
119    pub const fn get(self) -> usize {
120        self.index
121    }
122
123    /// Check if this index is valid for a given number of features
124    pub const fn is_valid(self, n_features: usize) -> bool {
125        self.index < n_features
126    }
127}
128
129/// Type-safe feature mask with const generic size
130#[derive(Debug, Clone)]
131pub struct FeatureMask<const N_FEATURES: usize> {
132    mask: [bool; N_FEATURES],
133}
134
135impl<const N_FEATURES: usize> FeatureMask<N_FEATURES> {
136    /// Create a new feature mask with all features selected
137    pub const fn all_selected() -> Self {
138        Self {
139            mask: [true; N_FEATURES],
140        }
141    }
142
143    /// Create a new feature mask with no features selected
144    pub const fn none_selected() -> Self {
145        Self {
146            mask: [false; N_FEATURES],
147        }
148    }
149
150    /// Create a feature mask from a boolean array
151    pub const fn from_array(mask: [bool; N_FEATURES]) -> Self {
152        Self { mask }
153    }
154
155    /// Create a feature mask from selected indices
156    pub fn from_indices(indices: &[FeatureIndex<N_FEATURES>]) -> Self {
157        let mut mask = [false; N_FEATURES];
158        for &index in indices {
159            mask[index.get()] = true;
160        }
161        Self { mask }
162    }
163
164    /// Get the mask as a boolean array
165    pub const fn as_array(&self) -> &[bool; N_FEATURES] {
166        &self.mask
167    }
168
169    /// Check if a feature is selected
170    pub const fn is_selected(&self, index: FeatureIndex<N_FEATURES>) -> bool {
171        self.mask[index.get()]
172    }
173
174    /// Set a feature as selected or unselected
175    pub fn set(&mut self, index: FeatureIndex<N_FEATURES>, selected: bool) {
176        self.mask[index.get()] = selected;
177    }
178
179    /// Get the number of selected features
180    pub fn count_selected(&self) -> usize {
181        self.mask.iter().filter(|&&x| x).count()
182    }
183
184    /// Get indices of selected features
185    pub fn selected_indices(&self) -> Vec<FeatureIndex<N_FEATURES>> {
186        self.mask
187            .iter()
188            .enumerate()
189            .filter_map(|(i, &selected)| {
190                if selected {
191                    // Safety: i is always < N_FEATURES in this context
192                    Some(unsafe { FeatureIndex::new_unchecked(i) })
193                } else {
194                    None
195                }
196            })
197            .collect()
198    }
199
200    /// Combine with another mask using logical AND
201    pub fn and(&self, other: &Self) -> Self {
202        let mut result = [false; N_FEATURES];
203        for i in 0..N_FEATURES {
204            result[i] = self.mask[i] && other.mask[i];
205        }
206        Self::from_array(result)
207    }
208
209    /// Combine with another mask using logical OR
210    pub fn or(&self, other: &Self) -> Self {
211        let mut result = [false; N_FEATURES];
212        for i in 0..N_FEATURES {
213            result[i] = self.mask[i] || other.mask[i];
214        }
215        Self::from_array(result)
216    }
217
218    /// Invert the mask
219    pub fn not(&self) -> Self {
220        let mut result = [false; N_FEATURES];
221        for i in 0..N_FEATURES {
222            result[i] = !self.mask[i];
223        }
224        Self::from_array(result)
225    }
226}
227
228/// Type-safe feature matrix with compile-time feature count validation
229#[derive(Debug, Clone)]
230pub struct FeatureMatrix<T, const N_FEATURES: usize> {
231    data: Array2<T>,
232    _phantom: PhantomData<[T; N_FEATURES]>,
233}
234
235impl<T, const N_FEATURES: usize> FeatureMatrix<T, N_FEATURES>
236where
237    T: Clone + Default,
238{
239    /// Create a new feature matrix with compile-time feature count validation
240    pub fn new(data: Array2<T>) -> Result<Self> {
241        if data.ncols() == N_FEATURES {
242            Ok(Self {
243                data,
244                _phantom: PhantomData,
245            })
246        } else {
247            Err(SklearsError::InvalidInput(format!(
248                "Expected {} features, got {}",
249                N_FEATURES,
250                data.ncols()
251            )))
252        }
253    }
254
255    /// Create a new feature matrix without validation (unsafe)
256    ///
257    /// # Safety
258    /// The caller must ensure that `data.ncols() == N_FEATURES`
259    pub unsafe fn new_unchecked(data: Array2<T>) -> Self {
260        Self {
261            data,
262            _phantom: PhantomData,
263        }
264    }
265
266    /// Get the number of samples
267    pub fn n_samples(&self) -> usize {
268        self.data.nrows()
269    }
270
271    /// Get the number of features (always N_FEATURES)
272    pub const fn n_features(&self) -> usize {
273        N_FEATURES
274    }
275
276    /// Get a view of the underlying data
277    pub fn view(&self) -> ArrayView2<'_, T> {
278        self.data.view()
279    }
280
281    /// Get a feature column by type-safe index
282    pub fn feature(&self, index: FeatureIndex<N_FEATURES>) -> ArrayView1<'_, T> {
283        self.data.column(index.get())
284    }
285
286    /// Select features using a type-safe mask
287    pub fn select_features<const N_SELECTED: usize>(
288        &self,
289        mask: &FeatureMask<N_FEATURES>,
290    ) -> Result<FeatureMatrix<T, N_SELECTED>> {
291        let selected_indices = mask.selected_indices();
292        if selected_indices.len() != N_SELECTED {
293            return Err(SklearsError::InvalidInput(format!(
294                "Expected {} selected features, got {}",
295                N_SELECTED,
296                selected_indices.len()
297            )));
298        }
299
300        let mut selected_data = Array2::default((self.n_samples(), N_SELECTED));
301        for (new_col, &old_index) in selected_indices.iter().enumerate() {
302            for row in 0..self.n_samples() {
303                selected_data[[row, new_col]] = self.data[[row, old_index.get()]].clone();
304            }
305        }
306
307        Ok(FeatureMatrix {
308            data: selected_data,
309            _phantom: PhantomData,
310        })
311    }
312
313    /// Convert to a dynamic feature matrix
314    pub fn to_dynamic(self) -> DynamicFeatureMatrix<T> {
315        DynamicFeatureMatrix::new(self.data)
316    }
317}
318
319/// Dynamic feature matrix for runtime feature count
320#[derive(Debug, Clone)]
321pub struct DynamicFeatureMatrix<T> {
322    data: Array2<T>,
323}
324
325impl<T> DynamicFeatureMatrix<T> {
326    /// Create a new dynamic feature matrix
327    pub fn new(data: Array2<T>) -> Self {
328        Self { data }
329    }
330
331    /// Get the number of samples
332    pub fn n_samples(&self) -> usize {
333        self.data.nrows()
334    }
335
336    /// Get the number of features
337    pub fn n_features(&self) -> usize {
338        self.data.ncols()
339    }
340
341    /// Get a view of the underlying data
342    pub fn view(&self) -> ArrayView2<'_, T> {
343        self.data.view()
344    }
345
346    /// Get a feature column by runtime index
347    pub fn feature(&self, index: RuntimeFeatureIndex) -> Result<ArrayView1<'_, T>> {
348        if index.is_valid(self.n_features()) {
349            Ok(self.data.column(index.get()))
350        } else {
351            Err(SklearsError::InvalidInput(format!(
352                "Feature index {} out of bounds for {} features",
353                index.get(),
354                self.n_features()
355            )))
356        }
357    }
358
359    /// Convert to a compile-time feature matrix if the size matches
360    pub fn to_static<const N_FEATURES: usize>(self) -> Result<FeatureMatrix<T, N_FEATURES>>
361    where
362        T: Clone + Default,
363    {
364        FeatureMatrix::new(self.data)
365    }
366}
367
368/// Zero-cost abstraction for feature selection algorithms
369pub trait TypeSafeSelector<Method, State = data_states::Untrained> {
370    /// The output state after fitting
371    type FittedState;
372
373    /// The type of selection results
374    type SelectionResult;
375
376    /// Fit the selector on training data
377    fn fit_typed<const N_FEATURES: usize>(
378        self,
379        X: &FeatureMatrix<f64, N_FEATURES>,
380        y: ArrayView1<f64>,
381    ) -> Result<TypeSafeSelectorWrapper<Method, Self::FittedState, N_FEATURES>>;
382}
383
384/// Zero-cost wrapper for type-safe selectors
385#[derive(Debug, Clone)]
386pub struct TypeSafeSelectorWrapper<Method, State, const N_FEATURES: usize> {
387    _method_params: MethodParameters,
388    selection_result: Option<FeatureMask<N_FEATURES>>,
389    _phantom: PhantomData<(Method, State)>,
390}
391
392impl<Method, State, const N_FEATURES: usize> TypeSafeSelectorWrapper<Method, State, N_FEATURES> {
393    /// Create a new type-safe selector wrapper
394    pub fn new(method_params: MethodParameters) -> Self {
395        Self {
396            _method_params: method_params,
397            selection_result: None,
398            _phantom: PhantomData,
399        }
400    }
401
402    /// Get the selection result
403    pub fn selection_mask(&self) -> Option<&FeatureMask<N_FEATURES>> {
404        self.selection_result.as_ref()
405    }
406
407    /// Set the selection result
408    pub fn set_selection(&mut self, mask: FeatureMask<N_FEATURES>) {
409        self.selection_result = Some(mask);
410    }
411}
412
413/// Method parameters for different selection algorithms
414#[derive(Debug, Clone)]
415pub enum MethodParameters {
416    /// VarianceThreshold
417    VarianceThreshold {
418        /// threshold
419        threshold: f64,
420    },
421    /// UnivariateFilter
422    UnivariateFilter {
423        /// k
424        k: usize,
425
426        /// score_function
427        score_function: String,
428    },
429    /// RecursiveElimination
430    RecursiveElimination {
431        /// n_features
432        n_features: usize,
433
434        /// step
435        step: f64,
436    },
437    /// LassoSelection
438    LassoSelection {
439        /// alpha
440        alpha: f64,
441        /// max_iter
442        max_iter: usize,
443    },
444    /// TreeBasedSelection
445    TreeBasedSelection {
446        /// n_estimators
447        n_estimators: usize,
448        /// max_depth
449        max_depth: Option<usize>,
450    },
451    /// CorrelationFilter
452    CorrelationFilter {
453        /// threshold
454        threshold: f64,
455    },
456    /// MutualInfoSelection
457    MutualInfoSelection {
458        /// k
459        k: usize,
460        /// discrete_features
461        discrete_features: Vec<bool>,
462    },
463}
464
465/// Compile-time variance threshold selector
466#[derive(Debug, Clone)]
467pub struct VarianceThresholdSelector<const N_FEATURES: usize> {
468    threshold: f64,
469    feature_variances: Option<[f64; N_FEATURES]>,
470}
471
472impl<const N_FEATURES: usize> VarianceThresholdSelector<N_FEATURES> {
473    /// Create a new variance threshold selector
474    pub const fn new(threshold: f64) -> Self {
475        Self {
476            threshold,
477            feature_variances: None,
478        }
479    }
480
481    /// Fit the selector on data
482    pub fn fit(&mut self, X: &FeatureMatrix<f64, N_FEATURES>) -> Result<FeatureMask<N_FEATURES>> {
483        let mut variances = [0.0; N_FEATURES];
484
485        for i in 0..N_FEATURES {
486            // Safety: i is always < N_FEATURES
487            let feature_index = unsafe { FeatureIndex::new_unchecked(i) };
488            let feature_data = X.feature(feature_index);
489            variances[i] = feature_data.var(1.0);
490        }
491
492        self.feature_variances = Some(variances);
493
494        let mut mask = [false; N_FEATURES];
495        for i in 0..N_FEATURES {
496            mask[i] = variances[i] > self.threshold;
497        }
498
499        Ok(FeatureMask::from_array(mask))
500    }
501
502    /// Transform data using the fitted selector
503    pub fn transform<const N_SELECTED: usize>(
504        &self,
505        X: &FeatureMatrix<f64, N_FEATURES>,
506        mask: &FeatureMask<N_FEATURES>,
507    ) -> Result<FeatureMatrix<f64, N_SELECTED>> {
508        X.select_features(mask)
509    }
510
511    /// Get feature variances (if fitted)
512    pub const fn feature_variances(&self) -> Option<&[f64; N_FEATURES]> {
513        self.feature_variances.as_ref()
514    }
515}
516
517/// Compile-time univariate feature selector
518#[derive(Debug, Clone)]
519pub struct UnivariateSelector<const N_FEATURES: usize, const K: usize> {
520    score_function: UnivariateScoreFunction,
521    feature_scores: Option<[f64; N_FEATURES]>,
522}
523
524impl<const N_FEATURES: usize, const K: usize> UnivariateSelector<N_FEATURES, K> {
525    /// Create a new univariate selector
526    ///
527    /// # Compile-time checks
528    /// - K must be <= N_FEATURES
529    pub const fn new(score_function: UnivariateScoreFunction) -> Option<Self> {
530        if K <= N_FEATURES {
531            Some(Self {
532                score_function,
533                feature_scores: None,
534            })
535        } else {
536            None
537        }
538    }
539
540    /// Fit the selector on data
541    pub fn fit(
542        &mut self,
543        X: &FeatureMatrix<f64, N_FEATURES>,
544        y: ArrayView1<f64>,
545    ) -> Result<FeatureMask<N_FEATURES>> {
546        let mut scores = [0.0; N_FEATURES];
547
548        for i in 0..N_FEATURES {
549            // Safety: i is always < N_FEATURES
550            let feature_index = unsafe { FeatureIndex::new_unchecked(i) };
551            let feature_data = X.feature(feature_index);
552            scores[i] = match self.score_function {
553                UnivariateScoreFunction::Correlation => self.compute_correlation(feature_data, y),
554                UnivariateScoreFunction::MutualInfo => self.compute_mutual_info(feature_data, y),
555                UnivariateScoreFunction::Chi2 => self.compute_chi2_score(feature_data, y),
556                UnivariateScoreFunction::FStatistic => self.compute_f_statistic(feature_data, y),
557            };
558        }
559
560        self.feature_scores = Some(scores);
561
562        // Select top K features
563        let mut indexed_scores: Vec<(usize, f64)> = Vec::with_capacity(N_FEATURES);
564        for i in 0..N_FEATURES {
565            indexed_scores.push((i, scores[i]));
566        }
567
568        // Sort by score (descending)
569        indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
570
571        let mut mask = [false; N_FEATURES];
572        for i in 0..K {
573            if let Some(&(feature_idx, _)) = indexed_scores.get(i) {
574                mask[feature_idx] = true;
575            }
576        }
577
578        Ok(FeatureMask::from_array(mask))
579    }
580
581    /// Transform data using the fitted selector
582    pub fn transform(
583        &self,
584        X: &FeatureMatrix<f64, N_FEATURES>,
585        mask: &FeatureMask<N_FEATURES>,
586    ) -> Result<FeatureMatrix<f64, K>> {
587        X.select_features(mask)
588    }
589
590    /// Get feature scores (if fitted)
591    pub const fn feature_scores(&self) -> Option<&[f64; N_FEATURES]> {
592        self.feature_scores.as_ref()
593    }
594
595    // Helper methods for score computation
596    fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
597        let n = x.len() as f64;
598        if n < 2.0 {
599            return 0.0;
600        }
601
602        let mean_x = x.mean().unwrap_or(0.0);
603        let mean_y = y.mean().unwrap_or(0.0);
604
605        let mut sum_xy = 0.0;
606        let mut sum_x2 = 0.0;
607        let mut sum_y2 = 0.0;
608
609        for i in 0..x.len() {
610            let dx = x[i] - mean_x;
611            let dy = y[i] - mean_y;
612            sum_xy += dx * dy;
613            sum_x2 += dx * dx;
614            sum_y2 += dy * dy;
615        }
616
617        let denom = (sum_x2 * sum_y2).sqrt();
618        if denom < 1e-10 {
619            0.0
620        } else {
621            (sum_xy / denom).abs()
622        }
623    }
624
625    fn compute_mutual_info(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
626        // Simplified mutual information computation
627        // In a real implementation, this would use proper MI estimation algorithms
628        self.compute_correlation(x, y)
629    }
630
631    fn compute_chi2_score(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
632        // Simplified chi-square test
633        // In a real implementation, this would compute proper chi-square statistics
634        self.compute_correlation(x, y)
635    }
636
637    fn compute_f_statistic(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
638        // Simplified F-statistic computation
639        // In a real implementation, this would compute ANOVA F-statistic
640        self.compute_correlation(x, y)
641    }
642}
643
644/// Score functions for univariate selection
645#[derive(Debug, Clone, Copy)]
646pub enum UnivariateScoreFunction {
647    /// Correlation
648    Correlation,
649    /// MutualInfo
650    MutualInfo,
651    /// Chi2
652    Chi2,
653    /// FStatistic
654    FStatistic,
655}
656
657/// Compile-time correlation-based feature selector
658#[derive(Debug, Clone)]
659pub struct CorrelationSelector<const N_FEATURES: usize> {
660    threshold: f64,
661    correlation_matrix: Option<[[f64; N_FEATURES]; N_FEATURES]>,
662}
663
664impl<const N_FEATURES: usize> CorrelationSelector<N_FEATURES> {
665    /// Create a new correlation-based selector
666    pub const fn new(threshold: f64) -> Self {
667        Self {
668            threshold,
669            correlation_matrix: None,
670        }
671    }
672
673    /// Fit the selector on data
674    pub fn fit(&mut self, X: &FeatureMatrix<f64, N_FEATURES>) -> Result<FeatureMask<N_FEATURES>> {
675        let mut corr_matrix = [[0.0; N_FEATURES]; N_FEATURES];
676
677        // Compute correlation matrix
678        for i in 0..N_FEATURES {
679            for j in 0..N_FEATURES {
680                if i == j {
681                    corr_matrix[i][j] = 1.0;
682                } else {
683                    // Safety: i and j are always < N_FEATURES
684                    let feature_i = unsafe { FeatureIndex::new_unchecked(i) };
685                    let feature_j = unsafe { FeatureIndex::new_unchecked(j) };
686                    let data_i = X.feature(feature_i);
687                    let data_j = X.feature(feature_j);
688                    corr_matrix[i][j] = self.compute_correlation(data_i, data_j);
689                }
690            }
691        }
692
693        self.correlation_matrix = Some(corr_matrix);
694
695        // Remove highly correlated features
696        let mut mask = [true; N_FEATURES];
697        for i in 0..N_FEATURES {
698            for j in (i + 1)..N_FEATURES {
699                if corr_matrix[i][j].abs() > self.threshold && mask[i] && mask[j] {
700                    // Keep the feature with higher variance
701                    // Safety: i and j are always < N_FEATURES
702                    let feature_i = unsafe { FeatureIndex::new_unchecked(i) };
703                    let feature_j = unsafe { FeatureIndex::new_unchecked(j) };
704                    let var_i = X.feature(feature_i).var(1.0);
705                    let var_j = X.feature(feature_j).var(1.0);
706                    if var_i < var_j {
707                        mask[i] = false;
708                    } else {
709                        mask[j] = false;
710                    }
711                }
712            }
713        }
714
715        Ok(FeatureMask::from_array(mask))
716    }
717
718    /// Transform data using the fitted selector
719    pub fn transform<const N_SELECTED: usize>(
720        &self,
721        X: &FeatureMatrix<f64, N_FEATURES>,
722        mask: &FeatureMask<N_FEATURES>,
723    ) -> Result<FeatureMatrix<f64, N_SELECTED>> {
724        X.select_features(mask)
725    }
726
727    /// Get correlation matrix (if fitted)
728    pub const fn correlation_matrix(&self) -> Option<&[[f64; N_FEATURES]; N_FEATURES]> {
729        self.correlation_matrix.as_ref()
730    }
731
732    fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
733        let n = x.len() as f64;
734        if n < 2.0 {
735            return 0.0;
736        }
737
738        let mean_x = x.mean().unwrap_or(0.0);
739        let mean_y = y.mean().unwrap_or(0.0);
740
741        let mut sum_xy = 0.0;
742        let mut sum_x2 = 0.0;
743        let mut sum_y2 = 0.0;
744
745        for i in 0..x.len() {
746            let dx = x[i] - mean_x;
747            let dy = y[i] - mean_y;
748            sum_xy += dx * dy;
749            sum_x2 += dx * dx;
750            sum_y2 += dy * dy;
751        }
752
753        let denom = (sum_x2 * sum_y2).sqrt();
754        if denom < 1e-10 {
755            0.0
756        } else {
757            sum_xy / denom
758        }
759    }
760}
761
762/// Type-safe feature selection pipeline with compile-time guarantees
763#[derive(Debug, Clone)]
764pub struct TypeSafeSelectionPipeline<const N_FEATURES: usize, State = data_states::Untrained> {
765    steps: Vec<PipelineStep>,
766    current_mask: Option<FeatureMask<N_FEATURES>>,
767    _phantom: PhantomData<State>,
768}
769
770impl<const N_FEATURES: usize> Default
771    for TypeSafeSelectionPipeline<N_FEATURES, data_states::Untrained>
772{
773    fn default() -> Self {
774        Self::new()
775    }
776}
777
778impl<const N_FEATURES: usize> TypeSafeSelectionPipeline<N_FEATURES, data_states::Untrained> {
779    /// Create a new type-safe selection pipeline
780    pub const fn new() -> Self {
781        Self {
782            steps: Vec::new(),
783            current_mask: None,
784            _phantom: PhantomData,
785        }
786    }
787
788    /// Add a variance threshold step
789    pub fn add_variance_threshold(mut self, threshold: f64) -> Self {
790        self.steps.push(PipelineStep::VarianceThreshold(threshold));
791        self
792    }
793
794    /// Add a correlation filter step
795    pub fn add_correlation_filter(mut self, threshold: f64) -> Self {
796        self.steps.push(PipelineStep::CorrelationFilter(threshold));
797        self
798    }
799
800    /// Add a univariate selection step
801    pub fn add_univariate_selection<const K: usize>(
802        mut self,
803        score_function: UnivariateScoreFunction,
804    ) -> Self {
805        self.steps.push(PipelineStep::UnivariateSelection {
806            _k: K,
807            score_function,
808        });
809        self
810    }
811
812    /// Fit the pipeline on training data
813    pub fn fit(
814        self,
815        X: &FeatureMatrix<f64, N_FEATURES>,
816        y: ArrayView1<f64>,
817    ) -> Result<TypeSafeSelectionPipeline<N_FEATURES, data_states::Trained>> {
818        let mut current_mask = FeatureMask::all_selected();
819
820        for step in &self.steps {
821            let step_mask = match step {
822                PipelineStep::VarianceThreshold(threshold) => {
823                    let mut selector = VarianceThresholdSelector::new(*threshold);
824                    selector.fit(X)?
825                }
826                PipelineStep::CorrelationFilter(threshold) => {
827                    let mut selector = CorrelationSelector::new(*threshold);
828                    selector.fit(X)?
829                }
830                PipelineStep::UnivariateSelection {
831                    _k: _,
832                    score_function,
833                } => {
834                    // This is a simplification - in practice we'd need to handle different K values
835                    // For demonstration, we'll use a fixed K
836                    const DEFAULT_K: usize = 10;
837                    if DEFAULT_K <= N_FEATURES {
838                        let mut selector =
839                            UnivariateSelector::<N_FEATURES, DEFAULT_K>::new(*score_function)
840                                .ok_or_else(|| {
841                                    SklearsError::InvalidInput(
842                                        "Invalid K for univariate selection".to_string(),
843                                    )
844                                })?;
845                        selector.fit(X, y)?
846                    } else {
847                        FeatureMask::all_selected()
848                    }
849                }
850            };
851
852            current_mask = current_mask.and(&step_mask);
853        }
854
855        Ok(TypeSafeSelectionPipeline {
856            steps: self.steps,
857            current_mask: Some(current_mask),
858            _phantom: PhantomData,
859        })
860    }
861}
862
863impl<const N_FEATURES: usize> TypeSafeSelectionPipeline<N_FEATURES, data_states::Trained> {
864    /// Transform data using the fitted pipeline
865    pub fn transform<const N_SELECTED: usize>(
866        &self,
867        X: &FeatureMatrix<f64, N_FEATURES>,
868    ) -> Result<FeatureMatrix<f64, N_SELECTED>> {
869        if let Some(ref mask) = self.current_mask {
870            X.select_features(mask)
871        } else {
872            Err(SklearsError::FitError("Pipeline not fitted".to_string()))
873        }
874    }
875
876    /// Get the feature selection mask
877    pub fn selection_mask(&self) -> Option<&FeatureMask<N_FEATURES>> {
878        self.current_mask.as_ref()
879    }
880
881    /// Get the number of selected features
882    pub fn n_selected_features(&self) -> usize {
883        self.current_mask
884            .as_ref()
885            .map(|mask| mask.count_selected())
886            .unwrap_or(0)
887    }
888}
889
890/// Pipeline step enumeration
891#[derive(Debug, Clone)]
892enum PipelineStep {
893    VarianceThreshold(f64),
894    CorrelationFilter(f64),
895    UnivariateSelection {
896        _k: usize,
897        score_function: UnivariateScoreFunction,
898    },
899}
900
901/// Zero-cost abstraction for feature transformations
902pub trait ZeroCostTransform<Input, Output> {
903    /// Apply the transformation with zero runtime cost
904    fn transform_zero_cost(input: Input) -> Output;
905}
906
907/// Zero-cost feature index conversion
908impl<const N: usize> ZeroCostTransform<FeatureIndex<N>, usize> for () {
909    fn transform_zero_cost(input: FeatureIndex<N>) -> usize {
910        input.get()
911    }
912}
913
914/// Zero-cost feature mask conversion
915impl<const N: usize> ZeroCostTransform<FeatureMask<N>, Vec<bool>> for () {
916    fn transform_zero_cost(input: FeatureMask<N>) -> Vec<bool> {
917        input.as_array().to_vec()
918    }
919}
920
921/// Compile-time feature count validator
922pub struct FeatureCountValidator<const EXPECTED: usize>;
923
924impl<const EXPECTED: usize> FeatureCountValidator<EXPECTED> {
925    /// Validate feature count at compile time
926    pub const fn validate<const ACTUAL: usize>() -> bool {
927        EXPECTED == ACTUAL
928    }
929
930    /// Validate and convert feature matrix type
931    pub fn validate_matrix<T>(matrix: FeatureMatrix<T, EXPECTED>) -> FeatureMatrix<T, EXPECTED>
932    where
933        T: Clone + Default,
934    {
935        matrix
936    }
937}
938
939/// Type-safe feature selection trait bounds
940pub trait TypeSafeFeatureSelection {
941    /// The feature matrix type
942    type FeatureMatrix;
943
944    /// The selection result type
945    type SelectionResult;
946
947    /// The number of input features (compile-time constant)
948    const INPUT_FEATURES: usize;
949
950    /// Perform feature selection with compile-time guarantees
951    fn select_features_typed(data: Self::FeatureMatrix) -> Result<Self::SelectionResult>;
952}
953
954/// Implementation macro for type-safe selectors
955#[macro_export]
956macro_rules! impl_type_safe_selector {
957    ($selector:ty, $method:ty, $n_features:expr, $n_selected:expr) => {
958        impl TypeSafeFeatureSelection for $selector {
959            type FeatureMatrix = FeatureMatrix<f64, $n_features>;
960            type SelectionResult = FeatureMatrix<f64, $n_selected>;
961            const INPUT_FEATURES: usize = $n_features;
962
963            fn select_features_typed(data: Self::FeatureMatrix) -> Result<Self::SelectionResult> {
964                // Default implementation using variance threshold
965                // This can be overridden by implementing the trait directly
966                use $crate::type_safe::VarianceThresholdSelector;
967
968                let mut selector = VarianceThresholdSelector::<$n_features>::new(0.0);
969                let mask = selector.fit(&data)?;
970
971                // Verify we have the expected number of selected features
972                if mask.count_selected() != $n_selected {
973                    return Err(SklearsError::InvalidInput(format!(
974                        "Expected {} selected features, got {}. Consider adjusting selection parameters.",
975                        $n_selected,
976                        mask.count_selected()
977                    )));
978                }
979
980                data.select_features(&mask)
981            }
982        }
983    };
984}
985
986/// Const generic helper for computing binomial coefficients at compile time
987pub const fn binomial_coefficient(n: usize, k: usize) -> usize {
988    if k > n {
989        0
990    } else if k == 0 || k == n {
991        1
992    } else {
993        let k = if k > n - k { n - k } else { k };
994        let mut result = 1;
995        let mut i = 0;
996        while i < k {
997            result = result * (n - i) / (i + 1);
998            i += 1;
999        }
1000        result
1001    }
1002}
1003
1004/// Compile-time validation that selection count is valid
1005pub const fn validate_selection_count<const N_FEATURES: usize, const K: usize>() -> bool {
1006    K <= N_FEATURES && K > 0
1007}
1008
1009/// Type-level boolean for compile-time feature validation
1010pub trait TypeBool {
1011    /// VALUE
1012    const VALUE: bool;
1013}
1014
1015/// True
1016pub struct True;
1017/// False
1018pub struct False;
1019
1020impl TypeBool for True {
1021    const VALUE: bool = true;
1022}
1023
1024impl TypeBool for False {
1025    const VALUE: bool = false;
1026}
1027
1028// Note: Advanced type-level programming features commented out due to requiring unstable Rust features
1029// These would be enabled once const generics operations and inherent associated types are stabilized
1030
1031// /// Compile-time assertion for feature selection validity
1032// pub type Assert<T> = <T as TypeBool>::Value;
1033
1034// pub trait TypeBoolTrait {
1035//     type Value: TypeBool;
1036// }
1037
1038// /// Feature selection validity checker
1039// pub struct FeatureSelectionValid<const N_FEATURES: usize, const K: usize>;
1040
1041// /// Conditional type selection (requires unstable features)
1042// pub type If<const CONDITION: bool, T, F> = IfImpl<{ CONDITION }, T, F>::Type;
1043
1044#[allow(non_snake_case)]
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048    use scirs2_core::ndarray::array;
1049
1050    #[test]
1051    fn test_feature_index() {
1052        const MAX_FEATURES: usize = 10;
1053
1054        // Valid index
1055        let valid_index = FeatureIndex::<MAX_FEATURES>::new(5).expect("operation should succeed");
1056        assert_eq!(valid_index.get(), 5);
1057
1058        // Invalid index
1059        assert!(FeatureIndex::<MAX_FEATURES>::new(15).is_none());
1060    }
1061
1062    #[test]
1063    fn test_feature_mask() {
1064        const N_FEATURES: usize = 5;
1065
1066        let mask = FeatureMask::<N_FEATURES>::from_array([true, false, true, false, true]);
1067        assert_eq!(mask.count_selected(), 3);
1068
1069        let indices = mask.selected_indices();
1070        assert_eq!(indices.len(), 3);
1071    }
1072
1073    #[test]
1074    fn test_feature_matrix() -> Result<()> {
1075        const N_FEATURES: usize = 3;
1076
1077        let data = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1078        let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
1079
1080        assert_eq!(matrix.n_features(), 3);
1081        assert_eq!(matrix.n_samples(), 2);
1082
1083        Ok(())
1084    }
1085
1086    #[test]
1087    fn test_variance_threshold_selector() -> Result<()> {
1088        const N_FEATURES: usize = 3;
1089
1090        let data = array![[1.0, 2.0, 3.0], [1.1, 5.0, 3.1], [0.9, 8.0, 2.9]];
1091        let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
1092
1093        let mut selector = VarianceThresholdSelector::new(0.1);
1094        let mask = selector.fit(&matrix)?;
1095
1096        // Should select features with variance > 0.1
1097        assert!(mask.count_selected() > 0);
1098
1099        Ok(())
1100    }
1101
1102    #[test]
1103    fn test_compile_time_validation() {
1104        const N_FEATURES: usize = 10;
1105        const K: usize = 5;
1106
1107        // This should compile
1108        assert!(validate_selection_count::<N_FEATURES, K>());
1109
1110        // This should not compile if uncommented:
1111        // assert!(validate_selection_count::<5, 10>());
1112    }
1113
1114    #[test]
1115    fn test_type_safe_pipeline() -> Result<()> {
1116        const N_FEATURES: usize = 4;
1117
1118        let data = array![
1119            [1.0, 2.0, 3.0, 4.0],
1120            [1.1, 5.0, 3.1, 4.1],
1121            [0.9, 8.0, 2.9, 3.9],
1122            [1.2, 2.1, 3.2, 4.2]
1123        ];
1124        let matrix = FeatureMatrix::<f64, N_FEATURES>::new(data)?;
1125        let y = array![0.0, 1.0, 0.0, 1.0];
1126
1127        let pipeline = TypeSafeSelectionPipeline::<N_FEATURES>::new()
1128            .add_variance_threshold(0.01)
1129            .add_correlation_filter(0.9);
1130
1131        let fitted_pipeline = pipeline.fit(&matrix, y.view())?;
1132        assert!(fitted_pipeline.n_selected_features() > 0);
1133
1134        Ok(())
1135    }
1136}