Random numbers#

How do I fill an array with reproducible randoms?

Construct an aether::random::Generator with a seed and call .normal<Real, D>(mean, sd) or .uniform<Real, D>(lo, hi) per sample — the same seed always produces the exact same stream, whether you draw it on the host or inside a GPU kernel, and regardless of what order the draws happen in.

One generator, both arms#

aether::random::Generator (aether/random/Generator.h) is a tiny, trivially-copyable value carrying one mixed 64-bit key — pass it by value into a kernel exactly like a View. There is no global mutable RNG state anywhere in this module: every draw is a pure function of (seed, sample id, sub-counter), built on aether’s own counter-based Philox4x32-10 generator, so a parallel fill and a serial fill agree exactly, and a host build and a device build draw the identical stream for the same seed. rng.substream(k) derives an independent, decorrelated sub-stream from the same generator when you need more than one distinct stream from one seed.

The distribution surface covers uniform, normal, lognormal, exponential, bernoulli, uniformInt and a correlated multivariateNormal (given a mean Item and a Cholesky factor, aether::random::LowerTriangular). Parameters are always a scalar or a small Item — never a full expression — since that already covers every real call site this module was built to serve.

The free-function shortcut#

For the common case where you either do not care about the seed or want one chosen once, aether/random/free.h offers the same distributions as free functions: aether::random::normal<double,3>(mean, sd) uses a fixed, reproducible default seed, and aether::random::seed(s) is a named Generator factory for when the seed does matter — there is deliberately no settable AMBIENT seed (a host-set device global runs into real trouble in multi-library GPU builds), so seed(s) or a Generator you build yourself are the only ways to control it.

A runnable example#

The free function and the equivalent explicit Generator produce the SAME stream, element for element:

aether::Array<double, 3> a(pn);
fillSerial(a, pn, [](auto& v, SampleIndex i) { v[i] = fr::normal<double, 3>(0.5, 2.0); });
aether::Array<double, 3> b(pn);
fillSerial(b, pn, [](auto& v, SampleIndex i) { v[i] = fr::Generator(fr::DEFAULT_SEED).normal<double, 3>(0.5, 2.0); });
expectIdentical(a.hostView(), b.hostView(), pn);

fr here is aether::random; fr::normal<double,3>(0.5, 2.0) (the default-seed free function) and fr::Generator(fr::DEFAULT_SEED).normal< double,3>(0.5, 2.0) (the same seed spelled out explicitly) are exactly the same call underneath.