Skip to content

RNG

Canonical seedable PRNG (xoshiro256++ / SplitMix64). Seeded runs are reproducible and cross-language portable.

xoshiro256++ generator seeded via SplitMix64.

Parameters:

Name Type Description Default
seed int | None

A 64-bit integer seed. None draws an OS-entropy seed (for non-deterministic production runs); conformance runs MUST pass an explicit seed.

None
Source code in src/rotalabs_redqueen/core/rng.py
class Rng:
    """xoshiro256++ generator seeded via SplitMix64.

    Args:
        seed: A 64-bit integer seed. ``None`` draws an OS-entropy seed (for
            non-deterministic production runs); conformance runs MUST pass an
            explicit seed.
    """

    __slots__ = ("s",)

    def __init__(self, seed: int | None = None):
        if seed is None:
            seed = int.from_bytes(os.urandom(8), "little")
        self.s = _seed_state(seed)

    # ---- normative primitives -------------------------------------------------

    def next_u64(self) -> int:
        s = self.s
        result = (_rotl((s[0] + s[3]) & _MASK, 23) + s[0]) & _MASK
        t = (s[1] << 17) & _MASK
        s[2] ^= s[0]
        s[3] ^= s[1]
        s[1] ^= s[2]
        s[0] ^= s[3]
        s[2] ^= t
        s[3] = _rotl(s[3], 45)
        return result

    def next_double(self) -> float:
        """Uniform double in [0, 1)."""
        return (self.next_u64() >> 11) / _TWO53

    def below(self, n: int) -> int:
        """Uniform integer in [0, n), unbiased (Lemire nearly-divisionless)."""
        if n <= 0:
            return 0
        x = self.next_u64()
        m = x * n
        low = m & _MASK
        if low < n:
            t = (1 << 64) % n
            while low < t:
                x = self.next_u64()
                m = x * n
                low = m & _MASK
        return m >> 64

    def shuffle(self, a: list) -> list:
        """In-place Fisher-Yates shuffle (descending). Returns ``a``."""
        for i in range(len(a) - 1, 0, -1):
            j = self.below(i + 1)
            a[i], a[j] = a[j], a[i]
        return a

    # ---- derived helpers (deterministic; mirror the used numpy API) -----------

    def random(self) -> float:
        """Alias of :meth:`next_double` (matches ``numpy.Generator.random``)."""
        return self.next_double()

    def uniform(self, low: float = 0.0, high: float = 1.0) -> float:
        """Uniform float in [low, high)."""
        return low + (high - low) * self.next_double()

    def integers(self, low: int, high: int | None = None) -> int:
        """Uniform integer. ``integers(n)`` -> [0, n); ``integers(a, b)`` -> [a, b)."""
        if high is None:
            return self.below(low)
        return low + self.below(high - low)

    def choice(self, n: int, size: int | None = None, replace: bool = False) -> int | list[int]:
        """Sample index/indices from range(n).

        ``size=None`` returns a single index. With ``replace=False`` this is a
        partial Fisher-Yates draw (consumes exactly ``size`` ``below`` calls).
        """
        if size is None:
            return self.below(n)
        if replace:
            return [self.below(n) for _ in range(size)]
        pool = list(range(n))
        out: list[int] = []
        for i in range(size):
            j = i + self.below(n - i)
            pool[i], pool[j] = pool[j], pool[i]
            out.append(pool[i])
        return out

next_double() -> float

Uniform double in [0, 1).

Source code in src/rotalabs_redqueen/core/rng.py
def next_double(self) -> float:
    """Uniform double in [0, 1)."""
    return (self.next_u64() >> 11) / _TWO53

below(n: int) -> int

Uniform integer in [0, n), unbiased (Lemire nearly-divisionless).

Source code in src/rotalabs_redqueen/core/rng.py
def below(self, n: int) -> int:
    """Uniform integer in [0, n), unbiased (Lemire nearly-divisionless)."""
    if n <= 0:
        return 0
    x = self.next_u64()
    m = x * n
    low = m & _MASK
    if low < n:
        t = (1 << 64) % n
        while low < t:
            x = self.next_u64()
            m = x * n
            low = m & _MASK
    return m >> 64

shuffle(a: list) -> list

In-place Fisher-Yates shuffle (descending). Returns a.

Source code in src/rotalabs_redqueen/core/rng.py
def shuffle(self, a: list) -> list:
    """In-place Fisher-Yates shuffle (descending). Returns ``a``."""
    for i in range(len(a) - 1, 0, -1):
        j = self.below(i + 1)
        a[i], a[j] = a[j], a[i]
    return a

random() -> float

Alias of :meth:next_double (matches numpy.Generator.random).

Source code in src/rotalabs_redqueen/core/rng.py
def random(self) -> float:
    """Alias of :meth:`next_double` (matches ``numpy.Generator.random``)."""
    return self.next_double()

uniform(low: float = 0.0, high: float = 1.0) -> float

Uniform float in [low, high).

Source code in src/rotalabs_redqueen/core/rng.py
def uniform(self, low: float = 0.0, high: float = 1.0) -> float:
    """Uniform float in [low, high)."""
    return low + (high - low) * self.next_double()

integers(low: int, high: int | None = None) -> int

Uniform integer. integers(n) -> [0, n); integers(a, b) -> [a, b).

Source code in src/rotalabs_redqueen/core/rng.py
def integers(self, low: int, high: int | None = None) -> int:
    """Uniform integer. ``integers(n)`` -> [0, n); ``integers(a, b)`` -> [a, b)."""
    if high is None:
        return self.below(low)
    return low + self.below(high - low)

choice(n: int, size: int | None = None, replace: bool = False) -> int | list[int]

Sample index/indices from range(n).

size=None returns a single index. With replace=False this is a partial Fisher-Yates draw (consumes exactly size below calls).

Source code in src/rotalabs_redqueen/core/rng.py
def choice(self, n: int, size: int | None = None, replace: bool = False) -> int | list[int]:
    """Sample index/indices from range(n).

    ``size=None`` returns a single index. With ``replace=False`` this is a
    partial Fisher-Yates draw (consumes exactly ``size`` ``below`` calls).
    """
    if size is None:
        return self.below(n)
    if replace:
        return [self.below(n) for _ in range(size)]
    pool = list(range(n))
    out: list[int] = []
    for i in range(size):
        j = i + self.below(n - i)
        pool[i], pool[j] = pool[j], pool[i]
        out.append(pool[i])
    return out