Watkins Random Number Generator — Step-by-Step Coding TutorialNote: “Watkins Random Number Generator” is not a widely established or standardized RNG name in the literature (as of my knowledge). For this tutorial I’ll treat it as a plausible custom or pedagogical algorithm inspired by common RNG design patterns (linear feedback, xorshift, multiply-with-carry, and combined generators). The article will: explain RNG basics, present a clear design for a Watkins RNG, give step-by-step implementation in C, Python, and Rust, include testing and statistical checks, optimization tips, and usage examples.
Overview: what an RNG must provide
A good pseudo-random number generator (PRNG) should provide:
- Determinism — same seed yields same sequence (useful for reproducibility).
- Uniformity — values should be distributed evenly across the target range.
- Long period — sequence should not repeat too soon.
- Speed — generate numbers efficiently.
- Statistical quality — pass standard randomness tests (Dieharder, TestU01, PractRand).
- Portability — runs consistently across platforms when required.
Design goals for the “Watkins RNG”
For this tutorial the Watkins RNG will aim to be:
- Simple to understand and implement.
- Fast in software with small state (128 bits).
- Better than trivial LCGs for many uses (but not necessarily cryptographically secure).
- Easily seedable and testable.
Key choices:
- Use a combined approach: a 64-bit xorshift* style step plus a multiply-with-carry (MWC) or Weyl sequence combined by addition/xor to improve period and distribution.
- Keep state as two 64-bit words (stateA, stateB) and a 64-bit increment (weyl).
- Provide 64-bit outputs and helpers for floats in [0,1).
Algorithm specification (Watkins RNG v1)
State:
- uint64_t s0, s1; // core state words
- uint64_t w; // Weyl increment (odd)
Seed:
- Provide a nonzero seed to initialize s0, s1, and w. If seed is a single 64-bit value, split it with a small scramble.
Step (next_u64):
- s0 ^= s0 << 23;
- s0 ^= s0 >> 17;
- s0 ^= s1 ^ (s1 >> 26);
- s1 = rotate_left(s1, 32) + some_constant;
- w += 0x9E3779B97F4A7C15; // golden ratio Weyl increment
- result = (s0 + s1) ^ w;
- Return result * 0x2545F4914F6CDD1D (a scramble multiplier)
Notes:
- Steps 1–3 are an xorshift-like scrambling on s0 using s1.
- Step 4 mixes s1 with rotation and addition for diffusion.
- Weyl sequence (step 5) avoids short cycles that can plague some xorshift families.
- Final multiply and xor scramble increases avalanche and disperses low-bit linearity.
Expected period: roughly on the order of 2^127 for well-chosen constants and nondegenerate seeds; exact period depends on the linear recurrence properties.
C implementation (step-by-step)
Header and helpers:
#include <stdint.h> #include <stddef.h> static inline uint64_t rotl64(uint64_t x, int k) { return (x << k) | (x >> (64 - k)); }
Watkins RNG struct and init:
typedef struct { uint64_t s0, s1, w; } watkins_rng_t; void watkins_seed(watkins_rng_t *r, uint64_t seed) { // split seed with SplitMix64 to initialize state robustly uint64_t z = seed + 0x9E3779B97F4A7C15ULL; uint64_t splitmix64(void) { z = (z + 0x9E3779B97F4A7C15ULL); uint64_t v = z; v = (v ^ (v >> 30)) * 0xBF58476D1CE4E5B9ULL; v = (v ^ (v >> 27)) * 0x94D049BB133111EBULL; return v ^ (v >> 31); } r->s0 = splitmix64(); r->s1 = splitmix64(); r->w = splitmix64() | 1ULL; // make odd }
Next function:
uint64_t watkins_next_u64(watkins_rng_t *r) { uint64_t s0 = r->s0; uint64_t s1 = r->s1; s0 ^= s0 << 23; s0 ^= s0 >> 17; s0 ^= s1 ^ (s1 >> 26); s1 = rotl64(s1, 32) + 0x9E3779B97F4A7C15ULL; r->s0 = s0; r->s1 = s1; r->w += 0x9E3779B97F4A7C15ULL; uint64_t result = (s0 + s1) ^ r->w; return result * 0x2545F4914F6CDD1DULL; }
Helpers for floats:
double watkins_next_double(watkins_rng_t *r) { // Generate 53-bit precision double in [0,1) uint64_t v = watkins_next_u64(r); // take top 53 bits return (v >> 11) * (1.0 / 9007199254740992.0); }
Python implementation
import struct class WatkinsRNG: def __init__(self, seed): self.z = (seed + 0x9E3779B97F4A7C15) & ((1<<64)-1) self.s0 = self._splitmix64() self.s1 = self._splitmix64() self.w = self._splitmix64() | 1 def _splitmix64(self): self.z = (self.z + 0x9E3779B97F4A7C15) & ((1<<64)-1) v = self.z v = (v ^ (v >> 30)) * 0xBF58476D1CE4E5B9 & ((1<<64)-1) v = (v ^ (v >> 27)) * 0x94D049BB133111EB & ((1<<64)-1) return (v ^ (v >> 31)) & ((1<<64)-1) @staticmethod def _rotl(x, k): return ((x << k) | (x >> (64 - k))) & ((1<<64)-1) def next_u64(self): s0 = self.s0 s1 = self.s1 s0 ^= (s0 << 23) & ((1<<64)-1) s0 ^= s0 >> 17 s0 ^= s1 ^ (s1 >> 26) s1 = WatkinsRNG._rotl(s1, 32) + 0x9E3779B97F4A7C15 & ((1<<64)-1) self.s0 = s0 & ((1<<64)-1) self.s1 = s1 & ((1<<64)-1) self.w = (self.w + 0x9E3779B97F4A7C15) & ((1<<64)-1) result = (self.s0 + self.s1) ^ self.w return (result * 0x2545F4914F6CDD1D) & ((1<<64)-1) def next_double(self): return (self.next_u64() >> 11) * (1.0 / 9007199254740992.0)
Rust implementation
pub struct WatkinsRng { s0: u64, s1: u64, w: u64, z: u64, } impl WatkinsRng { pub fn from_seed(seed: u64) -> Self { let mut r = WatkinsRng { s0:0, s1:0, w:0, z: seed.wrapping_add(0x9E3779B97F4A7C15) }; r.s0 = r.splitmix64(); r.s1 = r.splitmix64(); r.w = r.splitmix64() | 1; r } fn splitmix64(&mut self) -> u64 { self.z = self.z.wrapping_add(0x9E3779B97F4A7C15); let mut v = self.z; v = (v ^ (v >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); v = (v ^ (v >> 27)).wrapping_mul(0x94D049BB133111EB); v ^ (v >> 31) } #[inline] fn rotl(x: u64, k: u32) -> u64 { x.rotate_left(k) } pub fn next_u64(&mut self) -> u64 { let mut s0 = self.s0; let mut s1 = self.s1; s0 ^= s0 << 23; s0 ^= s0 >> 17; s0 ^= s1 ^ (s1 >> 26); s1 = WatkinsRng::rotl(s1, 32).wrapping_add(0x9E3779B97F4A7C15); self.s0 = s0; self.s1 = s1; self.w = self.w.wrapping_add(0x9E3779B97F4A7C15); let result = (s0.wrapping_add(s1)) ^ self.w; result.wrapping_mul(0x2545F4914F6CDD1D) } }
Testing and statistical checks
- Start with basic unit tests: reproducibility for same seed, different seeds give different sequences, and no immediate zeros for degenerate seeds.
- Empirical checks:
- Frequency histogram for 2^20 outputs should be approximately uniform.
- Autocorrelation should be low; compute Pearson correlation for lag 1..k.
- Birthday spacing and gap tests.
- Run standard test suites:
- PractRand (recommended for performance-oriented RNGs).
- TestU01 (SmallCrush, Crush, BigCrush) if available.
- Dieharder.
- Expectation: Watkins RNG v1 should pass many basic tests but may fail deep batteries; if so, iterate on constants and mixing.
Performance and optimization tips
- Use compiler intrinsics for rotation where available (rotl/ror).
- Keep state in registers; avoid memory writes when generating bursts (buffer outputs).
- Multiply constants chosen for avalanche should be odd and have good bit-mixing properties.
- For vectorized generation, consider parallel independent streams by varying the Weyl increment or seeds.
Use cases and limitations
- Suitable for simulations, games, procedural generation, and Monte Carlo where cryptographic strength is NOT required.
- Not suitable for cryptography, secure token generation, or anywhere adversarial predictability is a concern.
- If cryptographic security is needed, use ChaCha20, AES-CTR DRBG, or libsodium/OS-provided CSPRNGs.
Variants and improvements
- Increase state size (e.g., 256-bit) and combine multiple xorshift or xoroshiro streams.
- Replace Weyl increment with a full MWC for stronger period guarantees.
- Use PCG-style output functions (xorshift + random rotate) for different statistical profiles.
- Add jump functions to create independent substreams.
Example: seeding strategy recommendations
- Use SplitMix64 (shown above) to expand a single seed into internal state words.
- Avoid seeds that produce zero-state or trivial cycles.
- For reproducible parallel streams, derive per-thread seeds with a high-quality generator or a hash function (e.g., SHA-256) of a base seed plus stream index.
Conclusion
Watkins RNG v1 presented here is a compact, educational PRNG combining xorshift-like operations, a Weyl increment, and a final multiply scramble. It’s simple to implement in C, Python, or Rust and can be a drop-in RNG for non-cryptographic uses. Treat it as a teaching tool or starting point; validate with test suites before using it in production-critical simulations.
Leave a Reply