Native random sampling
Seed an owned native generator once or address reproducible uniform, integer, weighted choice and normal draws explicitly.
Implementedaner.random provides native pseudorandom draws for Monte Carlo simulation, discrete choices and normal sampling. It uses the versioned aner.philox4x32-10/v1 engine. Create an explicit RandomGenerator to seed once and obtain successive observations, or use stateless seed/draw calls when each sample needs a stable address. There is no process global generator or global seed setter.
Use aner.random for sampling, aner.math for deterministic scalar mathematics, and aner.nn for neural operations and their established initialization/training contracts. Keeping the random engine in its own module makes the seed and parallel identity model visible without changing existing neural or dataset sequences.
This API requires Aner 0.0.3 or newer containing aner.random; earlier binaries may reject the import. See installation for runtime availability. Complete Aner sources are basic sampling and custom neural initialization.
Seed once, then sample
import aner.random
let rng = random.generator(seed: 20261019)
let samples = Array<Float64>(size: 1000, fill: 0.0)
for index in Range(samples.len()) {
samples[index] = rng.uniform()
}
print(random.engine())
print(rng.seed())
print(rng.position()) // 1000: the next draw position.
print(samples[0])rng.uniform() returns the next Float64 in [0,1): zero is possible, one is excluded. Each value belongs to a finite 53 bit grid; a computer does not represent every mathematical real number. let keeps the handle bound to this generator while its owned cursor advances.
To start a fresh run without choosing a seed, use let rng = random.generator() and retain rng.seed(). The factory acquires a seed once; its sampling methods continue that sequence. The underlying random.seed() facility reads the operating system's entropy source and returns a nonnegative 63 bit Int64. Acquisition errors fail explicitly; there is no clock based fallback. Fresh seeds can repeat, so uniqueness and a different sequence on every run are not guarantees. This scientific generator is not suitable for passwords, tokens or cryptographic keys.
Generator API
| Call | Result and contract |
|---|---|
random.generator(seed) | New RandomGenerator with the specified nonnegative Int64 seed and position zero. |
random.generator() | New RandomGenerator with an OS generated replay seed and position zero. |
rng.seed() | Exact Int64 seed; does not advance the cursor. |
rng.position() | Next Int64 draw position; does not advance the cursor. |
rng.reseed(seed) | Unit; validate the nonnegative seed, then replace it and reset the cursor to zero. |
rng.uniform() | Next Float64 uniform value in [0,1). |
rng.integer(low, high) | Next Int64 integer with both endpoints included. |
rng.bernoulli(probability) | Next Bool outcome with finite probability in [0,1]. |
rng.normal(mean, stddev) | Next normal Float64 approximation with finite mean and finite nonnegative standard deviation. |
rng.choice(values: List) | Uniform choice from a nonempty List, returned as Any. |
rng.choice(values: List, weights: List) | Choice with relative weights, returned as Any. |
rng.weighted_index(weights: List) | Zero based Int64 index selected using relative weights. |
All sampling methods share one cursor. A successful call consumes exactly one position, including a degenerate call such as rng.normal(10.0, 0.0) or rng.bernoulli(1.0). Validation or sampling errors leave the position unchanged. Runtime failures still follow the usual notebook session reset policy; this cursor contract does not preserve a failed session. Integer rejection retries belong to that single position. INT64_MAX is the exhausted position: usable generator draw identities are 0 through INT64_MAX - 1, after which sampling fails explicitly.
Copying the handle shares its state: let alias = rng does not clone a sequence. Sampling or reseeding through either name affects both. Equality compares handle identity; two separately constructed generators compare unequal even with the same seed and position. To start an independent replay cursor, construct another generator with the same seed. Replay requires the seed and the same ordered sampling calls and parameters. Reseeding starts over; repeatedly reseeding inside a loop repeats the beginning of the sequence. Inspection and bounded print(rng)/String(rng) summaries do not consume a draw. Mutable generator handles are not thread safe; use addressed sampling for concurrent work.
import aner.random
let rng = random.generator(seed: 42)
let first = rng.uniform()
let next = rng.uniform()
rng.reseed(seed: 42)
print(first == rng.uniform()) // true: replay position zero.
print(rng.position()) // 1.Addressed API for stable sample identities
Addressed sampling functions require a nonnegative Int64 seed and accept a nonnegative Int64 draw, defaulting to 0. Calling random.uniform(seed: seed) repeatedly repeats draw zero; use a generator for automatically advancing calls. Named arguments make the two ownership models explicit.
| Call | Result and contract |
|---|---|
random.seed() | Int64; OS generated seed to retain for replay. |
random.engine() | String; "aner.philox4x32-10/v1". |
random.uniform(seed, draw: 0) | Float64; discrete uniform grid in [0,1). |
random.integer(low, high, seed, draw: 0) | Int64; inclusive endpoints; low <= high; rejection mapping avoids modulo bias, including the full Int64 range. |
random.bernoulli(probability, seed, draw: 0) | Bool; finite probability in [0,1]; true with the requested probability subject to grid resolution. Endpoints return false/true. |
random.normal(mean, stddev, seed, draw: 0) | Float64; finite mean and finite nonnegative standard deviation. Zero deviation returns the mean. Nonfinite output is rejected. |
random.choice(values: List, seed, draw: 0) | Any; uniform choice from a nonempty list, preserving the selected value's concrete type. |
random.choice(values: List, weights: List, seed, draw: 0) | Any; nonempty equal length lists; finite nonnegative relative weights with at least one positive weight. |
random.weighted_index(weights: List, seed, draw: 0) | Int64; zero based index under the same relative weight rules. |
low, high, seed and draw are integers. Distribution parameters and weights are numerical values under Aner's checked numeric conversion rules. Integer weights retrieved from a mixed List must be exactly representable as Float64; a huge Int64 that would lose precision is rejected. Use an explicit Float64(value) when rounding is intentional, or choose exactly represented proportional weights. An Any result needs a checked cast for typed operations:
import aner.random
let rng = random.generator(seed: 20261019)
let coin = cast<String>(rng.choice(List("tails", "heads"), weights: List(2, 8)))
let direction = cast<String>(rng.choice(List("N", "E", "W", "S")))
let heads = rng.bernoulli(probability: 0.8)
let measurement = rng.normal(mean: 10.0, stddev: 2.0)
print(coin)
print(direction)
print(heads)
print(measurement)List(2, 8) gives tails/head probabilities 0.2/0.8; List(1, 3) gives 0.25/0.75. Weights do not need to sum to one. Zero weight excludes an alternative; negative, nonnumeric, nonfinite or all zero weights fail. Normal sampling specifies the standard deviation, not variance. Its floating point transform is an approximation to a continuous normal distribution with a finite representable tail.
Parallel execution and replay
Assign draw identities from the scientific sample identity rather than worker identity or completion order:
import aner.random
import aner.parallel
fn sample(parameters: List, iteration: Int64, task_seed: Int64) -> List {
let seed = cast<Int64>(parameters.get(0))
return List(random.integer(low: 1, high: 10, seed: seed, draw: iteration))
}
let seed = 20261019
let result = parallel.repeat(iterations: 1000, task: sample,
parameters: List(seed), execution: parallel.local(workers: 4), seed: seed)
print(result.mean())This example deliberately uses one retained root seed and the global iteration identity; the task seed is not its draw owner. One worker or four workers therefore select the same ordered observations. If a sample has several random decisions, reserve distinct draw positions, such as sample * decisions_per_sample + decision, and check the arithmetic for Int64 overflow and identity reuse. Retrying the same identity replays the same observation. A distribution's internal rejection attempts belong to that draw and cannot shift later observations. Generators are rejected inside isolated repetition tasks, including construction, method calls and transport through parameters. Use the addressed API there. random.seed() is also rejected: acquire entropy before launching tasks.
Sharing addresses between scenarios is an intentional paired design. Different seeds or domain labels are useful separation mechanisms, but do not prove statistical independence. A seed alone also cannot reproduce an LLM service or the rest of a numerical program.
Retain code and input hashes/order, runtime/build identity, random.engine(), exact seed integers, generator call order or draw address schedule, distribution parameters, dtype, backend and numerical tolerances. A generator delegates each successful call to the same addressed distribution at its current position, then advances. This summary metadata layout describes the current development source. Published 0.0.3 archives retain their original summary fields. SimulationResults reports separate iteration seed derivation and supported scalar sampling contracts; its rng_task_usage is not-observed, so retain the task source and random.engine() to identify actual sampler use. Normal transforms use platform mathematical functions, so bitwise equality across math libraries is not promised. Dev and Prod select the same draws; inspection does not select another algorithm or a smaller workload.
Numerical protocol and limits
Philox is a published counter based design: it computes a keyed transformation of an explicit counter rather than stepping through a shared mutable stream. The Random123 paper describes the method, and its Philox reference specifies the ten round family. These sources motivate the native design; upstream statistical results do not certify Aner's complete mapping and application.
The v1 protocol uses a 64 bit key from the nonnegative seed and the four word counter {draw_low32, draw_high32, attempt, domain}. Domains separate uniform (1), integer/uniform choice (2), normal (3), Bernoulli (4) and weighted choice (5). Each integer draw owns its rejection attempts. Uniform conversion keeps 53 bits. Normal sampling applies Box Muller to two lanes from its own block without a cached spare or hidden state. Its location/scale transform uses explicit fma(stddev, z, mean); this fused operation is part of the versioned recipe. Distribution parameters transform the selected random input; changing them may couple paired observations intentionally.
Weighted choice scales by the maximum weight before accumulating in native extended precision, avoiding overflow from an unnecessary raw sum. It remains finite precision sampling: extremely small weights may underflow or become unreachable at the draw grid's resolution. This API cannot promise arbitrarily small mathematical probabilities. Preserve weight order and values for replay; mathematically proportional vectors need not have identical last bit numerical behavior. Native extended precision formats can also differ across platforms near a cumulative boundary.
Uniform, normal and Bernoulli draws, unweighted List selection, and generator seed/position/reseed operations use O(1) time and auxiliary storage. Inclusive integer mapping uses expected O(1) rejection work, with an explicit finite retry cap. Weighted choice and index selection scan n weights in O(n) time; interpreter conversion of the mixed List requires O(n) temporary storage. A generator retains O(1) seed/cursor state. OS seed acquisition has operating system latency and belongs outside a hot sampling loop.
Integer mapping, distribution transforms, counter layout and engine identity are compatibility contracts. Any future protocol change must be explicit and versioned. A performance improvement is not permission to silently change published observations. Raw vector and boundary tests plus deterministic distribution diagnostics catch regressions; they do not establish every scientific use case or an independent TestU01/PractRand certification.
Effect on the existing modules
| Module or operation | Effect of adding aner.random |
|---|---|
aner.math | Retains deterministic scalar functions; no random state is added. |
aner.nn, aner.tensor, Dense initialization and dropout | Existing seeded recipes, default initialization and zero biases remain unchanged. Explicit custom weights can opt into the new normal sampler. |
aner.dataset, aner.data, K means | Existing splits, shuffles, batches and initial center sequences remain unchanged. |
aner.parallel | Iteration seed derivation remains unchanged. Tasks use explicitly addressed Philox samplers; migrated examples produce different draws from earlier sampling recipes. |
| Generative/decoder training and checkpoints | Existing engine schedules and continuation contracts remain unchanged. |
aner.quantum | Existing classical shot sampling engine remains unchanged. |
aner.llm | No local model seed control or reproducibility guarantee is added. |
For ReLU weights, a caller can choose normal standard deviation sqrt(2 / fan_in), following He et al.'s rectifier initialization, fill an Array<Float64> using an explicitly owned generator, convert it with .to_tensor(rows, cols), and pass it to the existing custom Dense overload. The complete example uses zero bias and makes the choice visible. It does not change the default NN protocol; initialization suitability still depends on the architecture and activation.
The randomness and replay guide records operation specific engine ownership and historical validation. Existing checkpoints and publications remain tied to their recorded sources, runtime builds and operation recipes.
See the complete callable index for the native module and method name catalogue.