1use 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, pub structural_info: f64, pub remaining_entropy: f64, pub delta_compression_ratio: f64, 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(¤t);
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(¤t);
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 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 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 let structural_info = 1.0 - compression_ratio;
817
818 let remaining_entropy = compression_ratio;
820
821 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#[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
1257pub 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 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}