kizzasi_logic/constraint/
sliding_window.rs

1use serde::{Deserialize, Serialize};
2
3// ============================================================================
4// Sliding Window Constraints
5// ============================================================================
6
7/// Sliding window constraint for time-series data
8///
9/// Enforces constraints over a sliding window of recent values.
10/// Useful for smoothness, bounded variation, and statistical constraints.
11#[derive(Debug, Clone)]
12pub struct SlidingWindowConstraint {
13    name: String,
14    window_size: usize,
15    constraint_fn: SlidingWindowFn,
16    buffer: Vec<f32>,
17    weight: f32,
18}
19
20/// Types of sliding window constraints
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum SlidingWindowFn {
23    /// Mean of window must be in range [lo, hi]
24    MeanInRange { lo: f32, hi: f32 },
25    /// Variance of window must be <= max_var
26    MaxVariance { max_var: f32 },
27    /// Range (max - min) of window must be <= max_range
28    MaxRange { max_range: f32 },
29    /// Sum of absolute differences must be <= max_variation
30    BoundedVariation { max_variation: f32 },
31    /// All values in window must be in range [lo, hi]
32    AllInRange { lo: f32, hi: f32 },
33    /// At least one value in window must satisfy the range
34    AnyInRange { lo: f32, hi: f32 },
35    /// Trend constraint: regression slope must be in range
36    TrendInRange { min_slope: f32, max_slope: f32 },
37}
38
39impl SlidingWindowConstraint {
40    /// Create a new sliding window constraint
41    pub fn new(name: &str, window_size: usize, constraint_fn: SlidingWindowFn) -> Self {
42        Self {
43            name: name.to_string(),
44            window_size,
45            constraint_fn,
46            buffer: Vec::with_capacity(window_size),
47            weight: 1.0,
48        }
49    }
50
51    /// Set weight
52    pub fn with_weight(mut self, weight: f32) -> Self {
53        self.weight = weight;
54        self
55    }
56
57    /// Push a new value and check constraint
58    ///
59    /// Returns (satisfied, violation)
60    pub fn push_and_check(&mut self, value: f32) -> (bool, f32) {
61        // Add to buffer
62        self.buffer.push(value);
63        if self.buffer.len() > self.window_size {
64            self.buffer.remove(0);
65        }
66
67        // Not enough data yet
68        if self.buffer.len() < self.window_size {
69            return (true, 0.0);
70        }
71
72        self.check_window()
73    }
74
75    /// Check constraint on current window
76    pub(crate) fn check_window(&self) -> (bool, f32) {
77        match &self.constraint_fn {
78            SlidingWindowFn::MeanInRange { lo, hi } => {
79                let mean: f32 = self.buffer.iter().sum::<f32>() / self.buffer.len() as f32;
80                if mean >= *lo && mean <= *hi {
81                    (true, 0.0)
82                } else if mean < *lo {
83                    (false, lo - mean)
84                } else {
85                    (false, mean - hi)
86                }
87            }
88            SlidingWindowFn::MaxVariance { max_var } => {
89                let n = self.buffer.len() as f32;
90                let mean: f32 = self.buffer.iter().sum::<f32>() / n;
91                let var: f32 = self.buffer.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / n;
92                if var <= *max_var {
93                    (true, 0.0)
94                } else {
95                    (false, var - max_var)
96                }
97            }
98            SlidingWindowFn::MaxRange { max_range } => {
99                let min = self.buffer.iter().cloned().reduce(f32::min).unwrap_or(0.0);
100                let max = self.buffer.iter().cloned().reduce(f32::max).unwrap_or(0.0);
101                let range = max - min;
102                if range <= *max_range {
103                    (true, 0.0)
104                } else {
105                    (false, range - max_range)
106                }
107            }
108            SlidingWindowFn::BoundedVariation { max_variation } => {
109                let variation: f32 = self.buffer.windows(2).map(|w| (w[1] - w[0]).abs()).sum();
110                if variation <= *max_variation {
111                    (true, 0.0)
112                } else {
113                    (false, variation - max_variation)
114                }
115            }
116            SlidingWindowFn::AllInRange { lo, hi } => {
117                let violation: f32 = self
118                    .buffer
119                    .iter()
120                    .map(|&x| {
121                        if x < *lo {
122                            lo - x
123                        } else if x > *hi {
124                            x - hi
125                        } else {
126                            0.0
127                        }
128                    })
129                    .sum();
130                (violation == 0.0, violation)
131            }
132            SlidingWindowFn::AnyInRange { lo, hi } => {
133                let any_in_range = self.buffer.iter().any(|&x| x >= *lo && x <= *hi);
134                if any_in_range {
135                    (true, 0.0)
136                } else {
137                    // Violation is minimum distance to range
138                    let min_dist = self
139                        .buffer
140                        .iter()
141                        .map(|&x| if x < *lo { lo - x } else { x - hi })
142                        .reduce(f32::min)
143                        .unwrap_or(0.0);
144                    (false, min_dist)
145                }
146            }
147            SlidingWindowFn::TrendInRange {
148                min_slope,
149                max_slope,
150            } => {
151                // Simple linear regression: slope = Cov(i, x) / Var(i)
152                let n = self.buffer.len() as f32;
153                let mean_i = (n - 1.0) / 2.0;
154                let mean_x: f32 = self.buffer.iter().sum::<f32>() / n;
155
156                let cov: f32 = self
157                    .buffer
158                    .iter()
159                    .enumerate()
160                    .map(|(i, &x)| (i as f32 - mean_i) * (x - mean_x))
161                    .sum();
162                let var_i: f32 = (0..self.buffer.len())
163                    .map(|i| (i as f32 - mean_i).powi(2))
164                    .sum();
165
166                let slope = if var_i > f32::EPSILON {
167                    cov / var_i
168                } else {
169                    0.0
170                };
171
172                if slope >= *min_slope && slope <= *max_slope {
173                    (true, 0.0)
174                } else if slope < *min_slope {
175                    (false, min_slope - slope)
176                } else {
177                    (false, slope - max_slope)
178                }
179            }
180        }
181    }
182
183    /// Reset the buffer
184    pub fn reset(&mut self) {
185        self.buffer.clear();
186    }
187
188    /// Get current window contents
189    pub fn window(&self) -> &[f32] {
190        &self.buffer
191    }
192
193    /// Get constraint name
194    pub fn name(&self) -> &str {
195        &self.name
196    }
197
198    /// Get window size
199    pub fn window_size(&self) -> usize {
200        self.window_size
201    }
202
203    /// Get weight
204    pub fn weight(&self) -> f32 {
205        self.weight
206    }
207
208    /// Check if buffer is full
209    pub fn is_ready(&self) -> bool {
210        self.buffer.len() >= self.window_size
211    }
212}
213
214/// Collection of sliding window constraints
215#[derive(Debug, Default)]
216pub struct SlidingWindowChecker {
217    constraints: Vec<SlidingWindowConstraint>,
218}
219
220impl SlidingWindowChecker {
221    /// Create a new checker
222    pub fn new() -> Self {
223        Self::default()
224    }
225
226    /// Add a constraint
227    pub fn add(&mut self, constraint: SlidingWindowConstraint) {
228        self.constraints.push(constraint);
229    }
230
231    /// Push a value and check all constraints
232    pub fn push_and_check(&mut self, value: f32) -> Vec<(String, bool, f32)> {
233        self.constraints
234            .iter_mut()
235            .map(|c| {
236                let (sat, viol) = c.push_and_check(value);
237                (c.name().to_string(), sat, viol * c.weight())
238            })
239            .collect()
240    }
241
242    /// Total weighted violation
243    pub fn total_violation(&self) -> f32 {
244        self.constraints
245            .iter()
246            .filter(|c| c.is_ready())
247            .map(|c| {
248                let (_, viol) = c.check_window();
249                viol * c.weight()
250            })
251            .sum()
252    }
253
254    /// Reset all constraints
255    pub fn reset(&mut self) {
256        for c in &mut self.constraints {
257            c.reset();
258        }
259    }
260
261    /// Check if all constraints are satisfied
262    pub fn all_satisfied(&self) -> bool {
263        self.constraints
264            .iter()
265            .filter(|c| c.is_ready())
266            .all(|c| c.check_window().0)
267    }
268}