diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e99e85..73edac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,5 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a simple monobit test. - Added a block frequency test. - Added a runs test. +- Added a longest runs of ones test. [Unreleased]: https://git.chir.rs/ProcyOS/rand_testsuite diff --git a/Cargo.toml b/Cargo.toml index 9b24fd8..96c669f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ categories = ["development-tools::testing", "no-std", "no-std::no-alloc"] [dependencies] bitvec = { version = "1.0.1", default-features = false } -extra-math = { version = "0.1.0", registry = "procyos" } +extra-math = { version = "0.1.1", registry = "procyos" } libm = "0.2.8" num-traits = { version = "0.2.19", default-features = false, features = [ "libm", diff --git a/src/tests.rs b/src/tests.rs index e5c8c3f..b14c724 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,6 +1,7 @@ //! All supported tests are available in this module. mod block_frequency; +mod longest_runs_of_ones; mod monobit; mod runs; @@ -8,6 +9,7 @@ use core::f64; use bitvec::prelude::{BitOrder, BitSlice, BitStore}; pub use block_frequency::*; +pub use longest_runs_of_ones::*; pub use monobit::*; pub use runs::*; @@ -34,12 +36,14 @@ impl RandomTest for CombinedRandomTest { Monobit::RECOMMENDED_BIT_SIZE, BlockFrequency::RECOMMENDED_BIT_SIZE, Runs::RECOMMENDED_BIT_SIZE, + LongestRunOfOnes::RECOMMENDED_BIT_SIZE, ]); const MINIMUM_BIT_SIZE: usize = max_of([ Monobit::MINIMUM_BIT_SIZE, BlockFrequency::MINIMUM_BIT_SIZE, Runs::MINIMUM_BIT_SIZE, + LongestRunOfOnes::MINIMUM_BIT_SIZE, ]); fn evaluate(&self, bs: &BitSlice) -> Option { @@ -47,6 +51,7 @@ impl RandomTest for CombinedRandomTest { worst_test_score = worst_test_score.min(Monobit.evaluate(bs)?); worst_test_score = worst_test_score.min(BlockFrequency.evaluate(bs)?); worst_test_score = worst_test_score.min(Runs.evaluate(bs)?); + worst_test_score = worst_test_score.min(LongestRunOfOnes.evaluate(bs)?); Some(worst_test_score) } } diff --git a/src/tests/longest_runs_of_ones.rs b/src/tests/longest_runs_of_ones.rs new file mode 100644 index 0000000..731f69f --- /dev/null +++ b/src/tests/longest_runs_of_ones.rs @@ -0,0 +1,102 @@ +//! Longest runs of ones test + +use bitvec::prelude::{BitOrder, BitSlice, BitStore}; +use extra_math::gamma::Gamma; + +use crate::RandomTest; + +const EXPECTED_M8: [f64; 4] = [0.2148, 0.3672, 0.2305, 0.1875]; +const EXPECTED_M128: [f64; 6] = [0.1174, 0.2430, 0.2493, 0.1752, 0.1027, 0.1124]; +const EXPECTED_M10K: [f64; 7] = [0.0882, 0.2092, 0.2483, 0.1933, 0.1208, 0.0675, 0.0727]; + +/// Longest runs of ones test +/// +/// This test is defined in the [NIST Special Publication 800-22](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-22r1a.pdf). +#[derive(Copy, Clone, Debug)] +pub struct LongestRunOfOnes; + +impl RandomTest for LongestRunOfOnes { + const RECOMMENDED_BIT_SIZE: usize = 750_000; + const MINIMUM_BIT_SIZE: usize = 128; + + fn evaluate(&self, bs: &BitSlice) -> Option { + let block_size = match bs.len() { + 0..128 => { + return None; + } + 128..6272 => 8, + 6272..750_000 => 128, + 750_000.. => 10_000, + }; + let mut occurences = [0usize; 7]; + for block in bs.chunks(block_size) { + let mut c = 0usize; + for b in block.into_iter().map(|b| *b).chain([false]) { + if b { + c += 1; + } else if c > 0 { + match (block_size, c) { + (0..=8, 0..=1) | (9..=128, 0..=4) | (129.., 0..=10) => occurences[0] += 1, + (0..=8, 2) | (9..=128, 5) | (129.., 11) => occurences[1] += 1, + (0..=8, 3) | (9..=128, 6) | (129.., 12) => occurences[2] += 1, + (0..=8, 4..) | (9..=128, 7) | (129.., 13) => occurences[3] += 1, + (9..=128, 8) | (129.., 14) => occurences[4] += 1, + (9..=128, 9..) | (129.., 15) => occurences[5] += 1, + (129.., 16..) => occurences[6] += 1, + } + c = 0; + } + } + } + let mut chi_squared = 0.0; + let k = if block_size == 8 { + for i in 0..EXPECTED_M8.len() { + chi_squared += + (occurences[i] as f64 - 16.0 * EXPECTED_M8[i]) / (16.0 * EXPECTED_M8[i]); + } + EXPECTED_M8.len() + } else if block_size == 128 { + for i in 0..EXPECTED_M128.len() { + chi_squared += + (occurences[i] as f64 - 49.0 * EXPECTED_M128[i]) / (49.0 * EXPECTED_M128[i]); + } + EXPECTED_M128.len() + } else if block_size == 10_000 { + for i in 0..EXPECTED_M10K.len() { + chi_squared += + (occurences[i] as f64 - 75.0 * EXPECTED_M10K[i]) / (75.0 * EXPECTED_M10K[i]); + } + EXPECTED_M10K.len() + } else { + unreachable!(); + }; + Some(Gamma::upper_gamma_regularized( + k as f64 / 2.0, + chi_squared / 2.0, + )) + } +} + +#[cfg(test)] +mod tests { + use bitvec::prelude::*; + + use crate::RandomTest; + + use super::LongestRunOfOnes; + + #[test] + fn longest_run_of_ones_test_vector() { + let epsilon = bitvec![ + 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, + 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, + 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, + ]; + let res = LongestRunOfOnes.evaluate(epsilon.as_bitslice()); + assert!(res.is_some()); + let res = res.unwrap(); + assert!(res > 0.01, "Data should be evaluated as random"); + } +} diff --git a/src/utils.rs b/src/utils.rs index 98a2fc4..f582297 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,8 @@ //! Various utility functions -use core::convert::identity; - use bitvec::{order::BitOrder, slice::BitSlice, store::BitStore}; /// Calculates the proportion of ones in a bitslice -pub fn ones_proportion(bs: &BitSlice) -> f64 { +pub(crate) fn ones_proportion(bs: &BitSlice) -> f64 { bs.into_iter().filter(|b| **b).count() as f64 / bs.len() as f64 }