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. NO
11// 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 THAT DISTRIBUTION, NOT AN
21// ENTROPY THRESHOLD. LAMBDA IS THE PRIMARY REGIME DISCRIMINATOR AND
22// ENTROPY THE 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 IS MONOTONIC OR PERIODIC, 1 IS
27// MAXIMALLY DISORDERED.
28//
29// THE TWO ARE COMPLEMENTARY. HVG ENTROPY IS AMPLITUDE-SENSITIVE;
30// BANDT-POMPE IS AMPLITUDE-INVARIANT AND CAPTURES ORDINAL DYNAMICS.
31
32use std::sync::atomic::{AtomicU64, Ordering};
33
34// CRITICAL VALUES
35
36// LN(3/2). IID HVG DEGREE-DISTRIBUTION CHARACTERISTIC EXPONENT.
37// EXPOSED FOR DIAGNOSTICS / FUTURE USE; NOT USED AS A DIRECT THRESHOLD.
38pub const HVG_LN_3_2: f64 = 0.405_465_108_108_164_4;
39
40// HVG MEAN-DEGREE THRESHOLDS. IID RANDOM SATURATES AT 4 - 2/N; PURELY
41// PERIODIC SEQUENCES STAY NEAR 2. THE TWO THRESHOLDS BELOW DEFINE A
42// DEAD ZONE FOR THE MIXED REGIME. THE BAND ITSELF IS THE HYSTERESIS:
43// A WINDOW MUST CROSS IT ENTIRELY TO CHANGE THE VERDICT.
44pub const HVG_LAMBDA_PERIODIC_MAX: f64 = 2.6;
45pub const HVG_LAMBDA_CHAOTIC_MIN: f64 = 3.4;
46
47// IID-ASYMPTOTE HVG ENTROPY: LN(3) + 2*LN(3/2) ~= 1.910.
48pub const HVG_S_IID: f64 = 1.910_543_686_807_036;
49
50// BANDT-POMPE D=3 PATTERN COUNT.
51pub const BP_D3_PATTERNS: usize = 6;
52
53// LN(6): NORMALIZATION FACTOR FOR BP D=3 PERMUTATION ENTROPY.
54const LN_BP_D3: f64 = 1.791_759_469_228_055;
55
56// HIGH PERMUTATION-ENTROPY THRESHOLD. ABOVE THIS THE ORDINAL DYNAMICS
57// LOOK MAXIMALLY DISORDERED ON THE WINDOW; THE ADAPTIVE LAYER USES IT
58// AS A "WORKLOAD IS UNPREDICTABLE THIS WINDOW" SIGNAL.
59pub const BP_H_HIGH: f64 = 0.85;
60
61// RQA (RECURRENCE QUANTIFICATION ANALYSIS) DETERMINISM.
62// DET IS THE FRACTION OF RECURRENCE POINTS THAT LIE ON DIAGONAL LINE
63// SEGMENTS. A DETERMINISTIC / STEADY SIGNAL REVISITS PHASE-SPACE
64// NEIGHBORHOODS ALONG DIAGONALS (DET -> 1); AN IID SIGNAL SCATTERS
65// RECURRENCE POINTS WITH NO DIAGONAL STRUCTURE (DET -> 0).
66// THE ADAPTIVE QUIESCENCE GATE PAIRS HIGH DET WITH HVG LAMBDA IN THE
67// PERIODIC BAND TO DETECT "STOP RETUNING" STEADY STATE.
68
69// DELAY-EMBEDDING DIMENSION. 3-D VECTORS WITH UNIT DELAY -- MATCHES
70// THE D=3 ORDINAL SCALE USED BY BANDT-POMPE.
71pub const RQA_EMBED_DIM: usize = 3;
72
73// RECURRENCE THRESHOLD AS A FRACTION OF THE WINDOW STANDARD DEVIATION:
74// eps = RQA_THRESH_STD_FRAC * sigma. 0.20 IS THE STANDARD RQA DEFAULT
75// BAND FOR SHORT SERIES.
76pub const RQA_THRESH_STD_FRAC: f64 = 0.20;
77
78// MINIMUM DIAGONAL-LINE LENGTH COUNTED AS DETERMINISM (RQA l_min).
79pub const RQA_LMIN: usize = 2;
80
81// DET AT OR ABOVE THIS = DETERMINISTIC / STEADY. CONSUMED BY THE
82// ADAPTIVE QUIESCENCE GATE.
83pub const RQA_DET_STEADY_MIN: f64 = 0.90;
84
85// BELOW THIS WINDOW FILL, rqa_det RETURNS None (INSUFFICIENT DATA --
86// NEVER LET THE GATE FREEZE ON A HALF-FILLED WINDOW).
87pub const RQA_MIN_SAMPLES: usize = 8;
88
89// RAW WINDOW
90// FIXED-SIZE RING BUFFER OF f64 SAMPLES. NO HEAP ALLOC AT STEADY STATE.
91// SEMANTICS:
92//   - PUSH IS O(1)
93//   - SAMPLES ARE READ IN INSERTION ORDER (OLDEST FIRST)
94//   - UNFILLED SLOTS ARE NOT YIELDED
95//   - len() == 0 UNTIL FIRST PUSH; AT MOST N AFTER N PUSHES
96
97#[derive(Clone, Debug)]
98pub struct RawWindow<const N: usize> {
99    buf: [f64; N],
100    head: usize,
101    filled: usize,
102}
103
104impl<const N: usize> Default for RawWindow<N> {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl<const N: usize> RawWindow<N> {
111    pub const fn new() -> Self {
112        Self {
113            buf: [0.0; N],
114            head: 0,
115            filled: 0,
116        }
117    }
118
119    pub fn push(&mut self, x: f64) {
120        self.buf[self.head] = x;
121        self.head = (self.head + 1) % N;
122        if self.filled < N {
123            self.filled += 1;
124        }
125    }
126
127    pub fn len(&self) -> usize {
128        self.filled
129    }
130
131    pub fn is_empty(&self) -> bool {
132        self.filled == 0
133    }
134
135    pub fn capacity(&self) -> usize {
136        N
137    }
138
139    // YIELD SAMPLES IN INSERTION ORDER (OLDEST -> NEWEST).
140    pub fn iter(&self) -> RawWindowIter<'_, N> {
141        let start = if self.filled < N { 0 } else { self.head };
142        RawWindowIter {
143            win: self,
144            pos: 0,
145            start,
146        }
147    }
148
149    pub fn last(&self) -> Option<f64> {
150        if self.filled == 0 {
151            None
152        } else {
153            let i = (self.head + N - 1) % N;
154            Some(self.buf[i])
155        }
156    }
157}
158
159pub struct RawWindowIter<'a, const N: usize> {
160    win: &'a RawWindow<N>,
161    pos: usize,
162    start: usize,
163}
164
165impl<'a, const N: usize> Iterator for RawWindowIter<'a, N> {
166    type Item = f64;
167    fn next(&mut self) -> Option<f64> {
168        if self.pos >= self.win.filled {
169            return None;
170        }
171        let idx = (self.start + self.pos) % N;
172        self.pos += 1;
173        Some(self.win.buf[idx])
174    }
175}
176
177// MEAN / MIN / MAX HELPERS
178
179#[allow(dead_code)]
180pub fn mean<const N: usize>(w: &RawWindow<N>) -> f64 {
181    if w.filled == 0 {
182        return 0.0;
183    }
184    let mut s = 0.0;
185    for x in w.iter() {
186        s += x;
187    }
188    s / w.filled as f64
189}
190
191// HORIZONTAL VISIBILITY GRAPH
192//
193// FOR A SEQUENCE x_1..x_N, NODES i AND j (i < j) ARE HVG-CONNECTED IFF
194// x_k < min(x_i, x_j) FOR ALL i < k < j. ADJACENT NODES (j = i+1) ARE
195// ALWAYS CONNECTED.
196//
197// hvg_degrees BUILDS THE DEGREE VECTOR ONCE; hvg_stats DERIVES BOTH
198// LAMBDA AND ENTROPY FROM IT IN A SINGLE O(N^2) PASS.
199//
200// BRUTE-FORCE O(N^2). AT N <= 128 (>1-MINUTE WINDOW AT 1HZ) THIS IS
201// UNDER 16K COMPARISONS, DONE ONCE PER SECOND.
202
203fn hvg_degrees<const N: usize>(w: &RawWindow<N>) -> Option<([u32; N], usize)> {
204    let n = w.filled;
205    if n < 3 {
206        return None;
207    }
208
209    let mut s: [f64; N] = [0.0; N];
210    let mut k = 0;
211    for x in w.iter() {
212        s[k] = x;
213        k += 1;
214    }
215
216    let mut deg: [u32; N] = [0; N];
217    for i in 0..n {
218        let mut blocker = f64::NEG_INFINITY;
219        for j in (i + 1)..n {
220            let limit = s[i].min(s[j]);
221            if j == i + 1 || blocker < limit {
222                deg[i] += 1;
223                deg[j] += 1;
224            }
225            if s[j] > blocker {
226                blocker = s[j];
227            }
228        }
229    }
230    Some((deg, n))
231}
232
233// AMORTIZED LAMBDA + ENTROPY. ONE O(N^2) PASS BUILDS THE DEGREE VECTOR;
234// BOTH STATISTICS DERIVE FROM IT.
235pub fn hvg_stats<const N: usize>(w: &RawWindow<N>) -> (f64, f64) {
236    let (deg, n) = match hvg_degrees(w) {
237        Some(v) => v,
238        None => return (0.0, 0.0),
239    };
240
241    let mut sum: u64 = 0;
242    let mut hist: [u32; N] = [0; N];
243    for d in deg.iter().take(n) {
244        sum += *d as u64;
245        let bucket = (*d as usize).min(n - 1);
246        hist[bucket] += 1;
247    }
248    let lambda = sum as f64 / n as f64;
249
250    let total = n as f64;
251    let mut entropy = 0.0;
252    for c in hist.iter().take(n) {
253        if *c == 0 {
254            continue;
255        }
256        let p = *c as f64 / total;
257        entropy -= p * p.ln();
258    }
259    (lambda, entropy)
260}
261
262// BANDT-POMPE PERMUTATION ENTROPY (D=3)
263//
264// SLIDE A LENGTH-3 WINDOW. FOR EACH (a, b, c) MAP TO ONE OF SIX ORDINAL
265// PATTERNS BY THE RANK ORDER. BUILD THE EMPIRICAL DISTRIBUTION AND
266// RETURN H / LN(6) IN [0, 1].
267//
268// TIES ARE BROKEN BY POSITION (b > a IFF b STRICTLY GREATER, ELSE a > b).
269// IID RANDOM SAMPLES YIELD H ~= 1; PERIODIC OR MONOTONIC YIELD H << 1.
270pub fn bandt_pompe_d3<const N: usize>(w: &RawWindow<N>) -> f64 {
271    let n = w.filled;
272    if n < 3 {
273        return 0.0;
274    }
275
276    // INLINED RING WALK: WE NEED THREE CONSECUTIVE SAMPLES.
277    let mut counts = [0u32; BP_D3_PATTERNS];
278    let mut total = 0u32;
279
280    // COLLECT INTO LINEAR BUFFER ONCE; SAFE FOR N <= 128.
281    let mut s: [f64; N] = [0.0; N];
282    let mut k = 0;
283    for x in w.iter() {
284        s[k] = x;
285        k += 1;
286    }
287
288    for i in 0..(n - 2) {
289        let a = s[i];
290        let b = s[i + 1];
291        let c = s[i + 2];
292        // BREAK TIES BY POSITION (POSITIONAL ORDER WHEN VALUES EQUAL).
293        let pattern = match (a < b, b < c, a < c) {
294            (true, true, true) => 0,    // a < b < c
295            (true, false, true) => 1,   // a < c <= b
296            (true, false, false) => 2,  // c <= a < b
297            (false, true, true) => 3,   // b <= a < c
298            (false, true, false) => 4,  // b < c <= a
299            (false, false, false) => 5, // c <= b <= a
300            (true, true, false) => 1,   // DEGENERATE TIE: TREAT AS PATTERN 1
301            (false, false, true) => 4,  // DEGENERATE TIE: TREAT AS PATTERN 4
302        };
303        counts[pattern] += 1;
304        total += 1;
305    }
306
307    if total == 0 {
308        return 0.0;
309    }
310
311    let denom = total as f64;
312    let mut h = 0.0;
313    for c in counts.iter() {
314        if *c == 0 {
315            continue;
316        }
317        let p = *c as f64 / denom;
318        h -= p * p.ln();
319    }
320    h / LN_BP_D3
321}
322
323// RQA DETERMINISM (DET)
324//
325// DELAY-EMBED THE WINDOW INTO RQA_EMBED_DIM-D VECTORS WITH UNIT DELAY,
326// BUILD THE RECURRENCE MATRIX UNDER A CHEBYSHEV (L-INFINITY) BALL OF
327// RADIUS eps = RQA_THRESH_STD_FRAC * sigma, AND RETURN THE FRACTION OF
328// OFF-DIAGONAL RECURRENCE POINTS THAT LIE ON DIAGONAL RUNS OF LENGTH
329// >= RQA_LMIN.
330//
331// RETURNS Some(DET) IN [0, 1], OR None WHEN THE WINDOW HAS FEWER THAN
332// RQA_MIN_SAMPLES FILLED SLOTS -- THE QUIESCENCE GATE MUST NOT FREEZE
333// ON A HALF-FILLED WINDOW.
334//
335// BRUTE-FORCE O(N^2), SAME COST CLASS AS hvg_degrees. AT N <= 64 THIS
336// IS UNDER 4K COMPARISONS, DONE ONCE PER SECOND.
337
338// CHEBYSHEV (L-INFINITY) DISTANCE BETWEEN TWO 3-D EMBEDDED POINTS.
339fn chebyshev3(a: &[f64; RQA_EMBED_DIM], b: &[f64; RQA_EMBED_DIM]) -> f64 {
340    let mut m = 0.0;
341    for k in 0..RQA_EMBED_DIM {
342        let d = (a[k] - b[k]).abs();
343        if d > m {
344            m = d;
345        }
346    }
347    m
348}
349
350pub fn rqa_det<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
351    let n = w.filled;
352    if n < RQA_MIN_SAMPLES {
353        return None;
354    }
355
356    // COPY WINDOW INTO ORDER-PRESERVING SLICE FOR INDEXED ACCESS.
357    let mut s: [f64; N] = [0.0; N];
358    let mut k = 0;
359    for x in w.iter() {
360        s[k] = x;
361        k += 1;
362    }
363
364    // MEAN AND STANDARD DEVIATION OVER THE n FILLED SAMPLES.
365    let nf = n as f64;
366    let mut sum = 0.0;
367    for v in s.iter().take(n) {
368        sum += *v;
369    }
370    let mean = sum / nf;
371    let mut var = 0.0;
372    for v in s.iter().take(n) {
373        let d = *v - mean;
374        var += d * d;
375    }
376    let sigma = (var / nf).sqrt();
377
378    // FLAT-WINDOW SPECIAL CASE. A PERFECTLY STEADY SIGNAL HAS sigma = 0;
379    // EVERY EMBEDDED POINT RECURS WITH EVERY OTHER. THAT IS FULLY
380    // DETERMINISTIC -- RETURN 1.0 DIRECTLY (AND AVOID eps = 0 / A
381    // DEGENERATE RECURRENCE MATRIX). idle_pct IS INTEGER-PERCENT CAST
382    // TO f64, SO A STEADY COMPUTE WORKLOAD PRODUCES AN EXACTLY-FLAT
383    // WINDOW AND MUST READ AS QUIESCENT.
384    if sigma < 1e-9 {
385        return Some(1.0);
386    }
387
388    let eps = RQA_THRESH_STD_FRAC * sigma;
389
390    // DELAY-EMBED: m = n - (RQA_EMBED_DIM - 1) VECTORS.
391    let m = n - (RQA_EMBED_DIM - 1);
392    if m < 2 {
393        return None;
394    }
395    let mut emb: [[f64; RQA_EMBED_DIM]; N] = [[0.0; RQA_EMBED_DIM]; N];
396    for i in 0..m {
397        for d in 0..RQA_EMBED_DIM {
398            emb[i][d] = s[i + d];
399        }
400    }
401
402    // RECURRENCE MATRIX OVER THE m EMBEDDED POINTS.
403    let mut rec: [[bool; N]; N] = [[false; N]; N];
404    for i in 0..m {
405        for j in 0..m {
406            rec[i][j] = chebyshev3(&emb[i], &emb[j]) <= eps;
407        }
408    }
409
410    // WALK EVERY OFF-MAIN DIAGONAL. COUNT TOTAL RECURRENCE POINTS AND
411    // THE POINTS THAT BELONG TO DIAGONAL RUNS OF LENGTH >= RQA_LMIN.
412    let mut total_rec: u64 = 0;
413    let mut diag_rec: u64 = 0;
414    // OFFSET o > 0: PAIRS (i, i + o). OFFSET o < 0 IS THE SYMMETRIC
415    // MIRROR; THE RECURRENCE MATRIX IS SYMMETRIC SO WE WALK o IN
416    // [1, m) AND DOUBLE-COUNT NOTHING BY COUNTING BOTH (i,j) AND (j,i)
417    // VIA THE 2x FACTOR -- INSTEAD WE JUST WALK BOTH UPPER AND LOWER
418    // EXPLICITLY TO KEEP total_rec CONSISTENT WITH diag_rec.
419    for o in 1..m {
420        // UPPER DIAGONAL: (i, i + o).
421        let mut run: usize = 0;
422        for i in 0..(m - o) {
423            if rec[i][i + o] {
424                total_rec += 1;
425                run += 1;
426            } else {
427                if run >= RQA_LMIN {
428                    diag_rec += run as u64;
429                }
430                run = 0;
431            }
432        }
433        if run >= RQA_LMIN {
434            diag_rec += run as u64;
435        }
436        // LOWER DIAGONAL: (i + o, i).
437        run = 0;
438        for i in 0..(m - o) {
439            if rec[i + o][i] {
440                total_rec += 1;
441                run += 1;
442            } else {
443                if run >= RQA_LMIN {
444                    diag_rec += run as u64;
445                }
446                run = 0;
447            }
448        }
449        if run >= RQA_LMIN {
450            diag_rec += run as u64;
451        }
452    }
453
454    if total_rec == 0 {
455        return Some(0.0);
456    }
457    Some(diag_rec as f64 / total_rec as f64)
458}
459
460// A PRICED READING: A VALUE AND HOW MUCH IT IS WORTH TRUSTING.
461// EACH ESTIMATOR ANSWERS WHEREVER ITS ARITHMETIC IS DEFINED AND REPORTS
462// CONFIDENCE SEPARATELY, RATHER THAN REFUSING UNDER A SAMPLE FLOOR. A WINDOW AT
463// 7 SAMPLES IS NOT UNKNOWABLE, IT IS WEAKLY KNOWN -- PRICE IT, NEVER BAIL.
464// CONFIDENCE RAMPS FROM THE MATHEMATICAL MINIMUM TO THE STATISTICAL FLOOR, AND
465// THE CONSUMER MULTIPLIES ITS EFFECT BY IT, SO DATA FLOWS FROM THE FIRST SAMPLE
466// THE MATH ALLOWS AND ITS INFLUENCE GROWS WITH THE EVIDENCE.
467// THE Option-RETURNING FORMS REMAIN, ANSWERING WHETHER A READING IS COMPUTABLE
468// AT ALL, WHICH IS A DIFFERENT QUESTION FROM WHAT IT IS WORTH.
469#[derive(Clone, Copy, Debug)]
470pub struct Priced {
471    pub value: f64,
472    // [0, 1]. 0 means computable but on the thinnest possible evidence; 1 means
473    // the window carries at least what the old gate demanded.
474    pub confidence: f64,
475}
476
477impl Priced {
478    // Effect scaled by trust: a full-confidence reading moves the knob the whole
479    // way, a thin one moves it proportionally. Never a step.
480    pub fn weighted(&self, neutral: f64) -> f64 {
481        neutral + (self.value - neutral) * self.confidence
482    }
483}
484
485// Confidence for a window of `filled` samples against the floor that used to
486// gate it. Linear: at the mathematical minimum it is near zero, at the old gate
487// it is 1. Beyond the gate it stays 1 -- more samples do not make a statistic
488// more true than its own definition allows.
489fn confidence(filled: usize, math_min: usize, stat_floor: usize) -> f64 {
490    if filled <= math_min {
491        return 0.0;
492    }
493    if filled >= stat_floor {
494        return 1.0;
495    }
496    (filled - math_min) as f64 / (stat_floor - math_min) as f64
497}
498
499// LAG-1 AUTOCORRELATION (CRITICAL SLOWING DOWN)
500//
501// EVERY OTHER PRIMITIVE IN THIS MODULE DESCRIBES THE WINDOW THAT
502// ALREADY HAPPENED. THIS ONE LEADS. AS A SYSTEM APPROACHES A CRITICAL
503// TRANSITION ITS RECOVERY FROM SMALL PERTURBATIONS SLOWS, AND THE
504// SLOWING SHOWS UP AS RISING LAG-1 AUTOCORRELATION BEFORE THE
505// TRANSITION ITSELF (SCHEFFER 2009; DETECTION LIMITS IN BOETTIGER AND
506// HASTINGS 2012). ON A RUNQUEUE-LENGTH SERIES THAT IS THE DIFFERENCE
507// BETWEEN SEEING A BURST FORM AND SEEING THAT ONE HAPPENED.
508//
509// r1 = SUM (x_i - mean)(x_{i+1} - mean) / SUM (x_i - mean)^2, THE
510// STANDARD BIASED ESTIMATOR. BIASED IS THE LOWER-VARIANCE CHOICE, AND
511// VARIANCE IS WHAT MAKES A SHORT-WINDOW ESTIMATE USELESS.
512//   r1 NEAR 0      MEMORYLESS, EACH SAMPLE INDEPENDENT OF THE LAST
513//   r1 TOWARD 1    CRITICAL SLOWING, PERTURBATIONS PERSISTING
514//   r1 NEGATIVE    OSCILLATION, EACH SAMPLE OVERSHOOTING THE LAST
515// ONE MULTIPLY-ACCUMULATE PER SAMPLE. NOT WIRED TO A DECISION.
516
517// LAG-1 NEEDS ENOUGH PAIRS THAT ONE OUTLIER CANNOT SET THE ANSWER.
518pub const ACF1_MIN_SAMPLES: usize = 8;
519
520pub fn lag1_autocorr<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
521    if w.filled < ACF1_MIN_SAMPLES {
522        return None;
523    }
524    lag1_autocorr_raw(w)
525}
526
527// Unfloored body -- the priced form supplies its own minimum.
528fn lag1_autocorr_raw<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
529    let n = w.filled;
530    let mut s: [f64; N] = [0.0; N];
531    let mut k = 0;
532    for x in w.iter() {
533        s[k] = x;
534        k += 1;
535    }
536    let nf = n as f64;
537    let mut sum = 0.0;
538    for v in s.iter().take(n) {
539        sum += *v;
540    }
541    let mean = sum / nf;
542
543    let mut denom = 0.0;
544    for v in s.iter().take(n) {
545        let d = *v - mean;
546        denom += d * d;
547    }
548    // A FLAT WINDOW HAS NO PERTURBATION TO RECOVER FROM, SO IT CARRIES
549    // NO CRITICAL-SLOWING INFORMATION. None, NOT 1.0 -- AN IDLE CPU MUST
550    // NOT READ AS MAXIMALLY AUTOCORRELATED AND THEREFORE ABOUT TO TIP.
551    if denom < 1e-12 {
552        return None;
553    }
554    let mut numer = 0.0;
555    for i in 0..(n - 1) {
556        numer += (s[i] - mean) * (s[i + 1] - mean);
557    }
558    Some((numer / denom).clamp(-1.0, 1.0))
559}
560
561// KIM-JO FINITE-SIZE-CORRECTED BURSTINESS.
562// THE CLASSICAL B = (sigma - mu) / (sigma + mu) (GOH-BARABASI 2008) DRIFTS
563// TOWARD -1 AS n FALLS, SO A SHORT WINDOW READS AS REGULAR WHATEVER IT
564// CONTAINS -- DISQUALIFYING HERE, WHERE EVERY WINDOW IS SHORT.
565// KIM AND JO (2016) CORRECT FOR FINITE SIZE:
566//
567//   A_n(r) = (sqrt(n+1) r - sqrt(n-1)) /
568//            ((sqrt(n+1) - 2) r + sqrt(n-1))
569//
570// WITH r = sigma / mu. -1 IS PERFECTLY REGULAR, 0 IS POISSON, +1 IS
571// MAXIMALLY BURSTY, AT EVERY n -- WHICH IS WHAT MAKES IT COMPARABLE
572// ACROSS WINDOW SIZES. ONE SCALAR SEPARATING BURST-STARVATION (POSITIVE),
573// LONGRUN (NEAR ZERO) AND DEADLINE-PACED (NEGATIVE).
574// NOT WIRED TO A DECISION.
575
576pub const BURSTINESS_MIN_SAMPLES: usize = 4;
577
578pub fn kim_jo_burstiness<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
579    if w.filled < BURSTINESS_MIN_SAMPLES {
580        return None;
581    }
582    kim_jo_burstiness_raw(w)
583}
584
585// Unfloored body -- the priced form supplies its own minimum.
586fn kim_jo_burstiness_raw<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
587    let n = w.filled;
588    let mut s: [f64; N] = [0.0; N];
589    let mut k = 0;
590    for x in w.iter() {
591        s[k] = x;
592        k += 1;
593    }
594    let nf = n as f64;
595    let mut sum = 0.0;
596    for v in s.iter().take(n) {
597        sum += *v;
598    }
599    let mu = sum / nf;
600    // BURSTINESS IS DEFINED ON A POSITIVE INTERVAL SERIES (WAITING
601    // TIMES, QUEUE LENGTHS). A NON-POSITIVE MEAN MEANS THE CALLER HANDED
602    // OVER SOMETHING THAT IS NOT ONE, AND sigma/mu WOULD BE MEANINGLESS.
603    if mu <= 1e-12 {
604        return None;
605    }
606    let mut var = 0.0;
607    for v in s.iter().take(n) {
608        let d = *v - mu;
609        var += d * d;
610    }
611    // SAMPLE (n-1) STANDARD DEVIATION: THE KIM-JO CORRECTION IS DERIVED
612    // AGAINST THE UNBIASED VARIANCE.
613    let sigma = (var / (nf - 1.0)).sqrt();
614    let r = sigma / mu;
615
616    let sp = (nf + 1.0).sqrt();
617    let sm = (nf - 1.0).sqrt();
618    let denom = (sp - 2.0) * r + sm;
619    if denom.abs() < 1e-12 {
620        return None;
621    }
622    Some(((sp * r - sm) / denom).clamp(-1.0, 1.0))
623}
624
625// VEITCH-ABRY HURST (WAVELET-VARIANCE / LOGSCALE-DIAGRAM ESTIMATOR)
626//
627// H IS THE LONG-RANGE-DEPENDENCE EXPONENT. FOR FRACTIONAL GAUSSIAN
628// NOISE THE VARIANCE OF THE DETAIL COEFFICIENTS AT OCTAVE j SCALES AS
629//   E[d_j^2] ~ 2^(j*(2H - 1)),
630// SO A LEAST-SQUARES FIT OF log2(E[d_j^2]) AGAINST j HAS SLOPE 2H - 1
631// AND H = (SLOPE + 1) / 2. THIS IS THE VEITCH-ABRY (1999) ESTIMATOR
632// WITH A HAAR FILTER, WHICH IS THE CHEAPEST MULTIRESOLUTION ANALYSIS
633// AND NEEDS NO FILTER STATE.
634//
635// READING IT: H = 0.5 IS AN UNCORRELATED SEQUENCE -- THIS WINDOW SAYS
636// NOTHING ABOUT THE NEXT. H > 0.5 IS PERSISTENT: WHAT IS HAPPENING
637// TENDS TO KEEP HAPPENING, WHICH IS EXACTLY THE "THE SAME TASK IS
638// STATISTICALLY LIKELY TO RETURN" PROPERTY THAT MAKES CACHE
639// AMORTIZATION PAY. H < 0.5 IS ANTI-PERSISTENT / MEAN-REVERTING, WHERE
640// A BUSY WINDOW PREDICTS AN IDLE ONE.
641//
642// NOT WIRED TO ANY DECISION. PORTED AND MEASURED FIRST; ITS BEHAVIOR ON
643// THIS SCHEDULER'S OWN SIGNALS IS NOT YET KNOWN, AND A LONG-RANGE-
644// DEPENDENCE READ ON A 16-SAMPLE WINDOW IS THIN BY CONSTRUCTION.
645
646// MINIMUM SAMPLES: 16 GIVES THREE USABLE OCTAVES (8, 4 AND 2 DETAIL
647// COEFFICIENTS), WHICH IS THE FLOOR FOR A SLOPE THAT IS A FIT RATHER
648// THAN A LINE THROUGH TWO POINTS.
649pub const HURST_MIN_SAMPLES: usize = 16;
650
651// AN OCTAVE CONTRIBUTES TO THE FIT ONLY WITH AT LEAST THIS MANY DETAIL
652// COEFFICIENTS. THE COARSEST OCTAVES CARRY THE FEWEST AND THE NOISIEST
653// VARIANCE ESTIMATES; INCLUDING A ONE-COEFFICIENT OCTAVE LETS A SINGLE
654// SAMPLE SET THE SLOPE.
655const HURST_MIN_COEFFS: usize = 2;
656
657pub fn veitch_abry_hurst<const N: usize>(w: &RawWindow<N>) -> Option<f64> {
658    let n = w.filled;
659    if n < HURST_MIN_SAMPLES {
660        return None;
661    }
662
663    let mut a: [f64; N] = [0.0; N];
664    let mut k = 0;
665    for x in w.iter() {
666        a[k] = x;
667        k += 1;
668    }
669
670    // HAAR CASCADE. AT EACH OCTAVE THE APPROXIMATION HALVES IN LENGTH
671    // AND THE DETAIL VARIANCE IS ACCUMULATED. THE 1/sqrt(2) KEEPS THE
672    // TRANSFORM ORTHONORMAL, SO A WHITE SEQUENCE HOLDS ITS VARIANCE
673    // ACROSS OCTAVES AND LANDS ON SLOPE 0 -> H = 0.5.
674    const INV_SQRT2: f64 = std::f64::consts::FRAC_1_SQRT_2;
675    let mut len = n;
676    let mut octave = 0usize;
677    // (j, log2(mean d^2)) PAIRS, AT MOST log2(N) OF THEM.
678    let mut xs: [f64; 64] = [0.0; 64];
679    let mut ys: [f64; 64] = [0.0; 64];
680    let mut pts = 0usize;
681
682    while len >= 2 && pts < xs.len() {
683        let half = len / 2;
684        octave += 1;
685        let mut sq = 0.0;
686        for i in 0..half {
687            let lo = a[2 * i];
688            let hi = a[2 * i + 1];
689            let d = (lo - hi) * INV_SQRT2;
690            sq += d * d;
691            a[i] = (lo + hi) * INV_SQRT2;
692        }
693        if half >= HURST_MIN_COEFFS {
694            let mean_sq = sq / half as f64;
695            // A PERFECTLY FLAT OCTAVE HAS NO SCALING INFORMATION AND ITS
696            // log2 IS -inf; SKIP IT RATHER THAN POISON THE FIT.
697            if mean_sq > 1e-300 {
698                xs[pts] = octave as f64;
699                ys[pts] = mean_sq.log2();
700                pts += 1;
701            }
702        }
703        len = half;
704    }
705
706    if pts < 3 {
707        return None;
708    }
709
710    // ORDINARY LEAST SQUARES ON (j, log2 E[d_j^2]).
711    let m = pts as f64;
712    let mut sx = 0.0;
713    let mut sy = 0.0;
714    for i in 0..pts {
715        sx += xs[i];
716        sy += ys[i];
717    }
718    let mx = sx / m;
719    let my = sy / m;
720    let mut num = 0.0;
721    let mut den = 0.0;
722    for i in 0..pts {
723        let dx = xs[i] - mx;
724        num += dx * (ys[i] - my);
725        den += dx * dx;
726    }
727    if den < 1e-12 {
728        return None;
729    }
730    let slope = num / den;
731    let h = (slope + 1.0) / 2.0;
732    // H IS DEFINED ON [0, 1]. A SHORT WINDOW CAN PRODUCE A SLOPE OUTSIDE
733    // THAT; CLAMP RATHER THAN REPORT AN IMPOSSIBLE EXPONENT.
734    Some(h.clamp(0.0, 1.0))
735}
736
737// PECORA-CARROLL COUPLING
738//
739// ASKS WHETHER ONE SERIES IS A FUNCTION OF ANOTHER: IF x_i AND x_j ARE
740// NEIGHBORS, ARE y_i AND y_j ALSO NEIGHBORS? WHEN y IS DRIVEN BY x THE
741// ANSWER IS YES AND THE CROSS-PREDICTION ERROR COLLAPSES; WHEN THE TWO
742// ARE INDEPENDENT AN x-NEIGHBOR SAYS NOTHING ABOUT y AND THE ERROR
743// RISES TO THE SERIES' OWN MEAN PAIRWISE SPREAD.
744//
745// BOTH SERIES ARE STANDARDIZED FIRST, SO THIS MEASURES SHARED DYNAMICS
746// AND NOT SHARED UNITS -- TWO CPUS' QUEUE LENGTHS COUPLE OR DO NOT
747// REGARDLESS OF WHICH ONE CARRIES MORE WORK.
748//
749// RETURNS [0, 1]: 1 IS FULLY SLAVED, 0 IS INDEPENDENT. THIS IS THE
750// PRIMITIVE BEHIND "ARE THESE TWO RUNQUEUES CONVERGING OR DIVERGING",
751// WHICH IS THE SHAPE OF BOTH THE SEAT-TOPOLOGY AND THE THRASHING
752// QUESTION.
753//
754// NOT WIRED TO ANY DECISION -- SAME REASON AS THE HURST ESTIMATOR.
755
756pub const PC_MIN_SAMPLES: usize = 8;
757
758fn standardize<const N: usize>(w: &RawWindow<N>, out: &mut [f64; N]) -> Option<usize> {
759    let n = w.filled;
760    if n == 0 {
761        return None;
762    }
763    let mut k = 0;
764    for x in w.iter() {
765        out[k] = x;
766        k += 1;
767    }
768    let nf = n as f64;
769    let mut sum = 0.0;
770    for v in out.iter().take(n) {
771        sum += *v;
772    }
773    let mean = sum / nf;
774    let mut var = 0.0;
775    for v in out.iter().take(n) {
776        let d = *v - mean;
777        var += d * d;
778    }
779    let sigma = (var / nf).sqrt();
780    // A FLAT SERIES HAS NO DYNAMICS TO COUPLE. REPORT IT AS SUCH RATHER
781    // THAN DIVIDING BY ZERO AND CALLING THE RESULT SYNCHRONY.
782    if sigma < 1e-9 {
783        return None;
784    }
785    for v in out.iter_mut().take(n) {
786        *v = (*v - mean) / sigma;
787    }
788    Some(n)
789}
790
791pub fn pecora_carroll<const N: usize>(x: &RawWindow<N>, y: &RawWindow<N>) -> Option<f64> {
792    if x.filled.min(y.filled) < PC_MIN_SAMPLES {
793        return None;
794    }
795    pecora_carroll_raw(x, y)
796}
797
798// Unfloored body -- the priced form supplies its own minimum.
799fn pecora_carroll_raw<const N: usize>(x: &RawWindow<N>, y: &RawWindow<N>) -> Option<f64> {
800    let n = x.filled.min(y.filled);
801    let mut xs: [f64; N] = [0.0; N];
802    let mut ys: [f64; N] = [0.0; N];
803    let nx = standardize(x, &mut xs)?;
804    let ny = standardize(y, &mut ys)?;
805    let n = n.min(nx).min(ny);
806    if n < PC_MIN_SAMPLES {
807        return None;
808    }
809
810    // CROSS-PREDICTION ERROR: FOR EACH i, THE NEAREST OTHER POINT IN x,
811    // SCORED BY HOW FAR APART THE MATCHING y VALUES ARE.
812    let mut err = 0.0;
813    for i in 0..n {
814        let mut best = f64::INFINITY;
815        let mut best_j = usize::MAX;
816        for j in 0..n {
817            if j == i {
818                continue;
819            }
820            let d = (xs[i] - xs[j]).abs();
821            if d < best {
822                best = d;
823                best_j = j;
824            }
825        }
826        if best_j == usize::MAX {
827            return None;
828        }
829        err += (ys[i] - ys[best_j]).abs();
830    }
831    err /= n as f64;
832
833    // BASELINE: THE MEAN PAIRWISE SPREAD OF y ITSELF, WHICH IS WHAT THE
834    // ERROR APPROACHES WHEN x CARRIES NO INFORMATION ABOUT y. TAKING THE
835    // SERIES' OWN SPREAD RATHER THAN A GAUSSIAN CONSTANT KEEPS THIS
836    // HONEST ON THE SHORT, NON-NORMAL WINDOWS THIS SCHEDULER ACTUALLY
837    // SEES.
838    let mut base = 0.0;
839    let mut pairs = 0u64;
840    for i in 0..n {
841        for j in (i + 1)..n {
842            base += (ys[i] - ys[j]).abs();
843            pairs += 1;
844        }
845    }
846    if pairs == 0 {
847        return None;
848    }
849    base /= pairs as f64;
850    if base < 1e-12 {
851        return None;
852    }
853
854    Some((1.0 - err / base).clamp(0.0, 1.0))
855}
856
857// PRICED ESTIMATOR FORMS
858//
859// Each pairs the existing computation with a confidence, and lowers the hard
860// refusal to the point where the ARITHMETIC breaks rather than where the
861// STATISTICS get thin. The old floors survive as the confidence ceiling: at the
862// old gate the reading is worth its full weight, below it a proportional share.
863//
864// Hurst is the exception and stays gated at 16. Its floor is not statistical --
865// the wavelet fit needs three octaves with two coefficients each, and a window
866// of 8 yields two usable octaves, which is a line through two points rather
867// than a regression. There is no weakly-known answer there; there is no answer.
868
869// A single pair is enough to define lag-1; eight is where it stops being noise.
870const ACF1_MATH_MIN: usize = 3;
871pub fn lag1_autocorr_priced<const N: usize>(w: &RawWindow<N>) -> Option<Priced> {
872    let n = w.filled;
873    if n < ACF1_MATH_MIN {
874        return None;
875    }
876    let v = lag1_autocorr_raw(w)?;
877    Some(Priced {
878        value: v,
879        confidence: confidence(n, ACF1_MATH_MIN, ACF1_MIN_SAMPLES),
880    })
881}
882
883// Burstiness needs a sample stdev, so n >= 2. The Kim-Jo correction is defined
884// from there; four was a comfort floor, not a requirement.
885const BURSTINESS_MATH_MIN: usize = 2;
886pub fn kim_jo_burstiness_priced<const N: usize>(w: &RawWindow<N>) -> Option<Priced> {
887    let n = w.filled;
888    if n < BURSTINESS_MATH_MIN {
889        return None;
890    }
891    let v = kim_jo_burstiness_raw(w)?;
892    Some(Priced {
893        value: v,
894        confidence: confidence(n, BURSTINESS_MATH_MIN, BURSTINESS_MIN_SAMPLES),
895    })
896}
897
898// Coupling needs a nearest neighbour that is not the point itself, so n >= 3.
899const PC_MATH_MIN: usize = 3;
900pub fn pecora_carroll_priced<const N: usize>(x: &RawWindow<N>, y: &RawWindow<N>) -> Option<Priced> {
901    let n = x.filled.min(y.filled);
902    if n < PC_MATH_MIN {
903        return None;
904    }
905    let v = pecora_carroll_raw(x, y)?;
906    Some(Priced {
907        value: v,
908        confidence: confidence(n, PC_MATH_MIN, PC_MIN_SAMPLES),
909    })
910}
911
912// Hurst: gated, not priced. See above.
913pub fn veitch_abry_hurst_priced<const N: usize>(w: &RawWindow<N>) -> Option<Priced> {
914    veitch_abry_hurst(w).map(|v| Priced {
915        value: v,
916        confidence: 1.0,
917    })
918}
919
920// CHAOS COUNTER (DIAGNOSTIC)
921//
922// MONOTONIC COUNTER OF "WINDOW IS CHAOTIC" CROSSINGS. INCREMENT WHEN
923// HVG ENTROPY CROSSES LN(3/2) UPWARD OR WHEN PERMUTATION ENTROPY
924// CROSSES BP_H_HIGH UPWARD. EXPOSED FOR THE TELEMETRY LINE AND
925// FOR THE COMMITTED MWU PATHWAY THAT GATES OFF CROSSINGS.
926#[derive(Default, Debug)]
927pub struct ChaosCounter(AtomicU64);
928
929impl ChaosCounter {
930    pub const fn new() -> Self {
931        Self(AtomicU64::new(0))
932    }
933
934    pub fn bump(&self) {
935        self.0.fetch_add(1, Ordering::Relaxed);
936    }
937
938    pub fn load(&self) -> u64 {
939        self.0.load(Ordering::Relaxed)
940    }
941}