From a8c74caf9038e73cc25cd2d605f56f06e1f8204c Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 17 Jan 2025 13:27:34 +0200 Subject: [PATCH] added observation and z-score to results, emojis --- Cargo.lock | 1 - Cargo.toml | 1 - src/bin/main.rs | 69 +++++++++++++-------------------- src/uniformity/avalanche.rs | 6 +-- src/uniformity/chi_bit.rs | 12 ++++-- src/uniformity/chi_byte.rs | 11 ++++-- src/uniformity/compression.rs | 10 ++--- src/uniformity/gaps.rs | 13 +++++-- src/uniformity/ks.rs | 15 +++++-- src/uniformity/mean_byte.rs | 6 +-- src/uniformity/monobit.rs | 6 +-- src/uniformity/pi.rs | 6 +-- src/uniformity/prediction.rs | 8 ++-- src/uniformity/runs.rs | 8 ++-- src/uniformity/runups.rs | 11 ++++-- src/uniformity/sanity.rs | 9 +++-- src/uniformity/shannon.rs | 6 +-- src/uniformity/shells.rs | 15 +++++-- src/uniformity/uncorrelation.rs | 6 +-- 19 files changed, 121 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cbdd859..d2fd767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,7 +189,6 @@ version = "0.1.0" dependencies = [ "flate2", "kolmogorov_smirnov", - "rand_distr", "statrs", ] diff --git a/Cargo.toml b/Cargo.toml index 9b8dddf..9aa3b47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,4 @@ 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 index f734d29..a6adf64 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,7 +1,6 @@ use std::fs::File; use std::io::{self, Read}; use std::path::Path; -use statrs::distribution::{Normal, ContinuousCDF}; use onod3000::Onod; @@ -36,12 +35,16 @@ fn main() -> io::Result<()> { std::process::exit(1); } - println!("\nTesting {}", source); - println!("Testing {} bytes.", input_data.len()); - println!("--------------------------------------"); + println!("\nTesting {} bytes from {}.", input_data.len(), source); + println!("-----------------------------------------------------"); + println!("Randomness Test Observation Z-Score P-Value Pass"); + println!("-----------------------------------------------------"); - let alpha = 0.01; let mut passed_tests = 0; + let alpha = 0.01; + + // Create vectors to store results + let (mut observations, mut z_scores, mut p_values) = (Vec::new(), Vec::new(), Vec::new()); // Run each test with the appropriate handling let tests = [ @@ -52,7 +55,7 @@ fn main() -> io::Result<()> { ("ChiByte", Onod::chi_byte(&input_data)), ("MeanByte", Onod::mean_byte(&input_data)), ("Compression", Onod::compression(&input_data)), - ("KS", Onod::ks(&input_data)), + ("Kolm.-Smirnov", Onod::ks(&input_data)), ("Pi", Onod::pi(&input_data)), ("Shells", Onod::shells(&input_data)), ("Gaps", Onod::gaps(&input_data)), @@ -63,47 +66,31 @@ fn main() -> io::Result<()> { ("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 { + for (test_name, (observation, z_score, p_value)) in &tests { + let result = if *p_value >= alpha && *observation != -1.0 { passed_tests += 1; - "PASS" + "✅" } else { - "FAIL" + if *observation == -1.0 { + "SKIP" + } else { + "❌" + } }; - - 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!( + "{:<15} {:>15.3} {:>8.4} {:.4} {}", + test_name, observation, z_score, p_value, result + ); - println!("--------------------------------------"); + observations.push(*observation); + z_scores.push(*z_score); + p_values.push(*p_value); + } + + 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); + println!("-----------------------------------------------------"); 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/uniformity/avalanche.rs b/src/uniformity/avalanche.rs index 42a7f52..5d62c9f 100644 --- a/src/uniformity/avalanche.rs +++ b/src/uniformity/avalanche.rs @@ -6,12 +6,12 @@ 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 { + pub fn avalanche(samples: &[u8]) -> (f64, f64, 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 + return (-1.0, 0.0, 1.0); // Not enough data for meaningful calculation } let mut means = Vec::new(); @@ -46,7 +46,7 @@ impl Onod { 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 + (mean_observed, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/chi_bit.rs b/src/uniformity/chi_bit.rs index fa11b63..777741c 100644 --- a/src/uniformity/chi_bit.rs +++ b/src/uniformity/chi_bit.rs @@ -4,10 +4,10 @@ use crate::Onod; impl Onod { - pub fn chi_bit(samples: &[u8]) -> f64 { + pub fn chi_bit(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; // empty data + return (-1.0, 0.0, 1.0); // empty data } // Lookup table for the number of set bits in each byte (Hamming weight) @@ -57,6 +57,12 @@ impl Onod { 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 + // Z-score calculation (standardization of the chi-squared statistic) + let mean = degrees_of_freedom; // Mean of the chi-squared distribution + let std_dev = (2.0 * degrees_of_freedom).sqrt(); // Standard deviation of the chi-squared distribution + let z_score = (chi_squared_stat - mean) / std_dev; + + (chi_squared_stat, z_score, p_value) + } } \ No newline at end of file diff --git a/src/uniformity/chi_byte.rs b/src/uniformity/chi_byte.rs index 35e48ba..20b0a66 100644 --- a/src/uniformity/chi_byte.rs +++ b/src/uniformity/chi_byte.rs @@ -7,10 +7,10 @@ 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 { + pub fn chi_byte(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; // Default to perfect randomness for empty data + return (-1.0, 0.0, 1.0); // Default to perfect randomness for empty data } // Count occurrences of each byte value (0-255) @@ -35,7 +35,12 @@ impl Onod { 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 + // Z-score calculation (standardization of the chi-squared statistic) + let mean = degrees_of_freedom; // Mean of the chi-squared distribution + let std_dev = (2.0 * degrees_of_freedom).sqrt(); // Standard deviation of the chi-squared distribution + let z_score = (chi_squared_stat - mean) / std_dev; + + (chi_squared_stat, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/compression.rs b/src/uniformity/compression.rs index b33c459..9c01de3 100644 --- a/src/uniformity/compression.rs +++ b/src/uniformity/compression.rs @@ -8,19 +8,19 @@ impl Onod { /// Compression randomness test /// Estimates randomness by the compressibility of the data and returns a p-value. - pub fn compression(samples: &[u8]) -> f64 { + pub fn compression(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; // Perfect randomness for empty data + return (-1.0, 0.0, 1.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 + return (-1.0, 0.0, 1.0); // Compression failed } let compressed_data = match encoder.finish() { Ok(data) => data, - Err(_) => return 0.0, // Compression failed + Err(_) => return (-1.0, 0.0, 1.0), // Compression failed }; // Calculate compression ratio @@ -37,6 +37,6 @@ impl Onod { 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 + (compression_ratio, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/gaps.rs b/src/uniformity/gaps.rs index 19e7c0b..56057ae 100644 --- a/src/uniformity/gaps.rs +++ b/src/uniformity/gaps.rs @@ -6,10 +6,10 @@ 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 { + pub fn gaps(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; // empty data + return (-1.0, 0.0, 1.0); // empty data } // Bin edges and expected frequencies from the Java implementation @@ -41,7 +41,7 @@ impl Onod { } if all_gaps.is_empty() { - return 1.0; // No gaps found, assume randomness + return (-1.0, 0.0, 1.0); // No gaps found } // Create histogram of observed gaps @@ -73,7 +73,12 @@ impl Onod { 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 + // Z-score calculation (standardization of the chi-squared statistic) + let mean = degrees_of_freedom; // Mean of the chi-squared distribution + let std_dev = (2.0 * degrees_of_freedom).sqrt(); // Standard deviation of the chi-squared distribution + let z_score = (chi_squared_stat - mean) / std_dev; + + (chi_squared_stat, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/ks.rs b/src/uniformity/ks.rs index a778f06..f786971 100644 --- a/src/uniformity/ks.rs +++ b/src/uniformity/ks.rs @@ -14,10 +14,10 @@ impl Onod { // `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 { + pub fn ks(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; // empty data + return (-1.0, 0.0, 1.0); // empty data } // Normalize the input samples to [0, 1] range @@ -32,9 +32,16 @@ impl Onod { let confidence = 0.01; // Significance level let result = test_f64(&normalized_samples, &uniform_distribution, confidence); - // Extract p-value from the result + // Extract the KS statistic (D-statistic) + let ks_statistic = result.statistic; + + // Calculate the z-score + let sample_size = normalized_samples.len() as f64; + let z_score = ks_statistic * sample_size.sqrt(); + + // Extract the p-value let p_value = 1.0 - result.reject_probability; - p_value + (ks_statistic, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/mean_byte.rs b/src/uniformity/mean_byte.rs index 5967d96..bcdf609 100644 --- a/src/uniformity/mean_byte.rs +++ b/src/uniformity/mean_byte.rs @@ -6,11 +6,11 @@ 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 { + pub fn mean_byte(samples: &[u8]) -> (f64, f64, f64) { let len = samples.len() as f64; if len == 0.0 { - return 0.0; + return (-1.0, 0.0, 1.0); } // Calculate observed mean @@ -29,6 +29,6 @@ impl Onod { 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 + (observed_mean, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/monobit.rs b/src/uniformity/monobit.rs index d90221f..c834a1e 100644 --- a/src/uniformity/monobit.rs +++ b/src/uniformity/monobit.rs @@ -6,10 +6,10 @@ 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 { + pub fn monobit(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; // Default to perfect randomness for empty data + return (-1.0, 0.0, 1.0); // Default to perfect randomness for empty data } // Count the total number of 1s in the dataset @@ -35,6 +35,6 @@ impl Onod { 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 + (observed_proportion, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/pi.rs b/src/uniformity/pi.rs index ab3dfd3..a5a6058 100644 --- a/src/uniformity/pi.rs +++ b/src/uniformity/pi.rs @@ -24,10 +24,10 @@ impl Onod { /// /// 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 { + pub fn pi(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; + return (-1.0, 0.0, 1.0); } // Normalize samples to [0.0, 1.0) @@ -61,7 +61,7 @@ impl Onod { 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 + (test_statistic, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/prediction.rs b/src/uniformity/prediction.rs index 732de58..0dc8b65 100644 --- a/src/uniformity/prediction.rs +++ b/src/uniformity/prediction.rs @@ -6,10 +6,10 @@ 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 { + pub fn prediction(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; + return (-1.0, 0.0, 1.0); } let mut correct_predictions = 0; @@ -29,7 +29,7 @@ impl Onod { } if total_predictions == 0 { - return 0.0; // No predictions possible + return (-1.0, 0.0, 1.0); // No predictions possible } // Calculate observed proportion of correct predictions @@ -46,6 +46,6 @@ impl Onod { 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 + (observed_proportion, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/runs.rs b/src/uniformity/runs.rs index 7665f44..a41d550 100644 --- a/src/uniformity/runs.rs +++ b/src/uniformity/runs.rs @@ -6,10 +6,10 @@ 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 { + pub fn runs(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; + return (-1.0, 0.0, 1.0); } // Clone the samples to avoid modifying the original input @@ -53,10 +53,10 @@ impl Onod { 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 + return (-1.0, 0.0, 1.0); // Return 0 if p-value is NaN } - p_value + (runs_observed as f64, z_score, p_value) } } diff --git a/src/uniformity/runups.rs b/src/uniformity/runups.rs index 2b236c1..3cf2624 100644 --- a/src/uniformity/runups.rs +++ b/src/uniformity/runups.rs @@ -6,12 +6,12 @@ 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 { + pub fn run_ups(input: &[u8]) -> (f64, f64, f64) { let samples = input.iter().map(|&x| x as u32).collect::>(); if samples.len() < 4 { - return 0.0; // Not enough data for meaningful calculation + return (-1.0, 0.0, 1.0); // Not enough data for meaningful calculation } let mut test_statistic = 0; @@ -39,6 +39,11 @@ impl Onod { let p_value = 1.0 - chi_squared_dist.cdf(chi_squared_stat); - p_value + // Z-score calculation (standardization of the chi-squared statistic) + let mean = 1.0; // Mean of the chi-squared distribution + let std_dev = (2.0 as f64).sqrt(); // Standard deviation of the chi-squared distribution + let z_score = (chi_squared_stat - mean) / std_dev; + + (chi_squared_stat, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/sanity.rs b/src/uniformity/sanity.rs index fe233af..f1875ad 100644 --- a/src/uniformity/sanity.rs +++ b/src/uniformity/sanity.rs @@ -4,9 +4,9 @@ impl Onod { /// Sanity randomness test /// Checks for basic properties of randomness and returns a p-value. - pub fn sanity(samples: &[u8]) -> f64 { + pub fn sanity(samples: &[u8]) -> (f64, f64, f64) { if samples.is_empty() { - return 0.0; + return (-1.0, 0.0, 1.0); } // Check the proportion of ones and zeros in the byte data @@ -22,12 +22,15 @@ impl Onod { 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 + // Z-score calculation + let z_score = (observed_ratio - expected_ratio) / std_dev; + // 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 + (observed_ratio, z_score, p_value) } } \ No newline at end of file diff --git a/src/uniformity/shannon.rs b/src/uniformity/shannon.rs index aaa5ba6..5d9cf77 100644 --- a/src/uniformity/shannon.rs +++ b/src/uniformity/shannon.rs @@ -5,11 +5,11 @@ 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 { + pub fn shannon(samples: &[u8]) -> (f64, f64, f64) { let len = samples.len() as f64; if len == 0.0 { - return 0.0; + return (-1.0, 0.0, 1.0); } // Count occurrences of each byte @@ -38,6 +38,6 @@ impl Onod { 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 + (entropy, z_score, p_value) } } diff --git a/src/uniformity/shells.rs b/src/uniformity/shells.rs index dba5c53..5484e19 100644 --- a/src/uniformity/shells.rs +++ b/src/uniformity/shells.rs @@ -6,7 +6,7 @@ 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 { + pub fn shells(input: &[u8]) -> (f64, f64, f64) { // Define shell radii (precomputed to ensure equal volumes) const SHELL_RADII: [f64; 35] = [ @@ -24,8 +24,10 @@ impl Onod { 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 + // eprintln!("---------------------------------------------------------------"); + // eprintln!("ERROR: Shells test requires at least 25,000 points for statistical validity. Skipping."); + // eprintln!("---------------------------------------------------------------"); + return (-1.0, 0.0, 1.0); // Skip the test for small datasets } let sphere_radius = SHELL_RADII[0]; @@ -75,7 +77,12 @@ impl Onod { 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 + // Z-score calculation (standardization of the chi-squared statistic) + let mean = degrees_of_freedom; // Mean of the chi-squared distribution + let std_dev = (2.0 * degrees_of_freedom).sqrt(); // Standard deviation of the chi-squared distribution + let z_score = (chi_squared_stat - mean) / std_dev; + + (chi_squared_stat, z_score, p_value) } } diff --git a/src/uniformity/uncorrelation.rs b/src/uniformity/uncorrelation.rs index a64b0e5..40a94fe 100644 --- a/src/uniformity/uncorrelation.rs +++ b/src/uniformity/uncorrelation.rs @@ -6,12 +6,12 @@ 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 { + pub fn un_correlation(input: &[u8]) -> (f64, f64, 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 + return (-1.0, 0.0, 1.0); // Default to perfect randomness for insufficient data } // Convert samples to f64 for correlation computation @@ -51,7 +51,7 @@ impl Onod { 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 + (correlation, t_stat, p_value) } } \ No newline at end of file