Introduction to Random Sources

Random number generators (RNGs) produce sequences of numbers that, for practical purposes, appear to be random. Numerics.NET provides a comprehensive collection of modern high-quality random number generators, including state-of-the-art generators used by NumPy, Julia, .NET, and other major platforms.

This page introduces the core concepts and shows you how to get started quickly with random number generation. For comprehensive guidance on specific tasks, see the topic-specific pages listed in Random Numbers.

Quick Start: Two Ways to Get Random Numbers

Numerics.NET provides two primary ways to obtain random numbers:

Shared Convenience Randomness

For quick, incidental randomness such as initializing weights in a neural network or adding jitter to a visualization, use the thread-safe Shared accessor:

C#
// Quick, convenient randomness (not reproducible)
double value = RandomSources.Shared.NextDouble();
int index = RandomSources.Shared.Next(100);

Shared is safe to use from multiple threads concurrently. It uses thread-local storage internally (similar to System.Random.Shared in .NET 6+), so each thread gets its own generator instance.

  Important

Shared does not produce the same sequence of numbers across runs. Do not use it for experiments, simulations, or any scenario where you need to reproduce results.

Seeded Reproducible Randomness

For reproducible experiments, simulations, and tests, create an explicit random source from a seed using Create(Int64):

C#
// Reproducible randomness from a seed
var rng = RandomSources.Create(42);
double value1 = rng.NextDouble();  // Same value every time with seed 42
double value2 = rng.NextDouble();  // Next value in the sequence

// Create another RNG with the same seed - gets the same sequence
var rng2 = RandomSources.Create(42);
double value3 = rng2.NextDouble();  // Same as value1

The same seed always produces the same sequence of random numbers. This is essential for:

  • Reproducible scientific experiments

  • Debugging simulations

  • Unit testing code that uses randomness

  Note

By default, Create(Int64) returns an Xoshiro256StarStar generator, which offers excellent performance and statistical quality for most applications.

For reproducible code, follow this pattern:

  1. Create one random source at the start of your simulation or experiment.

  2. Pass it to all components that need randomness.

  3. Avoid creating multiple RNGs with the same seed within the same run.

Example:

C#
void RunSimulation(int seed)
{
    // Create one RNG for the entire simulation
    var rng = RandomSources.Create(seed);

    // Pass it to components that need randomness
    var initialState = GenerateInitialState(rng);
    var results = RunMonteCarlo(rng, iterations: 1000);
}

double[] GenerateInitialState(IRandomSource rng)
{
    var state = new double[10];
    for (int i = 0; i < state.Length; i++)
        state[i] = rng.NextDouble();
    return state;
}

double RunMonteCarlo(IRandomSource rng, int iterations)
{
    double sum = 0;
    for (int i = 0; i < iterations; i++)
        sum += rng.NextDouble();
    return sum / iterations;
}

This ensures that your entire simulation consumes numbers from a single, well-defined sequence.

Common Pitfalls

Avoid these common mistakes when using random number generators:

Repeatedly Constructing RNGs

Don't do this:

C#
// AVOID: Creates a new RNG for each call
double GetRandomValue()
{
    var rng = RandomSources.Create(42);
    return rng.NextDouble();
}

Every call to GetRandomValue() creates a fresh RNG with the same seed, so it returns the same value every time. Instead, create the RNG once and reuse it.

Assuming Shared Is Reproducible

Shared is designed for convenience, not reproducibility. If you need to reproduce results, use Create(Int64).

Sharing Mutable RNG Across Threads

Most random sources (created via Create or by directly constructing a generator class) are not thread-safe. Do not share the same RNG instance across threads.

For parallel scenarios, see Parallel Randomness and Independent Streams.

Choosing a Specific Algorithm

While Create(Int64) provides a sensible default, you can explicitly choose a random number generator algorithm by constructing it directly:

C#
// Use PCG64 (NumPy-compatible, recommended default)
var pcg = new Pcg64(42);

// Use Xoshiro256** (.NET 6+ default, very fast)
var xoshiro = new Xoshiro256StarStar(42);

// Use Philox (ideal for parallel/GPU computing)
var philox = new Philox4x64(42);

For detailed guidance on selecting an algorithm, see Choosing a Random Number Generator.

Understanding Random Sources

A random source is an object that produces sequences of pseudo-random numbers. In Numerics.NET, random sources implement the IRandomSource interface, which provides methods for generating random integers, doubles, bytes, and more.

Behind the scenes, each random source wraps a specific generator algorithm (such as PCG64, Xoshiro256**, or Philox). You don't need to understand these details to use random numbers effectively. Simply create a random source and call its methods.

Random Sources vs. System.Random

Numerics.NET random sources provide significant advantages over System.Random:

  • Modern algorithms: State-of-the-art generators (PCG64, Xoshiro256**, Philox) with better statistical quality than the legacy algorithm used in older .NET versions.

  • Guaranteed reproducibility: Same seed produces identical sequences within a library version, essential for scientific computing. System.Random implementation has changed across .NET versions.

  • High-performance bulk generation: Span-first APIs for filling arrays efficiently, often significantly faster than element-by-element loops.

  • Parallel-friendly streams: Built-in support for creating independent streams for parallel workloads without correlation.

  • Seamless interoperability: Convert to/from System.Random when needed for compatibility with existing .NET APIs. See Compatibility and Interop for details.

For new code, prefer Numerics.NET random sources. Use System.Random interop only when integrating with existing APIs that require it.

Creating Independent Streams for Parallel Computing

Modern applications often require multiple independent random number streams, particularly for parallel computing or Monte Carlo simulations. Random source types provide convenient methods for creating multiple independent streams from a single seed using CreateStreams():

C#
// Create 10 independent streams with a fixed base seed
var streams = RandomSources.CreateStreams(count: 10, seed: 42);

// Process data in parallel - each worker uses its own independent stream
Parallel.For(0, 10, i =>
{
    var myRng = streams[i];
    var randomValue = myRng.NextDouble();
});

You can also create streams manually by specifying stream IDs:

C#
// Create generators for different streams
var stream0 = new Pcg64(seed: 12345, streamAddress: 0);
var stream1 = new Pcg64(seed: 12345, streamAddress: 1);
var stream2 = new Pcg64(seed: 12345, streamAddress: 2);

For comprehensive guidance on parallel randomness, including hierarchical stream creation and advanced stream management, see Parallel Randomness and Independent Streams.

Seed Profiles and Initialization

When a generator is initialized from a seed, the seed must be expanded to fill the generator's internal state (typically 128-256 bits). The initialization behavior is controlled by a SeedProfile, which defines how seed material and stream indices are interpreted and expanded into RNG state. The seed profile does not affect the generation algorithm itself, only how the generator is initialized.

The SeedProfile enum provides a unified way to control seeding and stream behavior across all random number generators. Each profile defines whose seeding contract we are matching, if any:

  • Default - Uses Numerics.NET's own seeding choices with SplitMix64-based seed expansion and internal stream mixing. This is the recommended choice for most applications where compatibility with other platforms is not required.

  • Standard - Reproduces the reference or canonical behavior of each RNG family.

  • Numpy - Provides full NumPy compatibility for explicitly supported RNGs (PCG64, Philox, etc.). For these generators, the NumPy profile must reproduce NumPy behavior end-to-end, enabling exact reproducibility of results from NumPy code.

  • CPlusPlus - Uses std::seed_seq-compatible expansion. Most relevant for MersenneTwister64 and Philox4x64.

  • Direct - Treats provided seed words as internal state directly for advanced scenarios.

  • Independent - Provides non-deterministic, process-wide independent stream initialization. Used when no seed is provided to ensure different sequences across runs without sacrificing statistical quality.

The seed profile is specified in the constructor or through the RandomOptions class:

C#
// Default profile (Numerics.NET's choices)
var rng1 = new Pcg64(42, SeedProfile.Default);

// NumPy compatibility
var rng2 = new Pcg64(42, SeedProfile.Numpy);

// Standard/reference behavior
var rng3 = new Pcg64(42, SeedProfile.Standard);

The simple seeded constructors (e.g., new Pcg64(42)) use the Default profile for backward compatibility and ease of use.

Constructors

All random source types expose a standardized set of constructors with consistent parameter ordering:

C#
// Parameterless: uses cryptographic seed
var rng1a = new Pcg64();

// Seed with optional stream address
var rng2a = new Pcg64(seed: 42);
var rng3a = new Pcg64(seed: 42, streamAddress: 0);

// Advanced options with seed profile
var rng4a = new Pcg64(new RandomOptions(42, streamAddress: 0, seedProfile: SeedProfile.Numpy));

// State-based restoration
byte[] state = rng1a.GetState();
var rng5a = new Pcg64(state);

The streamAddress parameter (which corresponds to the StreamAddress in RandomOptions) is used for creating independent parallel streams from the same seed. All parameters except the seed itself are optional with sensible defaults.

Next Steps

Now that you understand the basics of random sources, explore these topics:

See Also

Other Resources