From 80b07fcfcff50d07b101320a4c16fbf4d2743c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charlotte=20=F0=9F=A6=9D=20Delenk?= Date: Mon, 9 Sep 2024 09:41:46 +0200 Subject: [PATCH] add runs test --- CHANGELOG.md | 1 + src/lib.rs | 1 + src/tests.rs | 11 ++++-- src/tests/block_frequency.rs | 8 ++--- src/tests/runs.rs | 67 ++++++++++++++++++++++++++++++++++++ src/utils.rs | 10 ++++++ 6 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 src/tests/runs.rs create mode 100644 src/utils.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b2b7b..9e99e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,5 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added a simple monobit test. - Added a block frequency test. +- Added a runs test. [Unreleased]: https://git.chir.rs/ProcyOS/rand_testsuite diff --git a/src/lib.rs b/src/lib.rs index 454c940..95bc39f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use bitvec::{order::BitOrder, slice::BitSlice, store::BitStore}; mod rand_core; pub mod tests; +pub(crate) mod utils; #[cfg(feature = "rand_core")] pub use rand_core::RandomTestExt; diff --git a/src/tests.rs b/src/tests.rs index e4c6a95..e5c8c3f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,12 +2,14 @@ mod block_frequency; mod monobit; +mod runs; use core::f64; use bitvec::prelude::{BitOrder, BitSlice, BitStore}; pub use block_frequency::*; pub use monobit::*; +pub use runs::*; use crate::RandomTest; @@ -31,15 +33,20 @@ impl RandomTest for CombinedRandomTest { const RECOMMENDED_BIT_SIZE: usize = max_of([ Monobit::RECOMMENDED_BIT_SIZE, BlockFrequency::RECOMMENDED_BIT_SIZE, + Runs::RECOMMENDED_BIT_SIZE, ]); - const MINIMUM_BIT_SIZE: usize = - max_of([Monobit::MINIMUM_BIT_SIZE, BlockFrequency::MINIMUM_BIT_SIZE]); + const MINIMUM_BIT_SIZE: usize = max_of([ + Monobit::MINIMUM_BIT_SIZE, + BlockFrequency::MINIMUM_BIT_SIZE, + Runs::MINIMUM_BIT_SIZE, + ]); fn evaluate(&self, bs: &BitSlice) -> Option { let mut worst_test_score = f64::MAX; 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)?); Some(worst_test_score) } } diff --git a/src/tests/block_frequency.rs b/src/tests/block_frequency.rs index 5fb0361..f703e38 100644 --- a/src/tests/block_frequency.rs +++ b/src/tests/block_frequency.rs @@ -5,7 +5,7 @@ use extra_math::gamma::Gamma; #[allow(unused_imports, reason = "redundant in std use cases")] use num_traits::Float; -use crate::RandomTest; +use crate::{utils::ones_proportion, RandomTest}; /// The Block Frequency Test splits the input data into multiple blocks, counts the frequency 1s and 0s within these blocks, and determines whether these are distributed well. /// @@ -26,11 +26,7 @@ impl RandomTest for BlockFrequency { let block_count = bs.len() / block_size; let mut statistic = 0.0; for i in 0..block_count { - let one_count: usize = bs[i * block_size..(i + 1) * block_size] - .into_iter() - .map(|v| if *v { 1 } else { 0 }) - .sum(); - let pi = (one_count as f64) / (block_size as f64); + let pi = ones_proportion(&bs[i * block_size..(i + 1) * block_size]); statistic += (pi - 0.5).powi(2); } statistic *= (4 * block_size) as f64; diff --git a/src/tests/runs.rs b/src/tests/runs.rs new file mode 100644 index 0000000..0db9226 --- /dev/null +++ b/src/tests/runs.rs @@ -0,0 +1,67 @@ +//! Runs test code + +use bitvec::{order::BitOrder, slice::BitSlice, store::BitStore}; +use libm::erfc; +#[allow(unused_imports, reason = "redundant in std use cases")] +use num_traits::Float; + +use crate::{utils::ones_proportion, RandomTest}; + +/// The runs test counts the number of bit transitions inside of a random number generator. +/// +/// 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 Runs; + +impl RandomTest for Runs { + const RECOMMENDED_BIT_SIZE: usize = 100; + const MINIMUM_BIT_SIZE: usize = 2; + + fn evaluate(&self, bs: &BitSlice) -> Option { + if bs.len() < Self::MINIMUM_BIT_SIZE { + return None; + } + let pi = ones_proportion(bs); + if (pi - 0.5).abs() > 2.0 / (bs.len() as f64).sqrt() { + return Some(0.0); + } + let (_, runs) = bs + .into_iter() + .skip(1) + .fold((bs[0], 1usize), |(last, cum), cur| { + if *cur == last { + (last, cum) + } else { + (*cur, cum + 1) + } + }); + + Some(erfc( + (runs as f64 - 2.0 * (bs.len() as f64) * pi * (1.0 - pi)).abs() + / (2.0 * (2.0 * bs.len() as f64).sqrt() * pi * (1.0 - pi)), + )) + } +} + +#[cfg(test)] +mod tests { + use bitvec::prelude::*; + + use crate::RandomTest; + + use super::Runs; + + #[test] + fn runs_test_vector() { + let epsilon = bitvec![ + 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, + 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, + 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0 + ]; + let res = Runs.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 new file mode 100644 index 0000000..98a2fc4 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,10 @@ +//! 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 { + bs.into_iter().filter(|b| **b).count() as f64 / bs.len() as f64 +} -- 2.47.0