Skip to main content

Crate cribler

Crate cribler 

Source
Expand description

§cribler

Batteries-included statistical randomness-test toolkit for Rust.

cribler re-exports every engine from cribler_core (the dependency-minimal, closure-based foundation) and adds ergonomic named-case batching (XxxSuite), a whole-battery aggregator (Suite), and optional rand_core/urng integration. Suite::run/XxxSuite::run return hashbrown::HashMap rather than std::collections::HashMap.

No RNG library is a hard dependency by default: every engine takes a plain FnMut() -> f64 / FnMut() -> u64 sampler closure, so any generator — a hand-rolled PRNG, urng, or anything exposing rand_core::Rng — plugs in directly without enabling any feature. Enable the rand or urng feature for pre-built typed convenience on top of that.

§Engines

EngineWhat it detects
Chi-squared (ChiSq)Uniformity of the output distribution
Monte Carlo pi (McPi)2D uniformity sanity check via pi estimation
Serial (Serial)Correlation between consecutive outputs (multi-dimensional chi-squared)
Runs (Runs)Sequential correlation via monotonic run lengths
Kolmogorov-Smirnov (Ks)Deviation of the empirical CDF from the ideal uniform CDF
Birthday spacing (Birthday)Short periods / lattice structure (Marsaglia’s DIEHARD test)
NIST SP 800-22 (Nist)Frequency, Block Frequency, Runs, Longest-Run-of-Ones, Cumulative Sums (bit-level subset)
Paranoid (Paranoid)Elevates any single test into a battery-level test (NIST SP 800-22 §4.2)

§Quick start

use cribler::{ChiSqConfig, run_chisq};

// Any FnMut() -> f64 works; this one is a tiny inline xorshift32.
let mut state: u32 = 1;
let mut sampler = || {
    state ^= state << 13;
    state ^= state >> 17;
    state ^= state << 5;
    state as f64 / 4294967296.0
};

let result = run_chisq("xorshift32", &mut sampler, ChiSqConfig::default()).unwrap();
println!("{result:?}");

§Running multiple named cases (XxxSuite)

use cribler::ChiSqSuite;

let mut state1: u32 = 1;
let mut state2: u32 = 2;
let mut next = |state: &mut u32| -> f64 {
    *state ^= *state << 13;
    *state ^= *state >> 17;
    *state ^= *state << 5;
    *state as f64 / 4294967296.0
};

let mut suite = ChiSqSuite::new(0);
suite.add_sampler("generator-a", || next(&mut state1)).unwrap();
suite.add_sampler("generator-b", || next(&mut state2)).unwrap();

for (name, result) in suite.run().unwrap() {
    println!("{name}: {:?}", result.verdict);
}

§Running the whole battery at once (Suite)

Suite wraps one ChiSqSuite/McPiSuite/SerialSuite/RunsSuite/ KsSuite/BirthdaySuite/NistSuite each, all sharing the same seed, so registering a generator once runs every engine against it. Suite::run returns one [SuiteResult] (all seven engines’ results) per registered generator, keyed by its case name (HashMap<String, SuiteResult>):

use cribler::{Suite, WordSource};

#[derive(Clone)]
struct MyRng(u64);

impl<'a> From<MyRng> for WordSource<'a> {
    fn from(mut rng: MyRng) -> Self {
        WordSource::new("MyRng", 64, move || {
            rng.0 = rng.0.wrapping_mul(6364136223846793005).wrapping_add(1);
            rng.0
        })
    }
}

let results = Suite::new(1).from_custom(MyRng(1)).unwrap().run().unwrap();
let result = &results["MyRng"];
println!("{:?}", result.chisq.verdict);
println!("{:?}", result.nist.verdict);

Suite also has from_urng32/from_urng64 (feature urng) and from_rand (feature rand), mirroring XxxSuite — the only difference is from_custom requires S: Clone, since each engine needs its own independent generator instance.

§Elevating a test to battery level (Paranoid)

A single p-value can pass by chance even for a bad generator. Paranoid re-runs a test many times over independent samples and checks both the pass proportion and the uniformity of the resulting p-values (NIST SP 800-22 §4.2):

use cribler::{ChiSqConfig, ParanoidConfig, p_value_from_z, run_chisq, run_paranoid};

let mut trial = |i: usize| {
    let mut state = (i as u32 + 1).wrapping_mul(2654435761) | 1;
    let mut sampler = || {
        state ^= state << 13;
        state ^= state >> 17;
        state ^= state << 5;
        state as f64 / 4294967296.0
    };
    let z = run_chisq(format!("trial-{i}"), &mut sampler, ChiSqConfig::default())
        .unwrap()
        .z_score;
    p_value_from_z(z)
};

let result = run_paranoid("xorshift32.paranoid", ParanoidConfig::default(), &mut trial).unwrap();
println!("{:?}", result.verdict);

§Suite::from_urng32/from_urng64/from_rand/from_custom

Every XxxSuite is created with a seed — ChiSqSuite::new(seed) — and has methods that register a named test case from a generator seeded from it:

  • from_urng32::<R>() / from_urng64::<R>() — a urng::Rng32/Rng64 R, constructed from the suite’s seed via [UrngSeed32]/[UrngSeed64] (feature urng). No instance to pass in.
  • from_rand::<R>() — a rand_core::Rng R, constructed via SeedableRng::seed_from_u64 from the suite’s seed (feature rand). No instance to pass in.
  • from_custom(source) — anything else. Custom generators are always integer-based, never float-based: implement From<YourType> for WordSource directly and pass an instance through (the suite’s seed isn’t involved; see the source module docs for a full example). On the [0, 1)-sampling engines, the raw u64 draws are converted with unit_f64_from_u32/unit_f64_from_u64 depending on word_bits.

The case name is taken from the generator’s type name (R’s name for from_urng32/from_urng64/from_rand, or whatever you pass to WordSource::new for from_custom).

§rand feature

[dependencies]
cribler = { version = "0.1", features = ["rand"] }
use cribler::ChiSqSuite;

let results = ChiSqSuite::new(0)
    .from_rand::<rand::rngs::StdRng>()
    .unwrap()
    .run()
    .unwrap();

§urng feature

[dependencies]
cribler = { version = "0.1", features = ["urng"] }
use cribler::ChiSqSuite;
use urng::Sfc32;

let results = ChiSqSuite::new(0)
    .from_urng32::<Sfc32>()
    .unwrap()
    .run()
    .unwrap();
println!("{:?}", results["Sfc32"].verdict);

Chain multiple generator types onto the same Suite before calling run() to batch them under a shared config (each type contributes one named case, so generators of the same type need telling apart some other way — e.g. by going through from_custom with an explicit name instead):

use cribler::ChiSqSuite;
use urng::{Sfc32, Xorshift32};

let results = ChiSqSuite::new(0)
    .from_urng32::<Sfc32>().unwrap()
    .from_urng32::<Xorshift32>().unwrap()
    .run()
    .unwrap();
assert_eq!(results.len(), 2);

No feature flag is required at all if you’d rather skip the typed bridges: every engine also takes a plain closure, so || cribler_core::unit_f64_from_u32(rng.nextu()) works without any extra dependency.

Note: cribler depends on urng here (not the other way around) — urng itself has no dependency on cribler, which is what makes this feature possible without a cyclic package dependency.

§serde feature

Enable serde to make every Config/Verdict/Result type (from both cribler_core and cribler, including SuiteResult) Serialize/ Deserialize:

[dependencies]
cribler = { version = "0.1", features = ["serde", "urng"] }
use cribler::ChiSqSuite;
use urng::Sfc32;

let results = ChiSqSuite::new(0).from_urng32::<Sfc32>().unwrap().run().unwrap();
let json = serde_json::to_string(&results).unwrap();
let round_tripped: hashbrown::HashMap<String, cribler::ChiSqResult> =
    serde_json::from_str(&json).unwrap();
assert_eq!(results["Sfc32"].verdict, round_tripped["Sfc32"].verdict);

§Crate layout

  • cribler_core: the engines themselves, with no dependency on any RNG crate. Use this directly for the smallest possible dependency footprint.
  • cribler (this crate): cribler_core plus Suite batching and optional rand_core integration.

§License

MIT

§Module map

  • [suite] / paranoid_suite: named-case batching (XxxSuite) over the cribler_core engines. suite::Suite aggregates every engine into one batch.
  • source: source::WordSource (used by from_custom — integer-based, never float) and the [source::UrngSeed32]/[source::UrngSeed64]/[source::RandSource] traits that back from_urng32/from_urng64/from_rand.
  • [urng_support] (feature urng): adds [urng_support::Test32]/[urng_support::Test64] (rng.run_chisq(name), …) directly to any urng::Rng32/Rng64 generator.

Re-exports§

pub use paranoid_suite::ParanoidSuite;
pub use source::WordSource;
pub use suite::BirthdaySuite;
pub use suite::ChiSqSuite;
pub use suite::KsSuite;
pub use suite::McPiSuite;
pub use suite::NistSuite;
pub use suite::RunsSuite;
pub use suite::SerialSuite;
pub use suite::Suite;
pub use suite::SuiteError;
pub use suite::SuiteResults;

Modules§

birthday
Birthday spacing test engine (Marsaglia’s DIEHARD “birthday spacings” test): detects short periods and lattice structure by checking whether collisions among sampled “birthdays” occur at the expected rate.
chisq
Chi-squared uniformity test engine.
internal
Numerical helpers shared by the test engines: special functions needed to turn test statistics into p-values.
ks
Kolmogorov-Smirnov test engine: checks the maximum deviation between the empirical CDF of the samples and the ideal uniform CDF.
macros
mcpi
Monte Carlo pi-estimation test engine.
nist
NIST SP 800-22 (rev. 1a) statistical test suite, bit-level subset.
paranoid
“Paranoid” meta-test engine: elevates any single statistical test into a battery-level test, following the “Testing Strategy and Result Interpretation” guidance of NIST SP 800-22 rev. 1a, 4.2.
paranoid_suite
Named-case batching for cribler_core::run_paranoid.
runs
Runs test engine: detects sequential correlation by analyzing the lengths of monotonic ascending/descending runs in the output sequence.
serial
Serial test engine: a multi-dimensional generalization of the chi-squared test that checks for correlation between consecutive outputs.
source
Generator sources for Suite::from_urng32/from_urng64/from_rand/from_custom.
suite
Named-case batching (XxxSuite) over the cribler_core engines.
units
Conversions from raw generator words to [0, 1) floats.
verdict

Macros§

suite

Structs§

BirthdayConfig
Configuration for a birthday spacing test.
BirthdayResult
Full result of a single birthday spacing test run.
ChiSqConfig
Configuration for a chi-squared randomness test.
ChiSqResult
Full result of a single chi-squared test run.
KsConfig
Configuration for a Kolmogorov-Smirnov uniformity test.
KsResult
Full result of a single Kolmogorov-Smirnov test run.
McPiConfig
Configuration for a Monte Carlo pi-estimation test.
McPiResult
Full result of a single Monte Carlo pi-estimation test run.
NistConfig
Configuration for the NIST SP 800-22 bit-level test suite.
NistItemResult
Result of a single NIST sub-test.
NistResult
Full result of one run of the NIST SP 800-22 bit-level suite.
ParanoidConfig
Configuration for a paranoid (battery-level) meta-test.
ParanoidResult
Full result of a paranoid meta-test run.
RunsConfig
Configuration for a runs test.
RunsResult
Full result of a single runs test run.
SerialConfig
Configuration for a serial (multi-dimensional uniformity) test.
SerialResult
Full result of a single serial test run.

Enums§

BirthdayError
Errors that can occur while configuring or running a birthday spacing test.
ChiSqError
Errors that can occur while configuring or running a chi-squared test.
KsError
Errors that can occur while configuring or running a KS test.
McPiError
Errors that can occur while configuring or running a Monte Carlo pi test.
NistError
Errors that can occur while configuring or running the NIST suite.
NistTest
The individual NIST SP 800-22 sub-tests implemented by this module.
ParanoidError
Errors that can occur while configuring or running a paranoid meta-test.
RunsError
Errors that can occur while configuring or running a runs test.
SerialError
Errors that can occur while configuring or running a serial test.
Verdict

Functions§

erfc
Complementary error function erfc(x), using the Abramowitz & Stegun 7.1.26 rational approximation (max absolute error ~1.5e-7). Sufficient precision for p-value computation in statistical randomness tests.
igamc
Upper regularized incomplete gamma function Q(a, x) = Gamma(a, x) / Gamma(a), used to derive chi-squared p-values: p = igamc(df / 2, chi2 / 2).
normal_cdf
Standard normal CDF Phi(x), derived from erfc.
p_value_from_z
Converts a two-sided z-score (as produced by crate::chisq::ChiSqResult, crate::serial::SerialResult, or crate::runs::RunsResult) into an approximate p-value, suitable as a trial input to run_paranoid.
run_birthday
Runs a birthday spacing test for the given named 64-bit-word sampler and configuration.
run_chisq
Runs a chi-squared uniformity test for the given named sampler and configuration.
run_ks
Runs a one-sample Kolmogorov-Smirnov uniformity test for the given named sampler and configuration.
run_mcpi
Runs a Monte Carlo pi-estimation test for the given named sampler and configuration.
run_nist
Runs the NIST SP 800-22 bit-level test suite for the given named word-sampler and configuration. word_bits is the number of low bits of each word_sampler() draw that carry entropy (typically 32 or 64).
run_paranoid
Runs a paranoid (battery-level) meta-test: calls trial once per configured trial count, where trial(i) must return the p-value of the i-th independent run of the underlying test, then applies the NIST SP 800-22 4.2 proportion-of-passes and p-value-uniformity checks.
run_runs
Runs a runs test (Wald-Wolfowitz-style, up/down variant) for the given named sampler and configuration.
run_serial
Runs a serial uniformity test for the given named sampler and configuration.
unit_f64_from_u32
Converts a raw 32-bit word into a uniform [0, 1) float.
unit_f64_from_u64
Converts a raw 64-bit word into a uniform [0, 1) float, using the top 53 bits.