code wiki / (root) / nx_sketch_zscore.nx

nx_sketch_zscore.nx source

↩ module page · 134 lines · 4364 B

1// sketch_zscore.nx -- point z-score anomaly detector. 2// 3// Composes against sketch_stream_stats (running mean + stddev) to flag 4// individual samples deviating from the streaming baseline: 5// 6// z = (x - mean) / stddev 7// alarm if |z| > threshold (typical 2.0 ~ 95% conf, 3.0 ~ 99.7%) 8// 9// CAPABILITY: 10// - O(1) state (mean + variance from StreamStats) 11// - online: baseline updates with every sample 12// - Returns Z in PPM ([-1e10, +1e10] practical range) 13// - Sealed verdict: NORMAL / HIGH / LOW 14// 15// COMPLEMENTS THE ANOMALY FAMILY: 16// - sketch_cusum cumulative shift over a window 17// - sketch_mann_kendall monotonic trend test 18// - sketch_zscore (this) point outlier vs running baseline 19// 20// CONFIGURATION: 21// "warm-up" period: do not raise alarms until count >= min_count. 22// Default: min_count = 30 (statistician's rule-of-thumb for CLT). 23// 24// LOSSLESS-LANGUAGE DISCIPLINE: returns z in PPM with NX_ENV_ABS, 25// param_a = 1 (PPM quantization). Production tier; exact integer 26// arithmetic over the running statistics. 27 28// nx_safety_envelope: 29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 30// sil_target: SIL1 31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 32// verdict: NOT_YET_EVALUATED 33 34import "nx_syscalls.nx" 35import "nx_sketch_stream_stats.nx" 36import "nx_sketch_types.nx" 37 38const NX_ZS_VERDICT_NORMAL: i64 = 0 39const NX_ZS_VERDICT_HIGH: i64 = 1 40const NX_ZS_VERDICT_LOW: i64 = 2 41 42const NX_ZS_MIN_COUNT_DEFAULT: i64 = 30 43 44struct ZScore { 45 stats: *StreamStats, 46 threshold_ppm: i64, // |z| > threshold_ppm/1e6 triggers alarm 47 min_count: i64, 48 n_alarms: i64, 49} 50 51// === construction ================================================= 52// 53// threshold_ppm: e.g. 2_000_000 = z >= 2.0 54// min_count: warm-up samples before alarms fire (default 30 if 0). 55 56func nx_zs_alloc(threshold_ppm: i64, min_count: i64) -> *ZScore { 57 if threshold_ppm <= 0 { return 0 as *ZScore } 58 let stats: *StreamStats = nx_stats_alloc() 59 if stats == (0 as *StreamStats) { return 0 as *ZScore } 60 let raw: *u8 = sys_mmap(40) 61 let z: *ZScore = raw as *ZScore 62 z.stats = stats 63 z.threshold_ppm = threshold_ppm 64 if min_count <= 0 { 65 z.min_count = NX_ZS_MIN_COUNT_DEFAULT 66 } 67 if min_count > 0 { 68 z.min_count = min_count 69 } 70 z.n_alarms = 0 71 return z 72} 73 74// === observe ===================================================== 75// 76// Compute z BEFORE updating stats (so the score reflects deviation 77// from the prior baseline rather than including the sample itself). 78// Caller can choose to call nx_zs_score_then_update vs separate 79// observe/score patterns; this primitive does observe-after-score. 80 81func nx_zs_score_ppm(z: *ZScore, x: i64) -> i64 { 82 if nx_stats_count(z.stats) < z.min_count { return 0 } 83 let mean: i64 = nx_stats_mean(z.stats) 84 let var_val: i64 = nx_stats_variance(z.stats) 85 if var_val <= 0 { return 0 } 86 let stddev: i64 = nx_stats_isqrt(var_val) 87 if stddev == 0 { return 0 } 88 return ((x - mean) * 1000000) / stddev 89} 90 91func nx_zs_verdict(z: *ZScore, x: i64) -> i64 { 92 let zppm: i64 = nx_zs_score_ppm(z, x) 93 if zppm >= z.threshold_ppm { return NX_ZS_VERDICT_HIGH } 94 if zppm <= -z.threshold_ppm { return NX_ZS_VERDICT_LOW } 95 return NX_ZS_VERDICT_NORMAL 96} 97 98// Full pipeline: score, classify, then incorporate sample into baseline. 99// Returns the verdict. 100func nx_zs_step(z: *ZScore, x: i64) -> i64 { 101 let v: i64 = nx_zs_verdict(z, x) 102 if v != NX_ZS_VERDICT_NORMAL { z.n_alarms = z.n_alarms + 1 } 103 nx_stats_add(z.stats, x) 104 return v 105} 106 107// === introspection ================================================ 108 109func nx_zs_count(z: *ZScore) -> i64 { 110 return nx_stats_count(z.stats) 111} 112 113func nx_zs_mean(z: *ZScore) -> i64 { 114 return nx_stats_mean(z.stats) 115} 116 117func nx_zs_stddev(z: *ZScore) -> i64 { 118 return nx_stats_stddev(z.stats) 119} 120 121func nx_zs_n_alarms(z: *ZScore) -> i64 { 122 return z.n_alarms 123} 124 125func nx_zs_query(z: *ZScore, x: i64) -> *ApproxI64 { 126 let v: i64 = nx_zs_verdict(z, x) 127 return nx_approx_new(v, NX_ENV_ABS, 0, 1000000000, 128 NX_MATURITY_PRODUCTION, 129 NX_ADV_HONEST) 130} 131 132func nx_zs_memory_bytes(z: *ZScore) -> i64 { 133 return 40 + nx_stats_memory_bytes(z.stats) 134}