Metadata-Version: 2.4
Name: randomg
Version: 1.0.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Summary: A scalar-first random number generator, built to be fast when drawing one number at a time in a loop rather than in one large batch.
License-Expression: Apache-2.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# RandomG

RandomG is an ultrafast random number generator built explicitly for tight Python loops where values are drawn one at a time and used immediately rather than in bulk — such as in MCMC chains, particle filters, or bootstrap loops. Powered by Xoshiro256++, it minimizes per-call overhead to deliver tens of millions of draws per second in pure Python loops.

Beyond speed, RandomG provides a comprehensive suite of continuous, discrete, and vector distributions (including Gaussian, Gamma, Poisson, Dirichlet, and Multivariate Normal) alongside specialized tools for particle resampling and exact state checkpointing. For parallel workloads, `split(n)` and `spawn()` use long-distance jump functions to create independent streams with mathematically guaranteed zero overlap. 

Every seed produces a fully reproducible sequence that is guaranteed stable across version updates, with distributions verified against standard statistical tests.


```bash
pip install randomg
```

```python
from randomg import Generator

rng = Generator(seed=42)
rng.random()          # float in [0, 1)
rng.normal(0.0, 1.0)  # a Gaussian draw
```

## What Is It For?

Standard scientific Python libraries are built for bulk generation — drawing millions of numbers at once into a contiguous array. But many simulations can't work that way. In an MCMC chain, a particle filter, or an adaptive bootstrap loop, the next random number depends directly on the result of the previous step. You cannot request them in advance.

In these unvectorizable loops, standard overhead — like Python object creation, dynamic dispatch, and C-API boundary crossing — starts to dominate execution time. RandomG is designed specifically to eliminate this per-call overhead, bringing peak performance to single-draw Python loops.

## Speed

Measured on a single thread drawing 2,000,000 values one at a time in a plain Python loop (warmed up prior to timing):

| Method | Draws per second |
|---|---|
| `random()` | 18.7 million |
| `normal()` | 10.3 million |

### Python Bound-Method Optimization

Binding the method reference outside a hot loop is a common CPython idiom:

```python
draw = rng.normal      # Bound once, outside the loop
for _ in range(n):
    x = draw(0.0, 1.0)
```

On Python 3.11 and later this makes no measurable difference to throughput. PEP 659's specializing interpreter caches the attribute lookup at each call site, so `rng.normal(0.0, 1.0)` written directly in the loop performs the same as the bound version — measured at 0.97x on Python 3.12, i.e. within noise.

The idiom still helps where the lookup cannot be specialized, most notably when the method is reached through `getattr(rng, name)` with a name held in a variable; that form runs about half as fast as either of the above, so binding it once outside the loop is worth roughly 1.9x.

## Parallel Streams

When running multi-threaded or multi-process simulations—such as parallel MCMC chains—each process requires an independent stream of random numbers that will never overlap or duplicate values.

RandomG handles this natively via `split(n)` and `spawn()`:

* **Mathematically Proven Separation:** Under the hood, the Xoshiro256++ algorithm uses long-distance jump functions to advance stream states by trillions of steps.
* **Zero Overlap Guarantee:** Unlike simple seed incrementing, this guarantees that generated streams are mathematically independent and will never intersect, regardless of run length.

## What's Inside

`Generator(seed)` creates a generator; the same seed always produces the same sequence.

| Method | Returns |
|---|---|
| `random()` | float in `[0, 1)` |
| `uniform(low, high)` | float in `[low, high)` |
| `normal(loc, scale)` | a Gaussian draw |
| `exponential(scale)` | an exponential draw |
| `gamma(shape, scale)` | a gamma-distributed draw |
| `beta(a, b)` | a beta-distributed draw in `[0, 1]` |
| `chisquare(df)` | a chi-squared-distributed draw |
| `student_t(df)` | a Student's t-distributed draw |
| `cauchy(loc, scale)` | a Cauchy-distributed draw |
| `laplace(loc, scale)` | a Laplace-distributed draw |
| `lognormal(mean, sigma)` | a lognormal-distributed, strictly positive draw |
| `poisson(lam)` | a Poisson-distributed non-negative integer |
| `geometric(p)` | a geometric-distributed positive integer |
| `binomial(n, p)` | a binomial-distributed integer in `[0, n]` |
| `bernoulli(p)` | `True` with probability `p` |
| `integers(low, high)` | int in `[low, high)` |
| `choice(seq)` | one random element from `seq` |
| `choices(seq, k)` | `k` elements from `seq`, with replacement, as a new list |
| `bootstrap_indices(n)` | `n` random indices in `[0, n)`, with replacement, as a new list |
| `Categorical(weights).sample(rng)` | one index, chosen with probability proportional to `weights`, at O(1) cost per draw after a one-time setup |
| `systematic_resample(weights, n)` | `n` indices, chosen with probability proportional to `weights`, using the low-variance resampling method particle filters use |
| `shuffle(seq)` | shuffles a list in place |
| `permutation(n)` | a new list, `0` to `n-1` in random order |
| `sample(seq, k)` | `k` distinct elements from `seq`, as a new list |
| `multivariate_normal(mean, cholesky_factor)` | one correlated Gaussian draw, as a new list |
| `dirichlet(alpha)` | a new list of proportions summing to 1 |
| `multinomial(n, probs)` | a new list of category counts summing to `n` |
| `fill(buf)` | fills a `bytearray` with random bytes |
| `state()` / `Generator.from_state(state)` | a plain, JSON-serializable snapshot of the generator, and a way to resume from one |
| `spawn()` | one independent generator, guaranteed not to overlap with this one |
| `split(n)` | `n` independent generators, guaranteed not to overlap with each other or with this one |

`choice(seq, k=...)` is not a thing — `choice()` draws one element, `choices()` draws several with replacement, and `sample()` draws several without replacement, matching the distinction the standard library's own `random` module makes. `multivariate_normal()`, `dirichlet()`, and `multinomial()` are the only distributions that return more than one number at once, since a single correlated vector, a set of proportions, or a set of category counts is what those distributions actually produce — a scalar version wouldn't make sense.

`Categorical` is the one part of the API that isn't a method on `Generator` — it's a small, separate object, because it holds a one-time setup cost (an alias-method sampling table, built from the weights when the object is constructed) that a single method call has nowhere to store between calls. Build it once for a set of weights, then call `.sample(rng)` as many times as needed at O(1) cost each — the natural shape for repeatedly sampling from the same weight distribution, as in a particle filter that resamples every timestep.

```python
rng = Generator(seed=1)
table = Categorical([0.1, 0.6, 0.3])  # built once
picks = [table.sample(rng) for _ in range(1000)]  # each draw is O(1)
```

`systematic_resample()` solves a related but different problem: not sampling from a fixed distribution many times, but resampling a whole population of particles at once, with less run-to-run variance than the same number of independent `Categorical` draws would have. Measured with equal weights across 5,000 repeats, `systematic_resample()`'s per-category count variance comes out essentially zero, against clearly nonzero variance for the same number of independent draws from an equivalent `Categorical` table.

A generator's state is a 5-element tuple — 4 integers plus a value that's `None` most of the time — so a long-running simulation can checkpoint and resume exactly, including `normal()`'s exact next value if one was pending when the checkpoint was taken:

```python
import json

rng = Generator(seed=1)
# ... run a long simulation ...

with open("checkpoint.json", "w") as f:
    json.dump(rng.state(), f)

# later, possibly on a different machine:
with open("checkpoint.json") as f:
    rng = Generator.from_state(tuple(json.load(f)))
# rng now continues exactly where the original left off
```

Type stubs are included, so IDEs and type checkers see real signatures rather than an opaque compiled extension.

## Testing & Stream Stability

Every distribution in RandomG is verified against real samples:

* **Continuous Distributions:** Checked using Kolmogorov-Smirnov tests to ensure output shapes match target theoretical distributions.
* **Discrete Distributions:** Tested with chi-squared goodness-of-fit tests across various rate parameters.
* **Parallel Streams:** `spawn()` and `split()` are checked directly for collisions across a large sample of draws from each derived stream — consistent with, though not itself proof of, the non-overlap guarantee that comes from `jump()`'s own mathematics.
* **Stream Stability:** Outputs are tested against golden reference samples (`tests/golden_values.json`). A seed's generated sequence is guaranteed to remain strictly identical across all 1.x version releases.
* **Cross-Platform Identity:** The same seed produces the same bits on every platform — Linux, macOS (Intel and Apple Silicon), and Windows alike. This is not incidental. `f64::sin`, `ln`, `exp` and friends call the operating system's own math library, and those libraries are only required to be accurate to within a small error bound, not to agree with each other bit for bit, so any distribution built on them drifts between platforms. RandomG therefore computes every transcendental itself (`engine/src/portable_math.rs`), using only the operations IEEE 754 defines exactly: `+`, `-`, `*`, `/`, `sqrt`, and integer arithmetic. The result is verified by building the distribution code for a second CPU architecture (aarch64), running it, and comparing every value bit for bit against the same code run natively — alongside a control showing the old libm-based code does diverge on exactly that comparison. See `tests/verify_cross_architecture_identity.py`.

## Design Philosophy

RandomG is scalar-first by design. Most methods return exactly one value, and this precise focus is what eliminates overhead and keeps each call extremely fast. 

However, vector-returning functions (`multivariate_normal()`, `dirichlet()`, `multinomial()`) and specialized utilities (`split()`, `spawn()`, `state()`, `fill()`) are intentional exceptions — they address specific, structural needs where a single scalar wouldn't make sense, without bloating the core API. Similarly, `Categorical` exists as a separate object strictly to hold its one-time setup cost (the alias table) across repeated $O(1)$ draws.

## Repository Layout

- `engine/` — the installable package (Rust/PyO3), built with [maturin](https://www.maturin.rs/).
- `tests/` — correctness checks: distributional tests, bias checks, independence checks for `spawn()`/`split()`, and the golden-value stream-stability fixture.
- `benchmarks/` — the speed measurements behind the numbers above.

