Files
rand-cli/src/dist.ts
T

146 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type Options } from "./types";
// ---------------------------------------------------------------------------
// Individual distribution samplers
// ---------------------------------------------------------------------------
// Cached second normal deviate from Box-Muller transform.
let spareNormal: number | null = null;
/** Box-Muller with pair caching: uses half the RNG calls of naive Box-Muller. */
function normalRandom(mean: number, stddev: number): number {
if (spareNormal !== null) {
const v = spareNormal;
spareNormal = null;
return mean + stddev * v;
}
let u1 = Math.random();
for (let attempt = 0; u1 === 0 && attempt < 100; attempt++) {
u1 = Math.random();
}
if (u1 === 0) u1 = Number.EPSILON;
const u2 = Math.random();
const r = Math.sqrt(-2 * Math.log(u1));
spareNormal = r * Math.sin(2 * Math.PI * u2);
return mean + stddev * r * Math.cos(2 * Math.PI * u2);
}
/** Bernoulli trials; Normal approximation when n>10_000 and np, n(1-p) both >5. */
function binomialRandom(n: number, p: number): number {
if (n > 10_000 && n * p > 5 && n * (1 - p) > 5) {
const mean = n * p;
const stddev = Math.sqrt(n * p * (1 - p));
return Math.max(0, Math.min(n, Math.round(normalRandom(mean, stddev))));
}
let s = 0;
for (let i = 0; i < n; i++) {
if (Math.random() < p) s++;
}
return s;
}
/** Knuth's algorithm; Normal approximation for λ > 100 (avoids exp underflow). */
function poissonRandom(lambda: number): number {
if (lambda > 100) {
return Math.max(0, Math.round(normalRandom(lambda, Math.sqrt(lambda))));
}
const L = Math.exp(-lambda);
let k = 0;
let p = 1;
do {
k++;
p *= Math.random();
} while (p > L);
return k - 1;
}
/** Inverse CDF. Caller must ensure λ > 0. */
function exponentialRandom(lambda: number): number {
return -Math.log(Math.random() || Number.EPSILON) / lambda;
}
/** Urn model — simulate drawing without replacement. */
function hypergeometricRandom(N: number, K: number, n: number): number {
let s = 0;
let remainingK = K;
let remainingTotal = N;
const draws = Math.min(n, N);
for (let i = 0; i < draws; i++) {
if (Math.random() < remainingK / remainingTotal) {
s++;
remainingK--;
}
remainingTotal--;
}
return s;
}
// ---------------------------------------------------------------------------
// Dispatcher
// ---------------------------------------------------------------------------
export function validateOptions(opts: Options): void {
if (!Number.isInteger(opts.count) || opts.count < 1) {
throw new Error(`invalid count: ${opts.count}`);
}
if (!Number.isInteger(opts.decimals) || opts.decimals < 0 || opts.decimals > 100) {
throw new Error(`decimals must be 0100, got ${opts.decimals}`);
}
switch (opts.dist) {
case "uniform":
if (opts.min > opts.max) throw new Error(`min (${opts.min}) > max (${opts.max})`);
break;
case "normal":
if (opts.stddev <= 0) throw new Error(`stddev must be > 0, got ${opts.stddev}`);
break;
case "binomial":
if (opts.trials < 0 || !Number.isInteger(opts.trials))
throw new Error(`trials must be a non-negative integer, got ${opts.trials}`);
if (opts.prob < 0 || opts.prob > 1)
throw new Error(`prob must be 01, got ${opts.prob}`);
break;
case "poisson":
case "exponential":
if (opts.lambda <= 0) throw new Error(`lambda must be > 0, got ${opts.lambda}`);
break;
case "hypergeometric":
if (opts.popSize < 0 || !Number.isInteger(opts.popSize))
throw new Error(`population size N must be a non-negative integer, got ${opts.popSize}`);
if (opts.successes < 0 || opts.successes > opts.popSize || !Number.isInteger(opts.successes))
throw new Error(`successes K must be 0N, got ${opts.successes} (N=${opts.popSize})`);
if (opts.draws < 0 || opts.draws > opts.popSize || !Number.isInteger(opts.draws))
throw new Error(`draws n must be 0N, got ${opts.draws} (N=${opts.popSize})`);
break;
}
}
export function* generate(opts: Options): Generator<number> {
validateOptions(opts);
for (let i = 0; i < opts.count; i++) {
let v: number;
switch (opts.dist) {
case "uniform":
v = opts.decimals === 0
? Math.floor(Math.random() * (opts.max - opts.min + 1)) + opts.min
: Math.random() * (opts.max - opts.min) + opts.min;
break;
case "normal":
v = normalRandom(opts.mean, opts.stddev);
break;
case "binomial":
v = binomialRandom(opts.trials, opts.prob);
break;
case "poisson":
v = poissonRandom(opts.lambda);
break;
case "exponential":
v = exponentialRandom(opts.lambda);
break;
case "hypergeometric":
v = hypergeometricRandom(opts.popSize, opts.successes, opts.draws);
break;
}
yield opts.decimals > 0 ? v : Math.round(v);
}
}