chronix_analytics/preprocess/
resampling.rs1use crate::compute::simd_sum;
4use crate::preprocess::interpolation::Interpolator;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub enum AggregationFn {
9 Mean,
11 Min,
13 Max,
15 Sum,
17 Last,
19 First,
21}
22
23#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25pub struct ResampleConfig {
26 pub target_interval_ns: i64,
28 pub aggregation: AggregationFn,
30 pub interpolation: Interpolator,
32}
33
34pub struct Resampler;
36
37impl Resampler {
38 pub fn resample(
43 timestamps: &[i64],
44 values: &[f64],
45 config: &ResampleConfig,
46 ) -> (Vec<i64>, Vec<f64>) {
47 if timestamps.is_empty() {
48 return (Vec::new(), Vec::new());
49 }
50 if timestamps.len() == 1 {
51 return (timestamps.to_vec(), values.to_vec());
52 }
53
54 let source_interval = Self::estimate_interval(timestamps);
56
57 if config.target_interval_ns > source_interval {
58 Self::downscale(timestamps, values, config)
59 } else if config.target_interval_ns < source_interval {
60 Self::upscale(timestamps, values, config)
61 } else {
62 (timestamps.to_vec(), values.to_vec())
63 }
64 }
65
66 fn estimate_interval(timestamps: &[i64]) -> i64 {
68 if timestamps.len() < 2 {
69 return 0;
70 }
71 let mut deltas: Vec<i64> = timestamps.windows(2).map(|w| w[1] - w[0]).collect();
72 deltas.sort_unstable();
73 deltas[deltas.len() / 2] }
75
76 fn downscale(
78 timestamps: &[i64],
79 values: &[f64],
80 config: &ResampleConfig,
81 ) -> (Vec<i64>, Vec<f64>) {
82 let interval = config.target_interval_ns;
83 if interval <= 0 {
84 return (timestamps.to_vec(), values.to_vec());
85 }
86 let start = timestamps[0];
87 let end = timestamps[timestamps.len() - 1];
88
89 let n_buckets = ((end - start) / interval).saturating_add(1) as usize;
92 let cap = n_buckets.min(timestamps.len());
95 let mut out_ts = Vec::with_capacity(cap);
96 let mut out_vals = Vec::with_capacity(cap);
97
98 let mut cursor = 0usize;
102 let mut bucket_start = start;
103 while bucket_start <= end && cursor < timestamps.len() {
104 let bucket_end = bucket_start.saturating_add(interval);
105
106 let bucket_begin = cursor;
108 while cursor < timestamps.len()
109 && timestamps[cursor] >= bucket_start
110 && timestamps[cursor] < bucket_end
111 {
112 cursor += 1;
113 }
114 let bucket_vals = &values[bucket_begin..cursor];
115
116 if !bucket_vals.is_empty() {
117 let agg = match config.aggregation {
118 AggregationFn::Mean => simd_sum(bucket_vals) / bucket_vals.len() as f64,
119 AggregationFn::Min => bucket_vals.iter().copied().fold(f64::INFINITY, f64::min),
120 AggregationFn::Max => bucket_vals
121 .iter()
122 .copied()
123 .fold(f64::NEG_INFINITY, f64::max),
124 AggregationFn::Sum => simd_sum(bucket_vals),
125 AggregationFn::Last => bucket_vals[bucket_vals.len() - 1],
126 AggregationFn::First => bucket_vals[0],
127 };
128 out_ts.push(bucket_start);
129 out_vals.push(agg);
130 bucket_start = bucket_end;
131 } else if cursor < timestamps.len() {
132 let offset = timestamps[cursor] - start;
137 bucket_start = start.saturating_add(offset - offset.rem_euclid(interval));
138 } else {
139 break;
141 }
142 }
143
144 (out_ts, out_vals)
145 }
146
147 fn upscale(
149 timestamps: &[i64],
150 values: &[f64],
151 config: &ResampleConfig,
152 ) -> (Vec<i64>, Vec<f64>) {
153 match config
156 .interpolation
157 .fill(timestamps, values, config.target_interval_ns)
158 {
159 Ok((ts, vals, _, _)) => (ts, vals),
160 Err(_) => (timestamps.to_vec(), values.to_vec()),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn make_ts(secs: &[i64]) -> Vec<i64> {
170 secs.iter().map(|&s| s * 1_000_000_000).collect()
171 }
172
173 #[test]
174 fn downscale_mean() {
175 let ts = make_ts(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
177 let vals: Vec<f64> = (0..9).map(|i| i as f64 * 10.0).collect();
178 let config = ResampleConfig {
179 target_interval_ns: 3_000_000_000,
180 aggregation: AggregationFn::Mean,
181 interpolation: Interpolator::Linear,
182 };
183 let (out_ts, out_vals) = Resampler::resample(&ts, &vals, &config);
184 assert_eq!(out_ts.len(), 3);
185 assert!((out_vals[0] - 10.0).abs() < 1e-10);
187 assert!((out_vals[1] - 40.0).abs() < 1e-10);
189 assert!((out_vals[2] - 70.0).abs() < 1e-10);
191 }
192
193 #[test]
194 fn downscale_min_max() {
195 let ts = make_ts(&[0, 1, 2, 3, 4, 5]);
196 let vals = vec![5.0, 1.0, 8.0, 3.0, 9.0, 2.0];
197 let min_config = ResampleConfig {
198 target_interval_ns: 3_000_000_000,
199 aggregation: AggregationFn::Min,
200 interpolation: Interpolator::Linear,
201 };
202 let (_, min_vals) = Resampler::resample(&ts, &vals, &min_config);
203 assert!((min_vals[0] - 1.0).abs() < 1e-10);
204
205 let max_config = ResampleConfig {
206 target_interval_ns: 3_000_000_000,
207 aggregation: AggregationFn::Max,
208 interpolation: Interpolator::Linear,
209 };
210 let (_, max_vals) = Resampler::resample(&ts, &vals, &max_config);
211 assert!((max_vals[0] - 8.0).abs() < 1e-10);
212 }
213
214 #[test]
215 fn empty_data() {
216 let config = ResampleConfig {
217 target_interval_ns: 1_000_000_000,
218 aggregation: AggregationFn::Mean,
219 interpolation: Interpolator::Linear,
220 };
221 let (ts, vals) = Resampler::resample(&[], &[], &config);
222 assert!(ts.is_empty());
223 assert!(vals.is_empty());
224 }
225
226 #[test]
227 fn same_interval_passthrough() {
228 let ts = make_ts(&[0, 1, 2, 3]);
229 let vals = vec![1.0, 2.0, 3.0, 4.0];
230 let config = ResampleConfig {
231 target_interval_ns: 1_000_000_000,
232 aggregation: AggregationFn::Mean,
233 interpolation: Interpolator::Linear,
234 };
235 let (out_ts, out_vals) = Resampler::resample(&ts, &vals, &config);
236 assert_eq!(out_ts, ts);
237 assert_eq!(out_vals, vals);
238 }
239
240 #[test]
241 fn sparse_downscale_skips_empty_buckets() {
242 let ts = make_ts(&[0, 1_000_000]);
246 let vals = vec![10.0, 20.0];
247 let config = ResampleConfig {
248 target_interval_ns: 1_000_000_000, aggregation: AggregationFn::Mean,
250 interpolation: Interpolator::Linear,
251 };
252 let start = std::time::Instant::now();
253 let (out_ts, out_vals) = Resampler::resample(&ts, &vals, &config);
254 let elapsed = start.elapsed();
255
256 assert_eq!(out_ts.len(), 2);
258 assert!((out_vals[0] - 10.0).abs() < 1e-10);
259 assert!((out_vals[1] - 20.0).abs() < 1e-10);
260
261 assert!(
263 elapsed.as_millis() < 100,
264 "Sparse downscale took {}ms — skip-ahead not working",
265 elapsed.as_millis()
266 );
267 }
268
269 #[test]
270 fn sparse_downscale_correctness() {
271 let ts = make_ts(&[0, 1, 2, 100, 101, 500]);
273 let vals = vec![1.0, 2.0, 3.0, 10.0, 11.0, 50.0];
274 let config = ResampleConfig {
275 target_interval_ns: 3_000_000_000, aggregation: AggregationFn::Mean,
277 interpolation: Interpolator::Linear,
278 };
279 let (out_ts, out_vals) = Resampler::resample(&ts, &vals, &config);
280
281 assert_eq!(out_ts.len(), 3);
285 assert!((out_vals[0] - 2.0).abs() < 1e-10);
286 assert!((out_vals[1] - 10.5).abs() < 1e-10);
287 assert!((out_vals[2] - 50.0).abs() < 1e-10);
288 }
289}