scx_pandemonium/chaos.rs
1// PANDEMONIUM CHAOS PRIMITIVES
2// PURE-RUST RAW-WINDOW STATISTICS USED BY THE ADAPTIVE LAYER.
3//
4// EVERY PUBLIC ITEM IN THIS MODULE IS PART OF THE CHAOS API SURFACE:
5// SOME ARE CONSUMED BY THE BINARY (adaptive.rs), SOME BY TESTS, AND
6// SOME ARE EXPORTED CONSTANTS FOR DIAGNOSTICS / FUTURE EXPANSION.
7// CARGO'S DEAD-CODE LINT FIRES PER COMPILATION TARGET; SILENCE IT.
8#![allow(dead_code)]
9
10// EVERY MEASURE IS COMPUTED FROM THE RAW SAMPLE WINDOW EACH CALL:
11// NO ACCUMULATOR CARRIES STATE BETWEEN TICKS.
12//
13// HVG MEAN DEGREE / HVG ENTROPY (LUQUE-LACASA 2009): TWO STATISTICS OF
14// THE HORIZONTAL VISIBILITY GRAPH'S DEGREE DISTRIBUTION.
15// - MEAN DEGREE LAMBDA = <k>. IID RANDOM SEQUENCES SATURATE AT 4 - 2/N
16// (-> 4 IN THE LIMIT). PURELY PERIODIC SEQUENCES STAY NEAR 2.
17// - SHANNON ENTROPY S OVER THE DEGREE DISTRIBUTION. IID HAS THE EXACT
18// CLOSED FORM P(k) = (1/3)*(2/3)^(k-2), k >= 2, GIVING
19// S_IID = LN(3) + 2*LN(3/2) ~= 1.910.
20// LN(3/2) IS THE CHARACTERISTIC EXPONENT OF THE IID DEGREE DISTRIBUTION,
21// NOT THE ENTROPY THRESHOLD. WE EXPOSE LAMBDA AS THE PRIMARY REGIME
22// DISCRIMINATOR (DIRECTLY INTERPRETABLE) AND ENTROPY AS A CORROBORATOR.
23//
24// BANDT-POMPE PERMUTATION ENTROPY (D=3) (BANDT-POMPE 2002): SHANNON
25// ENTROPY OF THE ORDINAL-PATTERN DISTRIBUTION OF LENGTH-3 SUB-WINDOWS,
26// NORMALIZED TO [0, 1] BY LN(6). 0 = PERFECTLY MONOTONIC / PERIODIC,
27// 1 = MAXIMALLY DISORDERED.
28//
29// THESE TWO PRIMITIVES ARE COMPLEMENTARY: HVG ENTROPY IS AMPLITUDE-
30// SENSITIVE (TWO SEQUENCES WITH IDENTICAL ORDINAL STRUCTURE BUT
31// DIFFERENT VALUES CAN DIFFER IN HVG-S), BANDT-POMPE IS AMPLITUDE-
32// INVARIANT (CAPTURES PURE ORDINAL DYNAMICS).
33
34use std::sync::atomic::{AtomicU64, Ordering};
35
36// CRITICAL VALUES
37
38// LN(3/2). IID HVG DEGREE-DISTRIBUTION CHARACTERISTIC EXPONENT.
39// EXPOSED FOR DIAGNOSTICS / FUTURE USE; NOT USED AS A DIRECT THRESHOLD.
40pub const HVG_LN_3_2: f64 = 0.405_465_108_108_164_4;
41
42// HVG MEAN-DEGREE THRESHOLDS. IID RANDOM SATURATES AT 4 - 2/N; PURELY
43// PERIODIC SEQUENCES STAY NEAR 2. THE TWO THRESHOLDS BELOW DEFINE A
44// DEAD ZONE FOR THE MIXED REGIME. THE BAND ITSELF IS THE HYSTERESIS:
45// A WINDOW MUST CROSS IT ENTIRELY TO CHANGE THE VERDICT.
46pub const HVG_LAMBDA_PERIODIC_MAX: f64 = 2.6;
47pub const HVG_LAMBDA_CHAOTIC_MIN: f64 = 3.4;
48
49// IID-ASYMPTOTE HVG ENTROPY: LN(3) + 2*LN(3/2) ~= 1.910.
50pub const HVG_S_IID: f64 = 1.910_543_686_807_036;
51
52// BANDT-POMPE D=3 PATTERN COUNT.
53pub const BP_D3_PATTERNS: usize = 6;
54
55// LN(6): NORMALIZATION FACTOR FOR BP D=3 PERMUTATION ENTROPY.
56const LN_BP_D3: f64 = 1.791_759_469_228_055;
57
58// HIGH PERMUTATION-ENTROPY THRESHOLD. ABOVE THIS THE ORDINAL DYNAMICS
59// LOOK MAXIMALLY DISORDERED ON THE WINDOW; THE ADAPTIVE LAYER USES IT
60// AS A "WORKLOAD IS UNPREDICTABLE THIS WINDOW" SIGNAL.
61pub const BP_H_HIGH: f64 = 0.85;
62
63// RQA (RECURRENCE QUANTIFICATION ANALYSIS) DETERMINISM.
64// DET IS THE FRACTION OF RECURRENCE POINTS THAT LIE ON DIAGONAL LINE
65// SEGMENTS. A DETERMINISTIC / STEADY SIGNAL REVISITS PHASE-SPACE
66// NEIGHBORHOODS ALONG DIAGONALS (DET -> 1); AN IID SIGNAL SCATTERS
67// RECURRENCE POINTS WITH NO DIAGONAL STRUCTURE (DET -> 0).
68// THE ADAPTIVE QUIESCENCE GATE PAIRS HIGH DET WITH HVG LAMBDA IN THE
69// PERIODIC BAND TO DETECT "STOP RETUNING" STEADY STATE.
70
71// DELAY-EMBEDDING DIMENSION. 3-D VECTORS WITH UNIT DELAY -- MATCHES
72// THE D=3 ORDINAL SCALE USED BY BANDT-POMPE.
73pub const RQA_EMBED_DIM: usize = 3;
74
75// RECURRENCE THRESHOLD AS A FRACTION OF THE WINDOW STANDARD DEVIATION:
76// eps = RQA_THRESH_STD_FRAC * sigma. 0.20 IS THE STANDARD RQA DEFAULT
77// BAND FOR SHORT SERIES.
78pub const RQA_THRESH_STD_FRAC: f64 = 0.20;
79
80// MINIMUM DIAGONAL-LINE LENGTH COUNTED AS DETERMINISM (RQA l_min).
81pub const RQA_LMIN: usize = 2;
82
83// DET AT OR ABOVE THIS = DETERMINISTIC / STEADY. CONSUMED BY THE
84// ADAPTIVE QUIESCENCE GATE.
85pub const RQA_DET_STEADY_MIN: f64 = 0.90;
86
87// BELOW THIS WINDOW FILL, rqa_det RETURNS None (INSUFFICIENT DATA --
88// NEVER LET THE GATE FREEZE ON A HALF-FILLED WINDOW).
89pub const RQA_MIN_SAMPLES: usize = 8;
90
91// RAW WINDOW
92// FIXED-SIZE RING BUFFER OF f64 SAMPLES. NO HEAP ALLOC AT STEADY STATE.
93// SEMANTICS:
94// - PUSH IS O(1)
95// - SAMPLES ARE READ IN INSERTION ORDER (OLDEST FIRST)
96// - UNFILLED SLOTS ARE NOT YIELDED
97// - len() == 0 UNTIL FIRST PUSH; AT MOST N AFTER N PUSHES
98
99#[derive(Clone, Debug)]
100pub struct RawWindow<const N: usize> {
101 buf: [f64; N],
102 head: usize,
103 filled: usize,
104}
105
106impl<const N: usize> Default for RawWindow<N> {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl<const N: usize> RawWindow<N> {
113 pub const fn new() -> Self {
114 Self {
115 buf: [0.0; N],
116 head: 0,
117 filled: 0,
118 }
119 }
120
121 pub fn push(&mut self, x: f64) {
122 self.buf[self.head] = x;
123 self.head = (self.head + 1) % N;
124 if self.filled < N {
125 self.filled += 1;
126 }
127 }
128
129 pub fn len(&self) -> usize {
130 self.filled
131 }
132
133 pub fn is_empty(&self) -> bool {
134 self.filled == 0
135 }
136
137 pub fn capacity(&self) -> usize {
138 N
139 }
140
141 // YIELD SAMPLES IN INSERTION ORDER (OLDEST -> NEWEST).
142 pub fn iter(&self) -> RawWindowIter<'_, N> {
143 let start = if self.filled < N { 0 } else { self.head };
144 RawWindowIter {
145 win: self,
146 pos: 0,
147 start,
148 }
149 }
150
151 pub fn last(&self) -> Option<f64> {
152 if self.filled == 0 {
153 None
154 } else {
155 let i = (self.head + N - 1) % N;
156 Some(self.buf[i])
157 }
158 }
159}
160
161pub struct RawWindowIter<'a, const N: usize> {
162 win: &'a RawWindow<N>,
163 pos: usize,
164 start: usize,
165}
166
167impl<'a, const N: usize> Iterator for RawWindowIter<'a, N> {
168 type Item = f64;
169 fn next(&mut self) -> Option<f64> {
170 if self.pos >= self.win.filled {
171 return None;
172 }
173 let idx = (self.start + self.pos) % N;
174 self.pos += 1;
175 Some(self.win.buf[idx])
176 }
177}
178
179// MEAN / MIN / MAX HELPERS
180
181#[allow(dead_code)]
182pub fn mean<const N: usize>(w: &RawWindow<N>) -> f64 {
183 if w.filled == 0 {
184 return 0.0;
185 }
186 let mut s = 0.0;
187 for x in w.iter() {
188 s += x;
189 }
190 s / w.filled as f64
191}
192
193// HORIZONTAL VISIBILITY GRAPH
194//
195// FOR A SEQUENCE x_1..x_N, NODES i AND j (i < j) ARE HVG-CONNECTED IFF
196// x_k < min(x_i, x_j) FOR ALL i < k < j. ADJACENT NODES (j = i+1) ARE
197// ALWAYS CONNECTED.
198//
199// hvg_degrees BUILDS THE DEGREE VECTOR ONCE; hvg_stats DERIVES BOTH
200// LAMBDA AND ENTROPY FROM IT IN A SINGLE O(N^2) PASS.
201//
202// BRUTE-FORCE O(N^2). AT N <= 128 (>1-MINUTE WINDOW AT 1HZ) THIS IS
203// UNDER 16K COMPARISONS, DONE ONCE PER SECOND.
204
205fn hvg_degrees<const N: usize>(w: &RawWindow<N>) -> Option<([u32; N], usize)> {
206 let n = w.filled;
207 if n < 3 {
208 return None;
209 }
210
211 let mut s: [f64; N] = [0.0; N];
212 let mut k = 0;
213 for x in w.iter() {
214 s[k] = x;
215 k += 1;
216 }
217
218 let mut deg: [u32; N] = [0; N];
219 for i in 0..n {
220 let mut blocker = f64::NEG_INFINITY;
221 for j in (i + 1)..n {
222 let limit = s[i].min(s[j]);
223 if j == i + 1 || blocker < limit {
224 deg[i] += 1;
225 deg[j] += 1;
226 }
227 if s[j] > blocker {
228 blocker = s[j];
229 }
230 }
231 }
232 Some((deg, n))
233}
234
235// AMORTIZED LAMBDA + ENTROPY. ONE O(N^2) PASS BUILDS THE DEGREE VECTOR;
236// BOTH STATISTICS DERIVE FROM IT.
237pub fn hvg_stats<const N: usize>(w: &RawWindow<N>) -> (f64, f64) {
238 let (deg, n) = match hvg_degrees(w) {
239 Some(v) => v,
240 None => return (0.0, 0.0),
241 };
242
243 let mut sum: u64 = 0;
244 let mut hist: [u32; N] = [0; N];
245 for d in deg.iter().take(n) {
246 sum += *d as u64;
247 let bucket = (*d as usize).min(n - 1);
248 hist[bucket] += 1;
249 }
250 let lambda = sum as f64 / n as f64;
251
252 let total = n as f64;
253 let mut entropy = 0.0;
254 for c in hist.iter().take(n) {
255 if *c == 0 {
256 continue;
257 }
258 let p = *c as f64 / total;
259 entropy -= p * p.ln();
260 }
261 (lambda, entropy)
262}
263
264// BANDT-POMPE PERMUTATION ENTROPY (D=3)
265//
266// SLIDE A LENGTH-3 WINDOW. FOR EACH (a, b, c) MAP TO ONE OF SIX ORDINAL
267// PATTERNS BY THE RANK ORDER. BUILD THE EMPIRICAL DISTRIBUTION AND
268// RETURN H / LN(6) IN [0, 1].
269//
270// TIES ARE BROKEN BY POSITION (b > a IFF b STRICTLY GREATER, ELSE a > b).
271// IID RANDOM SAMPLES YIELD H ~= 1; PERIODIC OR MONOTONIC YIELD H << 1.
272pub fn bandt_pompe_d3<const N: usize>(w: &RawWindow<N>) -> f64 {
273 let n = w.filled;
274 if n < 3 {
275 return 0.0;
276 }
277
278 // INLINED RING WALK: WE NEED THREE CONSECUTIVE SAMPLES.
279 let mut counts = [0u32; BP_D3_PATTERNS];
280 let mut total = 0u32;
281
282 // COLLECT INTO LINEAR BUFFER ONCE; SAFE FOR N <= 128.
283 let mut s: [f64; N] = [0.0; N];
284 let mut k = 0;
285 for x in w.iter() {
286 s[k] = x;
287 k += 1;
288 }
289
290 for i in 0..(n - 2) {
291 let a = s[i];
292 let b = s[i + 1];
293 let c = s[i + 2];
294 // BREAK TIES BY POSITION (POSITIONAL ORDER WHEN VALUES EQUAL).
295 let pattern = match (a < b, b < c, a < c) {
296 (true, true, true) => 0, // a < b < c
297 (true, false, true) => 1, // a < c <= b
298 (true, false, false) => 2, // c <= a < b
299 (false, true, true) => 3, // b <= a < c
300 (false, true, false) => 4, // b < c <= a
301 (false, false, false) => 5, // c <= b <= a
302 (true, true, false) => 1, // DEGENERATE TIE: TREAT AS PATTERN 1
303 (false, false, true) => 4, // DEGENERATE TIE: TREAT AS PATTERN 4
304 };
305 counts[pattern] += 1;
306 total += 1;
307 }
308
309 if total == 0 {
310 return 0.0;
311 }
312
313 let denom = total as f64;
314 let mut h = 0.0;
315 for c in counts.iter() {
316 if *c == 0 {
317 continue;
318 }
319 let p = *c as f64 / denom;
320 h -= p * p.ln();
321 }
322 h / LN_BP_D3
323}
324
325// RQA DETERMINISM (DET)
326//
327// DELAY-EMBED THE WINDOW INTO RQA_EMBED_DIM-D VECTORS WITH UNIT DELAY,
328// BUILD THE RECURRENCE MATRIX UNDER A CHEBYSHEV (L-INFINITY) BALL OF
329// RADIUS eps = RQA_THRESH_STD_FRAC * sigma, AND RETURN THE FRACTION OF
330// OFF-DIAGONAL RECURRENCE POINTS THAT LIE ON DIAGONAL RUNS OF LENGTH
331// >= RQA_LMIN.
332//
333// RETURNS Some(DET) IN [0, 1], OR None WHEN THE WINDOW HAS FEWER THAN
334// RQA_MIN_SAMPLES FILLED SLOTS -- THE QUIESCENCE GATE MUST NOT FREEZE
335// ON A HALF-FILLED WINDOW.
336//
337// BRUTE-FORCE O(N^2), SAME COST CLASS AS hvg_degrees. AT N <= 64 THIS
338// IS UNDER 4K COMPARISONS, DONE ONCE PER SECOND.
339
340// CHEBYSHEV (L-INFINITY) DISTANCE BETWEEN TWO 3-D EMBEDDED POINTS.
341fn chebyshev3(a: &[f64; RQA_EMBED_DIM], b: &[f64; RQA_EMBED_DIM]) -> f64 {
342 let mut m = 0.0;
343 for k in 0..RQA_EMBED_DIM {
344 let d = (a[k] - b[k]).abs();
345 if d > m {
346 m = d;
347 }
348 }
349 m
350}
351
352pub fn rqa_det<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
353 let n = w.filled;
354 if n < RQA_MIN_SAMPLES {
355 return None;
356 }
357
358 // COPY WINDOW INTO ORDER-PRESERVING SLICE FOR INDEXED ACCESS.
359 let mut s: [f64; N] = [0.0; N];
360 let mut k = 0;
361 for x in w.iter() {
362 s[k] = x;
363 k += 1;
364 }
365
366 // MEAN AND STANDARD DEVIATION OVER THE n FILLED SAMPLES.
367 let nf = n as f64;
368 let mut sum = 0.0;
369 for v in s.iter().take(n) {
370 sum += *v;
371 }
372 let mean = sum / nf;
373 let mut var = 0.0;
374 for v in s.iter().take(n) {
375 let d = *v - mean;
376 var += d * d;
377 }
378 let sigma = (var / nf).sqrt();
379
380 // FLAT-WINDOW SPECIAL CASE. A PERFECTLY STEADY SIGNAL HAS sigma = 0;
381 // EVERY EMBEDDED POINT RECURS WITH EVERY OTHER. THAT IS FULLY
382 // DETERMINISTIC -- RETURN 1.0 DIRECTLY (AND AVOID eps = 0 / A
383 // DEGENERATE RECURRENCE MATRIX). idle_pct IS INTEGER-PERCENT CAST
384 // TO f64, SO A STEADY COMPUTE WORKLOAD PRODUCES AN EXACTLY-FLAT
385 // WINDOW AND MUST READ AS QUIESCENT.
386 if sigma < 1e-9 {
387 return Some(1.0);
388 }
389
390 let eps = RQA_THRESH_STD_FRAC * sigma;
391
392 // DELAY-EMBED: m = n - (RQA_EMBED_DIM - 1) VECTORS.
393 let m = n - (RQA_EMBED_DIM - 1);
394 if m < 2 {
395 return None;
396 }
397 let mut emb: [[f64; RQA_EMBED_DIM]; N] = [[0.0; RQA_EMBED_DIM]; N];
398 for i in 0..m {
399 for d in 0..RQA_EMBED_DIM {
400 emb[i][d] = s[i + d];
401 }
402 }
403
404 // RECURRENCE MATRIX OVER THE m EMBEDDED POINTS.
405 let mut rec: [[bool; N]; N] = [[false; N]; N];
406 for i in 0..m {
407 for j in 0..m {
408 rec[i][j] = chebyshev3(&emb[i], &emb[j]) <= eps;
409 }
410 }
411
412 // WALK EVERY OFF-MAIN DIAGONAL. COUNT TOTAL RECURRENCE POINTS AND
413 // THE POINTS THAT BELONG TO DIAGONAL RUNS OF LENGTH >= RQA_LMIN.
414 let mut total_rec: u64 = 0;
415 let mut diag_rec: u64 = 0;
416 // OFFSET o > 0: PAIRS (i, i + o). OFFSET o < 0 IS THE SYMMETRIC
417 // MIRROR; THE RECURRENCE MATRIX IS SYMMETRIC SO WE WALK o IN
418 // [1, m) AND DOUBLE-COUNT NOTHING BY COUNTING BOTH (i,j) AND (j,i)
419 // VIA THE 2x FACTOR -- INSTEAD WE JUST WALK BOTH UPPER AND LOWER
420 // EXPLICITLY TO KEEP total_rec CONSISTENT WITH diag_rec.
421 for o in 1..m {
422 // UPPER DIAGONAL: (i, i + o).
423 let mut run: usize = 0;
424 for i in 0..(m - o) {
425 if rec[i][i + o] {
426 total_rec += 1;
427 run += 1;
428 } else {
429 if run >= RQA_LMIN {
430 diag_rec += run as u64;
431 }
432 run = 0;
433 }
434 }
435 if run >= RQA_LMIN {
436 diag_rec += run as u64;
437 }
438 // LOWER DIAGONAL: (i + o, i).
439 run = 0;
440 for i in 0..(m - o) {
441 if rec[i + o][i] {
442 total_rec += 1;
443 run += 1;
444 } else {
445 if run >= RQA_LMIN {
446 diag_rec += run as u64;
447 }
448 run = 0;
449 }
450 }
451 if run >= RQA_LMIN {
452 diag_rec += run as u64;
453 }
454 }
455
456 if total_rec == 0 {
457 return Some(0.0);
458 }
459 Some(diag_rec as f64 / total_rec as f64)
460}
461
462// A PRICED READING: A VALUE AND HOW MUCH IT IS WORTH TRUSTING
463//
464// Every estimator below used to refuse outright under a sample floor -- return
465// None, and the consumer skips. That is a gate, and it is the wrong shape for
466// this scheduler: THE FLAG says price, never bail. A window at 7 samples is not
467// unknowable, it is weakly known, and the difference between those two is the
468// difference between a knob that does not move at all and one that moves a
469// little.
470//
471// So each estimator now answers wherever its arithmetic is DEFINED, and reports
472// confidence separately. Confidence ramps from the mathematical minimum to the
473// statistical floor that used to be the gate: full window, full trust. The
474// consumer multiplies its effect by confidence, so data flows from the first
475// sample the math allows and its influence grows as the evidence does.
476//
477// The Option-returning forms are kept. They are the honest answer to "is this
478// computable at all", which is a different question from "how much is it worth".
479#[derive(Clone, Copy, Debug)]
480pub struct Priced {
481 pub value: f64,
482 // [0, 1]. 0 means computable but on the thinnest possible evidence; 1 means
483 // the window carries at least what the old gate demanded.
484 pub confidence: f64,
485}
486
487impl Priced {
488 // Effect scaled by trust: a full-confidence reading moves the knob the whole
489 // way, a thin one moves it proportionally. Never a step.
490 pub fn weighted(&self, neutral: f64) -> f64 {
491 neutral + (self.value - neutral) * self.confidence
492 }
493}
494
495// Confidence for a window of `filled` samples against the floor that used to
496// gate it. Linear: at the mathematical minimum it is near zero, at the old gate
497// it is 1. Beyond the gate it stays 1 -- more samples do not make a statistic
498// more true than its own definition allows.
499fn confidence(filled: usize, math_min: usize, stat_floor: usize) -> f64 {
500 if filled <= math_min {
501 return 0.0;
502 }
503 if filled >= stat_floor {
504 return 1.0;
505 }
506 (filled - math_min) as f64 / (stat_floor - math_min) as f64
507}
508
509// LAG-1 AUTOCORRELATION (CRITICAL SLOWING DOWN)
510//
511// EVERY OTHER PRIMITIVE IN THIS MODULE DESCRIBES THE WINDOW THAT
512// ALREADY HAPPENED. THIS ONE LEADS. AS A SYSTEM APPROACHES A CRITICAL
513// TRANSITION ITS RECOVERY FROM SMALL PERTURBATIONS SLOWS, AND THE
514// SLOWING SHOWS UP AS RISING LAG-1 AUTOCORRELATION BEFORE THE
515// TRANSITION ITSELF (SCHEFFER 2009; DETECTION LIMITS IN BOETTIGER AND
516// HASTINGS 2012). ON A RUNQUEUE-LENGTH SERIES THAT IS THE DIFFERENCE
517// BETWEEN SEEING A BURST FORM AND SEEING THAT ONE HAPPENED.
518//
519// r1 = SUM (x_i - mean)(x_{i+1} - mean) / SUM (x_i - mean)^2, THE
520// STANDARD BIASED ESTIMATOR. BIASED IS THE RIGHT CHOICE ON SHORT
521// WINDOWS: IT IS THE LOWER-VARIANCE ONE, AND VARIANCE IS WHAT MAKES A
522// SHORT-WINDOW ESTIMATE USELESS.
523//
524// READING IT: r1 NEAR 0 IS MEMORYLESS, EACH SAMPLE INDEPENDENT OF THE
525// LAST. r1 RISING TOWARD 1 IS CRITICAL SLOWING -- PERTURBATIONS ARE
526// PERSISTING RATHER THAN DAMPING, WHICH IS THE APPROACH TO SATURATION.
527// r1 NEGATIVE IS OSCILLATION, EACH SAMPLE OVERSHOOTING THE LAST.
528//
529// COST IS ONE MULTIPLY-ACCUMULATE PER SAMPLE. NOT WIRED TO A DECISION.
530
531// LAG-1 NEEDS ENOUGH PAIRS THAT ONE OUTLIER CANNOT SET THE ANSWER.
532pub const ACF1_MIN_SAMPLES: usize = 8;
533
534pub fn lag1_autocorr<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
535 if w.filled < ACF1_MIN_SAMPLES {
536 return None;
537 }
538 lag1_autocorr_raw(w)
539}
540
541// Unfloored body -- the priced form supplies its own minimum.
542fn lag1_autocorr_raw<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
543 let n = w.filled;
544 let mut s: [f64; N] = [0.0; N];
545 let mut k = 0;
546 for x in w.iter() {
547 s[k] = x;
548 k += 1;
549 }
550 let nf = n as f64;
551 let mut sum = 0.0;
552 for v in s.iter().take(n) {
553 sum += *v;
554 }
555 let mean = sum / nf;
556
557 let mut denom = 0.0;
558 for v in s.iter().take(n) {
559 let d = *v - mean;
560 denom += d * d;
561 }
562 // A FLAT WINDOW HAS NO PERTURBATION TO RECOVER FROM, SO IT CARRIES
563 // NO CRITICAL-SLOWING INFORMATION. None, NOT 1.0 -- AN IDLE CPU MUST
564 // NOT READ AS MAXIMALLY AUTOCORRELATED AND THEREFORE ABOUT TO TIP.
565 if denom < 1e-12 {
566 return None;
567 }
568 let mut numer = 0.0;
569 for i in 0..(n - 1) {
570 numer += (s[i] - mean) * (s[i + 1] - mean);
571 }
572 Some((numer / denom).clamp(-1.0, 1.0))
573}
574
575// KIM-JO FINITE-SIZE-CORRECTED BURSTINESS
576//
577// THE CLASSICAL BURSTINESS PARAMETER B = (sigma - mu) / (sigma + mu)
578// (GOH-BARABASI 2008) IS SEVERELY BIASED ON SHORT SERIES: IT DRIFTS
579// TOWARD -1 AS n FALLS, SO A SHORT WINDOW READS AS REGULAR NO MATTER
580// WHAT IT CONTAINS. THAT IS DISQUALIFYING HERE, WHERE EVERY WINDOW IS
581// SHORT BY CONSTRUCTION.
582//
583// KIM AND JO (2016) GIVE THE FINITE-SIZE-CORRECTED FORM:
584//
585// A_n(r) = (sqrt(n+1) r - sqrt(n-1)) /
586// ((sqrt(n+1) - 2) r + sqrt(n-1))
587//
588// WITH r = sigma / mu. IT IS -1 FOR PERFECTLY REGULAR, 0 FOR POISSON
589// AND +1 FOR MAXIMALLY BURSTY AT EVERY n, WHICH IS WHAT MAKES IT
590// COMPARABLE ACROSS WINDOW SIZES.
591//
592// ONE SCALAR THAT SEPARATES THE THREE TRAFFIC SHAPES THE TIER
593// CLASSIFIER AND THE MWU PATHWAYS KEEP CONFLATING: BURST-STARVATION
594// (POSITIVE), LONGRUN (NEAR ZERO) AND DEADLINE-PACED (NEGATIVE).
595//
596// NOT WIRED TO A DECISION.
597
598pub const BURSTINESS_MIN_SAMPLES: usize = 4;
599
600pub fn kim_jo_burstiness<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
601 if w.filled < BURSTINESS_MIN_SAMPLES {
602 return None;
603 }
604 kim_jo_burstiness_raw(w)
605}
606
607// Unfloored body -- the priced form supplies its own minimum.
608fn kim_jo_burstiness_raw<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
609 let n = w.filled;
610 let mut s: [f64; N] = [0.0; N];
611 let mut k = 0;
612 for x in w.iter() {
613 s[k] = x;
614 k += 1;
615 }
616 let nf = n as f64;
617 let mut sum = 0.0;
618 for v in s.iter().take(n) {
619 sum += *v;
620 }
621 let mu = sum / nf;
622 // BURSTINESS IS DEFINED ON A POSITIVE INTERVAL SERIES (WAITING
623 // TIMES, QUEUE LENGTHS). A NON-POSITIVE MEAN MEANS THE CALLER HANDED
624 // OVER SOMETHING THAT IS NOT ONE, AND sigma/mu WOULD BE MEANINGLESS.
625 if mu <= 1e-12 {
626 return None;
627 }
628 let mut var = 0.0;
629 for v in s.iter().take(n) {
630 let d = *v - mu;
631 var += d * d;
632 }
633 // SAMPLE (n-1) STANDARD DEVIATION: THE KIM-JO CORRECTION IS DERIVED
634 // AGAINST THE UNBIASED VARIANCE.
635 let sigma = (var / (nf - 1.0)).sqrt();
636 let r = sigma / mu;
637
638 let sp = (nf + 1.0).sqrt();
639 let sm = (nf - 1.0).sqrt();
640 let denom = (sp - 2.0) * r + sm;
641 if denom.abs() < 1e-12 {
642 return None;
643 }
644 Some(((sp * r - sm) / denom).clamp(-1.0, 1.0))
645}
646
647// VEITCH-ABRY HURST (WAVELET-VARIANCE / LOGSCALE-DIAGRAM ESTIMATOR)
648//
649// H IS THE LONG-RANGE-DEPENDENCE EXPONENT. FOR FRACTIONAL GAUSSIAN
650// NOISE THE VARIANCE OF THE DETAIL COEFFICIENTS AT OCTAVE j SCALES AS
651// E[d_j^2] ~ 2^(j*(2H - 1)),
652// SO A LEAST-SQUARES FIT OF log2(E[d_j^2]) AGAINST j HAS SLOPE 2H - 1
653// AND H = (SLOPE + 1) / 2. THIS IS THE VEITCH-ABRY (1999) ESTIMATOR
654// WITH A HAAR FILTER, WHICH IS THE CHEAPEST MULTIRESOLUTION ANALYSIS
655// AND NEEDS NO FILTER STATE.
656//
657// READING IT: H = 0.5 IS AN UNCORRELATED SEQUENCE -- THIS WINDOW SAYS
658// NOTHING ABOUT THE NEXT. H > 0.5 IS PERSISTENT: WHAT IS HAPPENING
659// TENDS TO KEEP HAPPENING, WHICH IS EXACTLY THE "THE SAME TASK IS
660// STATISTICALLY LIKELY TO RETURN" PROPERTY THAT MAKES CACHE
661// AMORTIZATION PAY. H < 0.5 IS ANTI-PERSISTENT / MEAN-REVERTING, WHERE
662// A BUSY WINDOW PREDICTS AN IDLE ONE.
663//
664// NOT WIRED TO ANY DECISION. PORTED AND MEASURED FIRST; ITS BEHAVIOR ON
665// THIS SCHEDULER'S OWN SIGNALS IS NOT YET KNOWN, AND A LONG-RANGE-
666// DEPENDENCE READ ON A 16-SAMPLE WINDOW IS THIN BY CONSTRUCTION.
667
668// MINIMUM SAMPLES: 16 GIVES THREE USABLE OCTAVES (8, 4 AND 2 DETAIL
669// COEFFICIENTS), WHICH IS THE FLOOR FOR A SLOPE THAT IS A FIT RATHER
670// THAN A LINE THROUGH TWO POINTS.
671pub const HURST_MIN_SAMPLES: usize = 16;
672
673// AN OCTAVE CONTRIBUTES TO THE FIT ONLY WITH AT LEAST THIS MANY DETAIL
674// COEFFICIENTS. THE COARSEST OCTAVES CARRY THE FEWEST AND THE NOISIEST
675// VARIANCE ESTIMATES; INCLUDING A ONE-COEFFICIENT OCTAVE LETS A SINGLE
676// SAMPLE SET THE SLOPE.
677const HURST_MIN_COEFFS: usize = 2;
678
679pub fn veitch_abry_hurst<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
680 let n = w.filled;
681 if n < HURST_MIN_SAMPLES {
682 return None;
683 }
684
685 let mut a: [f64; N] = [0.0; N];
686 let mut k = 0;
687 for x in w.iter() {
688 a[k] = x;
689 k += 1;
690 }
691
692 // HAAR CASCADE. AT EACH OCTAVE THE APPROXIMATION HALVES IN LENGTH
693 // AND THE DETAIL VARIANCE IS ACCUMULATED. THE 1/sqrt(2) KEEPS THE
694 // TRANSFORM ORTHONORMAL, SO A WHITE SEQUENCE HOLDS ITS VARIANCE
695 // ACROSS OCTAVES AND LANDS ON SLOPE 0 -> H = 0.5.
696 const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
697 let mut len = n;
698 let mut octave = 0usize;
699 // (j, log2(mean d^2)) PAIRS, AT MOST log2(N) OF THEM.
700 let mut xs: [f64; 64] = [0.0; 64];
701 let mut ys: [f64; 64] = [0.0; 64];
702 let mut pts = 0usize;
703
704 while len >= 2 && pts < xs.len() {
705 let half = len / 2;
706 octave += 1;
707 let mut sq = 0.0;
708 for i in 0..half {
709 let lo = a[2 * i];
710 let hi = a[2 * i + 1];
711 let d = (lo - hi) * INV_SQRT2;
712 sq += d * d;
713 a[i] = (lo + hi) * INV_SQRT2;
714 }
715 if half >= HURST_MIN_COEFFS {
716 let mean_sq = sq / half as f64;
717 // A PERFECTLY FLAT OCTAVE HAS NO SCALING INFORMATION AND ITS
718 // log2 IS -inf; SKIP IT RATHER THAN POISON THE FIT.
719 if mean_sq > 1e-300 {
720 xs[pts] = octave as f64;
721 ys[pts] = mean_sq.log2();
722 pts += 1;
723 }
724 }
725 len = half;
726 }
727
728 if pts < 3 {
729 return None;
730 }
731
732 // ORDINARY LEAST SQUARES ON (j, log2 E[d_j^2]).
733 let m = pts as f64;
734 let mut sx = 0.0;
735 let mut sy = 0.0;
736 for i in 0..pts {
737 sx += xs[i];
738 sy += ys[i];
739 }
740 let mx = sx / m;
741 let my = sy / m;
742 let mut num = 0.0;
743 let mut den = 0.0;
744 for i in 0..pts {
745 let dx = xs[i] - mx;
746 num += dx * (ys[i] - my);
747 den += dx * dx;
748 }
749 if den < 1e-12 {
750 return None;
751 }
752 let slope = num / den;
753 let h = (slope + 1.0) / 2.0;
754 // H IS DEFINED ON [0, 1]. A SHORT WINDOW CAN PRODUCE A SLOPE OUTSIDE
755 // THAT; CLAMP RATHER THAN REPORT AN IMPOSSIBLE EXPONENT.
756 Some(h.clamp(0.0, 1.0))
757}
758
759// PECORA-CARROLL COUPLING
760//
761// ASKS WHETHER ONE SERIES IS A FUNCTION OF ANOTHER: IF x_i AND x_j ARE
762// NEIGHBORS, ARE y_i AND y_j ALSO NEIGHBORS? WHEN y IS DRIVEN BY x THE
763// ANSWER IS YES AND THE CROSS-PREDICTION ERROR COLLAPSES; WHEN THE TWO
764// ARE INDEPENDENT AN x-NEIGHBOR SAYS NOTHING ABOUT y AND THE ERROR
765// RISES TO THE SERIES' OWN MEAN PAIRWISE SPREAD.
766//
767// BOTH SERIES ARE STANDARDIZED FIRST, SO THIS MEASURES SHARED DYNAMICS
768// AND NOT SHARED UNITS -- TWO CPUS' QUEUE LENGTHS COUPLE OR DO NOT
769// REGARDLESS OF WHICH ONE CARRIES MORE WORK.
770//
771// RETURNS [0, 1]: 1 IS FULLY SLAVED, 0 IS INDEPENDENT. THIS IS THE
772// PRIMITIVE BEHIND "ARE THESE TWO RUNQUEUES CONVERGING OR DIVERGING",
773// WHICH IS THE SHAPE OF BOTH THE SEAT-TOPOLOGY AND THE THRASHING
774// QUESTION.
775//
776// NOT WIRED TO ANY DECISION -- SAME REASON AS THE HURST ESTIMATOR.
777
778pub const PC_MIN_SAMPLES: usize = 8;
779
780fn standardize<const N: usize>(w: &RawWindow<N>, out: &mut [f64; N]) -> Option<usize> {
781 let n = w.filled;
782 if n == 0 {
783 return None;
784 }
785 let mut k = 0;
786 for x in w.iter() {
787 out[k] = x;
788 k += 1;
789 }
790 let nf = n as f64;
791 let mut sum = 0.0;
792 for v in out.iter().take(n) {
793 sum += *v;
794 }
795 let mean = sum / nf;
796 let mut var = 0.0;
797 for v in out.iter().take(n) {
798 let d = *v - mean;
799 var += d * d;
800 }
801 let sigma = (var / nf).sqrt();
802 // A FLAT SERIES HAS NO DYNAMICS TO COUPLE. REPORT IT AS SUCH RATHER
803 // THAN DIVIDING BY ZERO AND CALLING THE RESULT SYNCHRONY.
804 if sigma < 1e-9 {
805 return None;
806 }
807 for v in out.iter_mut().take(n) {
808 *v = (*v - mean) / sigma;
809 }
810 Some(n)
811}
812
813pub fn pecora_carroll<const N: usize>(x: &RawWindow<N>, y: &RawWindow<N>) -> Option<f64> {
814 if x.filled.min(y.filled) < PC_MIN_SAMPLES {
815 return None;
816 }
817 pecora_carroll_raw(x, y)
818}
819
820// Unfloored body -- the priced form supplies its own minimum.
821fn pecora_carroll_raw<const N: usize>(x: &RawWindow<N>, y: &RawWindow<N>) -> Option<f64> {
822 let n = x.filled.min(y.filled);
823 let mut xs: [f64; N] = [0.0; N];
824 let mut ys: [f64; N] = [0.0; N];
825 let nx = standardize(x, &mut xs)?;
826 let ny = standardize(y, &mut ys)?;
827 let n = n.min(nx).min(ny);
828 if n < PC_MIN_SAMPLES {
829 return None;
830 }
831
832 // CROSS-PREDICTION ERROR: FOR EACH i, THE NEAREST OTHER POINT IN x,
833 // SCORED BY HOW FAR APART THE MATCHING y VALUES ARE.
834 let mut err = 0.0;
835 for i in 0..n {
836 let mut best = f64::INFINITY;
837 let mut best_j = usize::MAX;
838 for j in 0..n {
839 if j == i {
840 continue;
841 }
842 let d = (xs[i] - xs[j]).abs();
843 if d < best {
844 best = d;
845 best_j = j;
846 }
847 }
848 if best_j == usize::MAX {
849 return None;
850 }
851 err += (ys[i] - ys[best_j]).abs();
852 }
853 err /= n as f64;
854
855 // BASELINE: THE MEAN PAIRWISE SPREAD OF y ITSELF, WHICH IS WHAT THE
856 // ERROR APPROACHES WHEN x CARRIES NO INFORMATION ABOUT y. TAKING THE
857 // SERIES' OWN SPREAD RATHER THAN A GAUSSIAN CONSTANT KEEPS THIS
858 // HONEST ON THE SHORT, NON-NORMAL WINDOWS THIS SCHEDULER ACTUALLY
859 // SEES.
860 let mut base = 0.0;
861 let mut pairs = 0u64;
862 for i in 0..n {
863 for j in (i + 1)..n {
864 base += (ys[i] - ys[j]).abs();
865 pairs += 1;
866 }
867 }
868 if pairs == 0 {
869 return None;
870 }
871 base /= pairs as f64;
872 if base < 1e-12 {
873 return None;
874 }
875
876 Some((1.0 - err / base).clamp(0.0, 1.0))
877}
878
879// PRICED ESTIMATOR FORMS
880//
881// Each pairs the existing computation with a confidence, and lowers the hard
882// refusal to the point where the ARITHMETIC breaks rather than where the
883// STATISTICS get thin. The old floors survive as the confidence ceiling: at the
884// old gate the reading is worth its full weight, below it a proportional share.
885//
886// Hurst is the exception and stays gated at 16. Its floor is not statistical --
887// the wavelet fit needs three octaves with two coefficients each, and a window
888// of 8 yields two usable octaves, which is a line through two points rather
889// than a regression. There is no weakly-known answer there; there is no answer.
890
891// A single pair is enough to define lag-1; eight is where it stops being noise.
892const ACF1_MATH_MIN: usize = 3;
893pub fn lag1_autocorr_priced<const N: usize>(w: &RawWindow<N>) -> Option<Priced> {
894 let n = w.filled;
895 if n < ACF1_MATH_MIN {
896 return None;
897 }
898 let v = lag1_autocorr_raw(w)?;
899 Some(Priced {
900 value: v,
901 confidence: confidence(n, ACF1_MATH_MIN, ACF1_MIN_SAMPLES),
902 })
903}
904
905// Burstiness needs a sample stdev, so n >= 2. The Kim-Jo correction is defined
906// from there; four was a comfort floor, not a requirement.
907const BURSTINESS_MATH_MIN: usize = 2;
908pub fn kim_jo_burstiness_priced<const N: usize>(w: &RawWindow<N>) -> Option<Priced> {
909 let n = w.filled;
910 if n < BURSTINESS_MATH_MIN {
911 return None;
912 }
913 let v = kim_jo_burstiness_raw(w)?;
914 Some(Priced {
915 value: v,
916 confidence: confidence(n, BURSTINESS_MATH_MIN, BURSTINESS_MIN_SAMPLES),
917 })
918}
919
920// Coupling needs a nearest neighbour that is not the point itself, so n >= 3.
921const PC_MATH_MIN: usize = 3;
922pub fn pecora_carroll_priced<const N: usize>(x: &RawWindow<N>, y: &RawWindow<N>) -> Option<Priced> {
923 let n = x.filled.min(y.filled);
924 if n < PC_MATH_MIN {
925 return None;
926 }
927 let v = pecora_carroll_raw(x, y)?;
928 Some(Priced {
929 value: v,
930 confidence: confidence(n, PC_MATH_MIN, PC_MIN_SAMPLES),
931 })
932}
933
934// Hurst: gated, not priced. See above.
935pub fn veitch_abry_hurst_priced<const N: usize>(w: &RawWindow<N>) -> Option<Priced> {
936 veitch_abry_hurst(w).map(|v| Priced {
937 value: v,
938 confidence: 1.0,
939 })
940}
941
942// CHAOS COUNTER (DIAGNOSTIC)
943//
944// MONOTONIC COUNTER OF "WINDOW IS CHAOTIC" CROSSINGS. INCREMENT WHEN
945// HVG ENTROPY CROSSES LN(3/2) UPWARD OR WHEN PERMUTATION ENTROPY
946// CROSSES BP_H_HIGH UPWARD. EXPOSED FOR THE TELEMETRY LINE AND
947// FOR THE COMMITTED MWU PATHWAY THAT GATES OFF CROSSINGS.
948#[derive(Default, Debug)]
949pub struct ChaosCounter(AtomicU64);
950
951impl ChaosCounter {
952 pub const fn new() -> Self {
953 Self(AtomicU64::new(0))
954 }
955
956 pub fn bump(&self) {
957 self.0.fetch_add(1, Ordering::Relaxed);
958 }
959
960 pub fn load(&self) -> u64 {
961 self.0.load(Ordering::Relaxed)
962 }
963}
964
965// ESTIMATOR TESTS
966//
967// THE PORTED ESTIMATORS ARE CHECKED AGAINST SIGNALS WHOSE ANSWER IS
968// KNOWN BY CONSTRUCTION, NOT AGAINST A GOLDEN NUMBER: A DETERMINISTIC
969// PSEUDO-RANDOM SEQUENCE IS UNCORRELATED (H NEAR 0.5), A RAMP IS
970// MAXIMALLY PERSISTENT (H HIGH), AN ALTERNATING SEQUENCE IS
971// ANTI-PERSISTENT (H LOW). THE BANDS ARE WIDE ON PURPOSE -- A
972// 16-TO-64-SAMPLE WINDOW CANNOT PIN AN LRD EXPONENT TIGHTLY, AND A
973// TEST THAT PRETENDS OTHERWISE WOULD BE ASSERTING NOISE.
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 // DETERMINISTIC LCG. NO rand DEPENDENCY, AND THE SAME SEQUENCE EVERY
979 // RUN, SO A FAILURE IS REPRODUCIBLE RATHER THAN A DRAW.
980 fn lcg(seed: &mut u64) -> f64 {
981 *seed = seed
982 .wrapping_mul(6364136223846793005)
983 .wrapping_add(1442695040888963407);
984 ((*seed >> 33) as f64 / (1u64 << 31) as f64) - 0.5
985 }
986
987 fn fill<const N: usize>(vals: &[f64]) -> RawWindow<N> {
988 let mut w = RawWindow::<N>::new();
989 for v in vals {
990 w.push(*v);
991 }
992 w
993 }
994
995 #[test]
996 fn acf1_white_noise_is_memoryless() {
997 let mut seed = 0xABCDEFu64;
998 let vals: Vec<f64> = (0..64).map(|_| lcg(&mut seed)).collect();
999 let r = lag1_autocorr(&fill::<64>(&vals)).expect("64 samples is enough");
1000 assert!(
1001 r.abs() < 0.3,
1002 "uncorrelated samples should not persist, got r1={r}"
1003 );
1004 }
1005
1006 #[test]
1007 fn acf1_ramp_is_strongly_persistent() {
1008 // THE CRITICAL-SLOWING SIGNATURE: EACH SAMPLE ALMOST ENTIRELY
1009 // DETERMINED BY THE ONE BEFORE IT.
1010 let vals: Vec<f64> = (0..32).map(|i| i as f64).collect();
1011 let r = lag1_autocorr(&fill::<32>(&vals)).expect("32 samples is enough");
1012 assert!(r > 0.8, "a ramp should read as slowing, got r1={r}");
1013 }
1014
1015 #[test]
1016 fn acf1_alternating_is_negative() {
1017 let vals: Vec<f64> = (0..32)
1018 .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1019 .collect();
1020 let r = lag1_autocorr(&fill::<32>(&vals)).expect("32 samples is enough");
1021 assert!(r < -0.8, "an oscillation should overshoot, got r1={r}");
1022 }
1023
1024 #[test]
1025 fn acf1_flat_window_is_none() {
1026 // AN IDLE CPU MUST NOT READ AS MAXIMALLY AUTOCORRELATED AND
1027 // THEREFORE ABOUT TO TIP. IDLE IS THE COMMON CASE.
1028 assert!(lag1_autocorr(&fill::<32>(&[7.0; 32])).is_none());
1029 }
1030
1031 #[test]
1032 fn acf1_needs_samples() {
1033 assert!(lag1_autocorr(&fill::<32>(&[1.0, 2.0, 3.0])).is_none());
1034 }
1035
1036 #[test]
1037 fn burstiness_regular_is_negative() {
1038 // EVENLY SPACED TRAFFIC IS THE DEADLINE-PACED SHAPE.
1039 let b = kim_jo_burstiness(&fill::<32>(&[5.0; 16])).expect("16 samples is enough");
1040 assert!(
1041 b < -0.5,
1042 "perfectly regular traffic should read regular, got B={b}"
1043 );
1044 }
1045
1046 #[test]
1047 fn burstiness_bursty_is_positive() {
1048 // LONG QUIET RUNS PUNCTUATED BY SPIKES: THE BURST-STARVATION
1049 // SHAPE, WHERE sigma GREATLY EXCEEDS mu.
1050 let mut vals = vec![0.01f64; 30];
1051 vals[7] = 40.0;
1052 vals[23] = 55.0;
1053 let b = kim_jo_burstiness(&fill::<32>(&vals)).expect("30 samples is enough");
1054 assert!(b > 0.4, "spiky traffic should read bursty, got B={b}");
1055 }
1056
1057 #[test]
1058 fn burstiness_ordering_holds() {
1059 // THE THREE SHAPES THE TIER CLASSIFIER CONFLATES MUST SEPARATE:
1060 // DEADLINE-PACED < LONGRUN < BURST-STARVATION.
1061 let mut seed = 24680u64;
1062 let regular = fill::<32>(&[5.0; 24]);
1063 // POSITIVE, MODERATELY VARIABLE: THE LONGRUN SHAPE.
1064 let longrun = fill::<32>(&(0..24).map(|_| 5.0 + lcg(&mut seed)).collect::<Vec<_>>());
1065 let mut spiky = vec![0.05f64; 24];
1066 spiky[5] = 30.0;
1067 spiky[17] = 45.0;
1068 let bursty = fill::<32>(&spiky);
1069 let (r, l, b) = (
1070 kim_jo_burstiness(®ular).unwrap(),
1071 kim_jo_burstiness(&longrun).unwrap(),
1072 kim_jo_burstiness(&bursty).unwrap(),
1073 );
1074 assert!(
1075 r < l && l < b,
1076 "expected regular {r} < longrun {l} < bursty {b}"
1077 );
1078 }
1079
1080 #[test]
1081 fn burstiness_is_stable_across_window_sizes() {
1082 // THE ENTIRE POINT OF THE KIM-JO CORRECTION: THE CLASSICAL
1083 // STATISTIC DRIFTS TOWARD -1 AS n FALLS, SO THE SAME TRAFFIC
1084 // WOULD READ DIFFERENTLY ON AN 8-SAMPLE AND A 32-SAMPLE WINDOW.
1085 let mut seed = 1357u64;
1086 let long: Vec<f64> = (0..32).map(|_| 5.0 + 2.0 * lcg(&mut seed)).collect();
1087 let short: Vec<f64> = long[..8].to_vec();
1088 let bl = kim_jo_burstiness(&fill::<32>(&long)).unwrap();
1089 let bs = kim_jo_burstiness(&fill::<8>(&short)).unwrap();
1090 assert!(
1091 (bl - bs).abs() < 0.5,
1092 "same traffic read {bl} at n=32 and {bs} at n=8; the correction is not holding"
1093 );
1094 }
1095
1096 #[test]
1097 fn burstiness_rejects_non_positive_series() {
1098 // BURSTINESS IS DEFINED ON A POSITIVE INTERVAL SERIES. A CALLER
1099 // HANDING OVER A CENTERED SIGNAL GETS None, NOT A NUMBER.
1100 let centered: Vec<f64> = (0..16)
1101 .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1102 .collect();
1103 assert!(kim_jo_burstiness(&fill::<16>(¢ered)).is_none());
1104 }
1105
1106 #[test]
1107 fn hurst_needs_a_full_window() {
1108 let w = fill::<64>(&[1.0; 8]);
1109 assert!(
1110 veitch_abry_hurst(&w).is_none(),
1111 "an 8-sample window cannot support three octaves"
1112 );
1113 }
1114
1115 #[test]
1116 fn hurst_white_noise_near_half() {
1117 let mut seed = 0x9E3779B97F4A7C15u64;
1118 let vals: Vec<f64> = (0..64).map(|_| lcg(&mut seed)).collect();
1119 let w = fill::<64>(&vals);
1120 let h = veitch_abry_hurst(&w).expect("64 samples is enough");
1121 assert!(
1122 (0.25..=0.75).contains(&h),
1123 "uncorrelated sequence read H={h}"
1124 );
1125 }
1126
1127 #[test]
1128 fn hurst_ramp_is_persistent() {
1129 let vals: Vec<f64> = (0..64).map(|i| i as f64).collect();
1130 let w = fill::<64>(&vals);
1131 let h = veitch_abry_hurst(&w).expect("64 samples is enough");
1132 assert!(
1133 h > 0.75,
1134 "a monotonic ramp should read persistent, got H={h}"
1135 );
1136 }
1137
1138 #[test]
1139 fn hurst_differenced_noise_is_antipersistent() {
1140 // FIRST-DIFFERENCING WHITE NOISE IS THE STANDARD ANTI-PERSISTENT
1141 // CONSTRUCTION: EVERY STEP TENDS TO UNDO THE ONE BEFORE IT, SO
1142 // H -> 0. UNLIKE A BARE ALTERNATION IT STILL CARRIES ENERGY AT
1143 // EVERY OCTAVE, WHICH IS WHAT THE FIT NEEDS.
1144 let mut seed = 12345u64;
1145 let noise: Vec<f64> = (0..65).map(|_| lcg(&mut seed)).collect();
1146 let diff: Vec<f64> = (1..65).map(|i| noise[i] - noise[i - 1]).collect();
1147 let h = veitch_abry_hurst(&fill::<64>(&diff)).expect("64 samples is enough");
1148 assert!(h < 0.35, "differenced noise should mean-revert, got H={h}");
1149 }
1150
1151 #[test]
1152 fn hurst_pure_alternation_cannot_be_fit() {
1153 // A PERFECT ALTERNATION PUTS ALL OF ITS ENERGY IN OCTAVE 1: EVERY
1154 // COARSER APPROXIMATION IS EXACTLY ZERO, SO THERE IS ONE POINT
1155 // AND NO SCALING LAW TO MEASURE. REPORTING None IS THE HONEST
1156 // ANSWER; INVENTING AN EXPONENT FROM A SINGLE OCTAVE WOULD NOT
1157 // BE AN ESTIMATE. THE NOISY VERSION OF THE SAME SHAPE READS AS
1158 // MAXIMALLY ANTI-PERSISTENT, WHICH IS THE CASE THAT MATTERS.
1159 let pure: Vec<f64> = (0..64)
1160 .map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
1161 .collect();
1162 assert!(veitch_abry_hurst(&fill::<64>(&pure)).is_none());
1163
1164 let mut seed = 999u64;
1165 let noisy: Vec<f64> = (0..64)
1166 .map(|i| (if i % 2 == 0 { 1.0 } else { -1.0 }) + 0.3 * lcg(&mut seed))
1167 .collect();
1168 let h = veitch_abry_hurst(&fill::<64>(&noisy)).expect("noise restores the octaves");
1169 assert!(
1170 h < 0.35,
1171 "a noisy alternation should mean-revert, got H={h}"
1172 );
1173 }
1174
1175 #[test]
1176 fn hurst_ordering_holds() {
1177 // THE ORDERING IS THE LOAD-BEARING PROPERTY, NOT ANY ONE VALUE:
1178 // MEAN-REVERTING < UNCORRELATED < PERSISTENT. MEASURED HERE AT
1179 // ROUGHLY 0.04 / 0.47 / 1.00, WITH WHITE NOISE LANDING NEAR ITS
1180 // THEORETICAL 0.5.
1181 let mut seed = 12345u64;
1182 let noise: Vec<f64> = (0..65).map(|_| lcg(&mut seed)).collect();
1183 let diff: Vec<f64> = (1..65).map(|i| noise[i] - noise[i - 1]).collect();
1184 let mut s2 = 4242u64;
1185 let rnd = fill::<64>(&(0..64).map(|_| lcg(&mut s2)).collect::<Vec<_>>());
1186 let ramp = fill::<64>(&(0..64).map(|i| i as f64).collect::<Vec<_>>());
1187 let (a, r, p) = (
1188 veitch_abry_hurst(&fill::<64>(&diff)).unwrap(),
1189 veitch_abry_hurst(&rnd).unwrap(),
1190 veitch_abry_hurst(&ramp).unwrap(),
1191 );
1192 assert!(
1193 a < r && r < p,
1194 "expected anti-persistent {a} < random {r} < ramp {p}"
1195 );
1196 }
1197
1198 #[test]
1199 fn hurst_random_walk_is_persistent() {
1200 // A RANDOM WALK IS THE CANONICAL PERSISTENT PROCESS, AND UNLIKE
1201 // THE RAMP IT IS NOT MONOTONIC -- SO THIS CHECKS PERSISTENCE
1202 // RATHER THAN A TREND THE ESTIMATOR COULD BE PICKING UP.
1203 let mut seed = 7u64;
1204 let mut acc = 0.0;
1205 let walk: Vec<f64> = (0..64)
1206 .map(|_| {
1207 acc += lcg(&mut seed);
1208 acc
1209 })
1210 .collect();
1211 let h = veitch_abry_hurst(&fill::<64>(&walk)).expect("64 samples is enough");
1212 assert!(h > 0.75, "a random walk should read persistent, got H={h}");
1213 }
1214
1215 #[test]
1216 fn pecora_carroll_identical_series_couple() {
1217 let mut seed = 777u64;
1218 let vals: Vec<f64> = (0..32).map(|_| lcg(&mut seed)).collect();
1219 let a = fill::<32>(&vals);
1220 let b = fill::<32>(&vals);
1221 let c = pecora_carroll(&a, &b).expect("32 samples is enough");
1222 assert!(c > 0.9, "a series against itself should be slaved, got {c}");
1223 }
1224
1225 #[test]
1226 fn pecora_carroll_affine_copy_couples() {
1227 // COUPLING IS ABOUT SHARED DYNAMICS, NOT SHARED UNITS: A SCALED
1228 // AND SHIFTED COPY IS STILL THE SAME SIGNAL.
1229 let mut seed = 31337u64;
1230 let vals: Vec<f64> = (0..32).map(|_| lcg(&mut seed)).collect();
1231 let scaled: Vec<f64> = vals.iter().map(|v| 100.0 * v + 42.0).collect();
1232 let c = pecora_carroll(&fill::<32>(&vals), &fill::<32>(&scaled)).unwrap();
1233 assert!(c > 0.9, "an affine copy should read as coupled, got {c}");
1234 }
1235
1236 #[test]
1237 fn pecora_carroll_independent_series_do_not() {
1238 let mut s1 = 1u64;
1239 let mut s2 = 0xDEADBEEFu64;
1240 let a = fill::<64>(&(0..64).map(|_| lcg(&mut s1)).collect::<Vec<_>>());
1241 let b = fill::<64>(&(0..64).map(|_| lcg(&mut s2)).collect::<Vec<_>>());
1242 let c = pecora_carroll(&a, &b).expect("64 samples is enough");
1243 assert!(
1244 c < 0.5,
1245 "independent streams should not read coupled, got {c}"
1246 );
1247 }
1248
1249 #[test]
1250 fn pecora_carroll_flat_series_is_none() {
1251 // A FLAT SERIES HAS NO DYNAMICS. REPORTING PERFECT SYNCHRONY
1252 // BETWEEN TWO IDLE CPUS WOULD BE THE WORST KIND OF FALSE
1253 // POSITIVE, SINCE IDLE IS THE COMMON CASE.
1254 let flat = fill::<32>(&[3.0; 32]);
1255 let mut seed = 5u64;
1256 let live = fill::<32>(&(0..32).map(|_| lcg(&mut seed)).collect::<Vec<_>>());
1257 assert!(pecora_carroll(&flat, &live).is_none());
1258 assert!(pecora_carroll(&flat, &flat).is_none());
1259 }
1260
1261 #[test]
1262 fn pecora_carroll_needs_samples() {
1263 let mut seed = 9u64;
1264 let short = fill::<32>(&(0..4).map(|_| lcg(&mut seed)).collect::<Vec<_>>());
1265 assert!(pecora_carroll(&short, &short).is_none());
1266 }
1267}
1268
1269// SATURATION-READS-AS-QUIESCENT PROBE
1270//
1271// NOT A PROPERTY TEST -- A PINNED OBSERVATION. THE ADAPTIVE QUIESCENCE
1272// GATE FREEZES ON (hvg_lambda <= 2.6) && (rqa_det >= 0.90) && converged,
1273// AND BOTH CHAOS TERMS READ THE SAME idle_pct WINDOW. A FULLY SATURATED
1274// BOX PINS idle_pct AT 0, WHICH MAKES THAT WINDOW EXACTLY FLAT. THIS
1275// RECORDS WHAT THE TWO TERMS THEN RETURN, SO THE CONSEQUENCE IS A
1276// MEASURED FACT IN THE TREE RATHER THAN AN ARGUMENT ON A BOARD.
1277#[cfg(test)]
1278mod saturation_probe {
1279 use super::*;
1280
1281 #[test]
1282 fn a_pegged_box_satisfies_both_chaos_terms_of_the_freeze_gate() {
1283 // idle_pct PINNED AT 0: FULL SATURATION, NOT IDLENESS.
1284 let mut w = RawWindow::<16>::new();
1285 for _ in 0..16 {
1286 w.push(0.0);
1287 }
1288 let (lambda, _s) = hvg_stats(&w);
1289 let det = rqa_det(&w).expect("a full window always answers");
1290
1291 assert!(
1292 lambda <= HVG_LAMBDA_PERIODIC_MAX,
1293 "flat idle_pct gives lambda={lambda}, inside the periodic band"
1294 );
1295 assert!(
1296 det >= RQA_DET_STEADY_MIN,
1297 "flat idle_pct gives det={det}, at or above the steady floor"
1298 );
1299
1300 // BOTH CHAOS TERMS OF THE GATE ARE THEREFORE SATISFIED BY A
1301 // PEGGED BOX. ONLY mwu_converged STANDS BETWEEN FULL SATURATION
1302 // AND A FROZEN ORCHESTRATOR, AND CONVERGENCE IS EXACTLY WHAT A
1303 // SUSTAINED LOAD PRODUCES.
1304 }
1305}
1306
1307// PRICING, IN ISOLATION
1308//
1309// The confidence ramp is the mechanism that replaces four sample-floor gates,
1310// so it is tested on its own rather than through a derivation where the value
1311// and the weight move together.
1312#[cfg(test)]
1313mod pricing_tests {
1314 use super::*;
1315
1316 #[test]
1317 fn confidence_ramps_from_the_arithmetic_minimum_to_the_old_gate() {
1318 assert_eq!(
1319 confidence(2, 2, 8),
1320 0.0,
1321 "at the minimum, nothing is trusted"
1322 );
1323 assert_eq!(confidence(8, 2, 8), 1.0, "at the old gate, fully trusted");
1324 assert_eq!(confidence(20, 2, 8), 1.0, "beyond it, no extra credit");
1325 let mid = confidence(5, 2, 8);
1326 assert!(
1327 (mid - 0.5).abs() < 1e-9,
1328 "halfway is half weight, got {mid}"
1329 );
1330 }
1331
1332 #[test]
1333 fn weighted_scales_the_same_value_by_trust() {
1334 // The property the gate could not express: identical readings, different
1335 // evidence, proportional effect. Neutral is 1.0 (a multiplier that does
1336 // nothing), so a thin reading collapses toward no-op instead of off.
1337 let full = Priced {
1338 value: 1.5,
1339 confidence: 1.0,
1340 };
1341 let half = Priced {
1342 value: 1.5,
1343 confidence: 0.5,
1344 };
1345 let none = Priced {
1346 value: 1.5,
1347 confidence: 0.0,
1348 };
1349 assert!((full.weighted(1.0) - 1.5).abs() < 1e-9);
1350 assert!((half.weighted(1.0) - 1.25).abs() < 1e-9);
1351 assert!(
1352 (none.weighted(1.0) - 1.0).abs() < 1e-9,
1353 "zero confidence must be exactly the neutral value"
1354 );
1355 }
1356
1357 #[test]
1358 fn priced_estimators_answer_below_their_old_floors() {
1359 // Each of the three now returns a reading where it used to return None.
1360 let mut w = RawWindow::<16>::new();
1361 for v in [0.1, 9.0, 0.1] {
1362 w.push(v);
1363 }
1364 assert!(
1365 kim_jo_burstiness(&w).is_none(),
1366 "the gated form still refuses"
1367 );
1368 let p = kim_jo_burstiness_priced(&w).expect("the priced form answers");
1369 assert!(
1370 p.confidence > 0.0 && p.confidence < 1.0,
1371 "and prices it below full, got {}",
1372 p.confidence
1373 );
1374
1375 let mut r = RawWindow::<16>::new();
1376 for v in [1.0, 2.0, 3.0, 4.0] {
1377 r.push(v);
1378 }
1379 assert!(lag1_autocorr(&r).is_none());
1380 assert!(lag1_autocorr_priced(&r).is_some());
1381 }
1382
1383 #[test]
1384 fn hurst_is_not_priced_because_its_floor_is_arithmetic() {
1385 // Three octaves or no fit. A short window has no weakly-known answer.
1386 let mut w = RawWindow::<16>::new();
1387 for v in [1.0, 2.0, 3.0, 4.0] {
1388 w.push(v);
1389 }
1390 assert!(veitch_abry_hurst_priced(&w).is_none());
1391 }
1392}