Skip to main content

llm_optimizer_processor/pipeline/
normalization_operator.rs

1//! Normalization operator for time-series data alignment
2//!
3//! This module provides the `NormalizationOperator` which aligns events to fixed intervals,
4//! fills missing data points, and handles out-of-order events.
5
6use crate::config::{AlignmentStrategy, FillStrategy, NormalizationConfig};
7use crate::error::Result;
8use crate::pipeline::operator::{OperatorContext, StreamOperator};
9use async_trait::async_trait;
10use chrono::{DateTime, Duration, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, VecDeque};
13use std::sync::{Arc, RwLock};
14
15/// Event with timestamp for normalization
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TimestampedEvent {
18    pub timestamp: DateTime<Utc>,
19    pub value: f64,
20}
21
22/// Statistics for normalization operations
23#[derive(Debug, Clone, Default)]
24pub struct NormalizationStats {
25    /// Total events processed
26    pub events_processed: u64,
27    /// Events aligned to intervals
28    pub events_aligned: u64,
29    /// Missing intervals filled
30    pub intervals_filled: u64,
31    /// Outliers detected and dropped
32    pub outliers_dropped: u64,
33    /// Out-of-order events reordered
34    pub out_of_order_events: u64,
35    /// Events dropped (buffer overflow or other reasons)
36    pub events_dropped: u64,
37}
38
39/// Normalization operator for time-series data
40///
41/// This operator normalizes time-series data by:
42/// - Aligning event timestamps to fixed intervals
43/// - Buffering and reordering out-of-order events
44/// - Filling missing data points using various strategies
45/// - Detecting and removing outliers
46/// - Emitting aligned events at consistent intervals
47///
48/// # Example
49///
50/// ```rust
51/// use processor::pipeline::operator::NormalizationOperator;
52/// use processor::config::{NormalizationConfig, FillStrategy, AlignmentStrategy};
53/// use std::time::Duration;
54///
55/// let config = NormalizationConfig::new()
56///     .enabled()
57///     .with_interval(Duration::from_secs(5))
58///     .with_fill_strategy(FillStrategy::LinearInterpolation)
59///     .with_alignment(AlignmentStrategy::Start);
60///
61/// let operator = NormalizationOperator::new("normalization", config);
62/// ```
63#[derive(Debug, Clone)]
64pub struct NormalizationOperator {
65    name: String,
66    config: NormalizationConfig,
67    // Buffer for out-of-order events (sorted by timestamp)
68    buffer: Arc<RwLock<BTreeMap<i64, Vec<f64>>>>,
69    // Last emitted timestamp
70    last_emitted: Arc<RwLock<Option<DateTime<Utc>>>>,
71    // Statistics
72    stats: Arc<RwLock<NormalizationStats>>,
73    // Recent values for outlier detection
74    recent_values: Arc<RwLock<VecDeque<f64>>>,
75}
76
77impl NormalizationOperator {
78    /// Create a new normalization operator
79    pub fn new<S: Into<String>>(name: S, config: NormalizationConfig) -> Self {
80        Self {
81            name: name.into(),
82            config,
83            buffer: Arc::new(RwLock::new(BTreeMap::new())),
84            last_emitted: Arc::new(RwLock::new(None)),
85            stats: Arc::new(RwLock::new(NormalizationStats::default())),
86            recent_values: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
87        }
88    }
89
90    /// Align a timestamp to the configured interval
91    fn align_timestamp(&self, timestamp: DateTime<Utc>) -> DateTime<Utc> {
92        let interval_ms = self.config.interval.as_millis() as i64;
93        let timestamp_ms = timestamp.timestamp_millis();
94
95        let aligned_ms = match self.config.alignment {
96            AlignmentStrategy::Start => {
97                // Round down to interval boundary
98                (timestamp_ms / interval_ms) * interval_ms
99            }
100            AlignmentStrategy::End => {
101                // Round up to interval boundary
102                ((timestamp_ms + interval_ms - 1) / interval_ms) * interval_ms
103            }
104            AlignmentStrategy::Center => {
105                // Round to interval and add half interval
106                let base = (timestamp_ms / interval_ms) * interval_ms;
107                base + interval_ms / 2
108            }
109            AlignmentStrategy::Nearest => {
110                // Round to nearest interval boundary
111                ((timestamp_ms + interval_ms / 2) / interval_ms) * interval_ms
112            }
113        };
114
115        DateTime::from_timestamp_millis(aligned_ms)
116            .unwrap_or(timestamp)
117    }
118
119    /// Check if a value is an outlier using z-score
120    fn is_outlier(&self, value: f64) -> bool {
121        if !self.config.drop_outliers {
122            return false;
123        }
124
125        let recent = self.recent_values.read().unwrap();
126        if recent.len() < 3 {
127            // Not enough data to determine outliers
128            return false;
129        }
130
131        // Calculate mean and standard deviation
132        let sum: f64 = recent.iter().sum();
133        let mean = sum / recent.len() as f64;
134
135        let variance: f64 = recent.iter()
136            .map(|&v| (v - mean).powi(2))
137            .sum::<f64>() / recent.len() as f64;
138        let std_dev = variance.sqrt();
139
140        if std_dev < 1e-10 {
141            // No variance, can't detect outliers
142            return false;
143        }
144
145        // Calculate z-score
146        let z_score = ((value - mean) / std_dev).abs();
147        z_score > self.config.outlier_threshold
148    }
149
150    /// Update recent values buffer for outlier detection
151    fn update_recent_values(&self, value: f64) {
152        let mut recent = self.recent_values.write().unwrap();
153        recent.push_back(value);
154        if recent.len() > 100 {
155            recent.pop_front();
156        }
157    }
158
159    /// Fill missing intervals between two timestamps
160    fn fill_missing_intervals(
161        &self,
162        start: DateTime<Utc>,
163        end: DateTime<Utc>,
164        prev_value: Option<f64>,
165        next_value: Option<f64>,
166    ) -> Vec<TimestampedEvent> {
167        let mut filled = Vec::new();
168        let interval = self.config.interval;
169
170        let mut current = start + interval;
171        while current < end {
172            let value = match self.config.fill_strategy {
173                FillStrategy::Zero => 0.0,
174                FillStrategy::ForwardFill => prev_value.unwrap_or(0.0),
175                FillStrategy::BackwardFill => next_value.unwrap_or(0.0),
176                FillStrategy::LinearInterpolation => {
177                    if let (Some(prev), Some(next)) = (prev_value, next_value) {
178                        // Linear interpolation
179                        let total_gap = (end.timestamp_millis() - start.timestamp_millis()) as f64;
180                        let current_offset = (current.timestamp_millis() - start.timestamp_millis()) as f64;
181                        let ratio = current_offset / total_gap;
182                        prev + (next - prev) * ratio
183                    } else {
184                        prev_value.or(next_value).unwrap_or(0.0)
185                    }
186                }
187                FillStrategy::Drop => {
188                    // Don't fill, skip to next
189                    current = current + interval;
190                    continue;
191                }
192                FillStrategy::Constant => {
193                    // Could be configurable, defaulting to 0 for now
194                    0.0
195                }
196                FillStrategy::Mean => {
197                    if let (Some(prev), Some(next)) = (prev_value, next_value) {
198                        (prev + next) / 2.0
199                    } else {
200                        prev_value.or(next_value).unwrap_or(0.0)
201                    }
202                }
203            };
204
205            filled.push(TimestampedEvent {
206                timestamp: current,
207                value,
208            });
209
210            current = current + interval;
211        }
212
213        filled
214    }
215
216    /// Get normalization statistics
217    pub fn stats(&self) -> NormalizationStats {
218        self.stats.read().unwrap().clone()
219    }
220
221    /// Reset the operator state
222    pub fn reset(&self) {
223        self.buffer.write().unwrap().clear();
224        *self.last_emitted.write().unwrap() = None;
225        *self.stats.write().unwrap() = NormalizationStats::default();
226        self.recent_values.write().unwrap().clear();
227    }
228}
229
230#[async_trait]
231impl StreamOperator for NormalizationOperator {
232    async fn process(
233        &self,
234        input: Vec<u8>,
235        _ctx: &OperatorContext,
236    ) -> Result<Vec<Vec<u8>>> {
237        // If normalization is disabled, pass through
238        if !self.config.enabled {
239            return Ok(vec![input]);
240        }
241
242        // Deserialize the event
243        let event: TimestampedEvent = bincode::deserialize(&input)?;
244
245        // Update statistics
246        {
247            let mut stats = self.stats.write().unwrap();
248            stats.events_processed += 1;
249        }
250
251        // Check for outliers
252        if self.is_outlier(event.value) {
253            let mut stats = self.stats.write().unwrap();
254            stats.outliers_dropped += 1;
255            return Ok(vec![]); // Drop outlier
256        }
257
258        // Update recent values for outlier detection
259        self.update_recent_values(event.value);
260
261        // Align timestamp
262        let aligned_timestamp = self.align_timestamp(event.timestamp);
263        let aligned_ms = aligned_timestamp.timestamp_millis();
264
265        // Add to buffer
266        {
267            let mut buffer = self.buffer.write().unwrap();
268            buffer.entry(aligned_ms)
269                .or_insert_with(Vec::new)
270                .push(event.value);
271
272            // Check for buffer overflow
273            if buffer.len() > self.config.buffer_size {
274                // Remove oldest entries
275                if let Some(&oldest_key) = buffer.keys().next() {
276                    buffer.remove(&oldest_key);
277                    let mut stats = self.stats.write().unwrap();
278                    stats.events_dropped += 1;
279                }
280            }
281        }
282
283        // Check if we can emit aligned events
284        let mut output = Vec::new();
285        let last_emitted = self.last_emitted.read().unwrap().clone();
286
287        // Emit all buffered events up to current time - interval
288        // (to allow for late arrivals within the buffer window)
289        let emit_threshold = aligned_timestamp - self.config.interval;
290
291        let events_to_emit: Vec<(DateTime<Utc>, f64)> = {
292            let buffer = self.buffer.read().unwrap();
293            buffer.iter()
294                .filter_map(|(&ts_ms, values)| {
295                    let ts = DateTime::from_timestamp_millis(ts_ms)?;
296                    if ts <= emit_threshold {
297                        // Average multiple values at same timestamp
298                        let avg = values.iter().sum::<f64>() / values.len() as f64;
299                        Some((ts, avg))
300                    } else {
301                        None
302                    }
303                })
304                .collect()
305        };
306
307        // Remove emitted events from buffer
308        {
309            let mut buffer = self.buffer.write().unwrap();
310            for (ts, _) in &events_to_emit {
311                buffer.remove(&ts.timestamp_millis());
312            }
313        }
314
315        // Fill missing intervals if needed
316        for (ts, value) in events_to_emit {
317            // Check for gaps and fill if necessary
318            if let Some(last) = last_emitted {
319                let gap = ts.signed_duration_since(last);
320                let interval_chrono = chrono::Duration::from_std(self.config.interval)
321                    .unwrap_or(chrono::Duration::seconds(60));
322                if gap > interval_chrono {
323                    // Fill the gap
324                    let filled = self.fill_missing_intervals(
325                        last,
326                        ts,
327                        None, // We don't have the previous value stored
328                        Some(value),
329                    );
330
331                    let mut stats = self.stats.write().unwrap();
332                    stats.intervals_filled += filled.len() as u64;
333
334                    for filled_event in filled {
335                        let serialized = bincode::serialize(&filled_event)?;
336                        output.push(serialized);
337                    }
338                }
339            }
340
341            // Emit the aligned event
342            let aligned_event = TimestampedEvent {
343                timestamp: ts,
344                value,
345            };
346
347            let serialized = bincode::serialize(&aligned_event)?;
348            output.push(serialized);
349
350            // Update last emitted
351            *self.last_emitted.write().unwrap() = Some(ts);
352
353            let mut stats = self.stats.write().unwrap();
354            stats.events_aligned += 1;
355        }
356
357        Ok(output)
358    }
359
360    fn name(&self) -> &str {
361        &self.name
362    }
363
364    async fn open(&mut self) -> Result<()> {
365        self.reset();
366        Ok(())
367    }
368
369    async fn close(&mut self) -> Result<()> {
370        // Emit any remaining buffered events
371        self.buffer.write().unwrap().clear();
372        Ok(())
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::watermark::Watermark;
380
381    #[tokio::test]
382    async fn test_normalization_operator_disabled() {
383        let config = NormalizationConfig::default(); // disabled by default
384        let operator = NormalizationOperator::new("test", config);
385        let ctx = OperatorContext::new(Watermark::min(), 0);
386
387        let event = TimestampedEvent {
388            timestamp: Utc::now(),
389            value: 42.0,
390        };
391
392        let input = bincode::serialize(&event).unwrap();
393        let output = operator.process(input.clone(), &ctx).await.unwrap();
394
395        // Should pass through when disabled
396        assert_eq!(output.len(), 1);
397        assert_eq!(output[0], input);
398    }
399
400    #[tokio::test]
401    async fn test_normalization_operator_alignment() {
402        let config = NormalizationConfig::new()
403            .enabled()
404            .with_interval(Duration::from_secs(5).to_std().unwrap())
405            .with_alignment(AlignmentStrategy::Start);
406
407        let operator = NormalizationOperator::new("test", config);
408
409        // Test alignment
410        let ts = DateTime::from_timestamp(1234567892, 0).unwrap(); // 2009-02-13 23:31:32
411        let aligned = operator.align_timestamp(ts);
412
413        // Should align to 2009-02-13 23:31:30 (nearest 5-second boundary)
414        assert_eq!(aligned.timestamp() % 5, 0);
415    }
416
417    #[tokio::test]
418    async fn test_normalization_operator_stats() {
419        let config = NormalizationConfig::new()
420            .enabled()
421            .with_interval(Duration::from_secs(1).to_std().unwrap());
422
423        let operator = NormalizationOperator::new("test", config);
424        let ctx = OperatorContext::new(Watermark::min(), 0);
425
426        let event = TimestampedEvent {
427            timestamp: Utc::now(),
428            value: 42.0,
429        };
430
431        let input = bincode::serialize(&event).unwrap();
432        operator.process(input, &ctx).await.unwrap();
433
434        let stats = operator.stats();
435        assert_eq!(stats.events_processed, 1);
436    }
437
438    #[tokio::test]
439    async fn test_normalization_operator_outlier_detection() {
440        let config = NormalizationConfig::new()
441            .enabled()
442            .with_interval(Duration::from_secs(1).to_std().unwrap())
443            .with_outlier_detection(2.0);
444
445        let operator = NormalizationOperator::new("test", config);
446        let ctx = OperatorContext::new(Watermark::min(), 0);
447
448        // Add some normal values
449        for i in 1..=10 {
450            let event = TimestampedEvent {
451                timestamp: Utc::now(),
452                value: 10.0 + i as f64,
453            };
454            let input = bincode::serialize(&event).unwrap();
455            operator.process(input, &ctx).await.unwrap();
456        }
457
458        // Add an outlier
459        let outlier = TimestampedEvent {
460            timestamp: Utc::now(),
461            value: 1000.0,
462        };
463        let input = bincode::serialize(&outlier).unwrap();
464        operator.process(input, &ctx).await.unwrap();
465
466        let stats = operator.stats();
467        assert!(stats.outliers_dropped > 0);
468    }
469
470    #[test]
471    fn test_alignment_strategies() {
472        let timestamp = DateTime::from_timestamp(1234567892, 0).unwrap();
473
474        // Test Start alignment
475        let config = NormalizationConfig::new()
476            .with_interval(Duration::from_secs(5).to_std().unwrap())
477            .with_alignment(AlignmentStrategy::Start);
478        let operator = NormalizationOperator::new("test", config);
479        let aligned = operator.align_timestamp(timestamp);
480        assert_eq!(aligned.timestamp() % 5, 0);
481        assert!(aligned <= timestamp);
482
483        // Test End alignment
484        let config = NormalizationConfig::new()
485            .with_interval(Duration::from_secs(5).to_std().unwrap())
486            .with_alignment(AlignmentStrategy::End);
487        let operator = NormalizationOperator::new("test", config);
488        let aligned = operator.align_timestamp(timestamp);
489        assert_eq!(aligned.timestamp() % 5, 0);
490        assert!(aligned >= timestamp);
491    }
492
493    #[test]
494    fn test_normalization_operator_reset() {
495        let config = NormalizationConfig::new().enabled();
496        let operator = NormalizationOperator::new("test", config);
497
498        // Add some state
499        operator.update_recent_values(42.0);
500
501        // Reset
502        operator.reset();
503
504        let stats = operator.stats();
505        assert_eq!(stats.events_processed, 0);
506    }
507}