
<!-- README.md is generated from README.Rmd. Please edit that file -->

# Ironseed

<!-- badges: start -->

[![R-CMD-check](https://github.com/reedacartwright/ironseed/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/reedacartwright/ironseed/actions/workflows/R-CMD-check.yaml)
[![Codecov test
coverage](https://codecov.io/gh/reedacartwright/ironseed/graph/badge.svg)](https://app.codecov.io/gh/reedacartwright/ironseed)
[![CRAN
status](https://www.r-pkg.org/badges/version/ironseed)](https://CRAN.R-project.org/package=ironseed)
<!-- badges: end -->

## Overview

Ironseed is an R package that improves seeding for R’s built in random
number generators. An ironseed is a finite-entropy (or fixed-entropy)
hash digest that can be used to generate an unlimited sequence of seeds
for initializing the state of a random number generator. It is inspired
by the work of M.E. O’Neill and others
\[[1](https://www.pcg-random.org/posts/developing-a-seed_seq-alternative.html),
[2](https://www.pcg-random.org/posts/simple-portable-cpp-seed-entropy.html),
[3](https://gist.github.com/imneme/540829265469e673d045)\].

An ironseed is a 256-bit hash digest constructed from a variable-length
sequence of 32-bit inputs. Each ironseed consists of eight 32-bit
sub-digests. The sub-digests are values of 32-bit multilinear hashes
\[[4](https://arxiv.org/pdf/1202.4961)\] that accumulate entropy from
the input sequence. Each input is included in every sub-digest. The
coefficients for the multilinear hashes are generated by a [Weyl
sequence](https://en.wikipedia.org/wiki/Weyl_sequence).

Multilinear hashes are also used to generate an output seed sequence
from an ironseed. Each 32-bit output value is generated by uniquely
hashing the sub-digests. The coefficients for the output are generated
by a second Weyl sequence.

To improve the observed randomness of each hash output, bits are mixed
using a finalizer adapted from SplitMix64
\[[5](https://doi.org/10.1145/2714064.2660195)\]. With the additional
mixing from the finalizer, the output seed sequence passes PractRand
tests \[[6](https://pracrand.sourceforge.net/)\].

## Installation

``` r
# Install the released version of the package from CRAN as usual:
install.packages("ironseed")

# Or the development version from GitHub:
# install.packages("pak")
pak::pak("reedacartwright/ironseed")
```

## Examples

### User Seeding

Ironseed can be used at the top of a script to robustly initialize R’s
builtin random number generator. The resulting ironseed is returned
invisibly, and a message is generated notifying the user that
initialization has occurred. This message can be logged and later used
to reproduce the run.

``` r
#!/usr/bin/env -S Rscript --vanilla
ironseed::ironseed("Experiment", 20251031, 1)
#> ** Ironseed v0.3.0: SetSeed L1S8a59jKMV-MXX2GrS1hrQ-nYwzqL14Rb2-yiBwupsfZUf
runif(10)
#>  [1] 0.20771860 0.05685812 0.46064640 0.03253283 0.21827857 0.23708437
#>  [7] 0.09678753 0.93051076 0.68834723 0.70048683
```

If your script is intended to be called multiple times as part of a
large study, you can also seed based on the command line arguments.

``` r
#!/usr/bin/env -S Rscript --vanilla
args <- commandArgs(trailingOnly = TRUE)
ironseed::ironseed("A Simulation Script 1", args)
#> ** Ironseed v0.3.0: SetSeed aGTMGJZrmQb-MmRAwvmajQ1-pEAyRkyfSWR-XvjHyVajxJW
runif(10)
#>  [1] 0.1990489 0.7884689 0.2698110 0.4761755 0.4127876 0.2025955 0.6249008
#>  [8] 0.9778754 0.8534247 0.3009149
```

Specific command line arguments can also be used. For large, nested
studies, it is useful for scripts to support seeding using multiple
seeds. Ironseed makes this easy to accomplish.

``` r
#!/usr/bin/env -S Rscript --vanilla
args <- commandArgs(trailingOnly = TRUE)
ironseed::ironseed("A Simulation Script 2", args[grepl("--seed=", args)])
#> ** Ironseed v0.3.0: SetSeed KnabwooccdH-Fr7UKEkKauC-MGJNQ4AjqDN-SnKkDqUT8Ch
runif(10)
#>  [1] 0.03714270 0.58069848 0.86104299 0.01920254 0.71453447 0.95131078
#>  [7] 0.42456432 0.68626742 0.58047906 0.87471196
```

### Automatic Seeding

Ironseed can also automatically initialize the random number generator
using an ironseed constructed from multiple sources of entropy. This
occurs if no data is passed to `ironseed()`.

``` r
#!/usr/bin/env -S Rscript --vanilla
ironseed::ironseed()
#> ** Ironseed v0.3.0: SetSeed MMP7kaVgit3-fdj8mH2Yuvb-XP4HvV9Z11e-tMES5isR5we
runif(10)
#>  [1] 0.1709656 0.9549136 0.9939423 0.7165402 0.9792717 0.5409108 0.4919766
#>  [8] 0.9735896 0.2513371 0.2296586

# Access the ironseed that was used for RNG seeding.
fe <- ironseed::get_ironseed()
fe
#> Ironseed: MMP7kaVgit3-fdj8mH2Yuvb-XP4HvV9Z11e-tMES5isR5we
```

Or achieving the same thing with one call. Note that the automatically
generated seed is different from the previous run.

``` r
#!/usr/bin/env -S Rscript --vanilla
fe <- ironseed::ironseed()
#> ** Ironseed v0.3.0: SetSeed XHtfF9L8fZ5-363rX2NV41B-Gu2D9ba1eNX-aqmBpurYLje
runif(10)
#>  [1] 0.48126361 0.86350382 0.62432849 0.58320541 0.02967282 0.21307074
#>  [7] 0.03946363 0.57550078 0.85126966 0.99778787
fe
#> Ironseed: XHtfF9L8fZ5-363rX2NV41B-Gu2D9ba1eNX-aqmBpurYLje
```

### Reproducible Code

An ironseed can also be used directly to reproduce a previous
initialization. This is most useful when automatic seeding has been
used, and the previously generated seed has been logged.

``` r
#!/usr/bin/env -S Rscript --vanilla
ironseed::ironseed("RW7vjwjeiHF-QG7RYPvrntR-6tGPoi65sVc-N1n5SQi5RH4")
#> ** Ironseed v0.3.0: SetSeed RW7vjwjeiHF-QG7RYPvrntR-6tGPoi65sVc-N1n5SQi5RH4
runif(10)
#>  [1] 0.89365116 0.02137079 0.95990727 0.17105063 0.59927593 0.98808313
#>  [7] 0.97298012 0.62492468 0.28697049 0.14086705
```

## Analysis

### Avalanche

A good hash function has good avalanche properties. If we change one bit
of information in the input, our goal is to change 50% of the bits in
the output. To test this we, will first build a function to construct a
random pair of ironseeds that differ by a single input bit.

``` r
rand_fe_pair <- function(w) {
  x <- sample(0:1, w, replace=TRUE)
  n <- sample(seq_along(x), 1)
  y <- x
  y[n] <- if(y[n] == 1) 0L else 1L
  x <- packBits(x, "integer")
  y <- packBits(y, "integer")
  x <- ironseed::ironseed(x, set_seed = FALSE)
  y <- ironseed::ironseed(y, set_seed = FALSE)
  list(x = x, y = y)
}
```

Next we will generate 100,000 pairs using 32-bit inputs. We will use R’s
built-in seeding algorithm so that the results are independent of
Ironseed’s seeding algorithm. We will also measure how many hash bits
were flipped by flipping one input bit.

``` r
set.seed(20251220)
z <- replicate(100000, rand_fe_pair(32), simplify = FALSE)
dat <- sapply(z, \(a) sum(intToBits(a$x) != intToBits(a$y)))
```

``` r
mean(dat) # expectation: 128
#> [1] 127.9878
sd(dat) # expectation: 8
#> [1] 8.025923
hist(dat, breaks = 86:170, main = NULL)
```

<img src="man/figures/README-analysis_32-1.png" width="100%" />

We will repeat the same analysis for 256-bit inputs.

``` r
set.seed(20251221)
z <- replicate(100000, rand_fe_pair(256), simplify = FALSE)
dat <- sapply(z, \(a) sum(intToBits(a$x) != intToBits(a$y)))
mean(dat) # expectation: 128
#> [1] 127.955
sd(dat) # expectation: 8
#> [1] 7.980782
hist(dat, breaks = 86:170, main = NULL)
```

<img src="man/figures/README-analysis_256-1.png" width="100%" />

As one can see, the avalanche behavior of the input hash is excellent.
