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(¤t_x, y)?;
259 current_x = fitted.transform_pipeline(¤t_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(¤t_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(¤t_x)?;
324 }
325
326 self.estimator.1.predict_pipeline(¤t_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}