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}