chronix_analytics/preprocess/
smoothing.rs1use crate::compute::simd_sum;
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7pub enum Smoother {
8 Exponential {
10 alpha: f64,
12 },
13 MovingAverage {
15 window: usize,
17 },
18 WeightedMovingAverage {
20 weights: Vec<f64>,
22 },
23}
24
25impl Smoother {
26 pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
34 match self {
35 Smoother::Exponential { alpha } => {
36 let safe_alpha = alpha.clamp(f64::MIN_POSITIVE, 1.0);
37 ExponentialSmoother::new(safe_alpha).smooth(values)
38 }
39 Smoother::MovingAverage { window } => {
40 let safe_window = (*window).max(1);
41 MovingAverageSmoother::new(safe_window).smooth(values)
42 }
43 Smoother::WeightedMovingAverage { weights } => {
44 if weights.is_empty() {
45 return values.to_vec();
46 }
47 WeightedMovingAverage::new(weights.clone()).smooth(values)
48 }
49 }
50 }
51}
52
53pub struct ExponentialSmoother {
57 alpha: f64,
58}
59
60impl ExponentialSmoother {
61 pub fn new(alpha: f64) -> Self {
66 assert!(
67 alpha > 0.0 && alpha <= 1.0,
68 "alpha must be in (0, 1], got {alpha}"
69 );
70 Self { alpha }
71 }
72
73 pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
75 if values.is_empty() {
76 return Vec::new();
77 }
78 let mut result = Vec::with_capacity(values.len());
79 result.push(values[0]);
80 let one_minus = 1.0 - self.alpha;
81 for i in 1..values.len() {
82 result.push(self.alpha * values[i] + one_minus * result[i - 1]);
83 }
84 result
85 }
86}
87
88pub struct MovingAverageSmoother {
93 window: usize,
94}
95
96impl MovingAverageSmoother {
97 pub fn new(window: usize) -> Self {
102 assert!(window > 0, "window must be > 0");
103 Self { window }
104 }
105
106 pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
112 if values.is_empty() {
113 return Vec::new();
114 }
115
116 let n = values.len();
117 let w = self.window.min(n);
118 let half_left = if w.is_multiple_of(2) {
121 w / 2 - 1
122 } else {
123 w / 2
124 };
125 let half_right = w / 2;
126 let mut result = Vec::with_capacity(n);
127
128 let mut prefix = Vec::with_capacity(n + 1);
131 prefix.push(0.0);
132 let mut acc = 0.0;
133 for &v in values {
134 acc += v;
135 prefix.push(acc);
136 }
137
138 for i in 0..n {
139 let lo = i.saturating_sub(half_left);
140 let hi = (i + half_right + 1).min(n);
141 let count = (hi - lo) as f64;
142 let sum = prefix[hi] - prefix[lo];
143 result.push(sum / count);
144 }
145
146 result
147 }
148}
149
150pub struct WeightedMovingAverage {
152 weights: Vec<f64>,
153 weight_sum: f64,
154}
155
156impl WeightedMovingAverage {
157 pub fn new(weights: Vec<f64>) -> Self {
162 assert!(!weights.is_empty(), "weights must not be empty");
163 let weight_sum = simd_sum(&weights);
164 Self {
165 weights,
166 weight_sum,
167 }
168 }
169
170 pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
175 if values.is_empty() {
176 return Vec::new();
177 }
178
179 let n = values.len();
180 let w = self.weights.len();
181 let mut result = Vec::with_capacity(n);
182
183 for i in 0..n {
184 if i + 1 < w {
185 let available = i + 1;
187 let offset = w - available;
188 let mut sum = 0.0;
189 let mut wsum = 0.0;
190 for (j, &val) in values[..available].iter().enumerate() {
191 sum += val * self.weights[offset + j];
192 wsum += self.weights[offset + j];
193 }
194 result.push(if wsum.abs() > 1e-15 {
195 sum / wsum
196 } else {
197 values[i]
198 });
199 } else {
200 let mut sum = 0.0;
201 let start = i + 1 - w;
202 for j in 0..w {
203 sum += values[start + j] * self.weights[j];
204 }
205 result.push(if self.weight_sum.abs() > 1e-15 {
206 sum / self.weight_sum
207 } else {
208 values[i]
209 });
210 }
211 }
212 result
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::compute::simd_mean;
220
221 #[test]
222 fn exponential_smoother_alpha_1() {
223 let s = ExponentialSmoother::new(1.0);
224 let vals = vec![1.0, 5.0, 3.0, 7.0];
225 let result = s.smooth(&vals);
226 assert_eq!(result, vals); }
228
229 #[test]
230 fn exponential_smoother_smooth() {
231 let s = ExponentialSmoother::new(0.5);
232 let vals = vec![10.0, 20.0, 30.0, 40.0];
233 let result = s.smooth(&vals);
234 assert!((result[0] - 10.0).abs() < 1e-10);
235 assert!((result[1] - 15.0).abs() < 1e-10); assert!((result[2] - 22.5).abs() < 1e-10); assert!((result[3] - 31.25).abs() < 1e-10); }
239
240 #[test]
241 fn moving_average_basic() {
242 let s = MovingAverageSmoother::new(3);
243 let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
244 let result = s.smooth(&vals);
245 assert_eq!(result.len(), 5);
246 assert!((result[1] - 2.0).abs() < 1e-10);
248 assert!((result[2] - 3.0).abs() < 1e-10);
250 }
251
252 #[test]
253 fn moving_average_window_larger_than_data() {
254 let s = MovingAverageSmoother::new(100);
255 let vals = vec![1.0, 2.0, 3.0];
256 let result = s.smooth(&vals);
257 assert_eq!(result.len(), 3);
258 let mean = simd_mean(&vals);
260 assert!((result[1] - mean).abs() < 1e-10);
262 }
263
264 #[test]
265 fn weighted_moving_average() {
266 let s = WeightedMovingAverage::new(vec![1.0, 2.0, 3.0]);
268 let vals = vec![10.0, 20.0, 30.0, 40.0];
269 let result = s.smooth(&vals);
270 assert_eq!(result.len(), 4);
271 assert!((result[2] - 140.0 / 6.0).abs() < 1e-10);
273 }
274
275 #[test]
276 fn empty_data() {
277 assert!(ExponentialSmoother::new(0.5).smooth(&[]).is_empty());
278 assert!(MovingAverageSmoother::new(3).smooth(&[]).is_empty());
279 assert!(WeightedMovingAverage::new(vec![1.0]).smooth(&[]).is_empty());
280 }
281
282 #[test]
283 fn smoother_enum_dispatch() {
284 let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
285 let s = Smoother::Exponential { alpha: 0.5 };
286 let r = s.smooth(&vals);
287 assert_eq!(r.len(), 5);
288 }
289}