Testing a generator package
Source:vignettes/articles/generator-packages.Rmd
generator-packages.RmdA package that provides random number generators should be able to say how its streams do in ‘PractRand’, the test suite the literature on generator design reports against. This article shows two ways to do that with zurngtest: from R, with any function that returns the generator’s bits, and from C, where ‘PractRand’ reads the generator’s words with no R in the loop.
The examples use zurand, a package of counter-based generators. zurand is not on CRAN yet, so its example is shown but not run.
From R
pract_fun() turns any R function that returns
n words into a source. zurand’s generators are stateless:
rng_bits(key, n) is the first n words of the
stream that key names. A long stream therefore takes each
chunk from its own substream, rng_fold(key, i) for the
ith chunk, the way zurand describes substreams.
library(zurand)
key <- rng_key(2024)
chunk <- 0
philox_words <- function(n) {
chunk <<- chunk + 1
words <- rng_bits(rng_fold(key, chunk), n, bits = 32)
# 32-bit values as doubles, to the integers with the same bit patterns
words[words >= 2^31] <- words[words >= 2^31] - 2^32
suppressWarnings(as.integer(words))
}
pract_test(pract_fun(philox_words), min_bytes = 2^20, max_bytes = 2^22)Runs this short are smoke tests. ‘PractRand’ results that matter for a generator come from runs of gigabytes to terabytes, which belong in a continuous-integration job or a long-running session rather than in a package’s checks on CRAN.
From C
A generator written in C runs fastest when ‘PractRand’ asks it for
words directly. zurngtest registers a C function for that, declared in
its header zurngtest.h. A generator package adds
zurngtest to LinkingTo (and to
Suggests), writes a fill function, and calls
zurngtest_test_cfun() from C code that R calls through
.Call():
#include <zurngtest.h>
typedef struct { uint32_t x; } xorshift32;
static void fill(void *state, uint32_t *out, size_t n) {
xorshift32 *g = state;
for (size_t i = 0; i < n; i++) {
g->x ^= g->x << 13;
g->x ^= g->x >> 17;
g->x ^= g->x << 5;
out[i] = g->x;
}
}
SEXP test_xorshift32(SEXP options) {
xorshift32 g = {2463534242u};
return zurngtest_test_cfun(fill, &g, options);
}options is a named list of pract_test()‘s
other arguments, such as
list(min_bytes = 2^20, max_bytes = 2^30), and the result is
the same data frame pract_test() returns. The function is
found with R_GetCCallable(), so nothing links to zurngtest
at build time, and the fill function runs on the main thread. Through
this path zurngtest tests words at about the speed of ’PractRand’’s own
RNG_test.
zurand’s own tests build a small package against both headers and test a zurand stream this way.
Reading a result
The result has one row per test and length, with ‘PractRand’’s own
evaluation. Its failed_at attribute is the first length at
which any result was a failure, or NA:
lcg_low_bits <- local({
x <- 1
function(n) {
out <- raw(n)
for (i in seq_len(n)) {
x <<- (1664525 * x + 1013904223) %% 2^32
out[i] <- as.raw(x %% 256)
}
out
}
})
res <- pract_test(pract_fun(lcg_low_bits, bits = 8), min_bytes = 2^12, max_bytes = 2^16)
attr(res, "failed_at")
#> [1] 4096
head(as.data.frame(res)[order(res$p_value), ], 3)
#> bytes test p_value evaluation
#> 3 4096 Gap-16:A 0 FAIL !!!!!!!!
#> 4 4096 Gap-16:B 0 FAIL !!!!!!!!
#> 8 4096 FPF/16:all 0 FAIL !!!!!!!The low byte of a power-of-two linear congruential generator repeats every 256 values, which ‘PractRand’ finds in the first few kilobytes.