Skip to main content

rill_ml/preprocessing/
clipper.rs

1//! Feature clipper.
2//!
3//! Clips each feature to `[min, max]`. Time complexity per transform:
4//! `O(d)`. Space complexity: `O(1)`.
5
6use crate::error::{RillError, ensure_finite};
7use crate::traits::Transformer;
8
9/// A simple element-wise clipper that bounds feature values to a fixed range.
10#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct Clipper {
13    feature_count: usize,
14    min: f64,
15    max: f64,
16}
17
18impl Clipper {
19    /// Create a new clipper for `feature_count` features, clamping to `[min, max]`.
20    pub fn new(feature_count: usize, min: f64, max: f64) -> Result<Self, RillError> {
21        if feature_count == 0 {
22            return Err(RillError::EmptyFeatures);
23        }
24        ensure_finite("min", min)?;
25        ensure_finite("max", max)?;
26        if min > max {
27            return Err(RillError::InvalidParameter {
28                name: "min",
29                value: min,
30            });
31        }
32        Ok(Self {
33            feature_count,
34            min,
35            max,
36        })
37    }
38
39    /// The lower bound.
40    pub const fn min(&self) -> f64 {
41        self.min
42    }
43
44    /// The upper bound.
45    pub const fn max(&self) -> f64 {
46        self.max
47    }
48}
49
50impl Transformer for Clipper {
51    fn input_dim(&self) -> usize {
52        self.feature_count
53    }
54
55    fn output_dim(&self) -> usize {
56        self.feature_count
57    }
58
59    fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
60        crate::error::validate_features(self.feature_count, features)?;
61        Ok(features
62            .iter()
63            .map(|&x| x.clamp(self.min, self.max))
64            .collect())
65    }
66
67    fn update(&mut self, _features: &[f64]) -> Result<(), RillError> {
68        // Clipper is stateless; validate for consistency.
69        Ok(())
70    }
71
72    fn samples_seen(&self) -> u64 {
73        0
74    }
75
76    fn reset(&mut self) {}
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn clipper_clamps_values() {
85        let c = Clipper::new(3, -1.0, 1.0).unwrap();
86        let out = c.transform(&[-5.0, 0.5, 5.0]).unwrap();
87        assert_eq!(out, vec![-1.0, 0.5, 1.0]);
88    }
89
90    #[test]
91    fn invalid_bounds_rejected() {
92        assert!(Clipper::new(1, 5.0, 1.0).is_err());
93    }
94
95    #[test]
96    fn clipper_is_stateless() {
97        let mut c = Clipper::new(1, 0.0, 1.0).unwrap();
98        c.update(&[0.5]).unwrap();
99        assert_eq!(c.samples_seen(), 0);
100    }
101}