Comparing Watkins RNG to Mersenne Twister and PCG

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):

  1. s0 ^= s0 << 23;
  2. s0 ^= s0 >> 17;
  3. s0 ^= s1 ^ (s1 >> 26);
  4. s1 = rotate_left(s1, 32) + some_constant;
  5. w += 0x9E3779B97F4A7C15; // golden ratio Weyl increment
  6. result = (s0 + s1) ^ w;
  7. 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *