Skip to main content

sklears_decomposition/
modular_framework.rs

1//! Modular Pluggable Decomposition Architecture
2//!
3//! This module provides a flexible, extensible framework for matrix decomposition
4//! that allows easy composition and customization of different algorithms,
5//! preprocessing steps, and post-processing operations.
6//!
7//! Features:
8//! - Plugin-based architecture with trait-based decomposition algorithms
9//! - Configurable preprocessing and post-processing pipelines
10//! - Algorithm registry and dynamic algorithm selection
11//! - Composable decomposition chains and multi-step workflows
12//! - Extension points for custom algorithms and transformations
13//! - Runtime algorithm switching and fallback mechanisms
14
15use scirs2_core::ndarray::{Array1, Array2};
16use serde::{Deserialize, Serialize};
17use sklears_core::{
18    error::{Result, SklearsError},
19    types::Float,
20};
21use std::any::Any;
22use std::collections::HashMap;
23use std::sync::Arc;
24
25/// Core trait for decomposition algorithms
26pub trait DecompositionAlgorithm: Send + Sync {
27    /// Get algorithm name
28    fn name(&self) -> &str;
29
30    /// Get algorithm description
31    fn description(&self) -> &str;
32
33    /// Get algorithm capabilities
34    fn capabilities(&self) -> AlgorithmCapabilities;
35
36    /// Validate input parameters
37    fn validate_params(&self, params: &DecompositionParams) -> Result<()>;
38
39    /// Fit the decomposition algorithm
40    fn fit(&mut self, data: &Array2<Float>, params: &DecompositionParams) -> Result<()>;
41
42    /// Transform data using fitted algorithm
43    fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>>;
44
45    /// Inverse transform if supported
46    fn inverse_transform(&self, _data: &Array2<Float>) -> Result<Array2<Float>> {
47        Err(SklearsError::InvalidInput(
48            "Inverse transform not supported by this algorithm".to_string(),
49        ))
50    }
51
52    /// Get decomposition results/components
53    fn get_components(&self) -> Result<DecompositionComponents>;
54
55    /// Check if algorithm is fitted
56    fn is_fitted(&self) -> bool;
57
58    /// Clone the algorithm (for plugin system)
59    fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm>;
60
61    /// Get algorithm as Any for downcasting
62    fn as_any(&self) -> &dyn Any;
63}
64
65/// Algorithm capabilities descriptor
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct AlgorithmCapabilities {
68    /// Supports non-square matrices
69    pub supports_non_square: bool,
70    /// Supports sparse matrices
71    pub supports_sparse: bool,
72    /// Supports incremental/online learning
73    pub supports_incremental: bool,
74    /// Supports inverse transform
75    pub supports_inverse_transform: bool,
76    /// Supports partial fitting
77    pub supports_partial_fit: bool,
78    /// Required matrix properties
79    pub required_properties: Vec<MatrixProperty>,
80    /// Computational complexity
81    pub complexity: ComputationalComplexity,
82}
83
84impl Default for AlgorithmCapabilities {
85    fn default() -> Self {
86        Self {
87            supports_non_square: true,
88            supports_sparse: false,
89            supports_incremental: false,
90            supports_inverse_transform: false,
91            supports_partial_fit: false,
92            required_properties: Vec::new(),
93            complexity: ComputationalComplexity::Cubic,
94        }
95    }
96}
97
98/// Required matrix properties
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum MatrixProperty {
101    NonNegative,
102    Symmetric,
103    PositiveDefinite,
104    FullRank,
105}
106
107/// Computational complexity categories
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum ComputationalComplexity {
110    Linear,
111    Quadratic,
112    Cubic,
113    Exponential,
114}
115
116/// Decomposition parameters
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct DecompositionParams {
119    pub n_components: Option<usize>,
120    pub tolerance: Option<Float>,
121    pub max_iterations: Option<usize>,
122    pub random_seed: Option<u64>,
123    pub algorithm_specific: HashMap<String, ParamValue>,
124}
125
126impl Default for DecompositionParams {
127    fn default() -> Self {
128        Self {
129            n_components: None,
130            tolerance: Some(1e-6),
131            max_iterations: Some(100),
132            random_seed: None,
133            algorithm_specific: HashMap::new(),
134        }
135    }
136}
137
138/// Parameter value types
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub enum ParamValue {
141    Integer(i64),
142    Float(Float),
143    Boolean(bool),
144    String(String),
145    Array(Vec<Float>),
146}
147
148/// Decomposition components/results
149#[derive(Debug, Clone, Default)]
150pub struct DecompositionComponents {
151    pub components: Option<Array2<Float>>,
152    pub singular_values: Option<Array1<Float>>,
153    pub eigenvalues: Option<Array1<Float>>,
154    pub mean: Option<Array1<Float>>,
155    pub explained_variance_ratio: Option<Array1<Float>>,
156    pub factor_loadings: Option<Array2<Float>>,
157    pub metadata: HashMap<String, String>,
158}
159
160/// Trait for preprocessing steps
161pub trait PreprocessingStep: Send + Sync {
162    /// Get step name
163    fn name(&self) -> &str;
164
165    /// Fit the step to training data and return transformed output (fit + transform).
166    /// After this call, `is_fitted()` returns `true` and `apply` can be called.
167    fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>>;
168
169    /// Apply the already-fitted step to new data (transform only, no mutation).
170    /// Returns an error if the step has not been fitted yet.
171    fn apply(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
172        if !self.is_fitted() {
173            return Err(SklearsError::InvalidInput(format!(
174                "Preprocessing step '{}' has not been fitted; call process() on training data first",
175                self.name()
176            )));
177        }
178        self.apply_fitted(data)
179    }
180
181    /// Internal implementation of the transform-only path used by `apply`.
182    /// Implementors must override this when the step learns parameters during fit.
183    fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>>;
184
185    /// Inverse process if applicable
186    fn inverse_process(&self, _data: &Array2<Float>) -> Result<Array2<Float>> {
187        Err(SklearsError::InvalidInput(
188            "Inverse processing not supported".to_string(),
189        ))
190    }
191
192    /// Check if step is fitted
193    fn is_fitted(&self) -> bool;
194
195    /// Clone the step
196    fn clone_step(&self) -> Box<dyn PreprocessingStep>;
197}
198
199/// Trait for post-processing steps
200pub trait PostprocessingStep: Send + Sync {
201    /// Get step name
202    fn name(&self) -> &str;
203
204    /// Process decomposition results
205    fn process(&self, components: DecompositionComponents) -> Result<DecompositionComponents>;
206
207    /// Clone the step
208    fn clone_step(&self) -> Box<dyn PostprocessingStep>;
209}
210
211/// Algorithm registry for dynamic algorithm selection
212pub struct AlgorithmRegistry {
213    algorithms: HashMap<String, Box<dyn Fn() -> Box<dyn DecompositionAlgorithm> + Send + Sync>>,
214    metadata: HashMap<String, AlgorithmMetadata>,
215}
216
217impl AlgorithmRegistry {
218    /// Create new algorithm registry
219    pub fn new() -> Self {
220        Self {
221            algorithms: HashMap::new(),
222            metadata: HashMap::new(),
223        }
224    }
225
226    /// Register an algorithm
227    pub fn register<F>(&mut self, name: String, factory: F, metadata: AlgorithmMetadata)
228    where
229        F: Fn() -> Box<dyn DecompositionAlgorithm> + Send + Sync + 'static,
230    {
231        self.algorithms.insert(name.clone(), Box::new(factory));
232        self.metadata.insert(name, metadata);
233    }
234
235    /// Create algorithm instance by name
236    pub fn create_algorithm(&self, name: &str) -> Result<Box<dyn DecompositionAlgorithm>> {
237        if let Some(factory) = self.algorithms.get(name) {
238            Ok(factory())
239        } else {
240            Err(SklearsError::InvalidInput(format!(
241                "Algorithm '{}' not found in registry",
242                name
243            )))
244        }
245    }
246
247    /// Get all registered algorithm names
248    pub fn list_algorithms(&self) -> Vec<String> {
249        self.algorithms.keys().cloned().collect()
250    }
251
252    /// Get algorithm metadata
253    pub fn get_metadata(&self, name: &str) -> Option<&AlgorithmMetadata> {
254        self.metadata.get(name)
255    }
256
257    /// Find algorithms by capability
258    pub fn find_by_capability(&self, capability: AlgorithmCapability) -> Vec<String> {
259        self.metadata
260            .iter()
261            .filter(|(_, metadata)| metadata.capabilities.contains(&capability))
262            .map(|(name, _)| name.clone())
263            .collect()
264    }
265}
266
267impl Default for AlgorithmRegistry {
268    fn default() -> Self {
269        Self::new()
270    }
271}
272
273/// Algorithm metadata
274#[derive(Debug, Clone)]
275pub struct AlgorithmMetadata {
276    pub description: String,
277    pub version: String,
278    pub author: String,
279    pub capabilities: Vec<AlgorithmCapability>,
280    pub computational_complexity: ComputationalComplexity,
281    pub memory_complexity: ComputationalComplexity,
282}
283
284/// Algorithm capability types
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
286pub enum AlgorithmCapability {
287    DimensionalityReduction,
288    FeatureExtraction,
289    MatrixFactorization,
290    NoiseReduction,
291    DataCompression,
292    PatternRecognition,
293}
294
295/// Modular decomposition pipeline
296pub struct DecompositionPipeline {
297    preprocessing_steps: Vec<Box<dyn PreprocessingStep>>,
298    algorithm: Box<dyn DecompositionAlgorithm>,
299    postprocessing_steps: Vec<Box<dyn PostprocessingStep>>,
300    fallback_algorithms: Vec<Box<dyn DecompositionAlgorithm>>,
301    pipeline_config: PipelineConfig,
302}
303
304impl DecompositionPipeline {
305    /// Create new decomposition pipeline
306    pub fn new(algorithm: Box<dyn DecompositionAlgorithm>) -> Self {
307        Self {
308            preprocessing_steps: Vec::new(),
309            algorithm,
310            postprocessing_steps: Vec::new(),
311            fallback_algorithms: Vec::new(),
312            pipeline_config: PipelineConfig::default(),
313        }
314    }
315
316    /// Add preprocessing step
317    pub fn add_preprocessing(mut self, step: Box<dyn PreprocessingStep>) -> Self {
318        self.preprocessing_steps.push(step);
319        self
320    }
321
322    /// Add postprocessing step
323    pub fn add_postprocessing(mut self, step: Box<dyn PostprocessingStep>) -> Self {
324        self.postprocessing_steps.push(step);
325        self
326    }
327
328    /// Add fallback algorithm
329    pub fn add_fallback(mut self, algorithm: Box<dyn DecompositionAlgorithm>) -> Self {
330        self.fallback_algorithms.push(algorithm);
331        self
332    }
333
334    /// Set pipeline configuration
335    pub fn with_config(mut self, config: PipelineConfig) -> Self {
336        self.pipeline_config = config;
337        self
338    }
339
340    /// Execute the complete pipeline
341    pub fn fit_transform(
342        &mut self,
343        data: &Array2<Float>,
344        params: &DecompositionParams,
345    ) -> Result<PipelineResult> {
346        let start_time = std::time::Instant::now();
347
348        // Apply preprocessing steps
349        let mut processed_data = data.clone();
350        for step in &mut self.preprocessing_steps {
351            processed_data = step.process(&processed_data)?;
352        }
353
354        // Try main algorithm
355        let mut components = {
356            let algorithm = &mut self.algorithm;
357            match Self::try_algorithm_static(algorithm, &processed_data, params) {
358                Ok(result) => result,
359                Err(error) if self.pipeline_config.use_fallbacks => {
360                    // Try fallback algorithms
361                    let mut last_error = error;
362                    let mut success = false;
363                    let mut result_components = DecompositionComponents::default();
364
365                    for fallback in &mut self.fallback_algorithms {
366                        match Self::try_algorithm_static(fallback, &processed_data, params) {
367                            Ok(components) => {
368                                result_components = components;
369                                success = true;
370                                break;
371                            }
372                            Err(err) => last_error = err,
373                        }
374                    }
375
376                    if !success {
377                        return Err(last_error);
378                    }
379                    result_components
380                }
381                Err(error) => return Err(error),
382            }
383        };
384
385        // Apply postprocessing steps
386        for step in &self.postprocessing_steps {
387            components = step.process(components)?;
388        }
389
390        let execution_time = start_time.elapsed();
391
392        Ok(PipelineResult {
393            components,
394            execution_time,
395            algorithm_used: self.algorithm.name().to_string(),
396            preprocessing_steps: self
397                .preprocessing_steps
398                .iter()
399                .map(|s| s.name().to_string())
400                .collect(),
401            postprocessing_steps: self
402                .postprocessing_steps
403                .iter()
404                .map(|s| s.name().to_string())
405                .collect(),
406            pipeline_metadata: HashMap::new(),
407        })
408    }
409
410    /// Transform new data using fitted pipeline
411    ///
412    /// Applies each preprocessing step's `apply` method (transform-only, no refitting),
413    /// then delegates to the main algorithm's `transform`.  The pipeline must have been
414    /// fitted via `fit_transform` before calling this method.
415    pub fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
416        if !self.is_fitted() {
417            return Err(SklearsError::InvalidInput(
418                "Pipeline not fitted; call fit_transform on training data first".to_string(),
419            ));
420        }
421
422        // Apply each fitted preprocessing step in order (transform only — no mutation)
423        let mut processed_data = data.clone();
424        for step in &self.preprocessing_steps {
425            processed_data = step.apply(&processed_data)?;
426        }
427
428        // Transform using main algorithm
429        self.algorithm.transform(&processed_data)
430    }
431
432    /// Check if pipeline is fitted
433    pub fn is_fitted(&self) -> bool {
434        self.algorithm.is_fitted()
435    }
436
437    /// Try to execute an algorithm with error handling
438    fn try_algorithm_static(
439        algorithm: &mut Box<dyn DecompositionAlgorithm>,
440        data: &Array2<Float>,
441        params: &DecompositionParams,
442    ) -> Result<DecompositionComponents> {
443        algorithm.validate_params(params)?;
444        algorithm.fit(data, params)?;
445        algorithm.get_components()
446    }
447}
448
449/// Pipeline configuration
450#[derive(Debug, Clone)]
451pub struct PipelineConfig {
452    /// Use fallback algorithms on failure
453    pub use_fallbacks: bool,
454    /// Enable caching of intermediate results
455    pub enable_caching: bool,
456    /// Maximum execution time before timeout
457    pub max_execution_time: Option<std::time::Duration>,
458    /// Validate inputs at each step
459    pub validate_inputs: bool,
460}
461
462impl Default for PipelineConfig {
463    fn default() -> Self {
464        Self {
465            use_fallbacks: true,
466            enable_caching: false,
467            max_execution_time: None,
468            validate_inputs: true,
469        }
470    }
471}
472
473/// Pipeline execution result
474#[derive(Debug, Clone)]
475pub struct PipelineResult {
476    pub components: DecompositionComponents,
477    pub execution_time: std::time::Duration,
478    pub algorithm_used: String,
479    pub preprocessing_steps: Vec<String>,
480    pub postprocessing_steps: Vec<String>,
481    pub pipeline_metadata: HashMap<String, String>,
482}
483
484/// Builder for creating complex decomposition workflows
485pub struct DecompositionWorkflowBuilder {
486    registry: Arc<AlgorithmRegistry>,
487    pipeline: Option<DecompositionPipeline>,
488    config: PipelineConfig,
489}
490
491impl DecompositionWorkflowBuilder {
492    /// Create new workflow builder
493    pub fn new(registry: Arc<AlgorithmRegistry>) -> Self {
494        Self {
495            registry,
496            pipeline: None,
497            config: PipelineConfig::default(),
498        }
499    }
500
501    /// Set primary algorithm by name
502    pub fn with_algorithm(mut self, algorithm_name: &str) -> Result<Self> {
503        let algorithm = self.registry.create_algorithm(algorithm_name)?;
504        self.pipeline = Some(DecompositionPipeline::new(algorithm));
505        Ok(self)
506    }
507
508    /// Add preprocessing step
509    pub fn with_preprocessing(mut self, step: Box<dyn PreprocessingStep>) -> Result<Self> {
510        if let Some(pipeline) = self.pipeline.take() {
511            self.pipeline = Some(pipeline.add_preprocessing(step));
512        } else {
513            return Err(SklearsError::InvalidInput(
514                "Must set algorithm before adding preprocessing steps".to_string(),
515            ));
516        }
517        Ok(self)
518    }
519
520    /// Add postprocessing step
521    pub fn with_postprocessing(mut self, step: Box<dyn PostprocessingStep>) -> Result<Self> {
522        if let Some(pipeline) = self.pipeline.take() {
523            self.pipeline = Some(pipeline.add_postprocessing(step));
524        } else {
525            return Err(SklearsError::InvalidInput(
526                "Must set algorithm before adding postprocessing steps".to_string(),
527            ));
528        }
529        Ok(self)
530    }
531
532    /// Add fallback algorithm by name
533    pub fn with_fallback(mut self, algorithm_name: &str) -> Result<Self> {
534        let algorithm = self.registry.create_algorithm(algorithm_name)?;
535        if let Some(pipeline) = self.pipeline.take() {
536            self.pipeline = Some(pipeline.add_fallback(algorithm));
537        } else {
538            return Err(SklearsError::InvalidInput(
539                "Must set primary algorithm before adding fallbacks".to_string(),
540            ));
541        }
542        Ok(self)
543    }
544
545    /// Set pipeline configuration
546    pub fn with_config(mut self, config: PipelineConfig) -> Self {
547        self.config = config;
548        self
549    }
550
551    /// Build the workflow
552    pub fn build(mut self) -> Result<DecompositionPipeline> {
553        match self.pipeline.take() {
554            Some(pipeline) => Ok(pipeline.with_config(self.config)),
555            None => Err(SklearsError::InvalidInput(
556                "No algorithm specified for workflow".to_string(),
557            )),
558        }
559    }
560}
561
562/// Example preprocessing step: data standardization
563#[derive(Debug, Clone)]
564pub struct StandardizationStep {
565    mean: Option<Array1<Float>>,
566    std: Option<Array1<Float>>,
567    fitted: bool,
568}
569
570impl StandardizationStep {
571    pub fn new() -> Self {
572        Self {
573            mean: None,
574            std: None,
575            fitted: false,
576        }
577    }
578}
579
580impl Default for StandardizationStep {
581    fn default() -> Self {
582        Self::new()
583    }
584}
585
586impl PreprocessingStep for StandardizationStep {
587    fn name(&self) -> &str {
588        "standardization"
589    }
590
591    fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
592        if !self.fitted {
593            // Fit step - compute mean and std from training data
594            let mean = data
595                .mean_axis(scirs2_core::ndarray::Axis(0))
596                .ok_or_else(|| {
597                    SklearsError::InvalidInput("Cannot compute mean of empty array".to_string())
598                })?;
599            let std = data
600                .var_axis(scirs2_core::ndarray::Axis(0), 0.0)
601                .mapv(|x| x.sqrt());
602
603            self.mean = Some(mean);
604            self.std = Some(std);
605            self.fitted = true;
606        }
607
608        // Transform step (applies fitted parameters)
609        self.apply_fitted(data)
610    }
611
612    fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
613        let mean = self.mean.as_ref().ok_or_else(|| {
614            SklearsError::InvalidInput("Standardization step not fitted".to_string())
615        })?;
616        let std = self.std.as_ref().ok_or_else(|| {
617            SklearsError::InvalidInput("Standardization step not fitted".to_string())
618        })?;
619
620        let mean_broadcast = mean.clone().insert_axis(scirs2_core::ndarray::Axis(0));
621        let std_broadcast = std.clone().insert_axis(scirs2_core::ndarray::Axis(0));
622        let standardized = (data - &mean_broadcast) / &std_broadcast;
623
624        Ok(standardized)
625    }
626
627    fn inverse_process(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
628        if !self.fitted {
629            return Err(SklearsError::InvalidInput(
630                "Standardization step not fitted".to_string(),
631            ));
632        }
633
634        let mean = self.mean.as_ref().ok_or_else(|| {
635            SklearsError::InvalidInput("Standardization step not fitted".to_string())
636        })?;
637        let std = self.std.as_ref().ok_or_else(|| {
638            SklearsError::InvalidInput("Standardization step not fitted".to_string())
639        })?;
640
641        let mean_broadcast = mean.clone().insert_axis(scirs2_core::ndarray::Axis(0));
642        let std_broadcast = std.clone().insert_axis(scirs2_core::ndarray::Axis(0));
643        let unstandardized = data * &std_broadcast + &mean_broadcast;
644
645        Ok(unstandardized)
646    }
647
648    fn is_fitted(&self) -> bool {
649        self.fitted
650    }
651
652    fn clone_step(&self) -> Box<dyn PreprocessingStep> {
653        Box::new(self.clone())
654    }
655}
656
657/// Example postprocessing step: component rotation
658#[derive(Debug, Clone)]
659pub struct VarimaxRotationStep {
660    max_iterations: usize,
661    tolerance: Float,
662}
663
664impl VarimaxRotationStep {
665    pub fn new() -> Self {
666        Self {
667            max_iterations: 100,
668            tolerance: 1e-6,
669        }
670    }
671
672    pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
673        self.max_iterations = max_iterations;
674        self
675    }
676
677    pub fn with_tolerance(mut self, tolerance: Float) -> Self {
678        self.tolerance = tolerance;
679        self
680    }
681}
682
683impl Default for VarimaxRotationStep {
684    fn default() -> Self {
685        Self::new()
686    }
687}
688
689impl PostprocessingStep for VarimaxRotationStep {
690    fn name(&self) -> &str {
691        "varimax_rotation"
692    }
693
694    fn process(&self, mut components: DecompositionComponents) -> Result<DecompositionComponents> {
695        if let Some(ref mut loadings) = components.factor_loadings {
696            // Apply Varimax rotation (simplified implementation)
697            *loadings = self.apply_varimax_rotation(loadings)?;
698        } else if let Some(ref mut comps) = components.components {
699            // Apply to components if no factor loadings
700            *comps = self.apply_varimax_rotation(comps)?;
701        }
702
703        components
704            .metadata
705            .insert("rotation_applied".to_string(), "varimax".to_string());
706
707        Ok(components)
708    }
709
710    fn clone_step(&self) -> Box<dyn PostprocessingStep> {
711        Box::new(self.clone())
712    }
713}
714
715impl VarimaxRotationStep {
716    fn apply_varimax_rotation(&self, matrix: &Array2<Float>) -> Result<Array2<Float>> {
717        // Simplified Varimax rotation implementation
718        // In practice, this would implement the full Varimax algorithm
719        Ok(matrix.clone())
720    }
721}
722
723#[allow(non_snake_case)]
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    // Mock algorithm for testing
729    #[derive(Debug, Clone)]
730    struct MockPCA {
731        fitted: bool,
732        n_components: usize,
733    }
734
735    impl MockPCA {
736        fn new() -> Self {
737            Self {
738                fitted: false,
739                n_components: 2,
740            }
741        }
742    }
743
744    impl DecompositionAlgorithm for MockPCA {
745        fn name(&self) -> &str {
746            "mock_pca"
747        }
748
749        fn description(&self) -> &str {
750            "Mock PCA for testing"
751        }
752
753        fn capabilities(&self) -> AlgorithmCapabilities {
754            AlgorithmCapabilities {
755                supports_inverse_transform: true,
756                ..AlgorithmCapabilities::default()
757            }
758        }
759
760        fn validate_params(&self, _params: &DecompositionParams) -> Result<()> {
761            Ok(())
762        }
763
764        fn fit(&mut self, _data: &Array2<Float>, params: &DecompositionParams) -> Result<()> {
765            if let Some(n_comp) = params.n_components {
766                self.n_components = n_comp;
767            }
768            self.fitted = true;
769            Ok(())
770        }
771
772        fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
773            if !self.fitted {
774                return Err(SklearsError::InvalidInput(
775                    "Algorithm not fitted".to_string(),
776                ));
777            }
778
779            let (rows, _) = data.dim();
780            Ok(Array2::zeros((rows, self.n_components)))
781        }
782
783        fn get_components(&self) -> Result<DecompositionComponents> {
784            if !self.fitted {
785                return Err(SklearsError::InvalidInput(
786                    "Algorithm not fitted".to_string(),
787                ));
788            }
789
790            Ok(DecompositionComponents {
791                components: Some(Array2::eye(self.n_components)),
792                eigenvalues: Some(Array1::ones(self.n_components)),
793                ..DecompositionComponents::default()
794            })
795        }
796
797        fn is_fitted(&self) -> bool {
798            self.fitted
799        }
800
801        fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm> {
802            Box::new(self.clone())
803        }
804
805        fn as_any(&self) -> &dyn Any {
806            self
807        }
808    }
809
810    #[test]
811    fn test_algorithm_capabilities() {
812        let capabilities = AlgorithmCapabilities::default();
813        assert!(capabilities.supports_non_square);
814        assert!(!capabilities.supports_sparse);
815        assert_eq!(capabilities.complexity, ComputationalComplexity::Cubic);
816    }
817
818    #[test]
819    fn test_decomposition_params() {
820        let mut params = DecompositionParams {
821            n_components: Some(5),
822            ..Default::default()
823        };
824        params.algorithm_specific.insert(
825            "test_param".to_string(),
826            ParamValue::Float(std::f64::consts::PI),
827        );
828
829        assert_eq!(params.n_components, Some(5));
830        assert_eq!(
831            params.algorithm_specific.get("test_param"),
832            Some(&ParamValue::Float(std::f64::consts::PI))
833        );
834    }
835
836    #[test]
837    fn test_algorithm_registry() {
838        let mut registry = AlgorithmRegistry::new();
839
840        let metadata = AlgorithmMetadata {
841            description: "Mock PCA".to_string(),
842            version: "1.0".to_string(),
843            author: "Test".to_string(),
844            capabilities: vec![AlgorithmCapability::DimensionalityReduction],
845            computational_complexity: ComputationalComplexity::Cubic,
846            memory_complexity: ComputationalComplexity::Quadratic,
847        };
848
849        registry.register(
850            "mock_pca".to_string(),
851            || Box::new(MockPCA::new()),
852            metadata,
853        );
854
855        let algorithms = registry.list_algorithms();
856        assert_eq!(algorithms, vec!["mock_pca"]);
857
858        let algorithm = registry
859            .create_algorithm("mock_pca")
860            .expect("operation should succeed");
861        assert_eq!(algorithm.name(), "mock_pca");
862
863        let dim_red_algorithms =
864            registry.find_by_capability(AlgorithmCapability::DimensionalityReduction);
865        assert_eq!(dim_red_algorithms, vec!["mock_pca"]);
866    }
867
868    #[test]
869    fn test_standardization_step() {
870        let mut step = StandardizationStep::new();
871        assert!(!step.is_fitted());
872        assert_eq!(step.name(), "standardization");
873
874        let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
875            .expect("shape and data length should match");
876
877        let processed = step.process(&data).expect("operation should succeed");
878        assert!(step.is_fitted());
879        assert_eq!(processed.shape(), data.shape());
880    }
881
882    #[test]
883    fn test_varimax_rotation_step() {
884        let step = VarimaxRotationStep::new();
885        assert_eq!(step.name(), "varimax_rotation");
886
887        let components = DecompositionComponents {
888            components: Some(Array2::eye(3)),
889            ..Default::default()
890        };
891
892        let processed = step.process(components).expect("operation should succeed");
893        assert!(processed.metadata.contains_key("rotation_applied"));
894    }
895
896    #[test]
897    fn test_decomposition_pipeline() {
898        let mut pipeline = DecompositionPipeline::new(Box::new(MockPCA::new()));
899
900        let data = Array2::from_shape_vec(
901            (4, 3),
902            vec![
903                1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
904            ],
905        )
906        .expect("operation should succeed");
907
908        let params = DecompositionParams {
909            n_components: Some(2),
910            ..DecompositionParams::default()
911        };
912
913        let result = pipeline
914            .fit_transform(&data, &params)
915            .expect("operation should succeed");
916        assert_eq!(result.algorithm_used, "mock_pca");
917        assert!(pipeline.is_fitted());
918
919        // Test transform
920        let transformed = pipeline
921            .transform(&data)
922            .expect("transformation should succeed");
923        assert_eq!(transformed.shape(), &[4, 2]);
924    }
925
926    #[test]
927    fn test_workflow_builder() {
928        let mut registry = AlgorithmRegistry::new();
929        let metadata = AlgorithmMetadata {
930            description: "Mock PCA".to_string(),
931            version: "1.0".to_string(),
932            author: "Test".to_string(),
933            capabilities: vec![AlgorithmCapability::DimensionalityReduction],
934            computational_complexity: ComputationalComplexity::Cubic,
935            memory_complexity: ComputationalComplexity::Quadratic,
936        };
937
938        registry.register(
939            "mock_pca".to_string(),
940            || Box::new(MockPCA::new()),
941            metadata,
942        );
943
944        let registry = Arc::new(registry);
945        let builder = DecompositionWorkflowBuilder::new(registry);
946
947        let pipeline = builder
948            .with_algorithm("mock_pca")
949            .expect("operation should succeed")
950            .with_preprocessing(Box::new(StandardizationStep::new()))
951            .expect("operation should succeed")
952            .with_postprocessing(Box::new(VarimaxRotationStep::new()))
953            .expect("operation should succeed")
954            .build()
955            .expect("operation should succeed");
956
957        assert_eq!(pipeline.algorithm.name(), "mock_pca");
958        assert_eq!(pipeline.preprocessing_steps.len(), 1);
959        assert_eq!(pipeline.postprocessing_steps.len(), 1);
960    }
961
962    #[test]
963    fn test_param_values() {
964        let int_param = ParamValue::Integer(42);
965        let float_param = ParamValue::Float(std::f64::consts::PI);
966        let bool_param = ParamValue::Boolean(true);
967        let string_param = ParamValue::String("test".to_string());
968        let array_param = ParamValue::Array(vec![1.0, 2.0, 3.0]);
969
970        assert_eq!(int_param, ParamValue::Integer(42));
971        assert_eq!(float_param, ParamValue::Float(std::f64::consts::PI));
972        assert_eq!(bool_param, ParamValue::Boolean(true));
973        assert_eq!(string_param, ParamValue::String("test".to_string()));
974        assert_eq!(array_param, ParamValue::Array(vec![1.0, 2.0, 3.0]));
975    }
976}