Compatibility and Interop
Numerics.NET provides strong reproducibility guarantees and full interoperability with System.Random and other platforms like NumPy. This page explains what reproducibility commitments we make, how to achieve cross-platform compatibility, and how to use modern high-quality generators with existing .NET APIs.
Compatibility in a Nutshell
Numerics.NET makes these key compatibility commitments:
Within the same library version: The same seed, RNG type, and calling pattern produce identical output across machines, operating systems, and .NET runtimes.
NumPy compatibility: Specific RNGs with SeedProfile.Numpy match NumPy bit generator output exactly, including multiple levels of spawned streams.
System.Random interop: Full bidirectional conversion between Numerics.NET random sources and System.Random.
Guarantees and Non-Guarantees
Understanding what Numerics.NET guarantees is essential for reproducible scientific computing and debugging.
What Is Reproducible?
A random number sequence is reproducible if you can regenerate the exact same sequence of values by using the same seed and generation pattern:
// First run
var rng1 = RandomSources.Create(42);
double value1 = rng1.NextDouble();
// Second run with same seed
var rng2 = RandomSources.Create(42);
double value2 = rng2.NextDouble();
// value1 == value2 (exactly)
Console.WriteLine($"Values match: {value1 == value2}");When you create an RNG with the same seed and call the same methods in the same order, you get bit-for-bit identical output.
Within the Same Library Version
Guaranteed: The same seed, RNG type, and calling pattern produce identical output within the same version of Numerics.NET:
// Run on any machine, any OS, any time
var rng = new Pcg64(12345);
var results = new double[10];
rng.Fill(results);
// These 10 values will always be exactly the same
// with Pcg64(12345) in this version of Numerics.NETThis guarantee holds across different machines, operating systems, and .NET runtimes, as long as you use the same Numerics.NET version.
Across Library Versions
Not Guaranteed: Output may change between Numerics.NET versions. We reserve the right to:
Improve algorithms for better performance or quality
Fix bugs in RNG implementations
Change default seeding behavior
If cross-version reproducibility is critical, save and restore RNG state instead of relying on seeds.
NumPy Compatibility
Guaranteed for specific RNGs: When using SeedProfile.Numpy, these generators match NumPy bit generator output exactly:
// This matches NumPy exactly:
// import numpy as np
// rng = np.random.Generator(np.random.PCG64(seed=42))
// rng.random(5)
var rng1 = new Pcg64(42, SeedProfile.Numpy);
var values = new double[5];
rng1.Fill(values);
// values matches NumPy output exactlyThis guarantee applies to the specific RNG algorithm and basic primitive operations. Higher-level distribution sampling may differ due to algorithm choices.
Seeding Profiles
Seed expansion is the process of mapping a small input. typically a single integer, onto the much larger state vector of a modern PRNG (which can range from 256 to 1,000's of bits). Since most high-quality generators require a fully initialized, high-entropy state to function correctly, they cannot simply "zero-fill" the remaining bits without degrading the statistical quality of the stream.
Seed profiles act as the initialization logic for this process. They use mathematical transformations (like a simple split-mix or a more robust hash-based expansion) to ensure that even seeds with low Hamming distance result in linearly independent initial states, effectively preventing inter-stream correlation.
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 Profile
Default uses Numerics.NET's own seeding choices with SplitMix64-based seed expansion and internal stream mixing. This profile makes no external portability guarantees and is the recommended choice for most applications where compatibility with other platforms is not required.
Standard Profile
Standard reproduces the reference or canonical behavior of each RNG family, including reference stream semantics. For example, PCG uses setseq behavior, and xoshiro uses jump-based streams. If no standard behavior is defined for an RNG, the Default profile is used instead.
NumPy Profile
Numpy provides full NumPy compatibility for explicitly supported RNGs: Pcg64, Pcg64Dxsm, Sfc64, and Philox4x64.
For these generators, the NumPy profile must reproduce NumPy behavior end-to-end, including the same SeedSequence semantics, stream handling, internal initialization, and first outputs. This enables exact reproducibility of results from NumPy code.
For other RNGs, the NumPy profile uses NumPy's SeedSequence to derive per-stream seed material, then initializes via Standard or Default rules.
C++11 Profile
CPlusPlus uses std::seed_seq-compatible expansion with stream index incorporation. This profile is most relevant for MersenneTwister64, which implements the full C++11 seed_seq algorithm. For other RNGs, it uses similar seeding strategies but does not guarantee full C++11 compatibility.
Direct Profile
Direct treats provided seed words as internal state directly. Remaining state words are filled with zero, and invariants are validated (e.g., xoshiro non-zero state, PCG odd increment). No seeding logic is applied. This profile is useful for advanced scenarios where you want to set the internal state directly.
The seed profile is specified through the RandomOptions class:
// 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.
What Gets Matched
When using compatibility profiles, different levels of the RNG stack are matched:
Core algorithm: The underlying generator algorithm (e.g., PCG-XSL-RR) produces identical raw output.
Seeding and initialization: The way seeds are expanded to internal state matches the target platform.
Stream semantics: Independent streams are derived using the same mechanism as the target platform.
Primitive operations: Basic methods like NextUInt64() and NextDouble() produce identical output.
Higher-level operations like distribution sampling may differ based on the algorithms chosen for generating random variates, even when the underlying RNG is identical.
How to Opt In
To use a specific compatibility profile, pass it when constructing your RNG:
// NumPy compatibility
var rng = new Pcg64(new RandomOptions(seed: 42, seedProfile: SeedProfile.Numpy));
// Or using the constructor overload
var rng5 = new Pcg64(seed: 42, streamAddress: 0, seedProfile: SeedProfile.Numpy);
// For parallel streams with NumPy compatibility
var rng6 = new Pcg64(new RandomOptions(42, streamAddress: 0, seedProfile: SeedProfile.Numpy));The seed profile only affects initialization. Once created, the RNG behaves identically regardless of which profile was used to initialize it (assuming the same internal state was reached).
System.Random Interoperability
Numerics.NET random sources are fully interoperable with System.Random, allowing you to use modern high-quality generators with existing .NET APIs.
Using Numerics.NET RNGs as System.Random
Convert any Numerics.NET random source to System.Random using the AsRandom() extension method:
// Create a Numerics.NET RNG
var pcg = new Pcg64(42);
// Convert to System.Random
System.Random systemRandom = pcg.AsRandom();
// Use with any API expecting System.Random
int value = systemRandom.Next(100);
double fraction = systemRandom.NextDouble();This allows you to use modern RNGs like PCG64 or Xoshiro256** with any API that expects System.Random, while maintaining full control over seeding and reproducibility.
Using System.Random as a Random Source
Convert a System.Random instance to an IRandomSource using AsRandomSource():
// Get a System.Random instance (could be from a library)
System.Random sysRandom = new Random(42);
// Convert to IRandomSource
IRandomSource randomSource = sysRandom.AsRandomSource();
// Use with Numerics.NET APIs
var vector = Vector.Random(100, randomSource);This is useful when integrating with libraries that provide System.Random instances but you want to use Numerics.NET APIs that expect IRandomSource.
Round-Tripping and Unwrapping
The interop system automatically unwraps adapters when possible to avoid double-wrapping:
// Start with Numerics.NET RNG
var pcg1 = new Pcg64(42);
// Wrap as System.Random
System.Random wrapped = pcg1.AsRandom();
// Convert back - automatically unwraps to original
IRandomSource unwrapped = wrapped.AsRandomSource();
// unwrapped is the same instance as pcg1
bool isSame = ReferenceEquals(pcg1, unwrapped); // trueFor explicit unwrapping, use TryUnwrap() to recover the underlying Numerics.NET RNG from a wrapped System.Random:
var pcg2 = new Pcg64(42);
System.Random wrapped2 = pcg2.AsRandom();
// Try to unwrap to specific type
if (wrapped2.TryUnwrap<Pcg64>(out var recovered))
{
// recovered is the original Pcg64 instance
Console.WriteLine("Successfully unwrapped to Pcg64");
}Practical Scenarios
Here are common scenarios where interop is useful:
Working with Legacy APIs
Many existing .NET libraries accept System.Random. Use AsRandom() to provide a modern generator:
// Example: Using with a legacy API
void LegacyApiRequiringSystemRandom(System.Random rng)
{
// Some legacy code that needs System.Random
var items = new[] { "A", "B", "C", "D", "E" };
int index = rng.Next(items.Length);
}
// Provide a modern RNG with reproducibility
var myRng = new Pcg64(42);
LegacyApiRequiringSystemRandom(myRng.AsRandom());Using System.Random.Shared with Numerics.NET
In .NET 6+, System.Random.Shared provides thread-safe shared randomness. Convert it to use with Numerics.NET APIs:
#if NET6_0_OR_GREATER
// Use System.Random.Shared with Numerics.NET
IRandomSource sharedSource = System.Random.Shared.AsRandomSource();
// Generate random data using Numerics.NET APIs
var randomVector = Vector.Random(100, sharedSource);
var normalDist = new NormalDistribution(0.0, 1.0);
double sample = normalDist.Sample(sharedSource);
#endifNote that System.Random.Shared does not support reproducibility, just like Shared.
Support Matrix and Limitations
The following table summarizes compatibility guarantees by RNG and profile:
RNG | Default | Standard | NumPy | C++11 |
|---|---|---|---|---|
Pcg64, Pcg64Dxsm | Numerics.NET | PCG reference | NumPy exact | N/A |
Philox4x64 | Numerics.NET | Reference | NumPy exact | N/A |
Sfc64 | Numerics.NET | Reference | NumPy exact | N/A |
Xoshiro256**, Xoroshiro128** | Numerics.NET | Reference | SeedSeq only | N/A |
MersenneTwister64 | Numerics.NET | MT19937-64 | SeedSeq only | C++11 exact |
Limitations:
State persistence: System.Random does not support state serialization. If you wrap a System.Random as an IRandomSource, state persistence methods will throw NotSupportedException.
Performance overhead: Wrapping adds a small overhead due to virtual dispatch. For performance-critical hot paths, use Numerics.NET APIs directly rather than through interop.
Thread safety: Wrapping does not add thread safety. If the underlying RNG is not thread-safe, the wrapper won't be either.
Distribution sampling: Higher-level distribution sampling may differ between platforms even when the underlying RNG matches, due to algorithm choices.
Best Practices for Strict Reproducibility
Follow these guidelines to ensure reproducible random number generation:
Always use explicit seeds: Never rely on default or time-based seeding for reproducible work.
Document your seeds and profiles: Record the seeds and seed profiles used in experiments in your code, notebooks, or metadata.
Save library version: Record which version of Numerics.NET you used.
Use state persistence for critical work: For long-term reproducibility, save RNG state rather than just seeds:
// Generate some values
var rng2 = new Pcg64(42);
rng2.Fill(new double[100]);
// Save state
byte[] state = rng2.GetState();
// ... later, or in a different run ...
// Restore state
var rng3 = new Pcg64();
rng3.LoadState(state);
// rng3 continues exactly where rng2 left off
double next1 = rng2.NextDouble();
double next2 = rng3.NextDouble();
// next1 == next2State persistence captures the exact internal state of the generator, allowing you to resume exactly where you left off. This works across versions as long as the RNG algorithm itself doesn't change.
Control draw order: Ensure your code calls RNG methods in a consistent order. Changing the number of values drawn affects all subsequent outputs:
// First pattern: draw 10 values, then check the 11th
var rng1a = RandomSources.Create(42);
rng1a.Fill(new double[10]);
double eleventhValue1 = rng1a.NextDouble();
// Second pattern: draw 5 values, then check the 6th
var rng2a = RandomSources.Create(42);
rng2a.Fill(new double[5]);
double sixthValue = rng2a.NextDouble();
// These are different! Draw count matters.
Console.WriteLine($"Different: {eleventhValue1 != sixthValue}");Avoid shared sources for reproducible work: Shared and parameterless RNG constructors are not reproducible:
// These are NOT reproducible:
// Shared uses unpredictable seeds
double value1a = RandomSources.Shared.NextDouble();
double value2a = RandomSources.Shared.NextDouble(); // Different!
// Parameterless constructor uses cryptographic seeding
var rng4 = new Pcg64(); // Different state each runUse deterministic aggregation in parallel code: Even with independent streams, parallel code may produce different aggregate results if thread execution order varies. Combine results from parallel streams in a reproducible order:
// Each stream is reproducible individually...
var streams = Pcg64.CreateStreams(count: 4, seed: 42);
// But this sum may vary due to thread scheduling:
double sum = 0.0;
Parallel.For(0, 4, i =>
{
double local = streams[i].NextDouble();
lock (new object()) { sum += local; } // Race condition on order!
});
// Solution: Use deterministic aggregation
var partialSums = new double[4];
Parallel.For(0, 4, i =>
{
partialSums[i] = streams[i].NextDouble();
});
double deterministicSum = partialSums.Sum(); // Reproducible!Test reproducibility: Verify reproducibility in your code with simple tests:
// Simple reproducibility test
double[] GetRandomSample(long seed)
{
var rng = RandomSources.Create(seed);
var sample = new double[100];
rng.Fill(sample);
return sample;
}
// Test it
var sample1 = GetRandomSample(42);
var sample2 = GetRandomSample(42);
bool allMatch = sample1.SequenceEqual(sample2);
Console.WriteLine($"Reproducibility test: {(allMatch ? "PASS" : "FAIL")}");Include reproducibility tests in your test suite to catch accidental changes in RNG usage.
Convert at boundaries: When using interop, perform conversions at API boundaries, not in inner loops. Create the wrapper once and reuse it.
Prefer native APIs: When possible, use Numerics.NET random sources directly rather than wrapping as System.Random.
Next Steps
To learn more:
Introduction to Random Sources — Learn the basics of creating reproducible RNGs.
Choosing a Random Number Generator — Select an RNG with the right compatibility guarantees.
Choosing a Random Number Generator — Learn about the available RNG implementations.