Skip to main content

ferrolearn_core/
pipeline.rs

1//! Dynamic-dispatch pipeline for composing transformers and estimators.
2//!
3//! A [`Pipeline`] chains zero or more transformer steps followed by a final
4//! estimator step. Calling [`Fit::fit`] on a pipeline fits each step in
5//! sequence, producing a [`FittedPipeline`] that implements [`Predict`].
6//!
7//! The pipeline is generic over the float type `F`, supporting both `f32`
8//! and `f64` data. All steps in a pipeline must use the same float type.
9//! The type parameter defaults to `f64` for backward compatibility.
10//!
11//! # Examples
12//!
13//! ```
14//! use ferrolearn_core::pipeline::{Pipeline, PipelineTransformer, PipelineEstimator};
15//! use ferrolearn_core::{Fit, Predict, FerroError};
16//! use ndarray::{Array1, Array2};
17//!
18//! // A trivial identity transformer for demonstration.
19//! struct IdentityTransformer;
20//!
21//! impl PipelineTransformer<f64> for IdentityTransformer {
22//!     fn fit_pipeline(
23//!         &self,
24//!         x: &Array2<f64>,
25//!         _y: &Array1<f64>,
26//!     ) -> Result<Box<dyn FittedPipelineTransformer<f64>>, FerroError> {
27//!         Ok(Box::new(FittedIdentity))
28//!     }
29//! }
30//!
31//! struct FittedIdentity;
32//!
33//! impl FittedPipelineTransformer<f64> for FittedIdentity {
34//!     fn transform_pipeline(&self, x: &Array2<f64>) -> Result<Array2<f64>, FerroError> {
35//!         Ok(x.clone())
36//!     }
37//! }
38//!
39//! // A trivial estimator that predicts the first column.
40//! struct FirstColumnEstimator;
41//!
42//! impl PipelineEstimator<f64> for FirstColumnEstimator {
43//!     fn fit_pipeline(
44//!         &self,
45//!         _x: &Array2<f64>,
46//!         _y: &Array1<f64>,
47//!     ) -> Result<Box<dyn FittedPipelineEstimator<f64>>, FerroError> {
48//!         Ok(Box::new(FittedFirstColumn))
49//!     }
50//! }
51//!
52//! struct FittedFirstColumn;
53//!
54//! impl FittedPipelineEstimator<f64> for FittedFirstColumn {
55//!     fn predict_pipeline(&self, x: &Array2<f64>) -> Result<Array1<f64>, FerroError> {
56//!         Ok(x.column(0).to_owned())
57//!     }
58//! }
59//!
60//! // Build and use the pipeline.
61//! use ferrolearn_core::pipeline::FittedPipelineTransformer;
62//! use ferrolearn_core::pipeline::FittedPipelineEstimator;
63//!
64//! let pipeline = Pipeline::new()
65//!     .transform_step("scaler", Box::new(IdentityTransformer))
66//!     .estimator_step("model", Box::new(FirstColumnEstimator));
67//!
68//! let x = Array2::<f64>::zeros((5, 3));
69//! let y = Array1::<f64>::zeros(5);
70//!
71//! let fitted = pipeline.fit(&x, &y).unwrap();
72//! let preds = fitted.predict(&x).unwrap();
73//! assert_eq!(preds.len(), 5);
74//! ```
75
76use ndarray::{Array1, Array2};
77use num_traits::Float;
78
79use crate::error::FerroError;
80use crate::traits::{Fit, Predict};
81
82// ---------------------------------------------------------------------------
83// Trait-object interfaces for pipeline steps
84// ---------------------------------------------------------------------------
85
86/// An unfitted transformer step that can participate in a [`Pipeline`].
87///
88/// Implementors must be able to fit themselves on `Array2<F>` data and
89/// return a boxed [`FittedPipelineTransformer`].
90///
91/// The type parameter `F` is the float type (`f32` or `f64`).
92pub trait PipelineTransformer<F: Float + Send + Sync + 'static>: Send + Sync {
93    /// Fit this transformer on the given data.
94    ///
95    /// # Errors
96    ///
97    /// Returns a [`FerroError`] if fitting fails.
98    fn fit_pipeline(
99        &self,
100        x: &Array2<F>,
101        y: &Array1<F>,
102    ) -> Result<Box<dyn FittedPipelineTransformer<F>>, FerroError>;
103}
104
105/// A fitted transformer step in a [`FittedPipeline`].
106///
107/// Transforms `Array2<F>` data, producing a new `Array2<F>`.
108pub trait FittedPipelineTransformer<F: Float + Send + Sync + 'static>: Send + Sync {
109    /// Transform the input data.
110    ///
111    /// # Errors
112    ///
113    /// Returns a [`FerroError`] if the input shape is incompatible.
114    fn transform_pipeline(&self, x: &Array2<F>) -> Result<Array2<F>, FerroError>;
115}
116
117/// An unfitted estimator step that serves as the final step in a [`Pipeline`].
118///
119/// Implementors must be able to fit themselves on `Array2<F>` data and
120/// return a boxed [`FittedPipelineEstimator`].
121pub trait PipelineEstimator<F: Float + Send + Sync + 'static>: Send + Sync {
122    /// Fit this estimator on the given data.
123    ///
124    /// # Errors
125    ///
126    /// Returns a [`FerroError`] if fitting fails.
127    fn fit_pipeline(
128        &self,
129        x: &Array2<F>,
130        y: &Array1<F>,
131    ) -> Result<Box<dyn FittedPipelineEstimator<F>>, FerroError>;
132}
133
134/// A fitted estimator step in a [`FittedPipeline`].
135///
136/// Produces `Array1<F>` predictions from `Array2<F>` input.
137pub trait FittedPipelineEstimator<F: Float + Send + Sync + 'static>: Send + Sync {
138    /// Generate predictions for the input data.
139    ///
140    /// # Errors
141    ///
142    /// Returns a [`FerroError`] if the input shape is incompatible.
143    fn predict_pipeline(&self, x: &Array2<F>) -> Result<Array1<F>, FerroError>;
144}
145
146// ---------------------------------------------------------------------------
147// Pipeline (unfitted)
148// ---------------------------------------------------------------------------
149
150/// A named transformer step in an unfitted pipeline.
151struct TransformStep<F: Float + Send + Sync + 'static> {
152    /// Human-readable name for this step.
153    name: String,
154    /// The unfitted transformer.
155    step: Box<dyn PipelineTransformer<F>>,
156}
157
158/// A dynamic-dispatch pipeline that composes transformers and a final estimator.
159///
160/// Steps are added with [`transform_step`](Pipeline::transform_step) and the
161/// final estimator is set with [`estimator_step`](Pipeline::estimator_step).
162/// The pipeline implements [`Fit<Array2<F>, Array1<F>>`](Fit) and produces
163/// a [`FittedPipeline`] that implements [`Predict<Array2<F>>`](Predict).
164///
165/// All intermediate data flows as `Array2<F>`. The type parameter defaults
166/// to `f64` for backward compatibility.
167pub struct Pipeline<F: Float + Send + Sync + 'static = f64> {
168    /// Ordered transformer steps.
169    transforms: Vec<TransformStep<F>>,
170    /// The final estimator step (name + estimator).
171    estimator: Option<(String, Box<dyn PipelineEstimator<F>>)>,
172}
173
174impl<F: Float + Send + Sync + 'static> Pipeline<F> {
175    /// Create a new empty pipeline.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use ferrolearn_core::pipeline::Pipeline;
181    /// let pipeline = Pipeline::<f64>::new();
182    /// ```
183    pub fn new() -> Self {
184        Self {
185            transforms: Vec::new(),
186            estimator: None,
187        }
188    }
189
190    /// Add a named transformer step to the pipeline.
191    ///
192    /// Transformer steps are applied in the order they are added, before
193    /// the final estimator step.
194    #[must_use]
195    pub fn transform_step(mut self, name: &str, step: Box<dyn PipelineTransformer<F>>) -> Self {
196        self.transforms.push(TransformStep {
197            name: name.to_owned(),
198            step,
199        });
200        self
201    }
202
203    /// Set the final estimator step.
204    ///
205    /// A pipeline must have exactly one estimator step. Setting a new
206    /// estimator replaces any previously set estimator.
207    #[must_use]
208    pub fn estimator_step(mut self, name: &str, estimator: Box<dyn PipelineEstimator<F>>) -> Self {
209        self.estimator = Some((name.to_owned(), estimator));
210        self
211    }
212
213    /// Add a named step to the pipeline using the builder pattern.
214    ///
215    /// This is a convenience method that accepts either a transformer or
216    /// an estimator. The final step added via this method that is an
217    /// estimator becomes the pipeline's estimator. This provides the
218    /// `Pipeline::new().step("scaler", ...).step("clf", ...)` API.
219    #[must_use]
220    pub fn step(self, name: &str, step: Box<dyn PipelineStep<F>>) -> Self {
221        step.add_to_pipeline(self, name)
222    }
223}
224
225impl<F: Float + Send + Sync + 'static> Default for Pipeline<F> {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231impl<F: Float + Send + Sync + 'static> Fit<Array2<F>, Array1<F>> for Pipeline<F> {
232    type Fitted = FittedPipeline<F>;
233    type Error = FerroError;
234
235    /// Fit the pipeline by fitting each transformer step in order, then
236    /// fitting the final estimator on the transformed data.
237    ///
238    /// Each transformer is fit on the current data, then the data is
239    /// transformed before being passed to the next step.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`FerroError::InvalidParameter`] if no estimator step was set.
244    /// Propagates any errors from individual step fitting or transforming.
245    fn fit(&self, x: &Array2<F>, y: &Array1<F>) -> Result<FittedPipeline<F>, FerroError> {
246        if self.estimator.is_none() {
247            return Err(FerroError::InvalidParameter {
248                name: "estimator".into(),
249                reason: "pipeline must have a final estimator step".into(),
250            });
251        }
252
253        let mut current_x = x.clone();
254        let mut fitted_transforms = Vec::with_capacity(self.transforms.len());
255
256        // Fit and transform each transformer step.
257        for ts in &self.transforms {
258            let fitted = ts.step.fit_pipeline(&current_x, y)?;
259            current_x = fitted.transform_pipeline(&current_x)?;
260            fitted_transforms.push(FittedTransformStep {
261                name: ts.name.clone(),
262                step: fitted,
263            });
264        }
265
266        // Fit the final estimator on the transformed data.
267        let (est_name, est) = self.estimator.as_ref().unwrap();
268        let fitted_est = est.fit_pipeline(&current_x, y)?;
269
270        Ok(FittedPipeline {
271            transforms: fitted_transforms,
272            estimator: (est_name.clone(), fitted_est),
273        })
274    }
275}
276
277// ---------------------------------------------------------------------------
278// FittedPipeline
279// ---------------------------------------------------------------------------
280
281/// A named fitted transformer step.
282struct FittedTransformStep<F: Float + Send + Sync + 'static> {
283    /// Human-readable name for this step.
284    name: String,
285    /// The fitted transformer.
286    step: Box<dyn FittedPipelineTransformer<F>>,
287}
288
289/// A fitted pipeline that chains fitted transformers and a fitted estimator.
290///
291/// Created by calling [`Fit::fit`] on a [`Pipeline`]. Implements
292/// [`Predict<Array2<F>>`](Predict), producing `Array1<F>` predictions.
293pub struct FittedPipeline<F: Float + Send + Sync + 'static = f64> {
294    /// Fitted transformer steps, in order.
295    transforms: Vec<FittedTransformStep<F>>,
296    /// The fitted estimator (name + estimator).
297    estimator: (String, Box<dyn FittedPipelineEstimator<F>>),
298}
299
300impl<F: Float + Send + Sync + 'static> FittedPipeline<F> {
301    /// Returns the names of all steps (transformers + estimator) in order.
302    pub fn step_names(&self) -> Vec<&str> {
303        let mut names: Vec<&str> = self.transforms.iter().map(|s| s.name.as_str()).collect();
304        names.push(&self.estimator.0);
305        names
306    }
307}
308
309impl<F: Float + Send + Sync + 'static> Predict<Array2<F>> for FittedPipeline<F> {
310    type Output = Array1<F>;
311    type Error = FerroError;
312
313    /// Generate predictions by transforming the input through each fitted
314    /// transformer step, then calling predict on the fitted estimator.
315    ///
316    /// # Errors
317    ///
318    /// Propagates any errors from transformer or estimator steps.
319    fn predict(&self, x: &Array2<F>) -> Result<Array1<F>, FerroError> {
320        let mut current_x = x.clone();
321
322        for ts in &self.transforms {
323            current_x = ts.step.transform_pipeline(&current_x)?;
324        }
325
326        self.estimator.1.predict_pipeline(&current_x)
327    }
328}
329
330// ---------------------------------------------------------------------------
331// PipelineStep: unified interface for the `.step()` builder method
332// ---------------------------------------------------------------------------
333
334/// A trait that unifies transformers and estimators for the
335/// [`Pipeline::step`] builder method.
336///
337/// Implementors of [`PipelineTransformer`] and [`PipelineEstimator`]
338/// automatically get a blanket implementation of this trait via the
339/// wrapper types [`TransformerStepWrapper`] and [`EstimatorStepWrapper`].
340///
341/// For convenience, use [`as_transform_step`] and [`as_estimator_step`]
342/// to wrap your types.
343pub trait PipelineStep<F: Float + Send + Sync + 'static>: Send + Sync {
344    /// Add this step to the pipeline under the given name.
345    ///
346    /// Transformer steps are added as intermediate transform steps.
347    /// Estimator steps are set as the final estimator.
348    fn add_to_pipeline(self: Box<Self>, pipeline: Pipeline<F>, name: &str) -> Pipeline<F>;
349}
350
351/// Wraps a [`PipelineTransformer`] to implement [`PipelineStep`].
352///
353/// Created by [`as_transform_step`].
354pub struct TransformerStepWrapper<F: Float + Send + Sync + 'static>(
355    Box<dyn PipelineTransformer<F>>,
356);
357
358impl<F: Float + Send + Sync + 'static> PipelineStep<F> for TransformerStepWrapper<F> {
359    fn add_to_pipeline(self: Box<Self>, pipeline: Pipeline<F>, name: &str) -> Pipeline<F> {
360        pipeline.transform_step(name, self.0)
361    }
362}
363
364/// Wraps a [`PipelineEstimator`] to implement [`PipelineStep`].
365///
366/// Created by [`as_estimator_step`].
367pub struct EstimatorStepWrapper<F: Float + Send + Sync + 'static>(Box<dyn PipelineEstimator<F>>);
368
369impl<F: Float + Send + Sync + 'static> PipelineStep<F> for EstimatorStepWrapper<F> {
370    fn add_to_pipeline(self: Box<Self>, pipeline: Pipeline<F>, name: &str) -> Pipeline<F> {
371        pipeline.estimator_step(name, self.0)
372    }
373}
374
375/// Wrap a [`PipelineTransformer`] as a [`PipelineStep`] for use with
376/// [`Pipeline::step`].
377///
378/// # Examples
379///
380/// ```
381/// use ferrolearn_core::pipeline::{Pipeline, as_transform_step};
382/// // Assuming `my_scaler` implements PipelineTransformer<f64>:
383/// // let pipeline = Pipeline::new().step("scaler", as_transform_step(my_scaler));
384/// ```
385pub fn as_transform_step<F: Float + Send + Sync + 'static>(
386    t: impl PipelineTransformer<F> + 'static,
387) -> Box<dyn PipelineStep<F>> {
388    Box::new(TransformerStepWrapper(Box::new(t)))
389}
390
391/// Wrap a [`PipelineEstimator`] as a [`PipelineStep`] for use with
392/// [`Pipeline::step`].
393///
394/// # Examples
395///
396/// ```
397/// use ferrolearn_core::pipeline::{Pipeline, as_estimator_step};
398/// // Assuming `my_model` implements PipelineEstimator<f64>:
399/// // let pipeline = Pipeline::new().step("model", as_estimator_step(my_model));
400/// ```
401pub fn as_estimator_step<F: Float + Send + Sync + 'static>(
402    e: impl PipelineEstimator<F> + 'static,
403) -> Box<dyn PipelineStep<F>> {
404    Box::new(EstimatorStepWrapper(Box::new(e)))
405}
406
407// ---------------------------------------------------------------------------
408// Tests
409// ---------------------------------------------------------------------------
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    // -- Test fixtures -------------------------------------------------------
416
417    /// A trivial transformer that doubles all values.
418    struct DoublingTransformer;
419
420    impl PipelineTransformer<f64> for DoublingTransformer {
421        fn fit_pipeline(
422            &self,
423            _x: &Array2<f64>,
424            _y: &Array1<f64>,
425        ) -> Result<Box<dyn FittedPipelineTransformer<f64>>, FerroError> {
426            Ok(Box::new(FittedDoublingTransformer))
427        }
428    }
429
430    struct FittedDoublingTransformer;
431
432    impl FittedPipelineTransformer<f64> for FittedDoublingTransformer {
433        fn transform_pipeline(&self, x: &Array2<f64>) -> Result<Array2<f64>, FerroError> {
434            Ok(x.mapv(|v| v * 2.0))
435        }
436    }
437
438    /// A trivial estimator that sums each row.
439    struct SumEstimator;
440
441    impl PipelineEstimator<f64> for SumEstimator {
442        fn fit_pipeline(
443            &self,
444            _x: &Array2<f64>,
445            _y: &Array1<f64>,
446        ) -> Result<Box<dyn FittedPipelineEstimator<f64>>, FerroError> {
447            Ok(Box::new(FittedSumEstimator))
448        }
449    }
450
451    struct FittedSumEstimator;
452
453    impl FittedPipelineEstimator<f64> for FittedSumEstimator {
454        fn predict_pipeline(&self, x: &Array2<f64>) -> Result<Array1<f64>, FerroError> {
455            let sums: Vec<f64> = x.rows().into_iter().map(|row| row.sum()).collect();
456            Ok(Array1::from_vec(sums))
457        }
458    }
459
460    // -- f32 test fixtures ---------------------------------------------------
461
462    /// A trivial f32 transformer that doubles all values.
463    struct DoublingTransformerF32;
464
465    impl PipelineTransformer<f32> for DoublingTransformerF32 {
466        fn fit_pipeline(
467            &self,
468            _x: &Array2<f32>,
469            _y: &Array1<f32>,
470        ) -> Result<Box<dyn FittedPipelineTransformer<f32>>, FerroError> {
471            Ok(Box::new(FittedDoublingTransformerF32))
472        }
473    }
474
475    struct FittedDoublingTransformerF32;
476
477    impl FittedPipelineTransformer<f32> for FittedDoublingTransformerF32 {
478        fn transform_pipeline(&self, x: &Array2<f32>) -> Result<Array2<f32>, FerroError> {
479            Ok(x.mapv(|v| v * 2.0))
480        }
481    }
482
483    /// A trivial f32 estimator that sums each row.
484    struct SumEstimatorF32;
485
486    impl PipelineEstimator<f32> for SumEstimatorF32 {
487        fn fit_pipeline(
488            &self,
489            _x: &Array2<f32>,
490            _y: &Array1<f32>,
491        ) -> Result<Box<dyn FittedPipelineEstimator<f32>>, FerroError> {
492            Ok(Box::new(FittedSumEstimatorF32))
493        }
494    }
495
496    struct FittedSumEstimatorF32;
497
498    impl FittedPipelineEstimator<f32> for FittedSumEstimatorF32 {
499        fn predict_pipeline(&self, x: &Array2<f32>) -> Result<Array1<f32>, FerroError> {
500            let sums: Vec<f32> = x.rows().into_iter().map(|row| row.sum()).collect();
501            Ok(Array1::from_vec(sums))
502        }
503    }
504
505    // -- Tests ---------------------------------------------------------------
506
507    #[test]
508    fn test_pipeline_fit_predict() {
509        let pipeline = Pipeline::new()
510            .transform_step("doubler", Box::new(DoublingTransformer))
511            .estimator_step("sum", Box::new(SumEstimator));
512
513        let x = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
514        let y = Array1::from_vec(vec![0.0, 1.0]);
515
516        let fitted = pipeline.fit(&x, &y).unwrap();
517        let preds = fitted.predict(&x).unwrap();
518
519        // After doubling: [[2,4,6],[8,10,12]], sums: [12, 30]
520        assert_eq!(preds.len(), 2);
521        assert!((preds[0] - 12.0).abs() < 1e-10);
522        assert!((preds[1] - 30.0).abs() < 1e-10);
523    }
524
525    #[test]
526    fn test_pipeline_f32_fit_predict() {
527        let pipeline = Pipeline::<f32>::new()
528            .transform_step("doubler", Box::new(DoublingTransformerF32))
529            .estimator_step("sum", Box::new(SumEstimatorF32));
530
531        let x = Array2::from_shape_vec((2, 3), vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
532        let y = Array1::from_vec(vec![0.0f32, 1.0]);
533
534        let fitted = pipeline.fit(&x, &y).unwrap();
535        let preds = fitted.predict(&x).unwrap();
536
537        assert_eq!(preds.len(), 2);
538        assert!((preds[0] - 12.0).abs() < 1e-5);
539        assert!((preds[1] - 30.0).abs() < 1e-5);
540    }
541
542    #[test]
543    fn test_pipeline_step_builder() {
544        let pipeline = Pipeline::new()
545            .step("doubler", as_transform_step(DoublingTransformer))
546            .step("sum", as_estimator_step(SumEstimator));
547
548        let x = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
549        let y = Array1::from_vec(vec![0.0, 1.0]);
550
551        let fitted = pipeline.fit(&x, &y).unwrap();
552        let preds = fitted.predict(&x).unwrap();
553
554        assert!((preds[0] - 12.0).abs() < 1e-10);
555        assert!((preds[1] - 30.0).abs() < 1e-10);
556    }
557
558    #[test]
559    fn test_pipeline_no_estimator_returns_error() {
560        let pipeline = Pipeline::new().transform_step("doubler", Box::new(DoublingTransformer));
561
562        let x = Array2::<f64>::zeros((2, 3));
563        let y = Array1::from_vec(vec![0.0, 1.0]);
564
565        let result = pipeline.fit(&x, &y);
566        assert!(result.is_err());
567    }
568
569    #[test]
570    fn test_pipeline_estimator_only() {
571        let pipeline = Pipeline::new().estimator_step("sum", Box::new(SumEstimator));
572
573        let x = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
574        let y = Array1::from_vec(vec![0.0, 1.0]);
575
576        let fitted = pipeline.fit(&x, &y).unwrap();
577        let preds = fitted.predict(&x).unwrap();
578
579        // No transform, just sum: [6, 15]
580        assert!((preds[0] - 6.0).abs() < 1e-10);
581        assert!((preds[1] - 15.0).abs() < 1e-10);
582    }
583
584    #[test]
585    fn test_fitted_pipeline_step_names() {
586        let pipeline = Pipeline::new()
587            .transform_step("scaler", Box::new(DoublingTransformer))
588            .transform_step("normalizer", Box::new(DoublingTransformer))
589            .estimator_step("clf", Box::new(SumEstimator));
590
591        let x = Array2::<f64>::zeros((2, 3));
592        let y = Array1::from_vec(vec![0.0, 1.0]);
593
594        let fitted = pipeline.fit(&x, &y).unwrap();
595        let names = fitted.step_names();
596        assert_eq!(names, vec!["scaler", "normalizer", "clf"]);
597    }
598
599    #[test]
600    fn test_multiple_transform_steps() {
601        // Two doublers in sequence should quadruple values.
602        let pipeline = Pipeline::new()
603            .transform_step("double1", Box::new(DoublingTransformer))
604            .transform_step("double2", Box::new(DoublingTransformer))
605            .estimator_step("sum", Box::new(SumEstimator));
606
607        let x = Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).unwrap();
608        let y = Array1::from_vec(vec![0.0]);
609
610        let fitted = pipeline.fit(&x, &y).unwrap();
611        let preds = fitted.predict(&x).unwrap();
612
613        // 1.0 * 2 * 2 = 4.0 per element, sum of 2 elements = 8.0
614        assert!((preds[0] - 8.0).abs() < 1e-10);
615    }
616
617    #[test]
618    fn test_pipeline_default() {
619        let pipeline = Pipeline::<f64>::default();
620        let x = Array2::<f64>::zeros((2, 3));
621        let y = Array1::from_vec(vec![0.0, 1.0]);
622        // Should error because no estimator.
623        assert!(pipeline.fit(&x, &y).is_err());
624    }
625
626    #[test]
627    fn test_pipeline_is_send_sync() {
628        fn assert_send_sync<T: Send + Sync>() {}
629        // Pipeline itself is Send+Sync because it only stores
630        // Send+Sync trait objects.
631        assert_send_sync::<Pipeline<f64>>();
632        assert_send_sync::<Pipeline<f32>>();
633        assert_send_sync::<FittedPipeline<f64>>();
634        assert_send_sync::<FittedPipeline<f32>>();
635    }
636}