rill_ml/preprocessing/
clipper.rs1use crate::error::{RillError, ensure_finite};
7use crate::traits::Transformer;
8
9#[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 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 pub const fn min(&self) -> f64 {
41 self.min
42 }
43
44 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 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}