Skip to main content

openentropy_core/
chaos.rs

1//! # Chaos Theory Analysis
2//!
3//! Methods for applying chaos theory analysis to QRNG output.
4//! These functions help distinguish genuine quantum randomness from
5//! deterministic or structured behavior in sampled byte streams.
6
7use flate2::Compression;
8use flate2::write::ZlibEncoder;
9use serde::Serialize;
10use std::f64::consts::LN_2;
11
12use crate::math;
13
14#[derive(Debug, Clone, Serialize)]
15pub struct HurstResult {
16    pub hurst_exponent: f64,
17    pub is_valid: bool,
18    pub r_squared: f64,
19    pub rs_values: Vec<(usize, f64)>,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct BootstrapHurstResult {
24    pub observed_hurst: f64,
25    pub mean_surrogate_hurst: f64,
26    pub std_surrogate_hurst: f64,
27    pub p_value: f64,
28    pub ci_lower: f64,
29    pub ci_upper: f64,
30    pub n_surrogates: usize,
31    pub is_significant: bool,
32    pub is_valid: bool,
33}
34
35#[derive(Debug, Clone, Serialize)]
36pub struct RollingHurstWindow {
37    pub offset: usize,
38    pub hurst: f64,
39    pub r_squared: f64,
40    pub is_valid: bool,
41}
42
43#[derive(Debug, Clone, Serialize)]
44pub struct RollingHurstResult {
45    pub windows: Vec<RollingHurstWindow>,
46    pub mean_hurst: f64,
47    pub std_hurst: f64,
48    pub min_hurst: f64,
49    pub max_hurst: f64,
50    pub window_size: usize,
51    pub step: usize,
52    pub is_valid: bool,
53}
54
55#[derive(Debug, Clone, Serialize)]
56pub struct DfaResult {
57    pub alpha: f64,
58    pub r_squared: f64,
59    pub fluctuations: Vec<(usize, f64)>,
60    pub order: usize,
61    pub is_valid: bool,
62}
63
64#[derive(Debug, Clone, Serialize)]
65pub struct BiEntropyResult {
66    pub bien: f64,
67    pub tbien: f64,
68    pub derivative_entropies: Vec<f64>,
69    pub num_derivatives: usize,
70    pub is_valid: bool,
71}
72
73#[derive(Debug, Clone, Serialize)]
74pub struct EpiplexityResult {
75    pub compression_ratio: f64,       // compressed_size / raw_size
76    pub structural_info: f64,         // 1.0 - compression_ratio (epiplexity)
77    pub remaining_entropy: f64,       // compression_ratio (unpredictability)
78    pub delta_compression_ratio: f64, // compress(diff(data)) / diff_size
79    pub is_valid: bool,
80}
81
82#[derive(Debug, Clone, Serialize)]
83pub struct SampleEntropyResult {
84    pub sample_entropy: f64,
85    pub m: usize,
86    pub r: f64,
87    pub count_a: u64,
88    pub count_b: u64,
89    pub actual_samples: usize,
90    pub is_valid: bool,
91}
92
93fn sample_std_dev(data: &[f64]) -> f64 {
94    let n = data.len();
95    if n < 2 {
96        return 0.0;
97    }
98
99    let mean = data.iter().sum::<f64>() / n as f64;
100    let variance = data
101        .iter()
102        .map(|x| {
103            let d = x - mean;
104            d * d
105        })
106        .sum::<f64>()
107        / (n as f64 - 1.0);
108
109    variance.sqrt()
110}
111
112fn percentile(sorted: &[f64], p: f64) -> f64 {
113    if sorted.is_empty() {
114        return f64::NAN;
115    }
116
117    let idx = (p / 100.0 * (sorted.len() - 1) as f64).round() as usize;
118    sorted[idx.min(sorted.len() - 1)]
119}
120
121fn log_spaced_windows(min: usize, max: usize, count: usize) -> Vec<usize> {
122    if max <= min || count < 2 {
123        return vec![min, max];
124    }
125
126    let log_min = (min as f64).ln();
127    let log_max = (max as f64).ln();
128    (0..count)
129        .map(|i| {
130            let t = i as f64 / (count - 1) as f64;
131            (log_min + t * (log_max - log_min)).exp().round() as usize
132        })
133        .collect::<std::collections::BTreeSet<_>>()
134        .into_iter()
135        .collect()
136}
137
138fn subsample_to_limit(data: &[u8], max_samples: usize) -> Vec<f64> {
139    let data_f64: Vec<f64> = data.iter().map(|&x| x as f64).collect();
140    if data_f64.len() > max_samples {
141        let step = (data_f64.len() / max_samples).max(1);
142        data_f64
143            .iter()
144            .step_by(step)
145            .take(max_samples)
146            .copied()
147            .collect()
148    } else {
149        data_f64
150    }
151}
152
153pub fn sample_entropy(data: &[u8], m: usize, r: f64) -> SampleEntropyResult {
154    const MAX_SAMPLES: usize = 5000;
155
156    if m == 0 || data.len() <= m + 1 {
157        return SampleEntropyResult {
158            sample_entropy: f64::NAN,
159            m,
160            r,
161            count_a: 0,
162            count_b: 0,
163            actual_samples: data.len().min(MAX_SAMPLES),
164            is_valid: false,
165        };
166    }
167
168    let sampled = subsample_to_limit(data, MAX_SAMPLES);
169    if sampled.len() <= m + 1 {
170        return SampleEntropyResult {
171            sample_entropy: f64::NAN,
172            m,
173            r,
174            count_a: 0,
175            count_b: 0,
176            actual_samples: sampled.len(),
177            is_valid: false,
178        };
179    }
180
181    let std_dev = sample_std_dev(&sampled);
182    if std_dev == 0.0 || !r.is_finite() || r <= 0.0 {
183        return SampleEntropyResult {
184            sample_entropy: f64::NAN,
185            m,
186            r,
187            count_a: 0,
188            count_b: 0,
189            actual_samples: sampled.len(),
190            is_valid: false,
191        };
192    }
193
194    let n = sampled.len();
195    let n_m = n - m;
196    let n_m1 = n - m - 1;
197    let mut count_b: u64 = 0;
198    let mut count_a: u64 = 0;
199
200    for i in 0..n_m {
201        for j in 0..n_m {
202            if i == j {
203                continue;
204            }
205
206            let mut matches_m = true;
207            for k in 0..m {
208                if (sampled[i + k] - sampled[j + k]).abs() >= r {
209                    matches_m = false;
210                    break;
211                }
212            }
213
214            if !matches_m {
215                continue;
216            }
217
218            count_b += 1;
219
220            if i < n_m1 && j < n_m1 && (sampled[i + m] - sampled[j + m]).abs() < r {
221                count_a += 1;
222            }
223        }
224    }
225
226    if count_b == 0 || count_a == 0 {
227        return SampleEntropyResult {
228            sample_entropy: f64::NAN,
229            m,
230            r,
231            count_a,
232            count_b,
233            actual_samples: sampled.len(),
234            is_valid: false,
235        };
236    }
237
238    let sampen = -((count_a as f64) / (count_b as f64)).ln();
239    SampleEntropyResult {
240        sample_entropy: sampen,
241        m,
242        r,
243        count_a,
244        count_b,
245        actual_samples: sampled.len(),
246        is_valid: sampen.is_finite(),
247    }
248}
249
250pub fn sample_entropy_default(data: &[u8]) -> SampleEntropyResult {
251    const DEFAULT_M: usize = 2;
252    const MAX_SAMPLES: usize = 5000;
253
254    if data.len() <= DEFAULT_M + 1 {
255        return SampleEntropyResult {
256            sample_entropy: f64::NAN,
257            m: DEFAULT_M,
258            r: f64::NAN,
259            count_a: 0,
260            count_b: 0,
261            actual_samples: data.len().min(MAX_SAMPLES),
262            is_valid: false,
263        };
264    }
265
266    let sampled = subsample_to_limit(data, MAX_SAMPLES);
267    let std_dev = sample_std_dev(&sampled);
268    if std_dev == 0.0 {
269        return SampleEntropyResult {
270            sample_entropy: f64::NAN,
271            m: DEFAULT_M,
272            r: 0.0,
273            count_a: 0,
274            count_b: 0,
275            actual_samples: sampled.len(),
276            is_valid: false,
277        };
278    }
279
280    sample_entropy(data, DEFAULT_M, 0.2 * std_dev)
281}
282
283#[allow(dead_code)]
284fn shannon_entropy_binary(data: &[u8]) -> f64 {
285    if data.is_empty() {
286        return 0.0;
287    }
288
289    let mut ones = 0usize;
290    let total_bits = data.len() * 8;
291    for &byte in data {
292        ones += byte.count_ones() as usize;
293    }
294
295    let p1 = ones as f64 / total_bits as f64;
296    let p0 = 1.0 - p1;
297
298    let h0 = if p0 <= 0.0 {
299        0.0
300    } else {
301        -p0 * (p0.ln() / LN_2)
302    };
303    let h1 = if p1 <= 0.0 {
304        0.0
305    } else {
306        -p1 * (p1.ln() / LN_2)
307    };
308    h0 + h1
309}
310
311pub fn hurst_exponent(data: &[u8]) -> HurstResult {
312    if data.len() < 100 {
313        return HurstResult {
314            hurst_exponent: f64::NAN,
315            is_valid: false,
316            r_squared: 0.0,
317            rs_values: vec![],
318        };
319    }
320
321    let n = data.len();
322    let min_window = 10usize;
323    let max_window = n / 4;
324    if max_window <= min_window {
325        return HurstResult {
326            hurst_exponent: f64::NAN,
327            is_valid: false,
328            r_squared: 0.0,
329            rs_values: vec![],
330        };
331    }
332
333    let step = ((max_window - min_window) / 20).max(1);
334    let mut window_sizes = Vec::new();
335    let mut rs_means = Vec::new();
336    let mut rs_values = Vec::new();
337
338    for window_size in (min_window..max_window).step_by(step) {
339        let mut rs_for_size = Vec::new();
340
341        for start in (0..=(n - window_size)).step_by(window_size) {
342            let segment = &data[start..start + window_size];
343            let segment_f64: Vec<f64> = segment.iter().map(|&x| x as f64).collect();
344
345            let mean = segment_f64.iter().sum::<f64>() / window_size as f64;
346
347            let mut cumulative = 0.0;
348            let mut min_cum = 0.0;
349            let mut max_cum = 0.0;
350            for &value in &segment_f64 {
351                cumulative += value - mean;
352                if cumulative < min_cum {
353                    min_cum = cumulative;
354                }
355                if cumulative > max_cum {
356                    max_cum = cumulative;
357                }
358            }
359
360            let range = max_cum - min_cum;
361            let std_dev = sample_std_dev(&segment_f64);
362
363            if std_dev > 1e-10 {
364                rs_for_size.push(range / std_dev);
365            }
366        }
367
368        if !rs_for_size.is_empty() {
369            let mean_rs = rs_for_size.iter().sum::<f64>() / rs_for_size.len() as f64;
370            window_sizes.push(window_size as f64);
371            rs_means.push(mean_rs);
372            rs_values.push((window_size, mean_rs));
373        }
374    }
375
376    if rs_values.len() < 5 {
377        return HurstResult {
378            hurst_exponent: f64::NAN,
379            is_valid: false,
380            r_squared: 0.0,
381            rs_values,
382        };
383    }
384
385    let log_n: Vec<f64> = window_sizes.iter().map(|&v| v.ln()).collect();
386    let log_rs: Vec<f64> = rs_means.iter().map(|&v| v.ln()).collect();
387    let (slope, _intercept, r_squared) = math::linear_regression(&log_n, &log_rs);
388
389    HurstResult {
390        hurst_exponent: slope,
391        is_valid: slope.is_finite(),
392        r_squared,
393        rs_values,
394    }
395}
396
397pub fn rolling_hurst(data: &[u8], window_size: usize, step: usize) -> RollingHurstResult {
398    let invalid = || RollingHurstResult {
399        windows: vec![],
400        mean_hurst: f64::NAN,
401        std_hurst: f64::NAN,
402        min_hurst: f64::NAN,
403        max_hurst: f64::NAN,
404        window_size,
405        step,
406        is_valid: false,
407    };
408
409    if window_size == 0 || step == 0 || data.len() < window_size {
410        return invalid();
411    }
412
413    let mut windows = Vec::new();
414    let mut offset = 0usize;
415    while offset + window_size <= data.len() {
416        let result = hurst_exponent(&data[offset..offset + window_size]);
417        windows.push(RollingHurstWindow {
418            offset,
419            hurst: result.hurst_exponent,
420            r_squared: result.r_squared,
421            is_valid: result.is_valid,
422        });
423        offset += step;
424    }
425
426    let valid_hursts: Vec<f64> = windows
427        .iter()
428        .filter_map(|w| {
429            if w.is_valid && w.hurst.is_finite() {
430                Some(w.hurst)
431            } else {
432                None
433            }
434        })
435        .collect();
436
437    if valid_hursts.is_empty() {
438        return RollingHurstResult {
439            windows,
440            mean_hurst: f64::NAN,
441            std_hurst: f64::NAN,
442            min_hurst: f64::NAN,
443            max_hurst: f64::NAN,
444            window_size,
445            step,
446            is_valid: false,
447        };
448    }
449
450    let mean_hurst = valid_hursts.iter().sum::<f64>() / valid_hursts.len() as f64;
451    let std_hurst = sample_std_dev(&valid_hursts);
452    let min_hurst = valid_hursts.iter().copied().fold(f64::INFINITY, f64::min);
453    let max_hurst = valid_hursts
454        .iter()
455        .copied()
456        .fold(f64::NEG_INFINITY, f64::max);
457
458    RollingHurstResult {
459        windows,
460        mean_hurst,
461        std_hurst,
462        min_hurst,
463        max_hurst,
464        window_size,
465        step,
466        is_valid: true,
467    }
468}
469
470pub fn rolling_hurst_default(data: &[u8]) -> RollingHurstResult {
471    rolling_hurst(data, 512, 64)
472}
473
474pub fn bootstrap_hurst(data: &[u8], n_bootstrap: usize) -> BootstrapHurstResult {
475    let invalid = || BootstrapHurstResult {
476        observed_hurst: f64::NAN,
477        mean_surrogate_hurst: f64::NAN,
478        std_surrogate_hurst: f64::NAN,
479        p_value: f64::NAN,
480        ci_lower: f64::NAN,
481        ci_upper: f64::NAN,
482        n_surrogates: 0,
483        is_significant: false,
484        is_valid: false,
485    };
486
487    let observed = hurst_exponent(data);
488    if !observed.is_valid || n_bootstrap == 0 {
489        return invalid();
490    }
491
492    let mut state = data
493        .iter()
494        .fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64));
495
496    let mut surrogate_hursts = Vec::with_capacity(n_bootstrap);
497    for _ in 0..n_bootstrap {
498        let mut surrogate = data.to_vec();
499        for i in (1..surrogate.len()).rev() {
500            state = state
501                .wrapping_mul(6364136223846793005)
502                .wrapping_add(1442695040888963407);
503            let j = (state as usize) % (i + 1);
504            surrogate.swap(i, j);
505        }
506
507        let result = hurst_exponent(&surrogate);
508        if result.is_valid {
509            surrogate_hursts.push(result.hurst_exponent);
510        }
511    }
512
513    if surrogate_hursts.is_empty() {
514        return invalid();
515    }
516
517    let mean_surrogate_hurst = surrogate_hursts.iter().sum::<f64>() / surrogate_hursts.len() as f64;
518    let std_surrogate_hurst = sample_std_dev(&surrogate_hursts);
519
520    let mut sorted = surrogate_hursts.clone();
521    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
522    let ci_lower = percentile(&sorted, 2.5);
523    let ci_upper = percentile(&sorted, 97.5);
524
525    let ge_count = surrogate_hursts
526        .iter()
527        .filter(|&&h| h >= observed.hurst_exponent)
528        .count();
529    let p_value = ge_count as f64 / surrogate_hursts.len() as f64;
530
531    BootstrapHurstResult {
532        observed_hurst: observed.hurst_exponent,
533        mean_surrogate_hurst,
534        std_surrogate_hurst,
535        p_value,
536        ci_lower,
537        ci_upper,
538        n_surrogates: surrogate_hursts.len(),
539        is_significant: observed.hurst_exponent < ci_lower || observed.hurst_exponent > ci_upper,
540        is_valid: true,
541    }
542}
543
544pub fn bootstrap_hurst_default(data: &[u8]) -> BootstrapHurstResult {
545    bootstrap_hurst(data, 200)
546}
547
548pub fn dfa(data: &[u8], order: usize) -> DfaResult {
549    if data.len() < 40 || order != 1 {
550        return DfaResult {
551            alpha: f64::NAN,
552            r_squared: 0.0,
553            fluctuations: vec![],
554            order,
555            is_valid: false,
556        };
557    }
558
559    let data_f64: Vec<f64> = data.iter().map(|&x| x as f64).collect();
560    let mean = data_f64.iter().sum::<f64>() / data_f64.len() as f64;
561
562    let mut integrated = Vec::with_capacity(data_f64.len());
563    let mut cumulative = 0.0;
564    for &v in &data_f64 {
565        cumulative += v - mean;
566        integrated.push(cumulative);
567    }
568
569    let min_window = 10usize;
570    let max_window = integrated.len() / 4;
571    if max_window <= min_window {
572        return DfaResult {
573            alpha: f64::NAN,
574            r_squared: 0.0,
575            fluctuations: vec![],
576            order,
577            is_valid: false,
578        };
579    }
580
581    let window_sizes = log_spaced_windows(min_window, max_window, 20);
582    let mut fluctuations = Vec::new();
583    let mut log_n = Vec::new();
584    let mut log_f = Vec::new();
585
586    for &window_size in &window_sizes {
587        let n_windows = integrated.len() / window_size;
588        if n_windows < 2 {
589            continue;
590        }
591
592        let t: Vec<f64> = (0..window_size).map(|i| i as f64).collect();
593        let mut rms_values = Vec::with_capacity(n_windows);
594
595        for w in 0..n_windows {
596            let start = w * window_size;
597            let end = start + window_size;
598            let segment = &integrated[start..end];
599
600            let (slope, intercept, _r2) = math::linear_regression(&t, segment);
601            if !slope.is_finite() || !intercept.is_finite() {
602                continue;
603            }
604
605            let mse = segment
606                .iter()
607                .enumerate()
608                .map(|(i, &y)| {
609                    let trend = slope * i as f64 + intercept;
610                    let d = y - trend;
611                    d * d
612                })
613                .sum::<f64>()
614                / window_size as f64;
615
616            let rms = mse.sqrt();
617            if rms.is_finite() {
618                rms_values.push(rms);
619            }
620        }
621
622        if !rms_values.is_empty() {
623            let f_n = rms_values.iter().sum::<f64>() / rms_values.len() as f64;
624            if f_n > 1e-12 {
625                fluctuations.push((window_size, f_n));
626                log_n.push((window_size as f64).ln());
627                log_f.push(f_n.ln());
628            }
629        }
630    }
631
632    if fluctuations.len() < 4 {
633        return DfaResult {
634            alpha: f64::NAN,
635            r_squared: 0.0,
636            fluctuations,
637            order,
638            is_valid: false,
639        };
640    }
641
642    let (alpha, _intercept, r_squared) = math::linear_regression(&log_n, &log_f);
643    let is_valid = alpha.is_finite();
644
645    DfaResult {
646        alpha,
647        r_squared,
648        fluctuations,
649        order,
650        is_valid,
651    }
652}
653
654pub fn dfa_default(data: &[u8]) -> DfaResult {
655    dfa(data, 1)
656}
657
658fn bytes_to_bits(data: &[u8]) -> Vec<bool> {
659    let mut bits = Vec::with_capacity(data.len() * 8);
660    for &byte in data {
661        for bit_index in 0..8 {
662            bits.push(((byte >> bit_index) & 1) == 1);
663        }
664    }
665    bits
666}
667
668fn binary_derivative(bits: &[bool]) -> Vec<bool> {
669    if bits.len() < 2 {
670        return vec![];
671    }
672
673    let mut derivative = Vec::with_capacity(bits.len() - 1);
674    for i in 0..(bits.len() - 1) {
675        derivative.push(bits[i] ^ bits[i + 1]);
676    }
677    derivative
678}
679
680fn shannon_entropy_bits(bits: &[bool]) -> f64 {
681    if bits.is_empty() {
682        return 0.0;
683    }
684
685    let ones = bits.iter().filter(|&&bit| bit).count();
686    let p = ones as f64 / bits.len() as f64;
687    if p <= 0.0 || p >= 1.0 {
688        return 0.0;
689    }
690
691    let q = 1.0 - p;
692    -p * (p.ln() / LN_2) - q * (q.ln() / LN_2)
693}
694
695pub fn bientropy(data: &[u8]) -> BiEntropyResult {
696    if data.len() < 2 {
697        return BiEntropyResult {
698            bien: 0.0,
699            tbien: 0.0,
700            derivative_entropies: vec![],
701            num_derivatives: 0,
702            is_valid: false,
703        };
704    }
705
706    let mut bien_sum = 0.0;
707    let mut tbien_sum = 0.0;
708    let mut chunk_count = 0usize;
709    let mut derivative_sums: Vec<f64> = vec![];
710    let mut derivative_counts: Vec<usize> = vec![];
711
712    for chunk in data.chunks(32) {
713        let bits = bytes_to_bits(chunk);
714        if bits.len() < 2 {
715            continue;
716        }
717
718        let max_k = 20usize.min(bits.len() - 1);
719        let mut current = bits;
720        let mut entropies = Vec::with_capacity(max_k + 1);
721
722        for k in 0..=max_k {
723            let h = shannon_entropy_bits(&current);
724            entropies.push(h);
725
726            if derivative_sums.len() <= k {
727                derivative_sums.push(0.0);
728                derivative_counts.push(0);
729            }
730            derivative_sums[k] += h;
731            derivative_counts[k] += 1;
732
733            if k < max_k {
734                current = binary_derivative(&current);
735            }
736        }
737
738        let mut bien_weighted = 0.0;
739        let mut tbien_weighted = 0.0;
740        let mut tbien_weight_total = 0.0;
741        for (k, &h) in entropies.iter().enumerate() {
742            let bien_weight = 2f64.powi(k as i32);
743            let tbien_weight = (k as f64 + 2.0).log2();
744            bien_weighted += bien_weight * h;
745            tbien_weighted += tbien_weight * h;
746            tbien_weight_total += tbien_weight;
747        }
748
749        let bien_normalizer = 2f64.powi((max_k + 1) as i32) - 1.0;
750        let chunk_bien = if bien_normalizer > 0.0 {
751            bien_weighted / bien_normalizer
752        } else {
753            0.0
754        };
755        let chunk_tbien = if tbien_weight_total > 0.0 {
756            tbien_weighted / tbien_weight_total
757        } else {
758            0.0
759        };
760
761        bien_sum += chunk_bien;
762        tbien_sum += chunk_tbien;
763        chunk_count += 1;
764    }
765
766    if chunk_count == 0 {
767        return BiEntropyResult {
768            bien: 0.0,
769            tbien: 0.0,
770            derivative_entropies: vec![],
771            num_derivatives: 0,
772            is_valid: false,
773        };
774    }
775
776    let derivative_entropies: Vec<f64> = derivative_sums
777        .iter()
778        .zip(derivative_counts.iter())
779        .map(|(&sum, &count)| if count > 0 { sum / count as f64 } else { 0.0 })
780        .collect();
781
782    BiEntropyResult {
783        bien: bien_sum / chunk_count as f64,
784        tbien: tbien_sum / chunk_count as f64,
785        num_derivatives: derivative_entropies.len(),
786        derivative_entropies,
787        is_valid: true,
788    }
789}
790
791fn compress_bytes(data: &[u8]) -> usize {
792    use std::io::Write;
793    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
794    let _ = encoder.write_all(data);
795    encoder.finish().map(|v| v.len()).unwrap_or(data.len())
796}
797
798pub fn epiplexity(data: &[u8]) -> EpiplexityResult {
799    // Guard: insufficient data
800    if data.len() < 20 {
801        return EpiplexityResult {
802            compression_ratio: 0.0,
803            structural_info: 0.0,
804            remaining_entropy: 0.0,
805            delta_compression_ratio: 0.0,
806            is_valid: false,
807        };
808    }
809
810    // Compression ratio of raw data
811    let compressed_size = compress_bytes(data) as f64;
812    let raw_size = data.len() as f64;
813    let compression_ratio = compressed_size / raw_size;
814
815    // Structural information (epiplexity) = 1 - compression_ratio
816    let structural_info = 1.0 - compression_ratio;
817
818    // Remaining entropy = compression_ratio (unpredictability)
819    let remaining_entropy = compression_ratio;
820
821    // Delta compression: differences between adjacent bytes
822    let delta: Vec<u8> = data.windows(2).map(|w| w[1].wrapping_sub(w[0])).collect();
823
824    let delta_compressed_size = compress_bytes(&delta) as f64;
825    let delta_size = delta.len() as f64;
826    let delta_compression_ratio = if delta_size > 0.0 {
827        delta_compressed_size / delta_size
828    } else {
829        0.0
830    };
831
832    EpiplexityResult {
833        compression_ratio,
834        structural_info,
835        remaining_entropy,
836        delta_compression_ratio,
837        is_valid: true,
838    }
839}
840
841fn time_delay_embedding(data: &[f64], m: usize, tau: usize) -> Vec<Vec<f64>> {
842    let n = data.len();
843    if n < m * tau {
844        return vec![];
845    }
846    let num_vectors = n - (m - 1) * tau;
847    (0..num_vectors)
848        .map(|i| (0..m).map(|j| data[i + j * tau]).collect())
849        .collect()
850}
851
852#[derive(Debug, Clone, Serialize)]
853pub struct LyapunovResult {
854    pub lyapunov_exponent: f64,
855    pub divergence_curve: Vec<f64>,
856    pub embedding_dim: usize,
857    pub delay: usize,
858    pub r_squared: f64,
859    pub is_valid: bool,
860}
861
862#[derive(Debug, Clone, Serialize)]
863pub struct CorrelationDimResult {
864    pub dimension: f64,
865    pub r_squared: f64,
866    pub embedding_dim_used: usize,
867    pub num_points_used: usize,
868    pub is_valid: bool,
869}
870
871pub fn lyapunov_exponent(data: &[u8]) -> LyapunovResult {
872    const EMBEDDING_DIM: usize = 3;
873    const TAU: usize = 1;
874    const MAX_ITER: usize = 50;
875    const THEILER_WINDOW: usize = 3;
876
877    let invalid = || LyapunovResult {
878        lyapunov_exponent: f64::NAN,
879        divergence_curve: vec![],
880        embedding_dim: EMBEDDING_DIM,
881        delay: TAU,
882        r_squared: 0.0,
883        is_valid: false,
884    };
885
886    if data.len() < 500 {
887        return invalid();
888    }
889
890    let data_f64: Vec<f64> = data.iter().map(|&b| b as f64).collect();
891    let sampled: Vec<f64> = if data_f64.len() > 5000 {
892        let step = (data_f64.len() / 5000).max(1);
893        data_f64.iter().step_by(step).take(5000).copied().collect()
894    } else {
895        data_f64
896    };
897
898    let embedded = time_delay_embedding(&sampled, EMBEDDING_DIM, TAU);
899    if embedded.len() < 100 {
900        return invalid();
901    }
902
903    let reference_indices: Vec<usize> = if embedded.len() > 200 {
904        let step = (embedded.len() / 200).max(1);
905        (0..embedded.len()).step_by(step).take(200).collect()
906    } else {
907        (0..embedded.len()).collect()
908    };
909
910    let mut divergence_sums = vec![0.0; MAX_ITER];
911    let mut divergence_counts = vec![0usize; MAX_ITER];
912
913    for &i in &reference_indices {
914        let mut nearest_neighbor = None;
915        let mut min_distance = f64::INFINITY;
916
917        for j in 0..embedded.len() {
918            if i.abs_diff(j) <= THEILER_WINDOW {
919                continue;
920            }
921            let d = math::euclidean_distance(&embedded[i], &embedded[j]);
922            if d <= 1e-10 {
923                continue;
924            }
925            if d < min_distance {
926                min_distance = d;
927                nearest_neighbor = Some(j);
928            }
929        }
930
931        let Some(j) = nearest_neighbor else {
932            continue;
933        };
934
935        for step in 0..MAX_ITER {
936            if i + step >= embedded.len() || j + step >= embedded.len() {
937                break;
938            }
939            let d = math::euclidean_distance(&embedded[i + step], &embedded[j + step]);
940            if d > 1e-10 {
941                divergence_sums[step] += d.ln();
942                divergence_counts[step] += 1;
943            }
944        }
945    }
946
947    let mut steps = Vec::new();
948    let mut mean_log_divergence = Vec::new();
949    for step in 0..MAX_ITER {
950        if divergence_counts[step] >= 5 {
951            steps.push(step as f64);
952            mean_log_divergence.push(divergence_sums[step] / divergence_counts[step] as f64);
953        }
954    }
955
956    if steps.len() < 5 {
957        return invalid();
958    }
959
960    let fit_len = steps.len().min(15);
961    let (slope, _intercept, r_squared) =
962        math::linear_regression(&steps[..fit_len], &mean_log_divergence[..fit_len]);
963    if !slope.is_finite() {
964        return invalid();
965    }
966
967    LyapunovResult {
968        lyapunov_exponent: slope,
969        divergence_curve: mean_log_divergence,
970        embedding_dim: EMBEDDING_DIM,
971        delay: TAU,
972        r_squared,
973        is_valid: true,
974    }
975}
976
977pub fn correlation_dimension(data: &[u8]) -> CorrelationDimResult {
978    const MAX_EMBEDDING: usize = 8;
979    const SUBSAMPLE_MAX: usize = 500;
980    const NUM_RADII: usize = 15;
981
982    if data.len() < 1000 {
983        return CorrelationDimResult {
984            dimension: f64::NAN,
985            r_squared: 0.0,
986            embedding_dim_used: 0,
987            num_points_used: 0,
988            is_valid: false,
989        };
990    }
991
992    let data_f64: Vec<f64> = data.iter().map(|&b| b as f64).collect();
993    let subsampled: Vec<f64> = if data_f64.len() > SUBSAMPLE_MAX {
994        let step = (data_f64.len() / SUBSAMPLE_MAX).max(1);
995        data_f64
996            .iter()
997            .step_by(step)
998            .take(SUBSAMPLE_MAX)
999            .copied()
1000            .collect()
1001    } else {
1002        data_f64
1003    };
1004
1005    let mut accepted = Vec::new();
1006
1007    for m in 2..=MAX_EMBEDDING {
1008        let embedded = time_delay_embedding(&subsampled, m, 1);
1009        if embedded.len() < 10 {
1010            continue;
1011        }
1012
1013        let n = embedded.len();
1014        let total_pairs = n * (n - 1) / 2;
1015        if total_pairs == 0 {
1016            continue;
1017        }
1018
1019        let mut distances = Vec::with_capacity(total_pairs);
1020        for i in 0..n {
1021            for j in (i + 1)..n {
1022                distances.push(math::euclidean_distance(&embedded[i], &embedded[j]));
1023            }
1024        }
1025
1026        distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1027
1028        if distances.len() < 100 {
1029            continue;
1030        }
1031
1032        let r_min = distances[distances.len() / 100];
1033        let r_max = distances[distances.len() / 2];
1034        if r_min <= 0.0 || r_max <= r_min {
1035            continue;
1036        }
1037
1038        let mut log_r = Vec::with_capacity(NUM_RADII);
1039        let mut log_c = Vec::with_capacity(NUM_RADII);
1040        let ratio = r_max / r_min;
1041
1042        for k in 0..NUM_RADII {
1043            let r = r_min * ratio.powf(k as f64 / (NUM_RADII - 1) as f64);
1044            let count = distances.partition_point(|&d| d < r);
1045            let c = count as f64 / total_pairs as f64;
1046            if c > 0.0 {
1047                log_r.push(r.ln());
1048                log_c.push(c.ln());
1049            }
1050        }
1051
1052        if log_r.len() < 2 {
1053            continue;
1054        }
1055
1056        let (slope, _intercept, r_squared) = math::linear_regression(&log_r, &log_c);
1057        if r_squared > 0.9 && slope.is_finite() && slope > 0.0 {
1058            accepted.push((m, slope, r_squared));
1059        }
1060    }
1061
1062    if accepted.is_empty() {
1063        return CorrelationDimResult {
1064            dimension: f64::NAN,
1065            r_squared: 0.0,
1066            embedding_dim_used: 0,
1067            num_points_used: subsampled.len(),
1068            is_valid: false,
1069        };
1070    }
1071
1072    let take_n = accepted.len().min(3);
1073    let avg_d2 = accepted[accepted.len() - take_n..]
1074        .iter()
1075        .map(|(_, slope, _)| *slope)
1076        .sum::<f64>()
1077        / take_n as f64;
1078
1079    let (last_m, _last_slope, last_r2) = accepted[accepted.len() - 1];
1080
1081    CorrelationDimResult {
1082        dimension: avg_d2,
1083        r_squared: last_r2,
1084        embedding_dim_used: last_m,
1085        num_points_used: subsampled.len(),
1086        is_valid: true,
1087    }
1088}
1089
1090#[derive(Debug, Clone, Serialize)]
1091pub struct RqaResult {
1092    pub recurrence_rate: f64,
1093    pub determinism: f64,
1094    pub laminarity: f64,
1095    pub avg_diagonal_length: f64,
1096    pub max_diagonal_length: usize,
1097    pub embedding_dim: usize,
1098    pub delay: usize,
1099    pub threshold: f64,
1100    pub num_points_used: usize,
1101    pub is_valid: bool,
1102}
1103
1104pub fn rqa(data: &[u8], embedding_dim: usize, delay: usize, threshold: f64) -> RqaResult {
1105    const SUBSAMPLE_MAX: usize = 1000;
1106
1107    let invalid = || RqaResult {
1108        recurrence_rate: 0.0,
1109        determinism: 0.0,
1110        laminarity: 0.0,
1111        avg_diagonal_length: 0.0,
1112        max_diagonal_length: 0,
1113        embedding_dim,
1114        delay,
1115        threshold,
1116        num_points_used: 0,
1117        is_valid: false,
1118    };
1119
1120    if data.is_empty()
1121        || embedding_dim == 0
1122        || delay == 0
1123        || !threshold.is_finite()
1124        || threshold <= 0.0
1125    {
1126        return invalid();
1127    }
1128
1129    let data_f64: Vec<f64> = data.iter().map(|&b| b as f64).collect();
1130    let embedded = time_delay_embedding(&data_f64, embedding_dim, delay);
1131    if embedded.len() < 2 {
1132        return invalid();
1133    }
1134
1135    let vectors: Vec<Vec<f64>> = if embedded.len() > SUBSAMPLE_MAX {
1136        let step = (embedded.len() / SUBSAMPLE_MAX).max(1);
1137        embedded
1138            .iter()
1139            .step_by(step)
1140            .take(SUBSAMPLE_MAX)
1141            .cloned()
1142            .collect()
1143    } else {
1144        embedded
1145    };
1146
1147    let n = vectors.len();
1148    if n < 2 {
1149        return invalid();
1150    }
1151
1152    let mut total_recurrence_points = 0usize;
1153    let mut diag_points_in_lines = 0usize;
1154    let mut diag_line_count = 0usize;
1155    let mut max_diagonal_length = 0usize;
1156    let mut vertical_points_in_lines = 0usize;
1157
1158    let mut prev_diag_lengths = vec![0usize; n + 1];
1159    let mut curr_diag_lengths = vec![0usize; n + 1];
1160    let mut vertical_lengths = vec![0usize; n];
1161
1162    for i in 0..n {
1163        curr_diag_lengths[0] = 0;
1164
1165        for j in 0..n {
1166            let recurrence =
1167                i != j && math::euclidean_distance(&vectors[i], &vectors[j]) < threshold;
1168
1169            if recurrence {
1170                total_recurrence_points += 1;
1171
1172                let diag_len = prev_diag_lengths[j] + 1;
1173                curr_diag_lengths[j + 1] = diag_len;
1174                if diag_len == 2 {
1175                    diag_points_in_lines += 2;
1176                    diag_line_count += 1;
1177                    max_diagonal_length = max_diagonal_length.max(2);
1178                } else if diag_len > 2 {
1179                    diag_points_in_lines += 1;
1180                    max_diagonal_length = max_diagonal_length.max(diag_len);
1181                }
1182
1183                let vert_len = vertical_lengths[j] + 1;
1184                vertical_lengths[j] = vert_len;
1185                if vert_len == 2 {
1186                    vertical_points_in_lines += 2;
1187                } else if vert_len > 2 {
1188                    vertical_points_in_lines += 1;
1189                }
1190            } else {
1191                curr_diag_lengths[j + 1] = 0;
1192                vertical_lengths[j] = 0;
1193            }
1194        }
1195
1196        std::mem::swap(&mut prev_diag_lengths, &mut curr_diag_lengths);
1197    }
1198
1199    let total_pairs = n * (n - 1);
1200    let recurrence_rate = if total_pairs > 0 {
1201        total_recurrence_points as f64 / total_pairs as f64
1202    } else {
1203        0.0
1204    };
1205
1206    let determinism = if total_recurrence_points > 0 {
1207        diag_points_in_lines as f64 / total_recurrence_points as f64
1208    } else {
1209        0.0
1210    };
1211
1212    let laminarity = if total_recurrence_points > 0 {
1213        vertical_points_in_lines as f64 / total_recurrence_points as f64
1214    } else {
1215        0.0
1216    };
1217
1218    let avg_diagonal_length = if diag_line_count > 0 {
1219        diag_points_in_lines as f64 / diag_line_count as f64
1220    } else {
1221        0.0
1222    };
1223
1224    RqaResult {
1225        recurrence_rate,
1226        determinism,
1227        laminarity,
1228        avg_diagonal_length,
1229        max_diagonal_length,
1230        embedding_dim,
1231        delay,
1232        threshold,
1233        num_points_used: n,
1234        is_valid: true,
1235    }
1236}
1237
1238pub fn rqa_default(data: &[u8]) -> RqaResult {
1239    rqa(data, 3, 1, 10.0)
1240}
1241
1242/// Aggregated chaos theory analysis results for a data stream.
1243#[derive(Debug, Clone, Serialize)]
1244pub struct ChaosAnalysis {
1245    pub hurst: HurstResult,
1246    pub lyapunov: LyapunovResult,
1247    pub correlation_dimension: CorrelationDimResult,
1248    pub bientropy: BiEntropyResult,
1249    pub epiplexity: EpiplexityResult,
1250    pub sample_entropy: SampleEntropyResult,
1251    pub dfa: DfaResult,
1252    pub rqa: RqaResult,
1253    pub bootstrap_hurst: BootstrapHurstResult,
1254    pub rolling_hurst: RollingHurstResult,
1255}
1256
1257/// Run all 5 chaos theory analysis methods on the given data.
1258///
1259/// This is a separate entry point from `full_analysis()` - chaos methods are
1260/// computationally expensive (O(n^2)) and should only be run when explicitly requested.
1261pub fn chaos_analysis(data: &[u8]) -> ChaosAnalysis {
1262    ChaosAnalysis {
1263        hurst: hurst_exponent(data),
1264        lyapunov: lyapunov_exponent(data),
1265        correlation_dimension: correlation_dimension(data),
1266        bientropy: bientropy(data),
1267        epiplexity: epiplexity(data),
1268        sample_entropy: sample_entropy_default(data),
1269        dfa: dfa_default(data),
1270        rqa: rqa_default(data),
1271        bootstrap_hurst: bootstrap_hurst_default(data),
1272        rolling_hurst: rolling_hurst_default(data),
1273    }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279
1280    fn random_data_seeded(n: usize, seed: u64) -> Vec<u8> {
1281        let mut data = Vec::with_capacity(n);
1282        let mut state: u64 = seed;
1283        for _ in 0..n {
1284            state = state
1285                .wrapping_mul(6364136223846793005)
1286                .wrapping_add(1442695040888963407);
1287            data.push((state >> 33) as u8);
1288        }
1289        data
1290    }
1291
1292    #[test]
1293    fn test_hurst_random() {
1294        let data = random_data_seeded(10000, 0xdeadbeef);
1295        let result = hurst_exponent(&data);
1296
1297        assert!(result.is_valid);
1298        assert!(result.hurst_exponent >= 0.35 && result.hurst_exponent <= 0.65);
1299    }
1300
1301    #[test]
1302    fn test_hurst_constant() {
1303        let data = vec![42u8; 1000];
1304        let result = hurst_exponent(&data);
1305
1306        assert!(!result.is_valid);
1307    }
1308
1309    #[test]
1310    fn test_rolling_hurst_random_seeded() {
1311        let data = random_data_seeded(10000, 0xdeadbeef);
1312        let result = rolling_hurst(&data, 1000, 100);
1313
1314        assert!(result.is_valid);
1315        assert_eq!(result.windows.len(), 91);
1316        for window in &result.windows {
1317            assert!(window.is_valid);
1318            assert!(window.hurst >= 0.3 && window.hurst <= 0.7);
1319        }
1320    }
1321
1322    #[test]
1323    fn test_rolling_hurst_data_too_short() {
1324        let data = random_data_seeded(500, 0x12345678);
1325        let result = rolling_hurst(&data, 1000, 100);
1326
1327        assert!(!result.is_valid);
1328        assert!(result.windows.is_empty());
1329    }
1330
1331    #[test]
1332    fn test_rolling_hurst_empty_invalid() {
1333        let data: &[u8] = &[];
1334        let result = rolling_hurst_default(data);
1335
1336        assert!(!result.is_valid);
1337        assert!(result.windows.is_empty());
1338    }
1339
1340    #[test]
1341    fn test_rolling_hurst_mean_consistency() {
1342        let data = random_data_seeded(10000, 0xcafebabe);
1343        let result = rolling_hurst_default(&data);
1344        assert!(result.is_valid);
1345
1346        let valid_hursts: Vec<f64> = result
1347            .windows
1348            .iter()
1349            .filter(|w| w.is_valid)
1350            .map(|w| w.hurst)
1351            .collect();
1352
1353        let expected_mean = valid_hursts.iter().sum::<f64>() / valid_hursts.len() as f64;
1354        assert!((result.mean_hurst - expected_mean).abs() < 1e-12);
1355    }
1356
1357    #[test]
1358    fn test_bootstrap_hurst_random_deterministic() {
1359        let data = random_data_seeded(5000, 0xdeadbeef);
1360        let result_a = bootstrap_hurst(&data, 200);
1361        let result_b = bootstrap_hurst(&data, 200);
1362
1363        assert!(result_a.is_valid);
1364        assert_eq!(result_a.n_surrogates, 200);
1365        assert_eq!(result_a.p_value, result_b.p_value);
1366        assert_eq!(result_a.mean_surrogate_hurst, result_b.mean_surrogate_hurst);
1367        assert_eq!(result_a.ci_lower, result_b.ci_lower);
1368        assert_eq!(result_a.ci_upper, result_b.ci_upper);
1369    }
1370
1371    #[test]
1372    fn test_bootstrap_hurst_correlated_significant() {
1373        let data: Vec<u8> = (0..5000).map(|i| (i / 20) as u8).collect();
1374        let result = bootstrap_hurst(&data, 200);
1375
1376        assert!(result.is_valid);
1377        assert!(result.is_significant);
1378    }
1379
1380    #[test]
1381    fn test_bootstrap_hurst_empty_invalid() {
1382        let result = bootstrap_hurst_default(&[]);
1383
1384        assert!(!result.is_valid);
1385        assert_eq!(result.n_surrogates, 0);
1386    }
1387
1388    #[test]
1389    fn test_bootstrap_hurst_too_short_invalid() {
1390        let data = random_data_seeded(50, 0x12345678);
1391        let result = bootstrap_hurst_default(&data);
1392
1393        assert!(!result.is_valid);
1394        assert_eq!(result.n_surrogates, 0);
1395    }
1396
1397    #[test]
1398    fn test_dfa_random() {
1399        let data = random_data_seeded(10000, 0xdeadbeef);
1400        let result = dfa(&data, 1);
1401
1402        assert!(result.is_valid);
1403        assert!(result.alpha >= 0.3 && result.alpha <= 0.7);
1404    }
1405
1406    #[test]
1407    fn test_dfa_constant() {
1408        let data = vec![42u8; 10000];
1409        let result = dfa_default(&data);
1410
1411        assert!(!result.is_valid);
1412    }
1413
1414    #[test]
1415    fn test_dfa_empty() {
1416        let data: &[u8] = &[];
1417        let result = dfa_default(data);
1418
1419        assert!(!result.is_valid);
1420        assert!(result.fluctuations.is_empty());
1421    }
1422
1423    #[test]
1424    fn test_dfa_too_short() {
1425        let data = random_data_seeded(20, 0x12345678);
1426        let result = dfa_default(&data);
1427
1428        assert!(!result.is_valid);
1429    }
1430
1431    #[test]
1432    fn test_bientropy_random() {
1433        let data = random_data_seeded(10000, 0xdeadbeef);
1434        let result = bientropy(&data);
1435
1436        assert!(result.is_valid);
1437        assert!(result.bien > 0.90);
1438        assert!(result.tbien > 0.90);
1439    }
1440
1441    #[test]
1442    fn test_bientropy_constant() {
1443        let data = vec![0u8; 1000];
1444        let result = bientropy(&data);
1445
1446        assert!(result.is_valid);
1447        assert!(result.bien < 0.05);
1448    }
1449
1450    #[test]
1451    fn test_bientropy_alternating() {
1452        let data = vec![0xAAu8; 1000];
1453        let result = bientropy(&data);
1454
1455        assert!(result.is_valid);
1456        assert!(result.bien < 0.50);
1457    }
1458
1459    #[test]
1460    fn test_epiplexity_random() {
1461        let data = random_data_seeded(10000, 0xdeadbeef);
1462        let result = epiplexity(&data);
1463
1464        assert!(result.is_valid);
1465        assert!(
1466            result.compression_ratio > 0.95,
1467            "Random data should have high compression ratio (low compressibility)"
1468        );
1469    }
1470
1471    #[test]
1472    fn test_epiplexity_patterned() {
1473        // Repeating [0, 1, 2, 3] pattern — highly compressible
1474        let data: Vec<u8> = (0..10000u32).map(|i| (i % 4) as u8).collect();
1475        let result = epiplexity(&data);
1476
1477        assert!(result.is_valid);
1478        assert!(
1479            result.compression_ratio < 0.10,
1480            "Patterned data should have low compression ratio (high compressibility)"
1481        );
1482    }
1483
1484    #[test]
1485    fn test_lyapunov_random() {
1486        let data = random_data_seeded(10000, 0xdeadbeef);
1487        let result = lyapunov_exponent(&data);
1488
1489        assert!(result.is_valid);
1490        assert!(result.lyapunov_exponent.abs() < 0.3);
1491    }
1492
1493    #[test]
1494    fn test_lyapunov_logistic() {
1495        let mut x = 0.1f64;
1496        let mut data = Vec::with_capacity(5000);
1497        for _ in 0..5000 {
1498            x = 4.0 * x * (1.0 - x);
1499            data.push((x * 255.0) as u8);
1500        }
1501
1502        let result = lyapunov_exponent(&data);
1503        assert!(result.is_valid);
1504        assert!(
1505            result.lyapunov_exponent > 0.3,
1506            "expected logistic Lyapunov > 0.3, got {}",
1507            result.lyapunov_exponent
1508        );
1509    }
1510
1511    #[test]
1512    fn test_lyapunov_short() {
1513        let data = random_data_seeded(100, 0x12345678);
1514        let result = lyapunov_exponent(&data);
1515
1516        assert!(!result.is_valid);
1517    }
1518
1519    #[test]
1520    fn test_corrdim_random() {
1521        let data = random_data_seeded(5000, 0xdeadbeef);
1522        let result = correlation_dimension(&data);
1523        assert!(
1524            result.is_valid,
1525            "correlation_dimension should be valid for 5000 random bytes"
1526        );
1527        assert!(
1528            result.dimension > 2.0,
1529            "random data should have D2 > 2.0, got {}",
1530            result.dimension
1531        );
1532    }
1533
1534    #[test]
1535    fn test_corrdim_short() {
1536        let data = random_data_seeded(500, 0x12345678);
1537        let result = correlation_dimension(&data);
1538        assert!(
1539            !result.is_valid,
1540            "correlation_dimension should be invalid for 500 bytes (below 1000 minimum)"
1541        );
1542    }
1543
1544    #[test]
1545    fn test_rqa_random_low_recurrence() {
1546        let data = random_data_seeded(5000, 0xdeadbeef);
1547        let result = rqa_default(&data);
1548
1549        assert!(result.is_valid);
1550        assert!(
1551            result.recurrence_rate < 0.1,
1552            "random data should have low RR, got {}",
1553            result.recurrence_rate
1554        );
1555    }
1556
1557    #[test]
1558    fn test_rqa_periodic_high_recurrence_and_determinism() {
1559        let data: Vec<u8> = (0..5000).map(|i| (i % 10) as u8).collect();
1560        let result = rqa_default(&data);
1561
1562        assert!(result.is_valid);
1563        assert!(
1564            result.recurrence_rate > 0.2,
1565            "periodic data should have high RR, got {}",
1566            result.recurrence_rate
1567        );
1568        assert!(
1569            result.determinism > 0.8,
1570            "periodic data should have high DET, got {}",
1571            result.determinism
1572        );
1573    }
1574
1575    #[test]
1576    fn test_rqa_constant_near_full_recurrence() {
1577        let data = vec![42u8; 5000];
1578        let result = rqa_default(&data);
1579
1580        assert!(result.is_valid);
1581        assert!(
1582            result.recurrence_rate > 0.99,
1583            "constant data should have RR near 1.0, got {}",
1584            result.recurrence_rate
1585        );
1586    }
1587
1588    #[test]
1589    fn test_rqa_empty_invalid() {
1590        let data: &[u8] = &[];
1591        let result = rqa_default(data);
1592
1593        assert!(!result.is_valid);
1594    }
1595
1596    #[test]
1597    fn test_sample_entropy_random() {
1598        let data = random_data_seeded(5000, 0xdeadbeef);
1599        let result = sample_entropy_default(&data);
1600        assert!(result.is_valid);
1601        assert!(result.sample_entropy > 0.0);
1602    }
1603
1604    #[test]
1605    fn test_sample_entropy_periodic_lower_than_random() {
1606        let random = random_data_seeded(5000, 0xdeadbeef);
1607        let periodic: Vec<u8> = (0..5000).map(|i| (i % 4) as u8).collect();
1608
1609        let random_result = sample_entropy_default(&random);
1610        let periodic_result = sample_entropy_default(&periodic);
1611
1612        assert!(random_result.is_valid);
1613        assert!(periodic_result.is_valid);
1614        assert!(periodic_result.sample_entropy < random_result.sample_entropy);
1615    }
1616
1617    #[test]
1618    fn test_sample_entropy_constant_invalid() {
1619        let data = vec![42u8; 5000];
1620        let result = sample_entropy_default(&data);
1621        assert!(!result.is_valid);
1622    }
1623
1624    #[test]
1625    fn test_sample_entropy_empty_invalid() {
1626        let data: &[u8] = &[];
1627        let result = sample_entropy_default(data);
1628        assert!(!result.is_valid);
1629    }
1630
1631    #[test]
1632    fn test_sample_entropy_single_byte_invalid() {
1633        let data: &[u8] = &[42];
1634        let result = sample_entropy_default(data);
1635        assert!(!result.is_valid);
1636    }
1637
1638    #[test]
1639    fn test_empty_input() {
1640        let data: &[u8] = &[];
1641        let h = hurst_exponent(data);
1642        let l = lyapunov_exponent(data);
1643        let c = correlation_dimension(data);
1644        let b = bientropy(data);
1645        let e = epiplexity(data);
1646        assert!(!h.is_valid);
1647        assert!(!l.is_valid);
1648        assert!(!c.is_valid);
1649        assert!(!b.is_valid);
1650        assert!(!e.is_valid);
1651    }
1652
1653    #[test]
1654    fn test_single_byte() {
1655        let data: &[u8] = &[42];
1656        let h = hurst_exponent(data);
1657        let l = lyapunov_exponent(data);
1658        let c = correlation_dimension(data);
1659        let e = epiplexity(data);
1660        assert!(!h.is_valid);
1661        assert!(!l.is_valid);
1662        assert!(!c.is_valid);
1663        assert!(!e.is_valid);
1664    }
1665
1666    #[test]
1667    fn test_chaos_analysis_orchestrator() {
1668        let data = random_data_seeded(5000, 0xdeadbeef);
1669        let result = chaos_analysis(&data);
1670        assert!(result.hurst.is_valid);
1671        assert!(result.lyapunov.is_valid);
1672        assert!(result.correlation_dimension.is_valid);
1673        assert!(result.bientropy.is_valid);
1674        assert!(result.epiplexity.is_valid);
1675        assert!(result.sample_entropy.is_valid);
1676        assert!(result.dfa.is_valid);
1677        assert!(result.rqa.is_valid);
1678        assert!(result.bootstrap_hurst.is_valid);
1679        assert!(result.rolling_hurst.is_valid);
1680    }
1681
1682    #[test]
1683    fn test_chaos_analysis_serializes() {
1684        let data = random_data_seeded(5000, 0xdeadbeef);
1685        let result = chaos_analysis(&data);
1686        let json = serde_json::to_string(&result).expect("serialization failed");
1687        assert!(json.contains("sample_entropy"));
1688        assert!(json.contains("dfa"));
1689    }
1690
1691    #[test]
1692    fn test_performance_100k() {
1693        let data = random_data_seeded(100_000, 0xCAFEBABE);
1694        let _result = chaos_analysis(&data);
1695    }
1696}