Distributions

Four probability distributions with inverse-CDF sampling, plus combinators for reshaping them.

BucketedDistribution

All distributions are instances of BucketedDistribution — a discrete approximation of a continuous PDF using a grid of x-values and probability densities. Sampling uses inverse-CDF interpolation via the trapezoid rule.

import io.peekandpoke.ultra.maths.stochastic.BucketedDistribution
import io.peekandpoke.ultra.maths.stochastic.sample
import kotlin.random.Random

val dist = BucketedDistribution.createTruncatedNormal()

// Raw sample in [0, 1]
val raw = dist.sample(Random)

// Mapped to a range
val mapped = dist.sample(Random, from = 10.0, to = 50.0)

Distribution types

Uniform

Equal probability across the entire range. Every value in [0, 1] is equally likely.

val uniform = BucketedDistribution.createUniform()
val value = uniform.sample(Random)  // any value in [0, 1] with equal probability

Truncated normal

A Gaussian bell curve truncated to [0, 1]. Values cluster around the mean, with spread controlled by std.

// Default: centered at 0.5, std = 0.15
val normal = BucketedDistribution.createTruncatedNormal()

// Custom: skewed toward the low end
val skewed = BucketedDistribution.createTruncatedNormal(mean = 0.2, std = 0.1)
Parameter Default Meaning
mean 0.5 Center of the bell curve (must be in [0, 1])
std 0.15 Standard deviation — smaller = tighter clustering
count 100 Number of buckets (higher = more precision)

Exponential

High probability near zero, decaying exponentially. The tail is truncated at a configurable cutoff.

// Default: lambda = 1.0
val exp = BucketedDistribution.createExponential()

// Steeper decay
val steep = BucketedDistribution.createExponential(lambda = 3.0)
Parameter Default Meaning
lambda 1.0 Rate parameter — higher = steeper decay
tailCut 1e-3 Tail probability cutoff for truncation

Triangular

Probability rises linearly to the mode, then falls linearly. Useful when you want a peak at a specific value with bounded tails.

val tri = BucketedDistribution.createTriangular(
    min = 0.0,
    mode = 0.7,  // peak probability here
    max = 1.0,
)

Combinators

Reshape existing distributions without creating new ones from scratch:

inverse()

Flips the PDF so high-probability regions become low-probability and vice versa.

val normal = BucketedDistribution.createTruncatedNormal()
val inverted = normal.inverse()  // rare values near 0.5, common at edges

reversed()

Mirrors the PDF shape. An exponential that decays left-to-right becomes one that decays right-to-left.

val exp = BucketedDistribution.createExponential(lambda = 2.0)
val rev = exp.reversed()  // high probability at the right end instead of the left

blend()

Linearly interpolates between two distributions. At ratio = 0.0 you get this distribution; at ratio = 1.0 you get the other.

val normal = BucketedDistribution.createTruncatedNormal()
val uniform = BucketedDistribution.createUniform()

// 70% normal, 30% uniform
val blended = normal.blend(uniform, ratio = 0.3)
Both distributions must have the same number of buckets to blend. The default bucket count is 100.

RandomRange

RandomRange wraps a distribution with explicit min/max bounds, or represents a constant value. Useful when you need a serializable "random or fixed" configuration:

import io.peekandpoke.ultra.maths.stochastic.RandomRange

// Distribution-backed range
val range = RandomRange.OfDistribution(
    distribution = BucketedDistribution.createTruncatedNormal(),
    min = 10.0,
    max = 50.0,
)

// Constant (always returns the same value)
val fixed = RandomRange.OfConstant(constant = 42.0)
println(fixed.min)  // 42.0
println(fixed.max)  // 42.0