Current section
Files
Jump to
Current section
Files
src/viva_math@random.erl
-module(viva_math@random).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/viva_math/random.gleam").
-export([from_int/1, with_algo/2, jump/1, uniform/1, uniform_in/3, integer/2, standard_normal/1, normal/3, bernoulli/2, categorical/2, choice/2, shuffle/2, standard_normals/2, uniforms/2]).
-export_type([seed/0, algorithm/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Pure functional random number generation.\n"
"\n"
" Built on top of Erlang's `:rand` module (OTP 22+). The `Seed` type is\n"
" **opaque and immutable**: every sampling function returns a new seed,\n"
" allowing fully reproducible deterministic streams without process state.\n"
"\n"
" The default algorithm is `exsss` (Xorshift116** with StarStar scrambler,\n"
" period 2¹¹⁶-1, 58-bit output). For longer streams or jump-ahead, use\n"
" `with_algo(Exro928ss, seed)`.\n"
"\n"
" ## Why not the hash-based generator in `common`?\n"
"\n"
" `common.deterministic_noise` is fine for reproducible *fixtures*, but it\n"
" is statistically weak. For Monte Carlo, neural net initialization or\n"
" Bayesian sampling, prefer this module.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import viva_math/random\n"
"\n"
" let seed0 = random.from_int(42)\n"
" let #(x, seed1) = random.uniform(seed0)\n"
" let #(y, seed2) = random.normal(seed1, 0.0, 1.0)\n"
" // Same seed0 always produces the same x, y sequence.\n"
" ```\n"
).
-type seed() :: any().
-type algorithm() :: exsss | exro928ss | exrop | exsp | exs1024s | mwc59.
-file("src/viva_math/random.gleam", 94).
?DOC(" Build a seed from an integer using the default algorithm (Exsss).\n").
-spec from_int(integer()) -> seed().
from_int(Seed) ->
viva_math_random_ffi:seed_default(Seed).
-file("src/viva_math/random.gleam", 99).
?DOC(" Build a seed with an explicit algorithm.\n").
-spec with_algo(algorithm(), integer()) -> seed().
with_algo(Algorithm, Seed) ->
viva_math_random_ffi:seed_with_algo(Algorithm, Seed).
-file("src/viva_math/random.gleam", 104).
?DOC(" Advance the seed by 2⁶⁴ calls. Useful for non-overlapping parallel streams.\n").
-spec jump(seed()) -> seed().
jump(Seed) ->
viva_math_random_ffi:jump(Seed).
-file("src/viva_math/random.gleam", 113).
?DOC(" Uniform float in [0.0, 1.0).\n").
-spec uniform(seed()) -> {float(), seed()}.
uniform(Seed) ->
viva_math_random_ffi:uniform_real(Seed).
-file("src/viva_math/random.gleam", 118).
?DOC(" Uniform float in [low, high).\n").
-spec uniform_in(seed(), float(), float()) -> {float(), seed()}.
uniform_in(Seed, Low, High) ->
{U, S} = viva_math_random_ffi:uniform_real(Seed),
{Low + (U * (High - Low)), S}.
-file("src/viva_math/random.gleam", 124).
?DOC(" Uniform integer in [1, n]. Returns an error if n < 1.\n").
-spec integer(seed(), integer()) -> {ok, {integer(), seed()}} | {error, nil}.
integer(Seed, N) ->
case N < 1 of
true ->
{error, nil};
false ->
{ok, viva_math_random_ffi:uniform_int(N, Seed)}
end.
-file("src/viva_math/random.gleam", 132).
?DOC(" Standard normal N(0, 1).\n").
-spec standard_normal(seed()) -> {float(), seed()}.
standard_normal(Seed) ->
viva_math_random_ffi:normal_standard(Seed).
-file("src/viva_math/random.gleam", 137).
?DOC(" Normal N(mu, sigma²).\n").
-spec normal(seed(), float(), float()) -> {float(), seed()}.
normal(Seed, Mu, Sigma) ->
viva_math_random_ffi:normal_with(Mu, Sigma, Seed).
-file("src/viva_math/random.gleam", 142).
?DOC(" Bernoulli sample: True with probability `p`. `p` is clamped to [0, 1].\n").
-spec bernoulli(seed(), float()) -> {boolean(), seed()}.
bernoulli(Seed, P) ->
P_clamped = case P of
P@1 when P@1 < +0.0 ->
+0.0;
P@2 when P@2 > 1.0 ->
1.0;
_ ->
P
end,
{U, S} = viva_math_random_ffi:uniform_real(Seed),
{U < P_clamped, S}.
-file("src/viva_math/random.gleam", 178).
-spec categorical_walk(list(float()), float(), integer(), float()) -> integer().
categorical_walk(Probs, Target, Index, Acc) ->
case Probs of
[] ->
Index - 1;
[P | Rest] ->
Next_acc = Acc + P,
case Target < Next_acc of
true ->
Index;
false ->
categorical_walk(Rest, Target, Index + 1, Next_acc)
end
end.
-file("src/viva_math/random.gleam", 160).
?DOC(
" Sample an index from a categorical distribution defined by `probs`.\n"
"\n"
" Probabilities are renormalised internally. Returns an error if the list\n"
" is empty or sums to a non-positive value.\n"
).
-spec categorical(seed(), list(float())) -> {ok, {integer(), seed()}} |
{error, nil}.
categorical(Seed, Probs) ->
Any_negative = gleam@list:any(Probs, fun(P) -> P < +0.0 end),
Total = gleam@list:fold(Probs, +0.0, fun(Acc, P@1) -> Acc + P@1 end),
case {Probs, Any_negative, Total > +0.0} of
{[], _, _} ->
{error, nil};
{_, true, _} ->
{error, nil};
{_, _, false} ->
{error, nil};
{_, _, true} ->
{U, S} = viva_math_random_ffi:uniform_real(Seed),
Target = U * Total,
{ok, {categorical_walk(Probs, Target, 0, +0.0), S}}
end.
-file("src/viva_math/random.gleam", 306).
-spec list_at(list(EXY), integer()) -> {ok, EXY} | {error, nil}.
list_at(Xs, Idx) ->
case {Xs, Idx} of
{[], _} ->
{error, nil};
{[X | _], 0} ->
{ok, X};
{[_ | Rest], N} ->
list_at(Rest, N - 1)
end.
-file("src/viva_math/random.gleam", 197).
?DOC(" Pick a uniformly random element from a list.\n").
-spec choice(seed(), list(EWS)) -> {ok, {EWS, seed()}} | {error, nil}.
choice(Seed, Xs) ->
case erlang:length(Xs) of
0 ->
{error, nil};
N ->
{Idx, S} = viva_math_random_ffi:uniform_int(N, Seed),
case list_at(Xs, Idx - 1) of
{ok, V} ->
{ok, {V, S}};
{error, _} ->
{error, nil}
end
end.
-file("src/viva_math/random.gleam", 238).
-spec dict_to_list(
gleam@dict:dict(integer(), EXF),
integer(),
integer(),
list(EXF)
) -> list(EXF).
dict_to_list(D, I, N, Acc) ->
case I >= N of
true ->
lists:reverse(Acc);
false ->
case gleam_stdlib:map_get(D, I) of
{ok, V} ->
dict_to_list(D, I + 1, N, [V | Acc]);
{error, _} ->
lists:reverse(Acc)
end
end.
-file("src/viva_math/random.gleam", 266).
-spec swap(gleam@dict:dict(integer(), EXP), integer(), integer()) -> gleam@dict:dict(integer(), EXP).
swap(D, I, J) ->
case {gleam_stdlib:map_get(D, I), gleam_stdlib:map_get(D, J)} of
{{ok, Vi}, {ok, Vj}} ->
gleam@dict:insert(gleam@dict:insert(D, I, Vj), J, Vi);
{_, _} ->
D
end.
-file("src/viva_math/random.gleam", 249).
-spec fisher_yates(gleam@dict:dict(integer(), EXK), integer(), seed()) -> {gleam@dict:dict(integer(), EXK),
seed()}.
fisher_yates(Arr, I, Seed) ->
case I =< 0 of
true ->
{Arr, Seed};
false ->
{J_raw, S1} = viva_math_random_ffi:uniform_int(I + 1, Seed),
J = J_raw - 1,
Swapped = swap(Arr, I, J),
fisher_yates(Swapped, I - 1, S1)
end.
-file("src/viva_math/random.gleam", 231).
-spec list_to_dict(list(EWZ), integer(), gleam@dict:dict(integer(), EWZ)) -> gleam@dict:dict(integer(), EWZ).
list_to_dict(Xs, I, Acc) ->
case Xs of
[] ->
Acc;
[X | Rest] ->
list_to_dict(Rest, I + 1, gleam@dict:insert(Acc, I, X))
end.
-file("src/viva_math/random.gleam", 219).
?DOC(
" Return a random permutation of `xs` via Fisher–Yates (modern Durstenfeld\n"
" variant), implemented on top of a `dict` for O(log n) indexed swaps.\n"
"\n"
" Truly uniform over all n! permutations (no float-key collision bias the\n"
" sort-based shuffle suffered from).\n"
).
-spec shuffle(seed(), list(EWW)) -> {list(EWW), seed()}.
shuffle(Seed, Xs) ->
N = erlang:length(Xs),
case N of
0 ->
{[], Seed};
_ ->
Initial = list_to_dict(Xs, 0, maps:new()),
{Shuffled, Final_seed} = fisher_yates(Initial, N - 1, Seed),
{dict_to_list(Shuffled, 0, N, []), Final_seed}
end.
-file("src/viva_math/random.gleam", 287).
-spec draw(integer(), seed(), fun((seed()) -> {float(), seed()}), list(float())) -> {list(float()),
seed()}.
draw(N, Seed, Sampler, Acc) ->
case N =< 0 of
true ->
{lists:reverse(Acc), Seed};
false ->
{X, S} = Sampler(Seed),
draw(N - 1, S, Sampler, [X | Acc])
end.
-file("src/viva_math/random.gleam", 278).
?DOC(" Sample n standard normals at once.\n").
-spec standard_normals(seed(), integer()) -> {list(float()), seed()}.
standard_normals(Seed, N) ->
draw(N, Seed, fun viva_math_random_ffi:normal_standard/1, []).
-file("src/viva_math/random.gleam", 283).
?DOC(" Sample n uniforms in [0, 1) at once.\n").
-spec uniforms(seed(), integer()) -> {list(float()), seed()}.
uniforms(Seed, N) ->
draw(N, Seed, fun viva_math_random_ffi:uniform_real/1, []).