Skip to content

Commit

Permalink
Allow registering as user-supplied RNG (#67)
Browse files Browse the repository at this point in the history
* Change RNG initialization

The actual RNG is only seeded in init() instead of (re)created. This allows for calling dqset.seed(NULL) w/o resetting the RNG to the default type.

* Register RNG as user-defined if option dqrng.register_methods is set

The RNG gets initialized by R via user_unif_init() making it unnecessary to call dqset_seed(NULL).

* Unscramble the seed from R

This way set.seed(<num>) and dqset.seed(<num>) give the same result. It is the task of the RNG to initialize its state in a sensible manner.

* Save and restore previous state of RNGkid

* New methods register_methods() and restore_methods() for registering the RNG
  • Loading branch information
rstub committed Sep 30, 2023
1 parent e938165 commit 7ef97be
Show file tree
Hide file tree
Showing 15 changed files with 338 additions and 36 deletions.
14 changes: 7 additions & 7 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
Package: dqrng
Type: Package
Title: Fast Pseudo Random Number Generators
Version: 0.3.1.3
Version: 0.3.1.4
Authors@R: c(
person("Ralf", "Stubner", email = "ralf.stubner@gmail.com", role = c("aut", "cre")),
person("daqana GmbH", role = "cph"),
person("David Blackman", role = "ctb"),
person("Melissa O'Neill", email = "oneill@pcg-random.org", role = "ctb"),
person("Sebastiano Vigna", email = "vigna@acm.org", role = "ctb"),
person("David Blackman", role = "cph", comment = "Xoroshiro / Xoshiro family"),
person("Melissa O'Neill", email = "oneill@pcg-random.org", role = "cph", comment = "PCG family"),
person("Sebastiano Vigna", email = "vigna@acm.org", role = "cph", comment = "Xoroshiro / Xoshiro family"),
person("Aaron", "Lun", role="ctb"),
person("Kyle", "Butts", role = "ctb", email = "kyle.butts@colorado.edu"),
person("Henrik", "Sloot", role = "ctb")
Expand All @@ -23,10 +23,10 @@ Description: Several fast random number generators are provided as C++
The fast sampling methods support unweighted sampling both with and without
replacement. These functions are exported to R and as a C++ interface and are
enabled for use with the default 64 bit generator from the PCG family,
Xoroshiro128+ and Xoshiro256+ as well as the 64 bit version of the 20 rounds
Threefry engine (Salmon et al., 2011, <doi:10.1145/2063384.2063405>) as
Xoroshiro128+/++ and Xoshiro256+/++ as well as the 64 bit version of the 20
rounds Threefry engine (Salmon et al., 2011, <doi:10.1145/2063384.2063405>) as
provided by the package 'sitmo'.
License: AGPL-3 | file LICENSE
License: AGPL-3
Depends: R (>= 3.5.0)
Imports: Rcpp (>= 0.12.16)
LinkingTo: Rcpp, BH (>= 1.64.0-1), sitmo (>= 2.0.0)
Expand Down
1 change: 1 addition & 0 deletions LICENSE → LICENSE.note
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ URL: https://github.com/imneme/pcg-cpp
File: inst/include/xoshiro.h
Copyright: 2016, 2018 David Blackman and Sebastiano Vigna
2018 Ralf Stubner (daqana GmbH)
2023 Ralf Stubner
License: CC0
URL: http://xoshiro.di.unimi.it/.

Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ export(dqsample)
export(dqsample.int)
export(dqset.seed)
export(generateSeedVectors)
export(register_methods)
export(restore_methods)
importFrom(Rcpp,evalCpp)
useDynLib(dqrng, .registration = TRUE)
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

## Breaking changes

* The default RNG has changed from Xoroshiro128+ to Xoroshiro128++. The older generators Xoroshiro128+ and Xoshiro256+ are still available but should only be used for backward compatibility or generating floating point numbers, i.e. not sampling etc. ([#57](https://github.com/daqana/dqrng/pull/57) fixing [#56](https://github.com/daqana/dqrng/issues/56))
* The default RNG has changed from Xoroshiro128+ to Xoroshiro128++. The older generators Xoroshiro128+ and Xoshiro256+ are still available but should only be used for backward compatibility or for generating floating point numbers, i.e. not sampling etc. ([#57](https://github.com/daqana/dqrng/pull/57) fixing [#56](https://github.com/daqana/dqrng/issues/56))

## Other changes

* Decoupled the 'sitmo' package. It is now possible to use, e.g., the distribution functions from the header-only library without having an explicit `LinkingTo: sitmo`.
* Make the internal RNG accessible from the outside (Henrik Sloot fixing [#41](https://github.com/daqana/dqrng/issues/41) in [#58](https://github.com/daqana/dqrng/pull/58))
* Add Xoroshiro128\*\*/++ and Xoshiro256\*\*/++ to `xoshiro.h`
* Allow uniform and normal distributions to be registered as user-supplied RNG within R. This happens automatically if the option `dqrng.register_methods` is set to `TRUE`.

# dqrng 0.3.1

Expand Down
79 changes: 79 additions & 0 deletions R/register_methods.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
state <- new.env(parent = emptyenv())
state$RNGkind <- rep.int("default", 3)

#' @title Registering as user-supplied RNG
#'
#' @description The random-number generators (RNG) from this package can be
#' registered as user-supplied RNG. This way all \code{r<dist>} functions make
#' use of the provided fast RNGs.
#'
#' @param kind Which methods should be registered? Either \code{"both"} or \code{"rng"}.
#'
#' @return Invisibly returns a three-element character vector of the RNG, normal
#' and sample kinds \emph{before} the call.
#'
#' @details Caveats:
#' \itemize{
#' \item While \code{runif} and \code{dqrunif} as well as \code{rnorm} and
#' \code{dqrnorm} will produce the same results, this is not the case for
#' \code{rexp} and \code{dqrexp}.
#' \item The \code{dqr<dist>} functions are still faster than \code{r<dist>}
#' when many random numbers are generated.
#' \item You can use only the RNG from this package using
#' \code{register_method("rng")} or both the RNG and the Ziggurat method
#' for normal draws with \code{register_method("both")}. The latter
#' approach is used by default. Using only the Ziggurat method will give
#' \emph{undefined} behavior and is not supported!
#' \item Calling \code{dqset.seed(NULL)} re-initializes the RNG from R's RNG.
#' This no longer makes sense when the RNG has been registered as user-supplied
#' RNG. In that case \code{set.seed{NULL}} needs to be used.
#' \item With R's in-build RNGs one can get access to the internal state using
#' \code{.Random.seed}. This is not possible here, since the internal state
#' is a private member of the used C++ classes.
#' }
#'
#' You can automatically register these methods when loading this package by
#' setting the option \code{dqrng.register_methods} to \code{TRUE}, e.g.
#' with \code{options(dqrng.register_methods=TRUE)}.
#'
#' Notes on seeding:
#' \itemize{
#' \item When a user-supplied RNG is registered, it is also seeded from the
#' previously used RNG. You will therefore get reproducible (but different)
#' whether you call \code{set.seed()} before or after \code{register_methods()}.
#' \item When called with a single integer as argument, both \code{set.seed()}
#' and \code{dqset.seed()} have the same effect. However, \code{dqset.seed()}
#' allows you to call it with two integers thereby supplying 64 bits of
#' initial state instead of just 32 bits.
#' }
#' @seealso \code{\link{RNGkind}} and \code{\link{Random.user}}
#'
#' @examples
#' register_methods()
#' # set.seed and dqset.seed influence both (dq)runif and (dq)rnorm
#' set.seed(4711); runif(5)
#' set.seed(4711); dqrunif(5)
#' dqset.seed(4711); rnorm(5)
#' dqset.seed(4711); dqrnorm(5)
#' # similarly for other r<dist> functions
#' set.seed(4711); rt(5, 10)
#' dqset.seed(4711); rt(5, 10)
#' # but (dq)rexp give different results
#' set.seed(4711); rexp(5, 10)
#' set.seed(4711); dqrexp(5, 10)
#' restore_methods()

#' @rdname user-supplied-rng
#' @export
register_methods <- function(kind = c("both", "rng")) {
kind <- match.arg(kind)
switch(kind,
both = state$RNGkind <- RNGkind("user", "user"),
rng = state$RNGkind <- RNGkind("user"))
}

#' @rdname user-supplied-rng
#' @export
restore_methods <- function() {
RNGkind(state$RNGkind[1], state$RNGkind[2], state$RNGkind[3])
}
19 changes: 14 additions & 5 deletions R/zzz.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
.onLoad <- function(libname, pkgname) {
if (!exists(".Random.seed", mode="numeric", envir=globalenv()))
set.seed(NULL)
original_seed <- get(".Random.seed", mode="numeric", envir=globalenv())
on.exit(assign(".Random.seed", original_seed, envir=globalenv()))
dqset_seed(seed = NULL, stream = NULL)
if (getOption("dqrng.register_methods", FALSE))
register_methods()
else {
if (!exists(".Random.seed", mode="numeric", envir=globalenv()))
set.seed(NULL)
original_seed <- get(".Random.seed", mode="numeric", envir=globalenv())
on.exit(assign(".Random.seed", original_seed, envir=globalenv()))
dqset_seed(seed = NULL, stream = NULL)
}
}

.onUnload <- function(libpath) {
if (getOption("dqrng.register_methods", FALSE))
restore_methods()
}
15 changes: 15 additions & 0 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ u2 <- dqrunif(N)
cor(u1, u2)
```

It is also possible to register the supplied generators as user-supplied RNGs. This way `set.seed()` and `dqset.seed()` influence both `(dq)runif` and `(dq)rnorm` in the same way. This is also true for other `r<dist>` functions, but note that `rexp` and `dqrexp` still give different results:

```{r register}
register_methods()
set.seed(4711); runif(5)
set.seed(4711); dqrunif(5)
dqset.seed(4711); rnorm(5)
dqset.seed(4711); dqrnorm(5)
set.seed(4711); rt(5, 10)
dqset.seed(4711); rt(5, 10)
set.seed(4711); rexp(5, 10)
set.seed(4711); dqrexp(5, 10)
restore_methods()
```

## Feedback

All feedback (bug reports, security issues, feature requests, ...) should be provided as [issues](https://github.com/daqana/dqrng/issues).
55 changes: 41 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ build-in RNGs:
library(dqrng)
dqset.seed(42)
dqrunif(5, min = 2, max = 10)
#> [1] 9.211802 2.616041 6.236331 4.588535 5.764814
#> [1] 9.266963 4.644899 9.607483 3.635770 4.742639
dqrexp(5, rate = 4)
#> [1] 0.35118613 0.17656197 0.06844976 0.16984095 0.10096744
#> [1] 0.111103883 0.084289794 0.003414377 0.042012033 0.143914583
```

They are quite a bit faster, though:
Expand All @@ -61,8 +61,8 @@ bm[, 1:4]
#> # A tibble: 2 × 4
#> expression min median `itr/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl>
#> 1 rnorm(N) 612µs 685.2µs 1397.
#> 2 dqrnorm(N) 86µs 88.6µs 10388.
#> 1 rnorm(N) 607µs 660.2µs 1451.
#> 2 dqrnorm(N) 89.8µs 92.7µs 9896.
```

This is also true for the provided sampling functions with replacement:
Expand All @@ -79,10 +79,10 @@ bm[, 1:4]
#> # A tibble: 4 × 4
#> expression min median `itr/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl>
#> 1 sample.int(m, n, replace = TRUE) 6.88ms 7.63ms 114.
#> 2 sample.int(1000 * m, n, replace = TRUE) 8.72ms 9.55ms 96.1
#> 3 dqsample.int(m, n, replace = TRUE) 482.21µs 810.29µs 1254.
#> 4 dqsample.int(1000 * m, n, replace = TRUE) 492.79µs 822.86µs 1275.
#> 1 sample.int(m, n, replace = TRUE) 6.88ms 7.08ms 139.
#> 2 sample.int(1000 * m, n, replace = TRUE) 8.72ms 8.93ms 110.
#> 3 dqsample.int(m, n, replace = TRUE) 410.9µs 434.24µs 2137.
#> 4 dqsample.int(1000 * m, n, replace = TRUE) 397.74µs 435.38µs 1930.
```

And without replacement:
Expand All @@ -100,11 +100,11 @@ bm[, 1:4]
#> # A tibble: 5 × 4
#> expression min median `itr/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl>
#> 1 sample.int(m, n) 40.1ms 42.54ms 23.5
#> 2 sample.int(1000 * m, n) 12.19ms 14.38ms 67.8
#> 3 sample.int(m, n, useHash = TRUE) 9.43ms 11.17ms 81.9
#> 4 dqsample.int(m, n) 1.22ms 1.35ms 638.
#> 5 dqsample.int(1000 * m, n) 1.98ms 2.51ms 358.
#> 1 sample.int(m, n) 22.73ms 24.05ms 36.9
#> 2 sample.int(1000 * m, n) 12.07ms 13.85ms 68.1
#> 3 sample.int(m, n, useHash = TRUE) 9.57ms 12.63ms 74.4
#> 4 dqsample.int(m, n) 1.11ms 1.2ms 696.
#> 5 dqsample.int(1000 * m, n) 1.95ms 2.69ms 293.
```

Note that sampling from `10^10` elements triggers “long-vector support”
Expand All @@ -120,7 +120,34 @@ u1 <- dqrunif(N)
dqset.seed(42, 2)
u2 <- dqrunif(N)
cor(u1, u2)
#> [1] -0.0005787967
#> [1] 0.0009574617
```

It is also possible to register the supplied generators as user-supplied
RNGs. This way `set.seed()` and `dqset.seed()` influence both
`(dq)runif` and `(dq)rnorm` in the same way. This is also true for other
`r<dist>` functions, but note that `rexp` and `dqrexp` still give
different results:

``` r
register_methods()
set.seed(4711); runif(5)
#> [1] 0.3143534 0.7835753 0.1443660 0.1109871 0.6433407
set.seed(4711); dqrunif(5)
#> [1] 0.3143534 0.7835753 0.1443660 0.1109871 0.6433407
dqset.seed(4711); rnorm(5)
#> [1] -0.3618122 0.8199887 -0.4075635 0.2073972 -0.8038326
dqset.seed(4711); dqrnorm(5)
#> [1] -0.3618122 0.8199887 -0.4075635 0.2073972 -0.8038326
set.seed(4711); rt(5, 10)
#> [1] -0.3196113 -0.4095873 -1.2928241 0.2399470 -0.1068945
dqset.seed(4711); rt(5, 10)
#> [1] -0.3196113 -0.4095873 -1.2928241 0.2399470 -0.1068945
set.seed(4711); rexp(5, 10)
#> [1] 0.0950560698 0.0567150561 0.1541222748 0.2512966671 0.0002175758
set.seed(4711); dqrexp(5, 10)
#> [1] 0.03254731 0.06855303 0.06977124 0.02579004 0.07629535
restore_methods()
```

## Feedback
Expand Down
14 changes: 13 additions & 1 deletion inst/include/dqrng_generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@

#include <type_traits>
#include <stdexcept>
#include <dqrng_types.h>
#include <xoshiro.h>
#include <pcg_random.hpp>
#include <dqrng_types.h>

namespace dqrng {
using default_64bit_generator = ::dqrng::xoroshiro128plusplus;
Expand Down Expand Up @@ -185,6 +185,18 @@ inline void random_64bit_wrapper<pcg64>::seed(result_type seed, result_type stre
}


template<typename RNG = default_64bit_generator>
typename std::enable_if<!std::is_base_of<random_64bit_generator, RNG>::value, rng64_t>::type
generator () {
return std::make_shared<random_64bit_wrapper<RNG>>();
}

template<typename RNG = default_64bit_generator>
typename std::enable_if<std::is_base_of<random_64bit_generator, RNG>::value, rng64_t>::type
generator () {
return std::make_shared<RNG>();
}

template<typename RNG = default_64bit_generator>
typename std::enable_if<!std::is_base_of<random_64bit_generator, RNG>::value, rng64_t>::type
generator (uint64_t seed) {
Expand Down
8 changes: 4 additions & 4 deletions man/dqrng-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7ef97be

Please sign in to comment.