Skip to main content

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(&regular).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>(&centered)).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}