Skip to content

Random expressions

KFR provides a compact SIMD-oriented pseudorandom generator for direct draws and for infinite generator expressions. Use an explicitly seeded random_state when a sequence must be repeatable. These APIs are for simulation, test data, dithering, and signal generation; they are not intended for cryptographic use.

State and seeding

A random_state contains the generator's 128-bit state. Initialize it from four 32-bit values with random_init, or from two 64-bit values with random_init:

#include <kfr/base.hpp>

using namespace kfr;

random_state rng = random_init(1u, 2u, 3u, 4u);

The initialization functions advance the supplied seed state once, so the first output is generated state rather than the literal seed words.

On supported x86 builds, the no-argument random_init overload seeds from the CPU cycle counter:

random_state rng = random_init(); // variable seed on supported x86 builds

This overload, and the generator-expression overloads that use it, are absent when cycle-counter access is disabled. In particular, KFR disables it on non-x86 targets. Cycle-counter seeding is convenient for nonrepeatable local noise, but it is not a substitute for explicit deterministic seeds when a result must be reproduced.

Raw bits and direct distributions

random_bits produces a vec<u8, N> and advances the supplied state:

random_state rng = random_init(1u, 2u, 3u, 4u);
auto tag   = random_bits<8>(rng);
auto block = random_bits<64>(rng);

The generator has no leftover-byte cache. Every request of 16 bytes or fewer advances the whole 128-bit state once, even if only one returned byte is used. Larger requests are assembled from 128-bit chunks. Changing request sizes or splitting one request into several requests therefore changes the sequence: random_bits<16>(rng) is not equivalent to four random_bits<4>(rng) calls.

Warning

The PRNG is deterministic only when both the seeds and the complete pattern of draws are the same. A draw consumes one or more 128-bit state chunks rather than a continuous stream of individual bytes. Changing a requested vector width, splitting a request, changing distributions, or changing generator-expression traversal changes subsequent output.

|||TEST_CASE("random.md/draw granularity")
|||{
random_state one_block = random_init(1u, 2u, 3u, 4u);
random_state two_parts = random_init(1u, 2u, 3u, 4u);

auto a  = random_bits<16>(one_block); // advances one 128-bit state block
auto b0 = random_bits<8>(two_parts);  // advances one block
auto b1 = random_bits<8>(two_parts);  // advances another block

// concat(b0, b1) is not equal to a, and the two states now differ.
|||CHECK(concat(b0, b1) != a);
|||CHECK(two_parts.v != one_block.v);
|||}

For reproducible output, retain the same seed, distribution, request sizes, and expression traversal.

The direct distribution functions write no storage; they return SIMD vectors and mutate the random_state passed by reference:

Function Distribution
random_uniform with an integral result type Raw uniformly distributed bit patterns
random_uniform with float or double Uniform values in \([0, 1)\)
random_range Values in \([\mathit{min}, \mathit{max})\)
random_normal Gaussian values from \(N(\mu, \sigma^2)\)
random_state rng = random_init(1u, 2u, 3u, 4u);
auto bits   = random_uniform<uint32_t, 4>(rng);
auto unit   = random_uniform<float, 8>(rng);
auto signed_noise = random_range<8>(rng, -1.0f, 1.0f);
auto gaussian = random_normal<8>(rng, 0.0f, 0.25f); // mu, sigma

The integer range mapping uses scaling rather than rejection sampling. Treat it as KFR's fast range generator rather than relying on exact statistical unbiasedness for every possible integer interval.

Note

random_normal takes mu, sigma, whereas the generator expression described below takes sigma, mu where mu defaults to 0. Name the arguments in a wrapper when that distinction would otherwise be unclear.

Normal samples use the Box-Muller transform. Requesting an odd vector width internally draws an even count and discards one result, so it can consume a different number of raw chunks from an even-width request.

Generator expressions

gen_random_uniform, gen_random_range, and gen_random_normal are the generator-expression factories. They generate data only when KFR evaluates them, so bound them before assigning to a new dynamic container:

random_state rng = random_init(10u, 20u, 30u, 40u);

auto noise = gen_random_range<float>(std::ref(rng), -1.0f, 1.0f);
univector<float> samples = truncate(noise, 4096);

The range factory accepts the inclusive lower and exclusive upper bound. The normal factory's arguments are sigma followed by mu (defaulted to 0):

random_state rng = random_init(10u, 20u, 30u, 40u);
auto distribution = gen_random_normal<float>(std::ref(rng),
                                             0.15f, // sigma
                                             0.50f  // mu
);
univector<float> samples = truncate(distribution, 4096);

Cycle-counter-seeded forms omit the state argument where supported:

// Available only when KFR supports reading the cycle counter.
univector<float> noise = truncate(gen_random_range<float>(-1.0f, 1.0f), 512);

As with other infinite sources, use truncate, a sized render overload, or sink to select how many values to consume. A random expression is a stateful stream, not an index-stable array: reading the same logical index twice produces new values. Different traversal, vector widths, partitioning, or surrounding expression structure can change how the state is consumed.

Owning versus referenced state

The factories distinguish a normal random_state argument from std::ref(state):

random_state rng = random_init(1u, 2u, 3u, 4u);

auto snapshot = gen_random_range<float>(rng, -1.0f, 1.0f); // owns a state copy
auto shared = gen_random_range<float>(std::ref(rng), -1.0f, 1.0f); // refers to rng

Passing rng copies its current state into the expression. Evaluating snapshot does not alter rng; two independently constructed expressions from the same state begin with the same stream. Copying such an expression copies its current state and branches that stream. Pass std::ref(rng) to the gen_random_range overload for shared state.

Passing std::ref(rng) stores a non-owning reference. Evaluating shared advances rng, and copies of shared refer to the same state and interleave one stream. Keep the referenced random_state alive until every expression that references it has been evaluated. Do not return or store a generator expression that refers to a local state object.

See also