1#[derive(Debug, Clone)]
33pub struct CusumDetector {
34 mu_0: f64,
36 delta: f64,
38 h: f64,
40 s_pos: f64,
42 s_neg: f64,
44 count: u64,
46 alarm_count: u64,
48 burnin: u64,
50 burnin_sum: f64,
52 burnin_done: bool,
54}
55
56impl CusumDetector {
57 pub fn new(mu_0: f64, delta: f64, h: f64) -> Self {
64 assert!(delta >= 0.0, "delta must be non-negative, got {delta}");
65 assert!(h > 0.0, "h must be positive, got {h}");
66 Self {
67 mu_0,
68 delta,
69 h,
70 s_pos: 0.0,
71 s_neg: 0.0,
72 count: 0,
73 alarm_count: 0,
74 burnin: 0,
75 burnin_sum: 0.0,
76 burnin_done: true,
77 }
78 }
79
80 pub fn with_burnin(burnin: u64, delta: f64, h: f64) -> Self {
85 assert!(burnin > 0, "burnin must be > 0, got {burnin}");
86 assert!(delta >= 0.0, "delta must be non-negative, got {delta}");
87 assert!(h > 0.0, "h must be positive, got {h}");
88 Self {
89 mu_0: 0.0,
90 delta,
91 h,
92 s_pos: 0.0,
93 s_neg: 0.0,
94 count: 0,
95 alarm_count: 0,
96 burnin,
97 burnin_sum: 0.0,
98 burnin_done: false,
99 }
100 }
101
102 pub fn update(&mut self, value: f64) -> bool {
104 self.count += 1;
105
106 if !self.burnin_done {
108 self.burnin_sum += value;
109 if self.count >= self.burnin {
110 self.mu_0 = self.burnin_sum / self.count as f64;
111 self.burnin_done = true;
112 }
113 return false;
114 }
115
116 self.s_pos = (self.s_pos + value - self.mu_0 - self.delta).max(0.0);
118 self.s_neg = (self.s_neg - value + self.mu_0 - self.delta).max(0.0);
119
120 let alarm = self.s_pos > self.h || self.s_neg > self.h;
121 if alarm {
122 self.alarm_count += 1;
123 }
124 alarm
125 }
126
127 pub fn batch_update(&mut self, values: &[f64]) -> Option<usize> {
129 for (i, &v) in values.iter().enumerate() {
130 if self.update(v) {
131 return Some(i);
132 }
133 }
134 None
135 }
136
137 pub fn reset(&mut self) {
143 self.s_pos = 0.0;
144 self.s_neg = 0.0;
145 }
146
147 pub fn reset_with_burnin(&mut self) {
149 self.s_pos = 0.0;
150 self.s_neg = 0.0;
151 self.count = 0;
152 self.burnin_sum = 0.0;
153 self.burnin_done = self.burnin == 0;
154 }
155
156 pub fn set_mu(&mut self, mu_0: f64) {
158 self.mu_0 = mu_0;
159 self.reset();
160 }
161
162 pub fn s_pos(&self) -> f64 {
164 self.s_pos
165 }
166
167 pub fn s_neg(&self) -> f64 {
169 self.s_neg
170 }
171
172 pub fn mu_0(&self) -> f64 {
174 self.mu_0
175 }
176
177 pub fn count(&self) -> u64 {
179 self.count
180 }
181
182 pub fn alarm_count(&self) -> u64 {
184 self.alarm_count
185 }
186
187 pub fn is_ready(&self) -> bool {
189 self.burnin_done
190 }
191}
192
193#[derive(Debug, Clone)]
204pub struct PageHinkleyDetector {
205 delta: f64,
206 lambda: f64,
207 sum: f64,
208 count: u64,
209 running_mean: f64,
210 cumsum: f64,
211 min_cumsum: f64,
212 alarm_count: u64,
213}
214
215impl PageHinkleyDetector {
216 pub fn new(delta: f64, lambda: f64) -> Self {
222 assert!(lambda > 0.0, "lambda must be positive, got {lambda}");
223 Self {
224 delta,
225 lambda,
226 sum: 0.0,
227 count: 0,
228 running_mean: 0.0,
229 cumsum: 0.0,
230 min_cumsum: 0.0,
231 alarm_count: 0,
232 }
233 }
234
235 pub fn update(&mut self, value: f64) -> bool {
237 self.count += 1;
238 self.sum += value;
239 self.running_mean = self.sum / self.count as f64;
240
241 self.cumsum += value - self.running_mean - self.delta;
242 if self.cumsum < self.min_cumsum {
243 self.min_cumsum = self.cumsum;
244 }
245
246 let alarm = (self.cumsum - self.min_cumsum) > self.lambda;
247 if alarm {
248 self.alarm_count += 1;
249 }
250 alarm
251 }
252
253 pub fn reset(&mut self) {
255 self.sum = 0.0;
256 self.count = 0;
257 self.running_mean = 0.0;
258 self.cumsum = 0.0;
259 self.min_cumsum = 0.0;
260 }
261
262 pub fn count(&self) -> u64 {
264 self.count
265 }
266
267 pub fn alarm_count(&self) -> u64 {
269 self.alarm_count
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn cusum_no_alarm_on_stationary() {
279 let mut det = CusumDetector::new(0.0, 0.5, 5.0);
280 for _ in 0..100 {
282 assert!(!det.update(0.1));
283 assert!(!det.update(-0.1));
284 }
285 assert_eq!(det.alarm_count(), 0);
286 }
287
288 #[test]
289 fn cusum_detects_upward_shift() {
290 let mut det = CusumDetector::new(0.0, 0.5, 5.0);
291 let mut alarm_step = None;
293 for i in 0..100 {
294 if det.update(3.0) {
295 alarm_step = Some(i);
296 break;
297 }
298 }
299 assert!(alarm_step.is_some(), "should detect upward shift");
300 assert!(alarm_step.unwrap() < 10, "should detect quickly");
301 }
302
303 #[test]
304 fn cusum_detects_downward_shift() {
305 let mut det = CusumDetector::new(0.0, 0.5, 5.0);
306 let mut alarm_step = None;
307 for i in 0..100 {
308 if det.update(-3.0) {
309 alarm_step = Some(i);
310 break;
311 }
312 }
313 assert!(alarm_step.is_some(), "should detect downward shift");
314 }
315
316 #[test]
317 fn cusum_burnin_delays_detection() {
318 let mut det = CusumDetector::with_burnin(20, 0.5, 5.0);
319 for _ in 0..19 {
321 assert!(!det.update(100.0)); }
323 assert!(!det.is_ready()); assert!(!det.update(100.0)); assert!(det.is_ready());
328 }
329
330 #[test]
331 fn cusum_reset_clears_statistics() {
332 let mut det = CusumDetector::new(0.0, 0.5, 5.0);
333 for _ in 0..5 {
334 det.update(3.0);
335 }
336 assert!(det.s_pos() > 0.0);
337 det.reset();
338 assert!((det.s_pos()).abs() < 1e-10);
339 assert!((det.s_neg()).abs() < 1e-10);
340 }
341
342 #[test]
343 fn cusum_batch_update_returns_first_alarm() {
344 let mut det = CusumDetector::new(0.0, 0.5, 2.0);
345 let values = vec![5.0, 5.0, 5.0, 5.0, 5.0];
346 let idx = det.batch_update(&values);
347 assert!(idx.is_some());
348 assert!(idx.unwrap() < 3);
349 }
350
351 #[test]
352 fn cusum_set_mu_resets_and_updates_reference() {
353 let mut det = CusumDetector::new(0.0, 0.5, 5.0);
354 for _ in 0..10 {
355 det.update(5.0);
356 }
357 det.set_mu(5.0);
358 assert!((det.s_pos()).abs() < 1e-10);
359 for _ in 0..50 {
361 assert!(!det.update(5.1));
362 assert!(!det.update(4.9));
363 }
364 }
365
366 #[test]
367 fn page_hinkley_no_alarm_stationary() {
368 let mut det = PageHinkleyDetector::new(0.5, 10.0);
369 for _ in 0..100 {
370 assert!(!det.update(1.0));
371 }
372 assert_eq!(det.alarm_count(), 0);
373 }
374
375 #[test]
376 fn page_hinkley_detects_increase() {
377 let mut det = PageHinkleyDetector::new(0.1, 5.0);
378 for _ in 0..50 {
380 det.update(0.0);
381 }
382 let mut detected = false;
384 for _ in 0..50 {
385 if det.update(5.0) {
386 detected = true;
387 break;
388 }
389 }
390 assert!(detected, "should detect upward shift");
391 }
392
393 #[test]
394 fn page_hinkley_reset_clears() {
395 let mut det = PageHinkleyDetector::new(0.1, 5.0);
396 for _ in 0..10 {
397 det.update(10.0);
398 }
399 det.reset();
400 assert_eq!(det.count(), 0);
401 }
402
403 #[test]
404 #[should_panic(expected = "h must be positive")]
405 fn cusum_invalid_h_panics() {
406 CusumDetector::new(0.0, 0.5, 0.0);
407 }
408
409 #[test]
410 #[should_panic(expected = "lambda must be positive")]
411 fn page_hinkley_invalid_lambda_panics() {
412 PageHinkleyDetector::new(0.1, 0.0);
413 }
414}