code wiki / (root) / sketch_cusum.nx

sketch_cusum.nx source

↩ module page · 133 lines · 4196 B

1// sketch_cusum.nx -- CUSUM change-point detector (Page 1954). 2// 3// Detects shifts in stream mean from a known reference value. Maintains 4// two cumulative sums: 5// 6// S+_t = max(0, S+_{t-1} + (x_t - k - delta)) 7// S-_t = max(0, S-_{t-1} - (x_t - k - delta)) 8// 9// where: 10// k = reference target value (expected mean) 11// delta = slack tolerance (don't alarm on tiny perturbations) 12// 13// ALARM: 14// S+ > h -> upward shift detected (mean increased) 15// S- > h -> downward shift detected (mean decreased) 16// h is the alarm threshold (caller-tuned for false-positive rate). 17// 18// CAPABILITY: 19// - Real-time change-point detection in O(1) state 20// - SRE: latency / error-rate shift detection 21// - manufacturing QC: process drift 22// - finance: regime change 23// - sensor: equipment fault 24// 25// THEORETICAL: 26// Average run length (ARL) under null: ~ exp(h) 27// ARL under shift of size delta: ~ h / delta 28// 29// All integer arithmetic, EXACT semantics. Production tier. 30// 31// LOSSLESS-LANGUAGE DISCIPLINE: alarm verdict is exact (sealed enum-like). 32// nx_cusum_query returns the current statistic with NX_ENV_ABS, param_a = 0. 33 34import "syscalls.nx" 35import "sketch_types.nx" 36 37// Alarm verdict sealed values. 38const NX_CUSUM_NORMAL: i64 = 0 39const NX_CUSUM_SHIFT_UP: i64 = 1 40const NX_CUSUM_SHIFT_DOWN: i64 = 2 41 42struct Cusum { 43 k: i64, // reference target value 44 delta: i64, // slack tolerance (sub-threshold drift ignored) 45 h: i64, // alarm threshold 46 s_pos: i64, // cumulative upward shift 47 s_neg: i64, // cumulative downward shift 48 n: i64, // observation count 49 n_alarms: i64, // total alarms fired 50 last_verdict: i64, // last query verdict 51} 52 53// === construction ================================================= 54 55func nx_cusum_alloc(k: i64, delta: i64, h: i64) -> *Cusum { 56 if delta < 0 { return 0 as *Cusum } 57 if h <= 0 { return 0 as *Cusum } 58 let raw: *u8 = sys_mmap(72) 59 let c: *Cusum = raw as *Cusum 60 c.k = k 61 c.delta = delta 62 c.h = h 63 c.s_pos = 0 64 c.s_neg = 0 65 c.n = 0 66 c.n_alarms = 0 67 c.last_verdict = NX_CUSUM_NORMAL 68 return c 69} 70 71// === add ========================================================== 72 73func nx_cusum_add(c: *Cusum, x: i64) -> i64 { 74 let dev: i64 = x - c.k 75 let upper_excess: i64 = dev - c.delta 76 let lower_excess: i64 = -dev - c.delta 77 // S+ = max(0, S+ + upper_excess) 78 var new_pos: i64 = c.s_pos + upper_excess 79 if new_pos < 0 { new_pos = 0 } 80 c.s_pos = new_pos 81 // S- = max(0, S- + lower_excess) 82 var new_neg: i64 = c.s_neg + lower_excess 83 if new_neg < 0 { new_neg = 0 } 84 c.s_neg = new_neg 85 c.n = c.n + 1 86 return 0 87} 88 89// === verdict ====================================================== 90 91func nx_cusum_verdict(c: *Cusum) -> i64 { 92 if c.s_pos > c.h { return NX_CUSUM_SHIFT_UP } 93 if c.s_neg > c.h { return NX_CUSUM_SHIFT_DOWN } 94 return NX_CUSUM_NORMAL 95} 96 97// Query verdict and reset statistic on alarm. This is the typical 98// usage pattern: detect, alarm, reset, continue monitoring. 99func nx_cusum_step(c: *Cusum) -> i64 { 100 let v: i64 = nx_cusum_verdict(c) 101 c.last_verdict = v 102 if v != NX_CUSUM_NORMAL { 103 c.n_alarms = c.n_alarms + 1 104 c.s_pos = 0 105 c.s_neg = 0 106 } 107 return v 108} 109 110// === introspection ================================================ 111 112func nx_cusum_s_pos(c: *Cusum) -> i64 { return c.s_pos } 113func nx_cusum_s_neg(c: *Cusum) -> i64 { return c.s_neg } 114func nx_cusum_n(c: *Cusum) -> i64 { return c.n } 115func nx_cusum_n_alarms(c: *Cusum) -> i64 { return c.n_alarms } 116 117func nx_cusum_query(c: *Cusum) -> *ApproxI64 { 118 let v: i64 = nx_cusum_verdict(c) 119 return nx_approx_new(v, NX_ENV_ABS, 0, 1000000000, 120 NX_MATURITY_PRODUCTION, 121 NX_ADV_HONEST) 122} 123 124func nx_cusum_memory_bytes(c: *Cusum) -> i64 { 125 return 72 126} 127 128func nx_cusum_reset(c: *Cusum) -> i64 { 129 c.s_pos = 0 130 c.s_neg = 0 131 c.last_verdict = NX_CUSUM_NORMAL 132 return 0 133}