add block frequency test #3

Merged
darkkirb merged 3 commits from block-frequency into main 2024-09-08 17:45:33 +00:00
9 changed files with 147 additions and 25 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[registries]
procyos = { index = "sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/" }

View file

@ -9,5 +9,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.
[Unreleased]: https://git.chir.rs/ProcyOS/rand_testsuite

11
Cargo.lock generated
View file

@ -32,6 +32,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "extra-math"
version = "0.1.0"
source = "sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/"
checksum = "fa1061c295d9aba46636f4a0b16372ea314fcd6c8dfaf447cc719a6a9dce63aa"
dependencies = [
"libm",
"num-traits",
]
[[package]]
name = "funty"
version = "2.0.0"
@ -139,6 +149,7 @@ name = "rand_testsuite"
version = "0.1.0"
dependencies = [
"bitvec",
"extra-math",
"libm",
"num-traits",
"rand",

View file

@ -22,7 +22,7 @@ args @ {
workspaceSrc,
ignoreLockHash,
}: let
nixifiedLockHash = "0c97699175df8652eb6708b342caa7bcc3794d5a6bd83ecc4362d8cf3b262349";
nixifiedLockHash = "74300319bd44e956ec1895afc04c903cca3100257d7c627ba18911908b487b47";
workspaceSrc =
if args.workspaceSrc == null
then ./.
@ -113,6 +113,25 @@ in
};
});
"sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/".extra-math."0.1.0" = overridableMkRustCrate (profileName: rec {
name = "extra-math";
version = "0.1.0";
registry = "sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/";
src = fetchCrateAlternativeRegistry {
index = sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/;
name = "extra-math";
version = "0.1.0";
sha256 = "fa1061c295d9aba46636f4a0b16372ea314fcd6c8dfaf447cc719a6a9dce63aa";
};
features = builtins.concatLists [
["default"]
];
dependencies = {
libm = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libm."0.2.8" {inherit profileName;}).out;
num_traits = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.19" {inherit profileName;}).out;
};
});
"registry+https://github.com/rust-lang/crates.io-index".funty."2.0.0" = overridableMkRustCrate (profileName: rec {
name = "funty";
version = "2.0.0";
@ -332,6 +351,7 @@ in
];
dependencies = {
bitvec = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bitvec."1.0.1" {inherit profileName;}).out;
extra_math = (rustPackages."sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/".extra-math."0.1.0" {inherit profileName;}).out;
libm = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libm."0.2.8" {inherit profileName;}).out;
num_traits = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".num-traits."0.2.19" {inherit profileName;}).out;
${

View file

@ -11,6 +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" }
libm = "0.2.8"
num-traits = { version = "0.2.19", default-features = false, features = [
"libm",

View file

@ -14,16 +14,16 @@
]
},
"locked": {
"lastModified": 1713199118,
"narHash": "sha256-MlLdAvk+zXCFUy280sY6LqtykqWXIkKVXo72J7a6HlU=",
"owner": "cargo2nix",
"lastModified": 1725812914,
"narHash": "sha256-xYdN9kbpLScjJGll6IE7Cnnn1UhIMKUwGyrzKjQvPgI=",
"owner": "DarkKirb",
"repo": "cargo2nix",
"rev": "1efb03f2f794ad5eed17e807e858c4da001dbc3e",
"rev": "7bb1ee00029ab2e7bd8ba03f5563cdbb998ab6f7",
"type": "github"
},
"original": {
"owner": "cargo2nix",
"ref": "main",
"owner": "DarkKirb",
"ref": "master",
"repo": "cargo2nix",
"type": "github"
}

View file

@ -12,7 +12,7 @@
};
cargo2nix = {
url = github:cargo2nix/cargo2nix/main;
url = github:DarkKirb/cargo2nix/master;
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
inputs.rust-overlay.follows = "rust-overlay";
@ -36,6 +36,9 @@
pkgs = import nixpkgs {
inherit system overlays;
};
registryDownloadPaths = {
"sparse+https://git.chir.rs/api/packages/ProcyOS/cargo/" = "https://git.chir.rs/api/packages/ProcyOS/cargo/api/v1/crates";
};
rustPkgs = pkgs.rustBuilder.makePackageSet {
packageFun = import ./Cargo.nix;
rustChannel = "nightly";
@ -45,6 +48,17 @@
"rand_testsuite/default"
"rand_testsuite/full"
];
fetchCrateAlternativeRegistry = {
index,
name,
version,
sha256,
}:
pkgs.fetchurl {
name = "${name}-${version}.tar.gz";
url = "${registryDownloadPaths.${index}}/${name}/${version}/download";
inherit sha256;
};
};
in rec {
devShells.default = with pkgs;

View file

@ -1,38 +1,46 @@
//! All supported tests are available in this module.
mod block_frequency;
mod monobit;
use core::f64;
use bitvec::prelude::{BitOrder, BitSlice, BitStore};
pub use block_frequency::*;
pub use monobit::*;
use crate::RandomTest;
/// This combines all supported tests into one
///
/// Specifically it returns the lowest metric and stops if it goes below the p-value threshold.
#[derive(Copy, Clone, Debug)]
pub struct CombinedRandomTest(f64);
pub struct CombinedRandomTest;
impl CombinedRandomTest {
/// Creates a new combined random test
pub fn new(p_threshold: f64) -> Self {
Self(p_threshold)
const fn max_of<const N: usize>(v: [usize; N]) -> usize {
let mut m = v[0];
let mut i = 1;
while i < v.len() {
if m < v[i] {
m = v[i];
}
i += 1;
}
m
}
impl RandomTest for CombinedRandomTest {
const RECOMMENDED_BIT_SIZE: usize = Monobit::RECOMMENDED_BIT_SIZE;
const RECOMMENDED_BIT_SIZE: usize = max_of([
Monobit::RECOMMENDED_BIT_SIZE,
BlockFrequency::RECOMMENDED_BIT_SIZE,
]);
const MINIMUM_BIT_SIZE: usize = Monobit::MINIMUM_BIT_SIZE;
const MINIMUM_BIT_SIZE: usize =
max_of([Monobit::MINIMUM_BIT_SIZE, BlockFrequency::MINIMUM_BIT_SIZE]);
fn evaluate<T: BitStore, O: BitOrder>(&self, bs: &BitSlice<T, O>) -> Option<f64> {
Monobit.evaluate(bs)
}
}
impl Default for CombinedRandomTest {
fn default() -> Self {
Self(0.01)
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)?);
Some(worst_test_score)
}
}
@ -46,7 +54,7 @@ mod tests {
use super::CombinedRandomTest;
let mut rng = rand::thread_rng();
assert!(
CombinedRandomTest::default()
CombinedRandomTest
.evaluate_rng(&mut rng)
.unwrap_or_default()
> 0.01

View file

@ -0,0 +1,65 @@
//! Block Frequency Test
use bitvec::prelude::{BitOrder, BitSlice, BitStore};
use extra_math::gamma::Gamma;
#[allow(unused_imports, reason = "redundant in std use cases")]
use num_traits::Float;
use crate::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.
///
/// The amount of blocks is chosen such that a block is at least 1% of the size of the total input, or at least 20.
///
/// 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 BlockFrequency;
impl RandomTest for BlockFrequency {
const RECOMMENDED_BIT_SIZE: usize = 100;
const MINIMUM_BIT_SIZE: usize = 0;
fn evaluate<T: BitStore, O: BitOrder>(&self, bs: &BitSlice<T, O>) -> Option<f64> {
let block_size = (bs.len() / 100).max(20);
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);
statistic += (pi - 0.5).powi(2);
}
statistic *= (4 * block_size) as f64;
Some(Gamma::upper_gamma_regularized(
block_size as f64 / 2.0,
statistic / 2.0,
))
}
}
#[cfg(test)]
mod tests {
use bitvec::prelude::*;
use crate::RandomTest;
use super::BlockFrequency;
#[test]
fn block_frequency_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 = BlockFrequency.evaluate(epsilon.as_bitslice());
assert!(res.is_some());
let res = res.unwrap();
assert!(res > 0.01, "Data should be evaluated as random");
}
}