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 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 |
|---|---|
|
Standard normal variates (Box-Muller) |
`uniform(lo=0.0, hi=1.0, size=None) -> float |
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` |
|
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.