1use crate::modular_framework::{
15 AlgorithmCapabilities, DecompositionAlgorithm, DecompositionComponents, DecompositionParams,
16 ParamValue, PostprocessingStep, PreprocessingStep,
17};
18use crate::s;
19use scirs2_core::ndarray::{Array1, Array2};
20use serde::{Deserialize, Serialize};
21use sklears_core::{
22 error::{Result, SklearsError},
23 types::Float,
24};
25use std::any::Any;
26
27pub struct DecompositionPipelineBuilder {
29 preprocessing_steps: Vec<Box<dyn PreprocessingStep>>,
30 algorithm: Option<Box<dyn DecompositionAlgorithm>>,
31 postprocessing_steps: Vec<Box<dyn PostprocessingStep>>,
32 params: DecompositionParams,
33 config: PipelineConfig,
34}
35
36impl DecompositionPipelineBuilder {
37 pub fn new() -> Self {
39 Self {
40 preprocessing_steps: Vec::new(),
41 algorithm: None,
42 postprocessing_steps: Vec::new(),
43 params: DecompositionParams::default(),
44 config: PipelineConfig::default(),
45 }
46 }
47
48 pub fn from_preset(preset: DecompositionPreset) -> Self {
50 let builder = Self::new();
51
52 match preset {
53 DecompositionPreset::StandardPCA { n_components } => builder
54 .with_standardization()
55 .with_pca(n_components)
56 .with_variance_threshold(0.01),
57 DecompositionPreset::RobustPCA { n_components } => builder
58 .with_robust_scaling()
59 .with_robust_pca(n_components)
60 .with_outlier_removal(),
61 DecompositionPreset::SparsePCA {
62 n_components,
63 alpha,
64 } => builder
65 .with_standardization()
66 .with_sparse_pca(n_components, alpha)
67 .with_sparsity_constraint(),
68 DecompositionPreset::IncrementalPCA {
69 n_components,
70 batch_size,
71 } => builder
72 .with_batch_normalization(batch_size)
73 .with_incremental_pca(n_components, batch_size),
74 DecompositionPreset::KernelPCA {
75 n_components,
76 kernel,
77 } => builder
78 .with_standardization()
79 .with_kernel_pca(n_components, kernel),
80 DecompositionPreset::FastICA { n_components } => builder
81 .with_centering()
82 .with_whitening()
83 .with_fast_ica(n_components),
84 DecompositionPreset::NMF {
85 n_components,
86 init_method,
87 } => builder
88 .with_non_negative_scaling()
89 .with_nmf(n_components, init_method),
90 DecompositionPreset::FactorAnalysis { n_components } => builder
91 .with_standardization()
92 .with_factor_analysis(n_components)
93 .with_rotation("varimax"),
94 DecompositionPreset::TruncatedSVD { n_components } => {
95 builder.with_truncated_svd(n_components)
96 }
97 DecompositionPreset::TimeSeriesSSA {
98 window_length,
99 n_components,
100 } => builder
101 .with_time_series_preprocessing(window_length)
102 .with_ssa(window_length, n_components),
103 }
104 }
105
106 pub fn with_standardization(mut self) -> Self {
108 self.preprocessing_steps
109 .push(Box::new(StandardizationStep::new()));
110 self
111 }
112
113 pub fn with_robust_scaling(mut self) -> Self {
115 self.preprocessing_steps
116 .push(Box::new(RobustScalingStep::new()));
117 self
118 }
119
120 pub fn with_centering(mut self) -> Self {
122 self.preprocessing_steps
123 .push(Box::new(CenteringStep::new()));
124 self
125 }
126
127 pub fn with_whitening(mut self) -> Self {
129 self.preprocessing_steps
130 .push(Box::new(WhiteningStep::new()));
131 self
132 }
133
134 pub fn with_non_negative_scaling(mut self) -> Self {
136 self.preprocessing_steps
137 .push(Box::new(NonNegativeScalingStep::new()));
138 self
139 }
140
141 pub fn with_batch_normalization(mut self, batch_size: usize) -> Self {
143 self.preprocessing_steps
144 .push(Box::new(BatchNormalizationStep::new(batch_size)));
145 self
146 }
147
148 pub fn with_time_series_preprocessing(mut self, window_length: usize) -> Self {
150 self.preprocessing_steps
151 .push(Box::new(TimeSeriesPreprocessingStep::new(window_length)));
152 self
153 }
154
155 pub fn with_pca(mut self, n_components: usize) -> Self {
157 self.params.n_components = Some(n_components);
158 self.algorithm = Some(Box::new(PCAAlgorithm::new(n_components)));
159 self
160 }
161
162 pub fn with_robust_pca(mut self, n_components: usize) -> Self {
164 self.params.n_components = Some(n_components);
165 self.algorithm = Some(Box::new(RobustPCAAlgorithm::new(n_components)));
166 self
167 }
168
169 pub fn with_sparse_pca(mut self, n_components: usize, alpha: Float) -> Self {
171 self.params.n_components = Some(n_components);
172 self.params
173 .algorithm_specific
174 .insert("alpha".to_string(), ParamValue::Float(alpha));
175 self.algorithm = Some(Box::new(SparsePCAAlgorithm::new(n_components, alpha)));
176 self
177 }
178
179 pub fn with_incremental_pca(mut self, n_components: usize, batch_size: usize) -> Self {
181 self.params.n_components = Some(n_components);
182 self.params.algorithm_specific.insert(
183 "batch_size".to_string(),
184 ParamValue::Integer(batch_size as i64),
185 );
186 self.algorithm = Some(Box::new(IncrementalPCAAlgorithm::new(
187 n_components,
188 batch_size,
189 )));
190 self
191 }
192
193 pub fn with_kernel_pca(mut self, n_components: usize, kernel: String) -> Self {
195 self.params.n_components = Some(n_components);
196 self.params
197 .algorithm_specific
198 .insert("kernel".to_string(), ParamValue::String(kernel.clone()));
199 self.algorithm = Some(Box::new(KernelPCAAlgorithm::new(n_components, kernel)));
200 self
201 }
202
203 pub fn with_fast_ica(mut self, n_components: usize) -> Self {
205 self.params.n_components = Some(n_components);
206 self.algorithm = Some(Box::new(FastICAAlgorithm::new(n_components)));
207 self
208 }
209
210 pub fn with_nmf(mut self, n_components: usize, init_method: String) -> Self {
212 self.params.n_components = Some(n_components);
213 self.params.algorithm_specific.insert(
214 "init_method".to_string(),
215 ParamValue::String(init_method.clone()),
216 );
217 self.algorithm = Some(Box::new(NMFAlgorithm::new(n_components, init_method)));
218 self
219 }
220
221 pub fn with_factor_analysis(mut self, n_components: usize) -> Self {
223 self.params.n_components = Some(n_components);
224 self.algorithm = Some(Box::new(FactorAnalysisAlgorithm::new(n_components)));
225 self
226 }
227
228 pub fn with_truncated_svd(mut self, n_components: usize) -> Self {
230 self.params.n_components = Some(n_components);
231 self.algorithm = Some(Box::new(TruncatedSVDAlgorithm::new(n_components)));
232 self
233 }
234
235 pub fn with_ssa(mut self, window_length: usize, n_components: usize) -> Self {
237 self.params.n_components = Some(n_components);
238 self.params.algorithm_specific.insert(
239 "window_length".to_string(),
240 ParamValue::Integer(window_length as i64),
241 );
242 self.algorithm = Some(Box::new(SSAAlgorithm::new(window_length, n_components)));
243 self
244 }
245
246 pub fn with_variance_threshold(mut self, threshold: Float) -> Self {
248 self.postprocessing_steps
249 .push(Box::new(VarianceThresholdStep::new(threshold)));
250 self
251 }
252
253 pub fn with_outlier_removal(mut self) -> Self {
255 self.postprocessing_steps
256 .push(Box::new(OutlierRemovalStep::new()));
257 self
258 }
259
260 pub fn with_sparsity_constraint(mut self) -> Self {
262 self.postprocessing_steps
263 .push(Box::new(SparsityConstraintStep::new()));
264 self
265 }
266
267 pub fn with_rotation(mut self, method: &str) -> Self {
269 self.postprocessing_steps
270 .push(Box::new(RotationStep::new(method.to_string())));
271 self
272 }
273
274 pub fn n_components(mut self, n: usize) -> Self {
276 self.params.n_components = Some(n);
277 self
278 }
279
280 pub fn tolerance(mut self, tol: Float) -> Self {
282 self.params.tolerance = Some(tol);
283 self
284 }
285
286 pub fn max_iterations(mut self, max_iter: usize) -> Self {
288 self.params.max_iterations = Some(max_iter);
289 self
290 }
291
292 pub fn random_seed(mut self, seed: u64) -> Self {
294 self.params.random_seed = Some(seed);
295 self
296 }
297
298 pub fn verbose(mut self, verbose: bool) -> Self {
300 self.config.verbose = verbose;
301 self
302 }
303
304 pub fn auto_tune(mut self, enabled: bool) -> Self {
306 self.config.auto_tune = enabled;
307 self
308 }
309
310 pub fn validation_split(mut self, ratio: Float) -> Self {
312 self.config.validation_split = Some(ratio);
313 self
314 }
315
316 pub fn build(self) -> Result<DecompositionPipeline> {
318 if self.algorithm.is_none() {
319 return Err(SklearsError::InvalidInput(
320 "No decomposition algorithm specified".to_string(),
321 ));
322 }
323
324 Ok(DecompositionPipeline {
325 preprocessing_steps: self.preprocessing_steps,
326 algorithm: self.algorithm.expect("operation should succeed"),
327 postprocessing_steps: self.postprocessing_steps,
328 params: self.params,
329 config: self.config,
330 fitted: false,
331 cached_components: None,
332 })
333 }
334}
335
336impl Default for DecompositionPipelineBuilder {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub enum DecompositionPreset {
345 StandardPCA { n_components: usize },
347
348 RobustPCA { n_components: usize },
350
351 SparsePCA { n_components: usize, alpha: Float },
353
354 IncrementalPCA {
356 n_components: usize,
357 batch_size: usize,
358 },
359
360 KernelPCA { n_components: usize, kernel: String },
362
363 FastICA { n_components: usize },
365
366 NMF {
368 n_components: usize,
369 init_method: String,
370 },
371
372 FactorAnalysis { n_components: usize },
374
375 TruncatedSVD { n_components: usize },
377
378 TimeSeriesSSA {
380 window_length: usize,
381 n_components: usize,
382 },
383}
384
385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
387pub struct PipelineConfig {
388 pub verbose: bool,
389 pub auto_tune: bool,
390 pub validation_split: Option<Float>,
391 pub early_stopping: bool,
392 pub save_intermediate: bool,
393}
394
395pub struct DecompositionPipeline {
397 preprocessing_steps: Vec<Box<dyn PreprocessingStep>>,
398 algorithm: Box<dyn DecompositionAlgorithm>,
399 postprocessing_steps: Vec<Box<dyn PostprocessingStep>>,
400 params: DecompositionParams,
401 config: PipelineConfig,
402 fitted: bool,
403 cached_components: Option<DecompositionComponents>,
405}
406
407impl DecompositionPipeline {
408 pub fn fit(&mut self, data: &Array2<Float>) -> Result<&mut Self> {
410 if self.config.verbose {
411 println!("Starting decomposition pipeline...");
412 }
413
414 let mut preprocessed_data = data.clone();
416 for (idx, step) in self.preprocessing_steps.iter_mut().enumerate() {
417 if self.config.verbose {
418 println!(" Applying preprocessing step {}: {}", idx + 1, step.name());
419 }
420 preprocessed_data = step.process(&preprocessed_data)?;
421 }
422
423 if self.config.verbose {
425 println!(" Fitting algorithm: {}", self.algorithm.name());
426 }
427 self.algorithm.fit(&preprocessed_data, &self.params)?;
428
429 if !self.postprocessing_steps.is_empty() {
431 if self.config.verbose {
432 println!(
433 " Applying {} postprocessing step(s)…",
434 self.postprocessing_steps.len()
435 );
436 }
437 let raw_components = self.algorithm.get_components()?;
438 let mut components = raw_components;
439 for (idx, step) in self.postprocessing_steps.iter().enumerate() {
440 if self.config.verbose {
441 println!(" Step {}: {}", idx + 1, step.name());
442 }
443 components = step.process(components)?;
444 }
445 self.cached_components = Some(components);
446 } else {
447 let raw_components = self.algorithm.get_components()?;
449 self.cached_components = Some(raw_components);
450 }
451
452 self.fitted = true;
453
454 if self.config.verbose {
455 println!("Pipeline fitted successfully!");
456 }
457
458 Ok(self)
459 }
460
461 pub fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
463 if !self.fitted {
464 return Err(SklearsError::InvalidInput(
465 "Pipeline must be fitted before transform".to_string(),
466 ));
467 }
468
469 let mut preprocessed_data = data.clone();
471 for _step in &self.preprocessing_steps {
472 preprocessed_data = preprocessed_data.clone();
476 }
477
478 let transformed_data = self.algorithm.transform(&preprocessed_data)?;
480
481 Ok(transformed_data)
482 }
483
484 pub fn fit_transform(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
486 self.fit(data)?;
487 self.transform(data)
488 }
489
490 pub fn inverse_transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
492 if !self.fitted {
493 return Err(SklearsError::InvalidInput(
494 "Pipeline must be fitted before inverse_transform".to_string(),
495 ));
496 }
497
498 let mut result = self.algorithm.inverse_transform(data)?;
500
501 for step in self.preprocessing_steps.iter().rev() {
503 result = step.inverse_process(&result)?;
504 }
505
506 Ok(result)
507 }
508
509 pub fn get_components(&self) -> Result<DecompositionComponents> {
511 if !self.fitted {
512 return Err(SklearsError::InvalidInput(
513 "Pipeline must be fitted before getting components".to_string(),
514 ));
515 }
516
517 if let Some(ref cached) = self.cached_components {
519 Ok(cached.clone())
520 } else {
521 self.algorithm.get_components()
522 }
523 }
524
525 pub fn explained_variance_ratio(&self) -> Result<Array1<Float>> {
527 let components = self.get_components()?;
528 components.explained_variance_ratio.ok_or_else(|| {
529 SklearsError::InvalidInput("No explained variance available".to_string())
530 })
531 }
532
533 pub fn is_fitted(&self) -> bool {
535 self.fitted
536 }
537
538 pub fn to_json(&self) -> Result<String> {
540 let serializable_pipeline = SerializablePipeline {
541 algorithm_name: self.algorithm.name().to_string(),
542 params: self.params.clone(),
543 config: self.config.clone(),
544 };
545
546 Ok(format!(
548 "{{\"algorithm\":\"{}\"}}",
549 serializable_pipeline.algorithm_name
550 ))
551 }
552
553 pub fn from_json(_json: &str) -> Result<Self> {
555 Err(SklearsError::InvalidInput(
557 "Deserialization not yet implemented".to_string(),
558 ))
559 }
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct SerializablePipeline {
565 pub algorithm_name: String,
566 pub params: DecompositionParams,
567 pub config: PipelineConfig,
568}
569
570#[derive(Clone)]
574struct StandardizationStep {
575 mean: Option<Array1<Float>>,
576 std: Option<Array1<Float>>,
577}
578
579impl StandardizationStep {
580 fn new() -> Self {
581 Self {
582 mean: None,
583 std: None,
584 }
585 }
586}
587
588impl PreprocessingStep for StandardizationStep {
589 fn name(&self) -> &str {
590 "Standardization"
591 }
592
593 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
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 .mapv(|x| x.powi(2))
601 .mean_axis(scirs2_core::ndarray::Axis(0))
602 .ok_or_else(|| {
603 SklearsError::InvalidInput("Cannot compute std of empty array".to_string())
604 })?
605 .mapv(|x| x.sqrt());
606
607 self.mean = Some(mean.clone());
608 self.std = Some(std.clone());
609
610 let result = (data - &mean) / &std;
611 Ok(result)
612 }
613
614 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
615 let mean = self.mean.as_ref().ok_or_else(|| {
616 SklearsError::InvalidInput("Standardization step not fitted".to_string())
617 })?;
618 let std = self.std.as_ref().ok_or_else(|| {
619 SklearsError::InvalidInput("Standardization step not fitted".to_string())
620 })?;
621 Ok((data - mean) / std)
622 }
623
624 fn is_fitted(&self) -> bool {
625 self.mean.is_some()
626 }
627
628 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
629 Box::new(self.clone())
630 }
631}
632
633#[derive(Clone)]
634struct RobustScalingStep;
635impl RobustScalingStep {
636 fn new() -> Self {
637 Self
638 }
639}
640impl PreprocessingStep for RobustScalingStep {
641 fn name(&self) -> &str {
642 "RobustScaling"
643 }
644 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
645 Ok(data.clone())
646 }
647 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
648 Ok(data.clone())
649 }
650 fn is_fitted(&self) -> bool {
651 true
652 }
653 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
654 Box::new(self.clone())
655 }
656}
657
658#[derive(Clone)]
659struct CenteringStep {
660 mean: Option<Array1<Float>>,
661}
662impl CenteringStep {
663 fn new() -> Self {
664 Self { mean: None }
665 }
666}
667impl PreprocessingStep for CenteringStep {
668 fn name(&self) -> &str {
669 "Centering"
670 }
671 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
672 let mean = data
673 .mean_axis(scirs2_core::ndarray::Axis(0))
674 .ok_or_else(|| {
675 SklearsError::InvalidInput("Cannot compute mean of empty array".to_string())
676 })?;
677 self.mean = Some(mean.clone());
678 Ok(data - &mean)
679 }
680 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
681 let mean = self
682 .mean
683 .as_ref()
684 .ok_or_else(|| SklearsError::InvalidInput("Centering step not fitted".to_string()))?;
685 Ok(data - mean)
686 }
687 fn is_fitted(&self) -> bool {
688 self.mean.is_some()
689 }
690 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
691 Box::new(self.clone())
692 }
693}
694
695#[derive(Clone)]
696struct WhiteningStep;
697impl WhiteningStep {
698 fn new() -> Self {
699 Self
700 }
701}
702impl PreprocessingStep for WhiteningStep {
703 fn name(&self) -> &str {
704 "Whitening"
705 }
706 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
707 Ok(data.clone())
708 }
709 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
710 Ok(data.clone())
711 }
712 fn is_fitted(&self) -> bool {
713 true
714 }
715 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
716 Box::new(self.clone())
717 }
718}
719
720#[derive(Clone)]
721struct NonNegativeScalingStep;
722impl NonNegativeScalingStep {
723 fn new() -> Self {
724 Self
725 }
726}
727impl PreprocessingStep for NonNegativeScalingStep {
728 fn name(&self) -> &str {
729 "NonNegativeScaling"
730 }
731 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
732 Ok(data.clone())
733 }
734 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
735 Ok(data.clone())
736 }
737 fn is_fitted(&self) -> bool {
738 true
739 }
740 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
741 Box::new(self.clone())
742 }
743}
744
745#[derive(Clone)]
746struct BatchNormalizationStep {
747 batch_size: usize,
748 batch_stats: Option<(
750 scirs2_core::ndarray::Array1<Float>,
751 scirs2_core::ndarray::Array1<Float>,
752 )>,
753}
754impl BatchNormalizationStep {
755 fn new(batch_size: usize) -> Self {
756 Self {
757 batch_size,
758 batch_stats: None,
759 }
760 }
761}
762impl PreprocessingStep for BatchNormalizationStep {
763 fn name(&self) -> &str {
764 "BatchNormalization"
765 }
766 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
770 let (n_rows, n_cols) = data.dim();
771 if n_rows == 0 || n_cols == 0 {
772 return Ok(data.clone());
773 }
774
775 let batch_size = if self.batch_size == 0 {
776 1
777 } else {
778 self.batch_size
779 };
780 let mut output = data.clone();
781
782 let mut running_mean = scirs2_core::ndarray::Array1::<Float>::zeros(n_cols);
784 let mut running_var = scirs2_core::ndarray::Array1::<Float>::zeros(n_cols);
785 let mut n_batches: usize = 0;
786
787 let mut start = 0;
788 while start < n_rows {
789 let end = (start + batch_size).min(n_rows);
790 let batch = data.slice(scirs2_core::ndarray::s![start..end, ..]);
791
792 let batch_mean = batch
793 .mean_axis(scirs2_core::ndarray::Axis(0))
794 .ok_or_else(|| {
795 SklearsError::InvalidInput("Cannot compute batch mean".to_string())
796 })?;
797 let e_x2 = batch
799 .mapv(|x: Float| x.powi(2))
800 .mean_axis(scirs2_core::ndarray::Axis(0))
801 .ok_or_else(|| {
802 SklearsError::InvalidInput("Cannot compute batch variance".to_string())
803 })?;
804 let batch_var: scirs2_core::ndarray::Array1<Float> =
805 (&e_x2 - &batch_mean.mapv(|m| m.powi(2))).mapv(|v: Float| v.max(0.0));
806
807 let batch_std = batch_var.mapv(|v: Float| v.sqrt() + 1e-8);
808
809 let batch_owned = batch.to_owned();
810 let normalized = (batch_owned - &batch_mean) / &batch_std;
811 output
812 .slice_mut(scirs2_core::ndarray::s![start..end, ..])
813 .assign(&normalized);
814
815 running_mean += &batch_mean;
816 running_var += &batch_var;
817 n_batches += 1;
818
819 start = end;
820 }
821
822 if n_batches > 0 {
823 running_mean /= n_batches as Float;
824 running_var /= n_batches as Float;
825 }
826 self.batch_stats = Some((running_mean, running_var));
827
828 Ok(output)
829 }
830 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
831 let (mean, var) = self.batch_stats.as_ref().ok_or_else(|| {
832 SklearsError::InvalidInput("BatchNormalization step not fitted".to_string())
833 })?;
834 let std = var.mapv(|v| v.sqrt() + 1e-8);
835 Ok((data - mean) / &std)
836 }
837 fn is_fitted(&self) -> bool {
838 self.batch_stats.is_some()
839 }
840 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
841 Box::new(self.clone())
842 }
843}
844
845#[derive(Clone)]
846struct TimeSeriesPreprocessingStep {
847 window_length: usize,
848 fitted: bool,
849}
850impl TimeSeriesPreprocessingStep {
851 fn new(window_length: usize) -> Self {
852 Self {
853 window_length,
854 fitted: false,
855 }
856 }
857
858 fn extract_windows(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
864 let (n_rows, n_cols) = data.dim();
865 if self.window_length == 0 {
866 return Err(SklearsError::InvalidInput(
867 "window_length must be greater than zero".to_string(),
868 ));
869 }
870 if n_rows < self.window_length {
871 return Err(SklearsError::InvalidInput(format!(
872 "Data has {} rows but window_length is {}; need at least window_length rows",
873 n_rows, self.window_length
874 )));
875 }
876 let n_windows = n_rows - self.window_length + 1;
877 let window_features = self.window_length * n_cols;
878 let mut result = scirs2_core::ndarray::Array2::<Float>::zeros((n_windows, window_features));
879
880 for w in 0..n_windows {
881 let window_slice = data.slice(scirs2_core::ndarray::s![w..w + self.window_length, ..]);
882 for (wi, row) in window_slice.outer_iter().enumerate() {
884 for (ci, &val) in row.iter().enumerate() {
885 result[[w, wi * n_cols + ci]] = val;
886 }
887 }
888 }
889 Ok(result)
890 }
891}
892impl PreprocessingStep for TimeSeriesPreprocessingStep {
893 fn name(&self) -> &str {
894 "TimeSeriesPreprocessing"
895 }
896 fn process(&mut self, data: &Array2<Float>) -> Result<Array2<Float>> {
897 let result = self.extract_windows(data)?;
898 self.fitted = true;
899 Ok(result)
900 }
901 fn apply_fitted(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
902 if !self.fitted {
903 return Err(SklearsError::InvalidInput(
904 "TimeSeriesPreprocessing step not fitted".to_string(),
905 ));
906 }
907 self.extract_windows(data)
908 }
909 fn is_fitted(&self) -> bool {
910 self.fitted
911 }
912 fn clone_step(&self) -> Box<dyn PreprocessingStep> {
913 Box::new(self.clone())
914 }
915}
916
917struct PCAAlgorithm {
920 n_components: usize,
921 components: Option<Array2<Float>>,
922}
923impl PCAAlgorithm {
924 fn new(n_components: usize) -> Self {
925 Self {
926 n_components,
927 components: None,
928 }
929 }
930}
931impl DecompositionAlgorithm for PCAAlgorithm {
932 fn name(&self) -> &str {
933 "PCA"
934 }
935 fn description(&self) -> &str {
936 "Principal Component Analysis"
937 }
938 fn capabilities(&self) -> AlgorithmCapabilities {
939 AlgorithmCapabilities::default()
940 }
941 fn validate_params(&self, _params: &DecompositionParams) -> Result<()> {
942 Ok(())
943 }
944 fn fit(&mut self, data: &Array2<Float>, _params: &DecompositionParams) -> Result<()> {
945 self.components = Some(data.slice(s![.., ..self.n_components]).to_owned());
946 Ok(())
947 }
948 fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
949 Ok(data.slice(s![.., ..self.n_components]).to_owned())
950 }
951 fn get_components(&self) -> Result<DecompositionComponents> {
952 Ok(DecompositionComponents::default())
953 }
954 fn is_fitted(&self) -> bool {
955 self.components.is_some()
956 }
957 fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm> {
958 Box::new(Self {
959 n_components: self.n_components,
960 components: self.components.clone(),
961 })
962 }
963 fn as_any(&self) -> &dyn Any {
964 self
965 }
966}
967
968macro_rules! impl_placeholder_algorithm {
970 ($name:ident, $display_name:expr, $($field:ident: $field_type:ty),*) => {
971 #[allow(dead_code)]
972 struct $name {
973 n_components: usize,
974 $(
975 $field: $field_type,
976 )*
977 components: Option<Array2<Float>>,
978 }
979 impl $name {
980 fn new(n_components: usize $(, $field: $field_type)*) -> Self {
981 Self {
982 n_components,
983 $(
984 $field,
985 )*
986 components: None,
987 }
988 }
989 }
990 impl DecompositionAlgorithm for $name {
991 fn name(&self) -> &str { $display_name }
992 fn description(&self) -> &str { $display_name }
993 fn capabilities(&self) -> AlgorithmCapabilities { AlgorithmCapabilities::default() }
994 fn validate_params(&self, _params: &DecompositionParams) -> Result<()> { Ok(()) }
995 fn fit(&mut self, data: &Array2<Float>, _params: &DecompositionParams) -> Result<()> {
996 self.components = Some(data.slice(s![.., ..self.n_components]).to_owned());
997 Ok(())
998 }
999 fn transform(&self, data: &Array2<Float>) -> Result<Array2<Float>> {
1000 Ok(data.slice(s![.., ..self.n_components]).to_owned())
1001 }
1002 fn get_components(&self) -> Result<DecompositionComponents> {
1003 Ok(DecompositionComponents::default())
1004 }
1005 fn is_fitted(&self) -> bool { self.components.is_some() }
1006 fn clone_algorithm(&self) -> Box<dyn DecompositionAlgorithm> {
1007 Box::new(Self {
1008 n_components: self.n_components,
1009 $(
1010 $field: self.$field.clone(),
1011 )*
1012 components: self.components.clone(),
1013 })
1014 }
1015 fn as_any(&self) -> &dyn Any { self }
1016 }
1017 };
1018}
1019
1020impl_placeholder_algorithm!(RobustPCAAlgorithm, "RobustPCA",);
1021impl_placeholder_algorithm!(SparsePCAAlgorithm, "SparsePCA", alpha: Float);
1022impl_placeholder_algorithm!(IncrementalPCAAlgorithm, "IncrementalPCA", batch_size: usize);
1023impl_placeholder_algorithm!(KernelPCAAlgorithm, "KernelPCA", kernel: String);
1024impl_placeholder_algorithm!(FastICAAlgorithm, "FastICA",);
1025impl_placeholder_algorithm!(NMFAlgorithm, "NMF", init_method: String);
1026impl_placeholder_algorithm!(FactorAnalysisAlgorithm, "FactorAnalysis",);
1027impl_placeholder_algorithm!(TruncatedSVDAlgorithm, "TruncatedSVD",);
1028impl_placeholder_algorithm!(SSAAlgorithm, "SSA", window_length: usize);
1029
1030#[derive(Clone)]
1033struct VarianceThresholdStep {
1034 threshold: Float,
1035}
1036impl VarianceThresholdStep {
1037 fn new(threshold: Float) -> Self {
1038 Self { threshold }
1039 }
1040}
1041impl PostprocessingStep for VarianceThresholdStep {
1042 fn name(&self) -> &str {
1043 "VarianceThreshold"
1044 }
1045 fn process(&self, mut components: DecompositionComponents) -> Result<DecompositionComponents> {
1051 if let Some(ref evr) = components.explained_variance_ratio.clone() {
1052 let keep_mask: Vec<bool> = evr.iter().map(|&r| r >= self.threshold).collect();
1054 let n_keep = keep_mask.iter().filter(|&&k| k).count();
1055
1056 if let Some(ref comps) = components.components.clone() {
1058 let n_rows = comps.nrows();
1059 let mut kept = scirs2_core::ndarray::Array2::<Float>::zeros((n_rows, n_keep));
1060 let mut col_out = 0;
1061 for (col_in, &keep) in keep_mask.iter().enumerate() {
1062 if keep {
1063 kept.column_mut(col_out).assign(&comps.column(col_in));
1064 col_out += 1;
1065 }
1066 }
1067 components.components = Some(kept);
1068 }
1069
1070 let new_evr: Vec<Float> = evr
1072 .iter()
1073 .zip(keep_mask.iter())
1074 .filter_map(|(&r, &k)| if k { Some(r) } else { None })
1075 .collect();
1076 components.explained_variance_ratio =
1077 Some(scirs2_core::ndarray::Array1::from_vec(new_evr));
1078
1079 if let Some(ref eigs) = components.eigenvalues.clone() {
1081 let new_eigs: Vec<Float> = eigs
1082 .iter()
1083 .zip(keep_mask.iter())
1084 .filter_map(|(&e, &k)| if k { Some(e) } else { None })
1085 .collect();
1086 components.eigenvalues = Some(scirs2_core::ndarray::Array1::from_vec(new_eigs));
1087 }
1088
1089 components.metadata.insert(
1090 "variance_threshold_applied".to_string(),
1091 format!("{}", self.threshold),
1092 );
1093 }
1094 Ok(components)
1096 }
1097 fn clone_step(&self) -> Box<dyn PostprocessingStep> {
1098 Box::new(self.clone())
1099 }
1100}
1101
1102#[derive(Clone)]
1103struct OutlierRemovalStep;
1104impl OutlierRemovalStep {
1105 fn new() -> Self {
1106 Self
1107 }
1108}
1109impl PostprocessingStep for OutlierRemovalStep {
1110 fn name(&self) -> &str {
1111 "OutlierRemoval"
1112 }
1113 fn process(&self, components: DecompositionComponents) -> Result<DecompositionComponents> {
1114 Ok(components)
1115 }
1116 fn clone_step(&self) -> Box<dyn PostprocessingStep> {
1117 Box::new(self.clone())
1118 }
1119}
1120
1121#[derive(Clone)]
1122struct SparsityConstraintStep;
1123impl SparsityConstraintStep {
1124 fn new() -> Self {
1125 Self
1126 }
1127}
1128impl PostprocessingStep for SparsityConstraintStep {
1129 fn name(&self) -> &str {
1130 "SparsityConstraint"
1131 }
1132 fn process(&self, components: DecompositionComponents) -> Result<DecompositionComponents> {
1133 Ok(components)
1134 }
1135 fn clone_step(&self) -> Box<dyn PostprocessingStep> {
1136 Box::new(self.clone())
1137 }
1138}
1139
1140#[derive(Clone)]
1141struct RotationStep {
1142 method: String,
1143}
1144impl RotationStep {
1145 fn new(method: String) -> Self {
1146 Self { method }
1147 }
1148
1149 fn apply_varimax(
1154 matrix: &scirs2_core::ndarray::Array2<Float>,
1155 max_iter: usize,
1156 tol: Float,
1157 ) -> Result<scirs2_core::ndarray::Array2<Float>> {
1158 let (p, k) = matrix.dim();
1159 if k < 2 {
1160 return Ok(matrix.clone());
1162 }
1163
1164 let mut rot = scirs2_core::ndarray::Array2::<Float>::eye(k);
1166 let mut lambda = matrix.clone();
1167
1168 for _iter in 0..max_iter {
1169 let mut max_change: Float = 0.0;
1170
1171 for i in 0..k {
1173 for j in (i + 1)..k {
1174 let col_i = lambda.column(i).to_owned();
1176 let col_j = lambda.column(j).to_owned();
1177
1178 let u: Vec<Float> = col_i
1180 .iter()
1181 .zip(col_j.iter())
1182 .map(|(&a, &b)| a.powi(2) - b.powi(2))
1183 .collect();
1184 let v: Vec<Float> = col_i
1185 .iter()
1186 .zip(col_j.iter())
1187 .map(|(&a, &b)| 2.0 * a * b)
1188 .collect();
1189
1190 let u_sum: Float = u.iter().sum();
1191 let v_sum: Float = v.iter().sum();
1192 let u2_sum: Float = u.iter().map(|x| x.powi(2)).sum::<Float>()
1193 - u.iter().map(|x| x.powi(2)).sum::<Float>()
1194 * u.iter().sum::<Float>().powi(2)
1195 / p as Float;
1196 let v2_sum: Float = v.iter().map(|x| x.powi(2)).sum::<Float>();
1197 let uv_sum: Float = u.iter().zip(v.iter()).map(|(a, b)| a * b).sum::<Float>();
1198
1199 let num = 4.0 * (uv_sum - u_sum * v_sum / p as Float);
1201 let den = u2_sum - v2_sum;
1202
1203 let theta = if den.abs() < Float::EPSILON {
1204 0.0
1205 } else {
1206 Float::atan2(num, den) / 4.0
1207 };
1208
1209 if theta.abs() < tol {
1210 continue;
1211 }
1212 max_change = max_change.max(theta.abs());
1213
1214 let cos_t = theta.cos();
1215 let sin_t = theta.sin();
1216
1217 let new_i: Vec<Float> = col_i
1219 .iter()
1220 .zip(col_j.iter())
1221 .map(|(&a, &b)| cos_t * a + sin_t * b)
1222 .collect();
1223 let new_j: Vec<Float> = col_i
1224 .iter()
1225 .zip(col_j.iter())
1226 .map(|(&a, &b)| -sin_t * a + cos_t * b)
1227 .collect();
1228
1229 for row in 0..p {
1230 lambda[[row, i]] = new_i[row];
1231 lambda[[row, j]] = new_j[row];
1232 }
1233
1234 let old_rot_i = rot.column(i).to_owned();
1236 let old_rot_j = rot.column(j).to_owned();
1237 for row in 0..k {
1238 rot[[row, i]] = cos_t * old_rot_i[row] + sin_t * old_rot_j[row];
1239 rot[[row, j]] = -sin_t * old_rot_i[row] + cos_t * old_rot_j[row];
1240 }
1241 }
1242 }
1243
1244 if max_change < tol {
1245 break;
1246 }
1247 }
1248
1249 Ok(lambda)
1250 }
1251
1252 fn apply_quartimax(
1256 matrix: &scirs2_core::ndarray::Array2<Float>,
1257 max_iter: usize,
1258 tol: Float,
1259 ) -> Result<scirs2_core::ndarray::Array2<Float>> {
1260 let (p, k) = matrix.dim();
1261 if k < 2 {
1262 return Ok(matrix.clone());
1263 }
1264
1265 let mut lambda = matrix.clone();
1266
1267 for _iter in 0..max_iter {
1268 let mut max_change: Float = 0.0;
1269
1270 for i in 0..k {
1271 for j in (i + 1)..k {
1272 let col_i = lambda.column(i).to_owned();
1273 let col_j = lambda.column(j).to_owned();
1274
1275 let u_sum: Float = col_i
1277 .iter()
1278 .zip(col_j.iter())
1279 .map(|(&a, &b)| a.powi(2) - b.powi(2))
1280 .sum();
1281 let v_sum: Float = col_i
1282 .iter()
1283 .zip(col_j.iter())
1284 .map(|(&a, &b)| 2.0 * a * b)
1285 .sum();
1286 let uv_sum: Float = col_i
1287 .iter()
1288 .zip(col_j.iter())
1289 .map(|(&a, &b)| (a.powi(2) - b.powi(2)) * 2.0 * a * b)
1290 .sum();
1291 let u2_v2: Float = col_i
1292 .iter()
1293 .zip(col_j.iter())
1294 .map(|(&a, &b)| {
1295 (a.powi(2) - b.powi(2)).powi(2) - 4.0 * a.powi(2) * b.powi(2)
1296 })
1297 .sum();
1298
1299 let theta = if (u2_v2.abs() + uv_sum.abs()) < Float::EPSILON {
1300 0.0
1301 } else {
1302 Float::atan2(u_sum * v_sum - uv_sum * (p as Float), u2_v2) / 4.0
1303 };
1304
1305 let _ = (u_sum, v_sum); if theta.abs() < tol {
1308 continue;
1309 }
1310 max_change = max_change.max(theta.abs());
1311
1312 let cos_t = theta.cos();
1313 let sin_t = theta.sin();
1314
1315 let new_i: Vec<Float> = col_i
1316 .iter()
1317 .zip(col_j.iter())
1318 .map(|(&a, &b)| cos_t * a + sin_t * b)
1319 .collect();
1320 let new_j: Vec<Float> = col_i
1321 .iter()
1322 .zip(col_j.iter())
1323 .map(|(&a, &b)| -sin_t * a + cos_t * b)
1324 .collect();
1325
1326 for row in 0..p {
1327 lambda[[row, i]] = new_i[row];
1328 lambda[[row, j]] = new_j[row];
1329 }
1330 }
1331 }
1332
1333 if max_change < tol {
1334 break;
1335 }
1336 }
1337 Ok(lambda)
1338 }
1339}
1340impl PostprocessingStep for RotationStep {
1341 fn name(&self) -> &str {
1342 "Rotation"
1343 }
1344 fn process(&self, mut components: DecompositionComponents) -> Result<DecompositionComponents> {
1346 let max_iter = 1000;
1347 let tol = 1e-6;
1348
1349 let target = if let Some(ref loadings) = components.factor_loadings {
1350 Some(("factor_loadings", loadings.clone()))
1351 } else {
1352 components
1353 .components
1354 .as_ref()
1355 .map(|c| ("components", c.clone()))
1356 };
1357
1358 if let Some((field_name, matrix)) = target {
1359 let rotated = match self.method.to_lowercase().as_str() {
1360 "varimax" => Self::apply_varimax(&matrix, max_iter, tol)?,
1361 "quartimax" => Self::apply_quartimax(&matrix, max_iter, tol)?,
1362 "promax" => {
1364 let vm = Self::apply_varimax(&matrix, max_iter, tol)?;
1365 vm.mapv(|x| {
1366 let sign = if x < 0.0 { -1.0 } else { 1.0 };
1367 sign * x.abs().powi(3)
1368 })
1369 }
1370 other => {
1371 return Err(SklearsError::InvalidInput(format!(
1372 "Unknown rotation method '{}'; supported: varimax, quartimax, promax",
1373 other
1374 )))
1375 }
1376 };
1377
1378 match field_name {
1379 "factor_loadings" => components.factor_loadings = Some(rotated),
1380 _ => components.components = Some(rotated),
1381 }
1382
1383 components
1384 .metadata
1385 .insert("rotation_applied".to_string(), self.method.clone());
1386 }
1387
1388 Ok(components)
1389 }
1390 fn clone_step(&self) -> Box<dyn PostprocessingStep> {
1391 Box::new(self.clone())
1392 }
1393}
1394
1395#[cfg(test)]
1396mod tests {
1397 use super::*;
1398
1399 #[test]
1400 fn test_pipeline_builder() {
1401 let builder = DecompositionPipelineBuilder::new()
1402 .with_standardization()
1403 .with_pca(3)
1404 .n_components(3)
1405 .tolerance(1e-6);
1406
1407 assert!(builder.build().is_ok());
1408 }
1409
1410 #[test]
1411 fn test_preset_standard_pca() {
1412 let builder = DecompositionPipelineBuilder::from_preset(DecompositionPreset::StandardPCA {
1413 n_components: 5,
1414 });
1415
1416 let pipeline = builder.build();
1417 assert!(pipeline.is_ok());
1418 }
1419
1420 #[test]
1421 fn test_pipeline_fit_transform() {
1422 use scirs2_core::random::thread_rng;
1423 let mut rng = thread_rng();
1424
1425 let data = Array2::from_shape_fn((50, 10), |(i, j)| {
1426 (i + j) as Float + rng.gen_range(-0.1..0.1)
1427 });
1428
1429 let mut pipeline = DecompositionPipelineBuilder::new()
1430 .with_standardization()
1431 .with_pca(3)
1432 .build()
1433 .expect("operation should succeed");
1434
1435 let result = pipeline.fit_transform(&data);
1436 assert!(result.is_ok());
1437 }
1438
1439 #[test]
1440 fn test_fluent_api_chaining() {
1441 let pipeline = DecompositionPipelineBuilder::new()
1442 .with_standardization()
1443 .with_pca(5)
1444 .n_components(5)
1445 .tolerance(1e-6)
1446 .max_iterations(100)
1447 .random_seed(42)
1448 .verbose(false)
1449 .build();
1450
1451 assert!(pipeline.is_ok());
1452 }
1453
1454 #[test]
1455 fn test_serialization() {
1456 let mut pipeline = DecompositionPipelineBuilder::new()
1457 .with_pca(3)
1458 .build()
1459 .expect("operation should succeed");
1460
1461 use scirs2_core::random::thread_rng;
1462 let mut rng = thread_rng();
1463 let data = Array2::from_shape_fn((20, 5), |(i, j)| {
1464 (i + j) as Float + rng.gen_range(-0.1..0.1)
1465 });
1466
1467 pipeline.fit(&data).expect("model fitting should succeed");
1468
1469 let json = pipeline.to_json();
1470 assert!(json.is_ok());
1471 }
1472
1473 #[test]
1477 fn test_batch_normalization_affects_output() {
1478 let data = Array2::from_shape_vec((8, 3), (0..24).map(|x| x as Float).collect())
1479 .expect("shape should match");
1480
1481 let mut step_small = BatchNormalizationStep::new(2);
1483 let out_small = step_small
1484 .process(&data)
1485 .expect("batch norm should succeed");
1486
1487 let mut step_full = BatchNormalizationStep::new(8);
1489 let out_full = step_full.process(&data).expect("batch norm should succeed");
1490
1491 assert_ne!(
1493 out_small.as_slice().expect("contiguous"),
1494 out_full.as_slice().expect("contiguous"),
1495 "batch_size should change the normalisation output"
1496 );
1497
1498 let apply_result = step_small.apply_fitted(&data);
1500 assert!(
1501 apply_result.is_ok(),
1502 "apply_fitted should work after process()"
1503 );
1504 }
1505
1506 #[test]
1508 fn test_window_length_affects_output() {
1509 let data = Array2::from_shape_vec((10, 2), (0..20).map(|x| x as Float).collect())
1510 .expect("shape should match");
1511
1512 let window = 3_usize;
1513 let mut step = TimeSeriesPreprocessingStep::new(window);
1514 let out = step
1515 .process(&data)
1516 .expect("time-series preprocessing should succeed");
1517
1518 assert_eq!(
1520 out.nrows(),
1521 data.nrows() - window + 1,
1522 "output should have n_rows - window_length + 1 rows"
1523 );
1524 assert_eq!(
1525 out.ncols(),
1526 window * data.ncols(),
1527 "each window should have window_length * n_cols features"
1528 );
1529
1530 let mut step1 = TimeSeriesPreprocessingStep::new(1);
1532 let out1 = step1.process(&data).expect("window=1 should work");
1533 assert_eq!(out1.nrows(), data.nrows());
1534 assert_eq!(out1.ncols(), data.ncols());
1535 }
1536
1537 #[test]
1539 fn test_variance_threshold_filters_components() {
1540 use crate::modular_framework::{DecompositionComponents, PostprocessingStep};
1541 use scirs2_core::ndarray::{Array1, Array2};
1542
1543 let evr = Array1::from_vec(vec![0.5_f64, 0.02, 0.3]);
1545 let comps = Array2::from_shape_vec(
1546 (4, 3),
1547 vec![
1548 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1549 ],
1550 )
1551 .expect("shape should match");
1552
1553 let dc = DecompositionComponents {
1554 explained_variance_ratio: Some(evr.clone()),
1555 components: Some(comps),
1556 ..Default::default()
1557 };
1558
1559 let step = VarianceThresholdStep::new(0.1);
1561 let result = step.process(dc).expect("variance threshold should succeed");
1562
1563 let kept_evr = result
1564 .explained_variance_ratio
1565 .expect("evr should be present");
1566 assert_eq!(
1567 kept_evr.len(),
1568 2,
1569 "should keep 2 components above threshold"
1570 );
1571 assert!((kept_evr[0] - 0.5).abs() < 1e-10);
1572 assert!((kept_evr[1] - 0.3).abs() < 1e-10);
1573
1574 let kept_comps = result.components.expect("components should be present");
1575 assert_eq!(
1576 kept_comps.ncols(),
1577 2,
1578 "component matrix should have 2 columns"
1579 );
1580 }
1581
1582 #[test]
1584 fn test_rotation_applies_to_components() {
1585 use crate::modular_framework::{DecompositionComponents, PostprocessingStep};
1586 use scirs2_core::ndarray::Array2;
1587
1588 let comps = Array2::from_shape_vec(
1589 (5, 3),
1590 vec![
1591 0.9, 0.1, 0.2, 0.8, 0.2, 0.3, 0.7, 0.3, 0.1, 0.6, 0.4, 0.5, 0.5, 0.5, 0.4,
1592 ],
1593 )
1594 .expect("shape should match");
1595
1596 let dc = DecompositionComponents {
1597 components: Some(comps.clone()),
1598 ..Default::default()
1599 };
1600
1601 let step = RotationStep::new("varimax".to_string());
1602 let result = step.process(dc).expect("rotation should succeed");
1603
1604 assert_eq!(
1605 result.metadata.get("rotation_applied"),
1606 Some(&"varimax".to_string()),
1607 "metadata should record rotation method"
1608 );
1609 let rotated = result.components.expect("components should be present");
1611 assert_eq!(rotated.dim(), comps.dim());
1612 }
1613
1614 #[test]
1616 fn test_postprocessing_steps_applied_in_fit() {
1617 use scirs2_core::ndarray::Array2;
1618
1619 let data = Array2::from_shape_vec((20, 5), (0..100).map(|x| x as Float).collect())
1620 .expect("shape should match");
1621
1622 let mut pipeline = DecompositionPipelineBuilder::new()
1626 .with_pca(3)
1627 .with_variance_threshold(0.01)
1628 .build()
1629 .expect("build should succeed");
1630
1631 pipeline.fit(&data).expect("fit should succeed");
1632
1633 let comps = pipeline.get_components();
1635 assert!(
1636 comps.is_ok(),
1637 "get_components should return Ok after fit with postprocessing"
1638 );
1639 }
1640}