commit 10ff2123ac0659f7c0da0160975ba8f9c7888d57 Author: randogoth Date: Thu Jan 16 23:44:12 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cbdd859 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,436 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "kolmogorov_smirnov" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ceadb630ffd3ceb5c4d739232d8a9d51d23c383b14f3b64ffc67368cbc59b5" +dependencies = [ + "rand 0.3.23", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "nalgebra" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "rand 0.8.5", + "rand_distr", + "simba", + "typenum", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "onod3000" +version = "0.1.0" +dependencies = [ + "flate2", + "kolmogorov_smirnov", + "rand_distr", + "statrs", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "nalgebra", + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wide" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9b8dddf --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "onod3000" +version = "0.1.0" +edition = "2021" + +[dependencies] +flate2 = "1.0.35" +kolmogorov_smirnov = "1.1.0" +rand_distr = "0.4.3" +statrs = "0.18.0" diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..f734d29 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,109 @@ +use std::fs::File; +use std::io::{self, Read}; +use std::path::Path; +use statrs::distribution::{Normal, ContinuousCDF}; + +use onod3000::Onod; + +fn main() -> io::Result<()> { + let mut args = std::env::args(); + args.next(); // Skip program name + + let mut input_data = Vec::new(); + let mut source = String::from("stdin"); + + if let Some(arg) = args.next() { + if arg == "-f" { + if let Some(file_path) = args.next() { + source = file_path.clone(); + let path = Path::new(&file_path); + let mut file = File::open(path)?; + file.read_to_end(&mut input_data)?; + } else { + eprintln!("Error: No file path provided after -f."); + std::process::exit(1); + } + } else { + eprintln!("Usage: {} [-f ]", std::env::args().next().unwrap()); + std::process::exit(1); + } + } else { + io::stdin().read_to_end(&mut input_data)?; + } + + if input_data.is_empty() { + eprintln!("Error: No input data provided."); + std::process::exit(1); + } + + println!("\nTesting {}", source); + println!("Testing {} bytes.", input_data.len()); + println!("--------------------------------------"); + + let alpha = 0.01; + let mut passed_tests = 0; + + // Run each test with the appropriate handling + let tests = [ + ("Shannon", Onod::shannon(&input_data)), + ("Sanity", Onod::sanity(&input_data)), + ("Monobit", Onod::monobit(&input_data)), + ("ChiBit", Onod::chi_bit(&input_data)), + ("ChiByte", Onod::chi_byte(&input_data)), + ("MeanByte", Onod::mean_byte(&input_data)), + ("Compression", Onod::compression(&input_data)), + ("KS", Onod::ks(&input_data)), + ("Pi", Onod::pi(&input_data)), + ("Shells", Onod::shells(&input_data)), + ("Gaps", Onod::gaps(&input_data)), + ("Avalanche", Onod::avalanche(&input_data)), + ("Runs", Onod::runs(&input_data)), + ("RunUps", Onod::run_ups(&input_data)), + ("Prediction", Onod::prediction(&input_data)), + ("UnCorrelation", Onod::un_correlation(&input_data)), + ]; + + let mut p_values = Vec::new(); + + for (test_name, p_value) in tests.iter() { + let result = if *p_value >= alpha { + passed_tests += 1; + "PASS" + } else { + "FAIL" + }; + + println!("{:<20} p = {:.4}, {}", test_name, p_value, result); + + p_values.push(*p_value); // Dereference the value and push it + } + + // Calculate combined p-value using Fisher's method + let (combined_z_score, combined_p_value) = combined_score_stouffer(&p_values); + let overall_result = if combined_p_value >= alpha { "PASS" } else { "FAIL" }; + + println!("--------------------------------------"); + println!("{}/{} tests passed.", passed_tests, tests.len()); + println!("--------------------------------------"); + println!("Combined Z-Score = {:.6}\nCombined P-Value = {:.6}\nOverall Result: {}", combined_z_score.abs(), combined_p_value, overall_result); + + Ok(()) +} + +pub fn combined_score_stouffer(p_values: &[f64]) -> (f64, f64) { + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + + // Calculate Z-scores for each p-value + let z_scores: Vec = p_values + .iter() + .map(|&p| normal_dist.inverse_cdf(1.0 - p)) + .collect(); + + // Combine Z-scores + let combined_z = z_scores.iter().sum::() / (p_values.len() as f64).sqrt(); + + // Convert combined Z-score back to a p-value + let p_value = 2.0 * (1.0 - normal_dist.cdf(combined_z.abs())); // Two-tailed + + (combined_z, p_value) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4d366df --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub struct Onod; + +mod uniformity; \ No newline at end of file diff --git a/src/uniformity/avalanche.rs b/src/uniformity/avalanche.rs new file mode 100644 index 0000000..42a7f52 --- /dev/null +++ b/src/uniformity/avalanche.rs @@ -0,0 +1,52 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Avalanche randomness test + /// Compares the bit-level differences between consecutive chunks of data and returns a p-value. + pub fn avalanche(samples: &[u8]) -> f64 { + + const XOR_WINDOW_SIZE: usize = 20; // Bytes. Equivalent to SHA-1 (160 bits). + + if samples.len() < 2 * XOR_WINDOW_SIZE { + return 0.0; // Not enough data for meaningful calculation + } + + let mut means = Vec::new(); + + for i in (0..samples.len() - (2 * XOR_WINDOW_SIZE)).step_by(2 * XOR_WINDOW_SIZE) { + let a_start = i; + let a_end = i + XOR_WINDOW_SIZE; + let b_start = a_end; + let b_end = b_start + XOR_WINDOW_SIZE; + + let a_bytes = &samples[a_start..a_end]; + let b_bytes = &samples[b_start..b_end]; + + // XOR the two chunks and count differing bits + let mut changed_bits = 0; + for (a, b) in a_bytes.iter().zip(b_bytes.iter()) { + changed_bits += (a ^ b).count_ones(); + } + + means.push(changed_bits as f64); + } + + // Calculate the mean and standard deviation of bit differences + let mean_observed = means.iter().sum::() / means.len() as f64; + let mean_ref = (XOR_WINDOW_SIZE * 8) as f64 / 2.0; // Expected mean bits + let std_dev_ref = 0.5 * ((XOR_WINDOW_SIZE * 8) as f64).sqrt(); // Expected standard deviation + + // Calculate Z score + let z_score = (mean_observed - mean_ref) / std_dev_ref; + + // Convert Z score to p-value + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); + + p_value + } + +} \ No newline at end of file diff --git a/src/uniformity/chi_bit.rs b/src/uniformity/chi_bit.rs new file mode 100644 index 0000000..fa11b63 --- /dev/null +++ b/src/uniformity/chi_bit.rs @@ -0,0 +1,62 @@ +use statrs::distribution::{ChiSquared, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + pub fn chi_bit(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; // empty data + } + + // Lookup table for the number of set bits in each byte (Hamming weight) + const SET_BITS_PER_BYTE: [usize; 256] = [ + 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, + 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, + 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, + 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, + 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, + 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, + 6, 7, 7, 8, + ]; + + // Expected number of occurrences for each Hamming weight + const EXPECTED_NUMBER: [f64; 9] = [1.0, 8.0, 28.0, 56.0, 70.0, 56.0, 28.0, 8.0, 1.0]; + + // Frequency count of Hamming weights + let mut frequency = vec![0; EXPECTED_NUMBER.len()]; + for &byte in samples { + let hamming_weight = SET_BITS_PER_BYTE[byte as usize]; + frequency[hamming_weight] += 1; + } + + // Calculate observed and expected counts + let total_samples = samples.len() as f64; + let expected: Vec = EXPECTED_NUMBER + .iter() + .map(|&e| e / 256.0 * total_samples) + .collect(); + + // Chi-squared statistic calculation + let chi_squared_stat = frequency + .iter() + .zip(expected.iter()) + .map(|(&observed, &expected)| { + if expected > 0.0 { + (observed as f64 - expected).powi(2) / expected + } else { + 0.0 + } + }) + .sum(); + + // Degrees of freedom: 9 categories - 1 + let degrees_of_freedom = (EXPECTED_NUMBER.len() - 1) as f64; + let chi_squared_dist = ChiSquared::new(degrees_of_freedom).expect("Failed to create ChiSquared distribution"); + let p_value = 1.0 - chi_squared_dist.cdf(chi_squared_stat); + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/chi_byte.rs b/src/uniformity/chi_byte.rs new file mode 100644 index 0000000..35e48ba --- /dev/null +++ b/src/uniformity/chi_byte.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; +use statrs::distribution::{ChiSquared, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// ChiByte randomness test + /// Evaluates the uniformity of byte values across the data and returns a p-value. + pub fn chi_byte(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; // Default to perfect randomness for empty data + } + + // Count occurrences of each byte value (0-255) + let mut counts = HashMap::new(); + for &byte in samples { + *counts.entry(byte).or_insert(0) += 1; + } + + // Calculate expected count assuming uniform distribution + let expected_count = samples.len() as f64 / 256.0; + + // Calculate chi-squared statistic + let mut chi_squared_stat = 0.0; + for i in 0..256 { + let observed = *counts.get(&(i as u8)).unwrap_or(&0) as f64; + let diff = observed - expected_count; + chi_squared_stat += (diff * diff) / expected_count; + } + + // Use chi-squared distribution to calculate p-value + let degrees_of_freedom = 256.0 - 1.0; // 256 possible byte values - 1 + let chi_squared_dist = ChiSquared::new(degrees_of_freedom).expect("Failed to create ChiSquared distribution"); + let p_value = 1.0 - chi_squared_dist.cdf(chi_squared_stat); + + p_value + } + +} \ No newline at end of file diff --git a/src/uniformity/compression.rs b/src/uniformity/compression.rs new file mode 100644 index 0000000..b33c459 --- /dev/null +++ b/src/uniformity/compression.rs @@ -0,0 +1,42 @@ +use flate2::{write::DeflateEncoder, Compression}; +use std::io::Write; +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Compression randomness test + /// Estimates randomness by the compressibility of the data and returns a p-value. + pub fn compression(samples: &[u8]) -> f64 { + if samples.is_empty() { + return 0.0; // Perfect randomness for empty data + } + + // Compress the data using deflate + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default()); + if encoder.write_all(samples).is_err() { + return 0.0; // Compression failed + } + let compressed_data = match encoder.finish() { + Ok(data) => data, + Err(_) => return 0.0, // Compression failed + }; + + // Calculate compression ratio + let original_size = samples.len() as f64; + let compressed_size = compressed_data.len() as f64; + let compression_ratio = compressed_size / original_size; + + // Z-score calculation + let expected_mean = 1.0; + let std_dev = 0.002; + let z_score = (compression_ratio - expected_mean) / std_dev; + + // Calculate p-value using the normal distribution + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); // Two-tailed test + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/gaps.rs b/src/uniformity/gaps.rs new file mode 100644 index 0000000..19e7c0b --- /dev/null +++ b/src/uniformity/gaps.rs @@ -0,0 +1,79 @@ +use statrs::distribution::{ChiSquared, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Gaps randomness test + /// Analyzes the gaps between occurrences of a specific byte value and returns a p-value. + pub fn gaps(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; // empty data + } + + // Bin edges and expected frequencies from the Java implementation + let bin_edges = [0, 26, 56, 90, 130, 176, 233, 307, 410, 587, 1_000_000_000]; + let expected_frequencies = [ + 0.10028324483130746, + 0.09967572210885733, + 0.09968386797087492, + 0.10149320063058609, + 0.09867042665174708, + 0.10001818308995936, + 0.10062746966933134, + 0.09938275862858448, + 0.10004382368984566, + 0.1001213027289063, + ]; + + let mut all_gaps = Vec::new(); + + // Measure gaps for all unique values + for category_pointer in 0..samples.len() - 1 { + for i in category_pointer + 1..samples.len() { + if samples[i] == samples[category_pointer] { + let gap = i - category_pointer - 1; + all_gaps.push(gap as u32); + break; + } + } + } + + if all_gaps.is_empty() { + return 1.0; // No gaps found, assume randomness + } + + // Create histogram of observed gaps + let mut observed = vec![0; bin_edges.len() - 1]; + for &gap in &all_gaps { + for i in 0..bin_edges.len() - 1 { + if (gap as u32) <= bin_edges[i + 1] { + observed[i] += 1; + break; + } + } + } + + // Calculate expected counts for each bin + let total_gaps = all_gaps.len() as f64; + let expected: Vec = expected_frequencies + .iter() + .map(|&freq| freq * total_gaps) + .collect(); + + // Perform chi-square test + let chi_squared_stat: f64 = observed + .iter() + .zip(expected.iter()) + .map(|(&o, &e)| if e > 0.0 { (o as f64 - e).powi(2) / e } else { 0.0 }) + .sum(); + + let degrees_of_freedom = bin_edges.len() as f64 - 2.0; // Number of bins - 1 + let chi_squared_dist = ChiSquared::new(degrees_of_freedom).expect("Failed to create ChiSquared distribution"); + let p_value = 1.0 - chi_squared_dist.cdf(chi_squared_stat); + + p_value + } + +} \ No newline at end of file diff --git a/src/uniformity/ks.rs b/src/uniformity/ks.rs new file mode 100644 index 0000000..a778f06 --- /dev/null +++ b/src/uniformity/ks.rs @@ -0,0 +1,40 @@ +use kolmogorov_smirnov::test_f64; + +use crate::Onod; + +impl Onod { + + /// KS randomness test + /// Performs the Kolmogorov-Smirnov test to evaluate uniformity of data distribution and returns a p-value. + // Note: This implementation of the Kolmogorov-Smirnov (KS) test differs slightly from the + // Java implementation due to differences in library behavior and floating-point handling. + // The Java implementation (Apache Commons Math) adds random jitter to handle ties in small + // datasets, uses strict inequality for small sample sizes, and applies specific precision + // rules based on the IEEE 754 standard for `double`. The Rust implementation, using the + // `kolmogorov_smirnov` crate, does not add jitter or handle ties in the same way, and + // adheres to the crate's internal handling of floating-point comparisons. These differences + // may result in slight variations in p-values or KS statistics between the two versions. + pub fn ks(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; // empty data + } + + // Normalize the input samples to [0, 1] range + let normalized_samples: Vec = samples.iter().map(|&x| x as f64 / 255.0).collect(); + + // Generate a uniform distribution for comparison + let uniform_distribution: Vec = (0..normalized_samples.len()) + .map(|i| i as f64 / (normalized_samples.len() as f64 - 1.0)) + .collect(); + + // Perform the Kolmogorov-Smirnov test + let confidence = 0.01; // Significance level + let result = test_f64(&normalized_samples, &uniform_distribution, confidence); + + // Extract p-value from the result + let p_value = 1.0 - result.reject_probability; + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/mean_byte.rs b/src/uniformity/mean_byte.rs new file mode 100644 index 0000000..5967d96 --- /dev/null +++ b/src/uniformity/mean_byte.rs @@ -0,0 +1,34 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Mean randomness test + /// Calculates the p-value for the mean of the byte slice compared to expected mean. + pub fn mean_byte(samples: &[u8]) -> f64 { + + let len = samples.len() as f64; + if len == 0.0 { + return 0.0; + } + + // Calculate observed mean + let observed_mean: f64 = samples.iter().map(|&x| x as f64).sum::() / len; + + // Expected mean for uniform distribution + let expected_mean = 127.5; + + // Calculate standard deviation of the mean + let std_dev_mean = ((256.0 * 256.0 - 1.0) / (12.0 * len)).sqrt(); + + // Calculate the z-score + let z_score = (observed_mean - expected_mean) / std_dev_mean; + + // Use normal distribution to calculate p-value + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); // Two-tailed test + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/mod.rs b/src/uniformity/mod.rs new file mode 100644 index 0000000..7537ef2 --- /dev/null +++ b/src/uniformity/mod.rs @@ -0,0 +1,16 @@ +pub mod avalanche; +pub mod chi_bit; +pub mod chi_byte; +pub mod compression; +pub mod gaps; +pub mod ks; +pub mod mean_byte; +pub mod monobit; +pub mod pi; +pub mod prediction; +pub mod runs; +pub mod runups; +pub mod sanity; +pub mod shannon; +pub mod shells; +pub mod uncorrelation; \ No newline at end of file diff --git a/src/uniformity/monobit.rs b/src/uniformity/monobit.rs new file mode 100644 index 0000000..d90221f --- /dev/null +++ b/src/uniformity/monobit.rs @@ -0,0 +1,40 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Monobit randomness test + /// Evaluates the balance of 0s and 1s in the binary representation of the data and returns a p-value. + pub fn monobit(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; // Default to perfect randomness for empty data + } + + // Count the total number of 1s in the dataset + let mut total_ones = 0; + let mut total_bits = 0; + + for &byte in samples { + total_ones += byte.count_ones() as usize; + total_bits += 8; + } + + // Calculate the observed proportion of 1s + let observed_proportion = total_ones as f64 / total_bits as f64; + + // Expected proportion for random data + let expected_proportion = 0.5; + let std_dev = (0.5 * 0.5 / total_bits as f64).sqrt(); // Standard deviation for a binomial distribution + + // Calculate the z-score + let z_score = (observed_proportion - expected_proportion) / std_dev; + + // Use normal distribution to calculate p-value + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/pi.rs b/src/uniformity/pi.rs new file mode 100644 index 0000000..ab3dfd3 --- /dev/null +++ b/src/uniformity/pi.rs @@ -0,0 +1,67 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Pi randomness test + /// Uses a Monte Carlo simulation to estimate randomness by calculating the approximation of Pi. + /// This implementation of the Pi randomness test closely follows the logic of the original Java implementation. + /// However, minor differences in the results may arise due to the following reasons: + /// + /// 1. **Floating-Point Precision**: + /// Rust and Java both use 64-bit floating-point numbers (`double` in Java, `f64` in Rust), but slight differences + /// in their implementations (e.g., rounding modes, intermediate representations) can lead to small deviations. + /// + /// 2. **Math Libraries**: + /// Java uses Apache Commons Math for statistical computations, which may implement certain calculations + /// (e.g., Z-scores and normal distribution CDFs) differently compared to the `statrs` crate used in Rust. + /// + /// 3. **Bit Accuracy**: + /// The Java implementation notes the significance of bit accuracy in floating-point computations, + /// as defined in the IEEE 754 standard. Differences in handling edge cases (e.g., subnormal values, + /// precision limits) could lead to slight variations. + /// + /// These differences are generally negligible for practical purposes and do not affect the overall functionality or + /// statistical significance of the test. + pub fn pi(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; + } + + // Normalize samples to [0.0, 1.0) + let normalized_samples: Vec = samples.iter().map(|&x| x as f64 / 255.0).collect(); + + // Initialize variables for summary statistics + let mut sum_y = 0.0; + let mut count = 0.0; + + // Compute y-values (sqrt(1 - x^2)) and update summary statistics + for &x in &normalized_samples { + let y = (1.0 - x * x).sqrt(); + sum_y += y; + count += 1.0; + } + + // Calculate mean of y-values + let mean_y = sum_y / count; + + // Calculate the test statistic + let test_statistic = 4.0 * mean_y; + + // Calculate variance and standard deviation + let variance = (16.0 / count) * ((2.0 / 3.0) - (std::f64::consts::PI / 4.0).powi(2)); + let std_dev = variance.sqrt(); + + // Calculate Z-score + let z_score = (test_statistic - std::f64::consts::PI) / std_dev; + + // Use normal distribution to calculate p-value + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); + + p_value + } + +} \ No newline at end of file diff --git a/src/uniformity/prediction.rs b/src/uniformity/prediction.rs new file mode 100644 index 0000000..732de58 --- /dev/null +++ b/src/uniformity/prediction.rs @@ -0,0 +1,51 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Prediction randomness test + /// Evaluates the predictability of the next bit based on current data and returns a p-value. + pub fn prediction(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; + } + + let mut correct_predictions = 0; + let mut total_predictions = 0; + + for window in samples.windows(2) { + if let [current, next] = window { + let predicted = if current & 0x01 == 0 { 0 } else { 1 }; // Predict next bit based on LSB + let actual = next & 0x01; // Check LSB of the next byte + + if predicted == actual { + correct_predictions += 1; + } + + total_predictions += 1; + } + } + + if total_predictions == 0 { + return 0.0; // No predictions possible + } + + // Calculate observed proportion of correct predictions + let observed_proportion = correct_predictions as f64 / total_predictions as f64; + + // Expected proportion for random data + let expected_proportion = 0.5; + let std_dev = (0.5 * 0.5 / total_predictions as f64).sqrt(); // Standard deviation for a binomial distribution + + // Calculate the z-score + let z_score = (observed_proportion - expected_proportion) / std_dev; + + // Use normal distribution to calculate p-value + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/runs.rs b/src/uniformity/runs.rs new file mode 100644 index 0000000..7665f44 --- /dev/null +++ b/src/uniformity/runs.rs @@ -0,0 +1,74 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Runs randomness test + /// Evaluates the randomness by counting both increasing and decreasing runs and returns a p-value. + pub fn runs(samples: &[u8]) -> f64 { + + if samples.is_empty() { + return 0.0; + } + + // Clone the samples to avoid modifying the original input + let samples = samples.to_vec(); + let median = calculate_median(&samples); + + let mut above = 0; + let mut below = 0; + + // Transform the data into a dichotomous vector and count above/below values + let mut purged_samples = Vec::new(); + for &sample in &samples { + if (sample as f64) > median { + purged_samples.push(1); // Mark as above + above += 1; + } else if (sample as f64) < median { + purged_samples.push(0); // Mark as below + below += 1; + } + } + + // Count runs + let mut runs_observed = 1; // At least one run exists + for window in purged_samples.windows(2) { + if window[0] != window[1] { + runs_observed += 1; + } + } + + // Calculate expected runs and standard deviation + let runs_expected = ((2.0 * above as f64 * below as f64) / (above + below) as f64) + 1.0; + let std_dev = ((2.0 * above as f64 * below as f64 * (2.0 * above as f64 * below as f64 - above as f64 - below as f64)) + / (((above + below) as f64).powi(2) * (above + below - 1) as f64)) + .sqrt(); + + // Calculate Z-score + let z_score = (runs_observed as f64 - runs_expected) / std_dev; + + // Use normal distribution to calculate p-value + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); + + if p_value.is_nan() { + return 0.0; // Return 0 if p-value is NaN + } + + p_value + } +} + +/// Helper function to calculate the median of a dataset +fn calculate_median(samples: &[u8]) -> f64 { + let mut sorted_samples = samples.to_vec(); + sorted_samples.sort_unstable(); + + let len = sorted_samples.len(); + if len % 2 == 0 { + (sorted_samples[len / 2 - 1] as f64 + sorted_samples[len / 2] as f64) / 2.0 + } else { + sorted_samples[len / 2] as f64 + } +} \ No newline at end of file diff --git a/src/uniformity/runups.rs b/src/uniformity/runups.rs new file mode 100644 index 0000000..2b236c1 --- /dev/null +++ b/src/uniformity/runups.rs @@ -0,0 +1,44 @@ +use statrs::distribution::{ChiSquared, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// RunUps randomness test + /// Evaluates the number of four-byte run-ups in the data and returns a p-value. + pub fn run_ups(input: &[u8]) -> f64 { + + let samples = input.iter().map(|&x| x as u32).collect::>(); + + if samples.len() < 4 { + return 0.0; // Not enough data for meaningful calculation + } + + let mut test_statistic = 0; + + for chunk in samples.chunks(4) { + if let [first, second, third, fourth] = chunk { + if first < second && second < third && third < fourth { + test_statistic += 1; + } + } + } + + let total_chunks = samples.len() / 4; + let no_expected = 2_731_135.0 / 67_108_864.0 * total_chunks as f64; + + let observed = [test_statistic as f64, total_chunks as f64 - test_statistic as f64]; + let expected = [no_expected, total_chunks as f64 - no_expected]; + + // Use chi-squared test to calculate p-value + let chi_squared_dist = ChiSquared::new(1.0).expect("Failed to create ChiSquared distribution"); + let chi_squared_stat: f64 = observed.iter() + .zip(expected.iter()) + .map(|(o, e)| (o - e).powi(2) / e) + .sum(); + + let p_value = 1.0 - chi_squared_dist.cdf(chi_squared_stat); + + p_value + } +} \ No newline at end of file diff --git a/src/uniformity/sanity.rs b/src/uniformity/sanity.rs new file mode 100644 index 0000000..fe233af --- /dev/null +++ b/src/uniformity/sanity.rs @@ -0,0 +1,33 @@ +use crate::Onod; + +impl Onod { + + /// Sanity randomness test + /// Checks for basic properties of randomness and returns a p-value. + pub fn sanity(samples: &[u8]) -> f64 { + if samples.is_empty() { + return 0.0; + } + + // Check the proportion of ones and zeros in the byte data + let mut one_bits = 0; + let mut total_bits = 0; + + for &byte in samples { + one_bits += byte.count_ones(); + total_bits += 8; + } + + let observed_ratio = one_bits as f64 / total_bits as f64; + let expected_ratio = 0.5; // Expected for a truly random sequence + let std_dev = (0.5 * 0.5 / total_bits as f64).sqrt(); // Standard deviation for binomial distribution + + // Use normal distribution to calculate p-value + use statrs::distribution::{Normal, ContinuousCDF}; + let normal_dist = Normal::new(expected_ratio, std_dev).expect("Failed to create Normal distribution"); + let p_value = 1.0 - normal_dist.cdf(observed_ratio); + + p_value + } + +} \ No newline at end of file diff --git a/src/uniformity/shannon.rs b/src/uniformity/shannon.rs new file mode 100644 index 0000000..aaa5ba6 --- /dev/null +++ b/src/uniformity/shannon.rs @@ -0,0 +1,43 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + /// Entropy randomness test + /// Calculates the Shannon entropy of a byte slice and outputs a p-value. + pub fn shannon(samples: &[u8]) -> f64 { + + let len = samples.len() as f64; + if len == 0.0 { + return 0.0; + } + + // Count occurrences of each byte + let mut counts = [0usize; 256]; + for &byte in samples { + counts[byte as usize] += 1; + } + + // Calculate Shannon entropy + let entropy: f64 = counts.iter() + .filter(|&&count| count > 0) + .map(|&count| { + let p = count as f64 / len; + -p * p.log2() + }) + .sum(); + + // Expected entropy for a uniform distribution + let expected_entropy = 8.0; + + // Calculate Z statistic + let std_dev = (0.833_f64).sqrt(); + let z_score = (entropy - expected_entropy) * len.sqrt() / std_dev; + + // Calculate p-value from Z score + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(z_score.abs())); // Two-tailed test + + p_value + } +} diff --git a/src/uniformity/shells.rs b/src/uniformity/shells.rs new file mode 100644 index 0000000..dba5c53 --- /dev/null +++ b/src/uniformity/shells.rs @@ -0,0 +1,96 @@ +use statrs::distribution::{ChiSquared, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// Shells randomness test + /// Evaluates the uniformity of distances between identical byte values and returns a p-value. + pub fn shells(input: &[u8]) -> f64 { + + // Define shell radii (precomputed to ensure equal volumes) + const SHELL_RADII: [f64; 35] = [ + 1., 0.990384019787941, 0.980577593308067, 0.970571001281035, 0.960353705642329, + 0.949914251592996, 0.939240154232372, 0.928317766722556, 0.91713212619864, + 0.905666772691187, 0.893903535096568, 0.881822276616739, 0.869400589952457, + 0.856613429672063, 0.843432665301749, 0.829826533366243, 0.815758959214771, + 0.801188709029197, 0.786068317431936, 0.770342714221672, 0.753947441129154, + 0.736806299728077, 0.718828193851318, 0.699902804775202, 0.67989452969576, + 0.65863375600835, 0.635903899768996, 0.61142141746576, 0.584803547642573, + 0.555513224287824, 0.52275795857471, 0.485285500640517, 0.440911138308369, + 0.385171357110836, 0.30571070873288 + ]; + + let samples = convert_to_3d_points(input); + + if samples.len() < 25000 { + eprintln!("Shells test requires at least 25,000 points for statistical validity."); + return 0.0; // Skip the test for small datasets + } + + let sphere_radius = SHELL_RADII[0]; + let no_shells = SHELL_RADII.len(); + + // Calculate sphere and cube volume proportions + let cube_side = 2.0 * sphere_radius; + let cube_volume = cube_side.powi(3); + let sphere_volume = (4.0 / 3.0) * std::f64::consts::PI * sphere_radius.powi(3); + let sphere_proportion = sphere_volume / cube_volume; // Theoretical value: π/6 + + let no_points = samples.len() as f64; + let no_points_per_shell = sphere_proportion * no_points / no_shells as f64; + + let mut observed = vec![0u64; no_shells]; + let expected: Vec = vec![no_points_per_shell; no_shells]; + + for (x, y, z) in samples { + // Compute radius from origin + let radius = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); + + // Ignore points outside the sphere + if radius > sphere_radius { + continue; + } + + // Assign to the correct shell + for j in 1..SHELL_RADII.len() { + if radius > SHELL_RADII[j] { + observed[j - 1] += 1; + break; + } + } + + if radius < SHELL_RADII[no_shells - 1] { + observed[no_shells - 1] += 1; + } + } + + // Perform Chi-Square Test + let chi_squared_stat: f64 = observed.iter() + .zip(expected.iter()) + .map(|(&o, &e)| (o as f64 - e).powi(2) / e) + .sum(); + + let degrees_of_freedom = no_shells as f64 - 1.0; + let chi_squared_dist = ChiSquared::new(degrees_of_freedom).expect("Failed to create ChiSquared distribution"); + let p_value = 1.0 - chi_squared_dist.cdf(chi_squared_stat); + + p_value + } +} + +fn convert_to_3d_points(data: &[u8]) -> Vec<(f64, f64, f64)> { + let mut points = Vec::new(); + + for chunk in data.chunks(3) { + if chunk.len() == 3 { + // Normalize the bytes to [0.0, 1.0) range + let x = chunk[0] as f64 / 255.0; + let y = chunk[1] as f64 / 255.0; + let z = chunk[2] as f64 / 255.0; + points.push((x, y, z)); + } + } + + points +} \ No newline at end of file diff --git a/src/uniformity/uncorrelation.rs b/src/uniformity/uncorrelation.rs new file mode 100644 index 0000000..a64b0e5 --- /dev/null +++ b/src/uniformity/uncorrelation.rs @@ -0,0 +1,57 @@ +use statrs::distribution::{Normal, ContinuousCDF}; + +use crate::Onod; + +impl Onod { + + /// UnCorrelation randomness test + /// Computes the Pearson correlation between the sequence and its shifted version, returning a p-value. + pub fn un_correlation(input: &[u8]) -> f64 { + + let samples = input.iter().map(|&x| x as i32).collect::>(); + + if samples.len() < 2 { + return 0.0; // Default to perfect randomness for insufficient data + } + + // Convert samples to f64 for correlation computation + let samples_a: Vec = samples.iter().map(|&x| x as f64).collect(); + + // Create a shifted version of the sequence + let mut samples_b = vec![0.0; samples.len()]; + samples_b[0] = samples_a[samples.len() - 1]; // Wrap around + for i in 1..samples.len() { + samples_b[i] = samples_a[i - 1]; + } + + // Calculate mean of both sequences + let mean_a = samples_a.iter().sum::() / samples_a.len() as f64; + let mean_b = samples_b.iter().sum::() / samples_b.len() as f64; + + // Compute Pearson correlation coefficient + let mut numerator = 0.0; + let mut denominator_a = 0.0; + let mut denominator_b = 0.0; + + for i in 0..samples.len() { + let diff_a = samples_a[i] - mean_a; + let diff_b = samples_b[i] - mean_b; + numerator += diff_a * diff_b; + denominator_a += diff_a.powi(2); + denominator_b += diff_b.powi(2); + } + + let correlation = numerator / (denominator_a.sqrt() * denominator_b.sqrt()); + + // Calculate p-value for null hypothesis of zero correlation + let n = samples.len() as f64; + let t_stat = correlation * ((n - 2.0) / (1.0 - correlation.powi(2))).sqrt(); + + // Use t-distribution approximation for large n + let normal_dist = Normal::new(0.0, 1.0).expect("Failed to create Normal distribution"); + let p_value = 2.0 * (1.0 - normal_dist.cdf(t_stat.abs())); + + p_value + } + +} \ No newline at end of file