1use 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
25pub trait DecompositionAlgorithm: Send + Sync {
27 fn name(&self) -> &str;
29
30 fn description(&self) -> &str;
32
33 fn capabilities(&self) -> AlgorithmCapabilities;
35
36 fn validate_params(&self, params: &DecompositionParams) -> Result<()>;
38
39 fn fit(&mut self, data: &Array2<Float>, params: &DecompositionParams) -> Result<()>;
41
42 fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>>;
44
45 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 fn get_components(&self) -> Result<DecompositionComponents>;
54
55 fn is_fitted(&self) -> bool;
57
58 fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm>;
60
61 fn as_any(&self) -> &dyn Any;
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct AlgorithmCapabilities {
68 pub supports_non_square: bool,
70 pub supports_sparse: bool,
72 pub supports_incremental: bool,
74 pub supports_inverse_transform: bool,
76 pub supports_partial_fit: bool,
78 pub required_properties: Vec<MatrixProperty>,
80 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum MatrixProperty {
101 NonNegative,
102 Symmetric,
103 PositiveDefinite,
104 FullRank,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum ComputationalComplexity {
110 Linear,
111 Quadratic,
112 Cubic,
113 Exponential,
114}
115
116#[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#[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#[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
160pub trait PreprocessingStep: Send + Sync {
162 fn name(&self) -> &str;
164
165 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>>;
168
169 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 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>>;
184
185 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 fn is_fitted(&self) -> bool;
194
195 fn clone_step(&self) -> Box<dyn PreprocessingStep>;
197}
198
199pub trait PostprocessingStep: Send + Sync {
201 fn name(&self) -> &str;
203
204 fn process(&self, components: DecompositionComponents) -> Result<DecompositionComponents>;
206
207 fn clone_step(&self) -> Box<dyn PostprocessingStep>;
209}
210
211pub struct AlgorithmRegistry {
213 algorithms: HashMap<String, Box<dyn Fn() -> Box<dyn DecompositionAlgorithm> + Send + Sync>>,
214 metadata: HashMap<String, AlgorithmMetadata>,
215}
216
217impl AlgorithmRegistry {
218 pub fn new() -> Self {
220 Self {
221 algorithms: HashMap::new(),
222 metadata: HashMap::new(),
223 }
224 }
225
226 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 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 pub fn list_algorithms(&self) -> Vec<String> {
249 self.algorithms.keys().cloned().collect()
250 }
251
252 pub fn get_metadata(&self, name: &str) -> Option<&AlgorithmMetadata> {
254 self.metadata.get(name)
255 }
256
257 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
286pub enum AlgorithmCapability {
287 DimensionalityReduction,
288 FeatureExtraction,
289 MatrixFactorization,
290 NoiseReduction,
291 DataCompression,
292 PatternRecognition,
293}
294
295pub 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 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 pub fn add_preprocessing(mut self, step: Box<dyn PreprocessingStep>) -> Self {
318 self.preprocessing_steps.push(step);
319 self
320 }
321
322 pub fn add_postprocessing(mut self, step: Box<dyn PostprocessingStep>) -> Self {
324 self.postprocessing_steps.push(step);
325 self
326 }
327
328 pub fn add_fallback(mut self, algorithm: Box<dyn DecompositionAlgorithm>) -> Self {
330 self.fallback_algorithms.push(algorithm);
331 self
332 }
333
334 pub fn with_config(mut self, config: PipelineConfig) -> Self {
336 self.pipeline_config = config;
337 self
338 }
339
340 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 let mut processed_data = data.clone();
350 for step in &mut self.preprocessing_steps {
351 processed_data = step.process(&processed_data)?;
352 }
353
354 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 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 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 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 let mut processed_data = data.clone();
424 for step in &self.preprocessing_steps {
425 processed_data = step.apply(&processed_data)?;
426 }
427
428 self.algorithm.transform(&processed_data)
430 }
431
432 pub fn is_fitted(&self) -> bool {
434 self.algorithm.is_fitted()
435 }
436
437 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#[derive(Debug, Clone)]
451pub struct PipelineConfig {
452 pub use_fallbacks: bool,
454 pub enable_caching: bool,
456 pub max_execution_time: Option<std::time::Duration>,
458 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#[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
484pub struct DecompositionWorkflowBuilder {
486 registry: Arc<AlgorithmRegistry>,
487 pipeline: Option<DecompositionPipeline>,
488 config: PipelineConfig,
489}
490
491impl DecompositionWorkflowBuilder {
492 pub fn new(registry: Arc<AlgorithmRegistry>) -> Self {
494 Self {
495 registry,
496 pipeline: None,
497 config: PipelineConfig::default(),
498 }
499 }
500
501 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 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 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 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 pub fn with_config(mut self, config: PipelineConfig) -> Self {
547 self.config = config;
548 self
549 }
550
551 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#[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 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 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#[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 *loadings = self.apply_varimax_rotation(loadings)?;
698 } else if let Some(ref mut comps) = components.components {
699 *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 Ok(matrix.clone())
720 }
721}
722
723#[allow(non_snake_case)]
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[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, ¶ms)
915 .expect("operation should succeed");
916 assert_eq!(result.algorithm_used, "mock_pca");
917 assert!(pipeline.is_fitted());
918
919 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}