Skip to content

Sampling with explicit keys

Nautilus provides sampling functions for seven distribution families. Every sampling function takes an explicit key as its first argument and a template tensor that determines the output shape.

Chelis random draws are pure functions of explicit keys. The same key and inputs produce the same draw. A function that draws needs no randomness effect annotation.

-- A sampler takes a key and passes it on
def my_sampler[n](k: key, t: tensor[n, f32]) -> tensor[n, f32] =
normal_sample(k, t, cast(0.0, f32), cast(1.0, f32))

Build a root key from a seed:

module Nautilus.BookSampling
import Nautilus.Distributions (normal_sample)
export (draw_pair)
def draw_pair() -> tensor[2, f32] = {
template = to_tensor([0.0f32, 0.0f32])
normal_sample(key_from_seed(42i64), template, 0.0f32, 1.0f32)
}

Keys are affine: each key has at most one consuming use on every control-flow path; a second consuming use of the same bound key is a type error. Derive children for separate draws. Two fresh keys made from the same seed intentionally replay the same draw. split_key(k) returns two child keys, split_keys(k, n) returns n of them, and fold_in(k, i) derives the child key of an integer.

-- two independent draws from one key
ks = split_key(k)
a = normal_sample(ks.0, copy(template), cast(0.0, f32), cast(1.0, f32))
b = normal_sample(ks.1, template, cast(0.0, f32), cast(1.0, f32))

Every sample function takes a template: tensor[n, f32] after its key. The template's shape determines how many samples are drawn. The actual values in the template are ignored. This pattern avoids runtime integer-to-shape conversion, which Chelis's type system does not support.

normal_sample uses the Box-Muller transform to convert pairs of uniform random variates into normally distributed samples. Given u1 ~ Uniform(0,1) and u2 ~ Uniform(0,1):

z = sqrt(-2 * ln(u1)) * cos(2 * pi * u2)
result = mean + std * z

The implementation evaluates cos(2piu2) as sin(pi/2 - 2piu2) over the whole tensor.

import Nautilus.Distributions (normal_sample)
samples = normal_sample(key_from_seed(42i64), template, cast(0.0, f32), cast(1.0, f32))
DistributionFunctionBehavior
Uniformuniform_sampleDirect scaling of uniform variates
Exponentialexponential_sampleInverse CDF: -ln(u) / rate
LogNormallognormal_sampleexp(normal_sample(mu, sigma))
Gammagamma_sampleConstant tensor for shape >= 1; see limits below
Chi-squaredchi_squared_sampleConstant tensor via gamma_sample(df/2, 2)
Student-tstudent_t_sampleNormal draw divided by a constant; not Student-t distributed
def uniform_sample[n](k: key, template: tensor[n, f32], lo: f32, hi: f32)
-> tensor[n, f32]
def exponential_sample[n](k: key, template: tensor[n, f32], rate: f32)
-> tensor[n, f32]
def normal_sample[n](k: key, template: tensor[n, f32], mean: f32, std: f32)
-> tensor[n, f32]
def lognormal_sample[n](k: key, template: tensor[n, f32], mu: f32, sigma: f32)
-> tensor[n, f32]
def gamma_sample[n](k: key, template: tensor[n, f32], shape: f32, scale: f32)
-> tensor[n, f32]
def chi_squared_sample[n](k: key, template: tensor[n, f32], df: f32)
-> tensor[n, f32]
def student_t_sample[n](k: key, template: tensor[n, f32], df: f32)
-> tensor[n, f32]
  • uniform_like is the internal Chelis primitive that generates raw uniform variates. It is not part of the Nautilus public API.
  • normal_sample derives two child keys internally, one per Box-Muller uniform draw.
  • gamma_sample uses shape >= 1. For finite shape >= 1 and positive finite scale, every output element equals (shape - 1/3) * scale regardless of key. It does not sample a gamma distribution.
  • chi_squared_sample and student_t_sample depend on gamma_sample. For df >= 2, the former is constant and the latter divides a normal draw by a constant. Neither samples its stated distribution.