latmath.random — Random Number Generation

Seedable pseudo-random number generation and sampling utilities.

What “seedable PRNG” means. Setting the seed with seed(n) initializes the internal MT19937 state deterministically. Given the same seed, the same sequence of draws will be produced every time, on every platform. This is essential for reproducible experiments: you can re-run a script and get identical results.

Rationale. The Mersenne Twister (MT19937) is the most widely tested PRNG in existence — it passes the stringent Diehard and BigCrush statistical batteries, has a massive period (2^19937 − 1), and is the default generator in NumPy, R, and many other systems. The Box-Muller transform is used for normal random variates because it is simple, correct, and produces exactly two independent normals from two uniforms with only elementary math (log, sin, cos).

Important: Not for cryptographic use. This PRNG is statistically uniform but cryptographically insecure. An adversary can reconstruct the internal state after observing ~624 outputs. Do not use for passwords, tokens, or any security-sensitive application.


Seeding

from latpy.latmath.random import seed

Signature

Description

seed(n)

Seed the shared PRNG for deterministic reproducibility

Examples

from latpy.latmath.random import seed, rand, randint

# Same seed produces identical output
seed(42)
print(rand())   # 0.3745401188473625
print(rand())   # 0.9507143064099162

seed(42)
print(rand())   # 0.3745401188473625  (same as first call above)
print(rand())   # 0.9507143064099162  (same as second call above)

# Different seeds produce different sequences
seed(123)
print(rand())   # 0.929616... (different from seed 42)

Continuous Distributions

from latpy.latmath.random import randn, uniform, rand

Signature

Description

randn(*shape) -> NDArray

Standard normal variates (Box-Muller)

`uniform(lo=0.0, hi=1.0, size=None) -> float

NDArray`

rand(*shape) -> NDArray

Uniform[0, 1) samples

rand(*shape) is shorthand for uniform(0.0, 1.0, size=shape).

Examples

from latpy.latmath.random import seed, randn, uniform, rand

seed(42)

# Scalar uniform
print(uniform())       # 0.3745401188473625  (default lo=0, hi=1)

# Uniform with custom range
print(uniform(5, 10))  # 9.75357151872753

# Uniform with size
u = uniform(0, 1, size=3)
print(u.tolist())      # [0.374540..., 0.950714..., 0.731993...]

# rand is equivalent to uniform(0, 1, shape)
r = rand(3)
print(r.tolist())      # [0.598658..., 0.156018..., 0.155994...]

# Standard normal via randn
n = randn(3)
print(n.tolist())      # [-0.020858..., 0.183145..., 1.087594...]

# randn with multi-dimensional shape
m = randn(2, 3)
print(m.shape)         # (2, 3)

# Edge: randn with empty shape () returns a scalar
randn()                # e.g. 0.374540...

# Edge: randn with large shape
big = randn(1000, 1000)  # 1 million samples, OK (uses Box-Muller)

Discrete Distributions

from latpy.latmath.random import randint

Signature

Description

`randint(lo, hi=None, size=None) -> int

NDArray`

If only lo is provided, randint(lo) is equivalent to randint(0, lo).

Examples

from latpy.latmath.random import seed, randint

seed(42)

# Single integer
print(randint(0, 10))      # e.g. 4

# Default lo=0 with single argument
print(randint(10))         # same as randint(0, 10)

# Multiple values
vals = randint(0, 10, size=5)
print(vals.tolist())       # [4, 8, 8, 3, 6]

# Edge: empty range [lo, hi) with lo == hi
randint(0, 0)   # DomainError: empty range
randint(5, 5)   # DomainError: empty range

# Edge: negative lo
randint(-5, 5)  # e.g. -3 (works, inclusive of -5 up to 4)

Sampling

from latpy.latmath.random import choice, shuffle

Signature

Description

`choice(a, size=None, p=None) -> scalar

NDArray`

shuffle(a)

In-place shuffle of NDArray or list

Sampling is with replacement by default (use size to draw multiple items). shuffle modifies the input in-place and returns None.

Examples

from latpy.latmath.random import seed, choice, shuffle
from latpy.latmath.array import array

seed(42)

# Single element from list
print(choice(['a', 'b', 'c']))        # e.g. 'c'

# Multiple draws with replacement
print(choice([1, 2, 3], size=5).tolist())  # e.g. [1, 1, 3, 1, 2]

# Weighted choice
print(choice([1, 2, 3], p=[0.1, 0.8, 0.1]))
# 2 (most likely, since it has weight 0.8)

# Edge: size > population (sampling with replacement — always works)
print(choice([1, 2], size=10).tolist())
# [2, 1, 1, 2, 1, 1, 2, 2, 1, 2]  (10 draws from 2 elements)

# Shuffle an array
a = array([1, 2, 3, 4, 5])
shuffle(a)
print(a.tolist())  # e.g. [4, 1, 3, 5, 2]  (modified in-place)

# Edge: shuffle on empty array
empty = array([])
shuffle(empty)   # no-op (empty array unchanged)

# Edge: shuffle on empty list
shuffle([])      # no-op

# Edge: choice with empty population
choice([], size=1)  # IndexError or DomainError

Complete Reproducible Example

from latpy.latmath.random import seed, randn, randint, shuffle
from latpy.latmath.array import array

seed(42)

# Generate 3 standard normal samples
print(randn(3).tolist())
# [-0.020858426917374108, 0.1831459003332166, 1.0875942574197174]

# Generate 5 random integers 0-9
print(randint(0, 10, size=5).tolist())
# [4, 8, 8, 3, 6]

# Shuffle an array
a = array([1, 2, 3, 4, 5])
shuffle(a)
print(a.tolist())
# [5, 3, 2, 4, 1]

Every run with seed(42) will produce these exact outputs.