add runs test #5

Merged
darkkirb merged 1 commit from add-runs-test into main 2024-09-09 09:14:20 +00:00
6 changed files with 90 additions and 8 deletions

View file

@ -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

View file

@ -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;

View file

@ -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<T: BitStore, O: BitOrder>(&self, bs: &BitSlice<T, O>) -> Option<f64> {
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)
}
}

View file

@ -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;

67
src/tests/runs.rs Normal file
View file

@ -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<T: BitStore, O: BitOrder>(&self, bs: &BitSlice<T, O>) -> Option<f64> {
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");
}
}

10
src/utils.rs Normal file
View file

@ -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<B: BitStore, O: BitOrder>(bs: &BitSlice<B, O>) -> f64 {
bs.into_iter().filter(|b| **b).count() as f64 / bs.len() as f64
}