Packages

This module contains pseudo-random number generators for various distributions ported from Python 3 random module for Elixir.

Current section

Files

Jump to
random lib Random.ex
Raw

lib/Random.ex

# Ported from Python 3
# See: http://hg.python.org/cpython/file/8c768bbacd92/Lib/random.py
#
# Translated by Guido van Rossum from C source provided by
# Adrian Baddeley. Adapted by Raymond Hettinger for use with
# the Mersenne Twister and os.urandom() core generators.
#
# Ported to Elixir by Yuce Tekol.
#
# Uniform random number generation code is provided by Kenji Rikitake.
# https://github.com/jj1bdx/tinymt-erlang/blob/master/src/tinymt32.erl
#
# shuffle code is taken straight from Elixir source, and adapted to use
# TinyMT Erlang.
defmodule Random do
use Bitwise
@moduledoc """
This module contains pseudo-random number generators for various distributionsported from Python 3 `random` module The documentation below is adapted from that module as well.
For integers, there is uniform selection from a range. For sequences, there is uniform selection of a random element, a function to generate a random permutation, and a function for random sampling without replacement.
On the real line, there are functions to compute uniform, normal (Gaussian), lognormal, negative exponential, gamma, and beta distributions. For generating distributions of angles, the von Mises distribution is available.
[Project homepage](https://github.com/yuce/random/)
[Original Python 3 documentation](http://docs.python.org/3/library/random.html)
Example:
iex(1)> Random.seed(42)
:undefined
iex(2)> Random.randint(5, 142)
40
iex(3)> Random.randrange(5, 142, 2)
127
iex(4)> Random.choice(10..1000)
779
"""
@nv_magicconst 4 * :math.exp(-0.5) / :math.sqrt(2.0)
@twopi 2 * :math.pi
@log4 :math.log(4)
@sg_magicconst 1 + :math.log(4.5)
@bpf 53
@maxwidth 1 <<< @bpf
@e 2.71828
@doc """
Return x % y
"""
def mod(x, y), do: rem(rem(x, y) + y, y)
def random_int(n) when n >= 1 do
trunc(random() * n)
end
@doc """
Seed the random generator.
This function accepts both erlang (tuple of 3 integers) and python (single integer) forms of seeding.
`Random.seed(n)` is equivalent to `Random.seed({0, n, 0})`.
Erlang form:
now = :erlang.timestamp
Random.seed(now)
Python form:
Random.seed(5)
"""
def seed({a, b, c}) do
:tinymt32.seed(a, b, c)
end
def seed(a), do: :tinymt32.seed(0, a, 0)
@doc """
Returns a random integer from range `[0, stop)`.
"""
def randrange(stop) do
randrange(0, stop, 1)
end
@doc """
Returns a random integer from range `[start, stop)`.
"""
def randrange(start, stop) do
randrange(start, stop, 1)
end
@doc """
Returns a random integer from range `[start, stop)` with steps `step`.
"""
def randrange(start, stop, step)
when trunc(start) != start or
trunc(stop) != stop or
trunc(step) != step do
raise ArgumentError, message: "non-integer argument for randrange(#{start}, #{stop}, #{step}"
end
def randrange(start, stop, step)
when step == 1 do
width = stop - start
if width > 0 do
if width >= @maxwidth do
start + randbelow(width)
else
start + random_int(width)
end
else
raise ArgumentError, message: "empty range for randrange(#{start}, #{stop}, #{step}"
end
end
def randrange(start, stop, step) do
width = stop - start
n = cond do
step > 0 ->
trunc((width + step - 1) / step)
step < 0 ->
trunc((width + step + 1) / step)
true ->
raise ArgumentError, message: "zero step for randrange(#{start}, #{stop}, #{step}"
end
if n <= 0 do
raise ArgumentError, message: "empty range for randrange(#{start}, #{stop}, #{step})"
end
if n >= @maxwidth do
start + step * randbelow(n)
else
start + step * random_int(n)
end
end
defp randbelow(n), do: random_int(n)
@doc """
Return a random integer N such that a <= N <= b. Alias for Random.randrange(a, b+1).
"""
def randint(a, b), do: randrange(a, b + 1)
@doc """
Returns a random element from a non-empty sequence.
If `seq` is a list, converts it to a tuple before picking.
"""
def choice(a..b)
when b >= a do
n = b - a + 1
random_int(n) + a
end
def choice(seq)
when is_list(seq) do
tp = :erlang.list_to_tuple(seq)
choice(tp)
end
def choice(seq)
when is_tuple(seq) do
elem(seq, random_int(:erlang.size(seq)))
end
@doc """
Shuffle sequence `x`. This function is currently an alias for `Enum.shuffle/1`.
Note that for even rather small `size(x)`, the total number of permutations of x is larger than the period of most random number generators; this implies that most permutations of a long sequence can never be generated.
"""
def shuffle(enumerable) do
randomized = Enum.reduce(enumerable, [], fn x, acc ->
[{random(), x}|acc]
end)
unwrap(:lists.keysort(1, randomized), [])
end
defp unwrap([{_, h} | enumerable], t) do
unwrap(enumerable, [h|t])
end
defp unwrap([], t), do: t
@doc """
Chooses k unique random elements from a population sequence or set.
Returns a new list containing elements from the population while
leaving the original population unchanged. The resulting list is
in selection order so that all sub-slices will also be valid random
samples. This allows raffle winners (the sample) to be partitioned
into grand prize and second place winners (the subslices).
Members of the population need not be unique. If the
population contains repeats, then each occurrence is a possible
selection in the sample.
To choose a sample in a range of integers, use range as an argument.
This is especially fast and space efficient for sampling from a
large population: `Random.sample(0..10000000, 60)`
"""
def sample(_pop, k)
when k <= 0 do
raise ArgumentError, message: "sample: k must be greater than 0"
end
def sample(a..b, k)
when b >= a and k <= (b - a + 1) do
n = (b - a) + 1
sel = MapSet.new
Enum.map(sample_helper(n, k, sel, 0), &(a + &1))
end
def sample(pop, k)
when is_list(pop) do
sample(:erlang.list_to_tuple(pop), k)
end
def sample(pop, k)
when is_tuple(pop) do
n = :erlang.size(pop)
sel = MapSet.new
Enum.map sample_helper(n, k, sel, 0), &(elem(pop, &1))
end
defp sample_helper(n, k, sel, sel_size) do
if sel_size < k do
j = random_int(n)
if MapSet.member?(sel, j) do
sample_helper(n, k, sel, sel_size)
else
sel = MapSet.put(sel, j)
sel_size = sel_size + 1
sample_helper(n, k, sel, sel_size)
end
else
MapSet.to_list(sel)
end
end
defp seed0 do
{:intstate32, 297425621, 2108342699, 4290625991,
2232209075, 2406486510, 4235788063,
932445695}
end
defp temper_float(r) do
:tinymt32.temper(r) * (1.0 / 4294967296.0)
end
defp uniform_s(r0) do
r1 = :tinymt32.next_state(r0)
{temper_float(r1), r1}
end
@doc """
Return the next random floating point number in the range [0.0, 1.0).
"""
def random do
r = case :erlang.get(:tinymt32_seed) do
:undefined -> seed0()
other -> other
end
{v, r2} = uniform_s(r)
:erlang.put(:tinymt32_seed, r2)
v
end
@doc """
Return a random floating point number N such that a <= N <= b for a <= b and b <= N <= a for b < a.
The end-point value b may or may not be included in the range depending on floating-point rounding in the equation `a + (b-a) * random()`.
"""
def uniform(a, b), do: a + (b - a) * random()
@doc """
Triangular distribution.
Return a random floating point number N such that low <= N <= high and with the specified mode between those bounds. The low and high bounds default to zero and one. The mode argument defaults to the midpoint between the bounds, giving a symmetric distribution.
http://en.wikipedia.org/wiki/Triangular_distribution
"""
def triangular(low\\0, high\\1, mode\\nil) do
u = random()
c = if mode == nil, do: 0.5, else: (mode - low) / (high - low)
{u, c, low, high} = if u > c do
{1 - u, 1 - c, high, low}
else
{u, c, low, high}
end
low + (high - low) * :math.pow(u * c, 0.5)
end
@doc """
Normal distribution. mu is the mean, and sigma is the standard deviation.
"""
def normalvariate(mu, sigma) do
z = normalvariate_helper()
mu + z * sigma
end
defp normalvariate_helper do
u1 = random()
u2 = 1.0 - random()
z = @nv_magicconst * (u1 - 0.5) / u2
zz = z * z / 4.0
if zz <= -:math.log(u2), do: z, else: normalvariate_helper()
end
@doc """
Log normal distribution. If you take the natural logarithm of this distribution, you’ll get a normal distribution with mean mu and standard deviation sigma. mu can have any value, and sigma must be greater than zero.
"""
def lognormvariate(mu, sigma), do: :math.exp(normalvariate(mu, sigma))
@doc """
Exponential distribution. `lambda` is 1.0 divided by the desired mean. It should be nonzero. Returned values range from 0 to positive infinity if lambda is positive, and from negative infinity to 0 if lambda is negative.
"""
def expovariate(lambda), do: -:math.log(1.0 - random()) / lambda
@doc """
mu is the mean angle, expressed in radians between 0 and 2*pi, and kappa is the concentration parameter, which must be greater than or equal to zero. If kappa is equal to zero, this distribution reduces to a uniform random angle over the range 0 to 2*pi.
"""
def vonmisesvariate(_mu, kappa)
when kappa <= 1.0e-6, do: @twopi * random()
def vonmisesvariate(mu, kappa) do
s = 0.5 / kappa
r = s + :math.sqrt(1.0 + s * s)
z = vonmisesvariate_helper(r)
q = 1.0 / r
f = (q + z) / (1.0 + q * z)
u3 = random()
if u3 > 0.5 do
mod((mu + :math.acos(f)), @twopi)
else
mod((mu - :math.acos(f)), @twopi)
end
end
defp vonmisesvariate_helper(r) do
u1 = random()
z = :math.cos(:math.pi * u1)
d = z / (r + 2)
u2 = random()
if (u2 < 1.0 - d * d) or (u2 <= (1.0 - d) * :math.exp(d)) do
z
else
vonmisesvariate_helper(r)
end
end
@doc """
Gamma distribution. Not the gamma function!
Conditions on the parameters are alpha > 0 and beta > 0.
The probability distribution function is:
x ** (alpha - 1) * exp(-x / beta)
pdf(x) = ---------------------------------
gamma(alpha) * beta ** alpha
"""
def gammavariate(alpha, beta)
when alpha <= 0 and beta <= 0 do
raise ArgumentError, message: "gammavariate: alpha and beta must be > 0.0"
end
def gammavariate(alpha, beta)
when alpha > 1 do
ainv = :math.sqrt(2 * alpha- 1)
bbb = alpha - @log4
ccc = alpha + ainv
gammavariate_helper(alpha, beta, ainv, bbb, ccc)
end
def gammavariate(alpha, beta)
when alpha == 1 do
u = random()
if u <= 1.0e-7, do: gammavariate(alpha, beta)
-:math.log(u) * beta
end
def gammavariate(alpha, beta) do
u = random()
b = (@e + alpha) / @e
p = b * u
x = if p <= 1.0 do
:math.pow(p, 1 / alpha)
else
-:math.log((b - p) / alpha)
end
u1 = random()
unless (p > 1 and u1 <= :math.pow(x, alpha - 1)) or (u1 <= :math.exp(-x)) do
gammavariate(alpha, beta)
end
x * beta
end
defp gammavariate_helper(alpha, beta, ainv, bbb, ccc) do
u1 = random()
if 1.0e-6 < u1 and u1 < 0.9999999 do
u2 = 1 - random()
v = :math.log(u1 / (1 - u1)) / ainv
x = alpha * :math.exp(v)
z = u1 * u1 * u2
r = bbb + ccc * v - x
if r + @sg_magicconst - 4.5 * z >= 0 or r >= :math.log(z) do
x * beta
else
gammavariate_helper(alpha, beta, ainv, bbb, ccc)
end
else
gammavariate_helper(alpha, beta, ainv, bbb, ccc)
end
end
@doc """
Gaussian distribution.
mu is the mean, and sigma is the standard deviation. This is
slightly faster than the `Random.normalvariate/2` function.
Returns {number, gauss_next}
Example:
iex(1)> {n, gauss_next} = Random.gauss(1, 2)
{-2.0056082102271917, 0.5561885306380824}
iex(2)> {n, gauss_next} = Random.gauss(1, 2, gauss_next)
{2.112377061276165, nil}
"""
def gauss(mu, sigma, gauss_next\\nil) do
z = gauss_next
{z, gauss_next} = if z == nil do
x2pi = random() * @twopi
g2rad = :math.sqrt(-2 * :math.log(1 - random()))
{:math.cos(x2pi) * g2rad, :math.sin(x2pi) * g2rad}
else
{gauss_next, nil}
end
{mu + z * sigma, gauss_next}
end
@doc """
Beta distribution.
Conditions on the parameters are alpha > 0 and beta > 0.
Returned values range between 0 and 1.
"""
def betavariate(alpha, beta) do
y = gammavariate(alpha, 1.0)
if y == 0, do: 0, else: y / (y + gammavariate(beta, 1))
end
@doc """
Pareto distribution.
alpha is the shape parameter.
"""
def paretovariate(alpha) do
u = 1 - random()
1 / :math.pow(u, 1 / alpha)
end
@doc """
Weibull distribution.
alpha is the scale parameter and beta is the shape parameter.
"""
def weibullvariate(alpha, beta) do
u = 1 - random()
alpha * :math.pow(-:math.log(u), 1 / beta)
end
end # module