# `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 ```python from latpy.latmath.random import seed ``` | Signature | Description | |---|---| | `seed(n)` | Seed the shared PRNG for deterministic reproducibility | ### Examples ```python 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 ```python 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` | Uniform[lo, hi) samples | | `rand(*shape) -> NDArray` | Uniform[0, 1) samples | `rand(*shape)` is shorthand for `uniform(0.0, 1.0, size=shape)`. ### Examples ```python 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 ```python from latpy.latmath.random import randint ``` | Signature | Description | |---|---| | `randint(lo, hi=None, size=None) -> int | NDArray` | Uniform integer from [lo, hi) | If only `lo` is provided, `randint(lo)` is equivalent to `randint(0, lo)`. ### Examples ```python 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 ```python from latpy.latmath.random import choice, shuffle ``` | Signature | Description | |---|---| | `choice(a, size=None, p=None) -> scalar | NDArray` | Weighted random sample from population `a` | | `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 ```python 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 ```python 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.