Skip to content

Randomness

stream

Seeded random number streams.

SimulSI never touches global random state. Every simulation owns a root :class:RandomStream built from a seed, and named sub-streams are derived from (seed, name) with NumPy's SeedSequence. A sub-stream therefore produces the same numbers no matter how many other streams exist or in which order they were created, which keeps common random numbers intact when a model changes (e.g. adding a resource does not shift the arrival stream).

RandomStream

RandomStream(seed: int | None = None, *, name: str = 'root', mode: SamplingMode = 'native')

A reproducible stream of random numbers (PCG64 under the hood).

mode selects how variates are produced: "native" (NumPy's fast samplers, the default), "inverse" (one uniform per variate via the inverse CDF) or "antithetic" (the same, using 1 - U). An inverse run and an antithetic run with the same seed form an antithetic pair. Sub-streams inherit the mode.

stream

stream(name: str) -> RandomStream

Return the named sub-stream (created on first use, then cached).

integers

integers(low: int, high: int) -> int

Uniform integer in [low, high).

exponential

exponential(mean: float) -> float

Exponential variate with the given mean (= 1 / rate).

lognormal

lognormal(mean: float = 0.0, sigma: float = 1.0) -> float

Log-normal variate; mean/sigma are those of the underlying normal.

derive_seed

derive_seed(seed: int, *keys: int | str) -> int

Deterministically derive an independent 63-bit seed from seed and keys.

fresh_seed

fresh_seed() -> int

A new seed from OS entropy, for runs where the user did not supply one.

distributions

Probability distributions.

Distributions are small immutable objects that know how to draw from a :class:~simulsi.randomness.stream.RandomStream. They validate their parameters eagerly, expose analytic mean/variance where they exist, and round-trip through plain dictionaries (to_spec / :func:from_spec) so they can appear in YAML configuration without any code execution.

Distribution

Bases: ABC, Generic[T]

Base class. Subclasses implement :meth:sample.

sample_n

sample_n(stream: RandomStream, n: int) -> npt.NDArray[Any]

Draw n values. Subclasses override with vectorised NumPy calls.

to_spec

to_spec() -> dict[str, Any]

Serialisable description, accepted by :func:from_spec.

Exponential dataclass

Exponential(rate: float | None = None, *, mean: float | None = None)

Bases: Distribution[float]

Exponential distribution. Give exactly one of rate or mean.

LogNormal dataclass

LogNormal(mu: float = 0.0, sigma: float = 1.0)

Bases: Distribution[float]

Log-normal; mu/sigma parameterise the underlying normal distribution.

Use :meth:from_moments to specify the mean and standard deviation of the log-normal itself, which is usually what data gives you.

Empirical dataclass

Empirical(values: Sequence[float], weights: Sequence[float] | None = None)

Bases: Distribution[float]

Resamples observed data (with replacement), optionally weighted.

Categorical dataclass

Categorical(categories: Sequence[Any] | Mapping[Any, float], probabilities: Sequence[float] | None = None)

Bases: Distribution[Any]

Draws one of categories with the given probabilities.

Custom dataclass

Custom(fn: Callable[[RandomStream], T], name: str = 'custom')

Bases: Distribution[T]

Wraps any function stream -> value. Not serialisable to configuration.

as_distribution

as_distribution(value: DistributionLike | Mapping[str, Any]) -> Distribution[Any]

Coerce numbers to :class:Constant and spec dicts via :func:from_spec.

from_spec

from_spec(spec: Mapping[str, Any]) -> Distribution[Any]

Build a distribution from {"distribution": "<kind>", **params}.

Only registered distribution classes can be created, so configuration files can never trigger arbitrary code.

fitting

Fit input distributions to data.

:func:fit_distribution fits a set of candidate families by maximum likelihood (closed form where one exists), ranks them by AIC, BIC or the Kolmogorov-Smirnov statistic, and returns ready-to-use SimulSI distributions:

import numpy as np data = np.random.default_rng(1).gamma(2.0, 3.0, size=500) report = fit_distribution(data) report.best.name 'gamma' service = report.best.distribution # a simulsi Gamma, use it in a model

Fitting is no substitute for looking at the data: check the histogram (report.plot() needs matplotlib), and consider :class:~simulsi.randomness.Empirical when no family fits well.

FitResult dataclass

FitResult(name: str, distribution: Distribution[Any], params: dict[str, float], loglik: float, aic: float, bic: float, ks_statistic: float, ks_pvalue: float)

One fitted family. Lower aic/bic/ks_statistic is better.

FitReport dataclass

FitReport(n: int, criterion: Criterion, results: list[FitResult], skipped: dict[str, str] = dict(), data: NDArray[float64] | None = None)

Ranked fits plus the families that were skipped and why.

plot

plot(top: int = 3, ax: Any = None) -> Any

Histogram of the data with the top fitted densities (needs matplotlib).

fit_distribution

fit_distribution(data: Iterable[float], candidates: Sequence[str] = CANDIDATES, *, criterion: Criterion = 'aic') -> FitReport

Fit each candidate family to data and rank them by criterion.

Families whose support does not match the data (e.g. gamma with zeros, Poisson with fractional values) are listed in report.skipped.

load_values

load_values(path: str | Path, column: str | None = None) -> list[float]

Read numbers from a CSV column (default: the first numeric one) or a plain text file.

Plain text files may separate values by whitespace or commas. Blank cells are skipped.