samplespace.repeatablerandom - Repeatable Random Sequences


RepeatableRandomSequence allows for generating repeatable, deterministic random sequences. It is compatible with the built-in random module as a drop-in replacement.

A key feature of RepeatableRandomSequence is its ability to get, serialize, and restore internal state. This is especially useful when generating procedural content from a fixed seed.

A RepeatableRandomSequence can also be used for unit testing by replacing the built-in random module. Because each random sequence is deterministic and repeatable for a given seed, expected values can be recorded and compared against within unit tests.

RepeatableRandomSequence produces high-quality pseudo-random values. See Random Generator Quality for results from randomness tests.


class samplespace.repeatablerandom.RepeatableRandomSequence(seed=None)

A deterministic and repeatable random number generator compatible with Python’s builtin random module.

Parameters

seed (int, str, bytes, bytearray) – The sequence’s initial seed. See the seed() method below for more details.

RepeatableRandomSequence.BLOCK_SIZE_BITS: int = 64

The number of bits generated for each unique index.

RepeatableRandomSequence.BLOCK_MASK: int = 18446744073709551615

A bitmask corresponding to (1 << BLOCK_SIZE_BITS) - 1

Bookkeeping functions

RepeatableRandomSequence.seed(value=None)None

Re-initialize the random generator with a new seed. Resets the sequence to its first value.

Caution

This method cannot be called from within cascade(), and will raise a RuntimeError if attempted.

Parameters

value (int, str, bytes, bytearray) – The value to seed with. If this is a sequence type, the sequence is first hashed with seed 0.

Raises

ValueError – If the seed value is not a supported type.

RepeatableRandomSequence.getseed()

Returns: the original value passed to RepeatableRandomSequence() or seed().

RepeatableRandomSequence.getstate()samplespace.repeatablerandom.RepeatableRandomSequenceState

Returns an opaque object representing the sequence’s current state, used for saving, restoring, and serializing the sequence. This object can be passed to setstate() to restore the sequence’s state.

RepeatableRandomSequence.setstate(state: samplespace.repeatablerandom.RepeatableRandomSequenceState)None

Restore the sequence’s state from previous call to getstate().

RepeatableRandomSequence.reset()None

Reset the sequence to the beginning, setting index to 0.

Caution

This method cannot be called from within cascade(), and will raise a RuntimeError if attempted.

RepeatableRandomSequence.index

The sequence’s current index. Generating random values will always increase the index.

Tip

Prefer getstate() and setstate() over index manipulation when saving and restoring state.

Caution

The index cannot be read or written within cascade(), and will raise a RuntimeError if attempted.

Type

int

RepeatableRandomSequence.getnextblock()int

Get a block of random bits from the random generator.

Tip

Calling this method will advance the sequence exactly one time. The resulting index depends on whether or not the call occurs within a cascade.

Returns

A block of BLOCK_SIZE_BITS random bits as an int

RepeatableRandomSequence.getrandbits(k: int)int

Generate an int with k random bits.

This method may call getnextblock() multiple times if k is greater than BLOCK_SIZE_BITS; regardless of the number of calls, the index will only advance once.

Tip

Note that k == 0 is a valid input, which will advance the sequence even though no elements are chosen.

Parameters

k (int) – The number of random bits to generate.

Returns

A random integer in [0, max(2^k, 1))

RepeatableRandomSequence.cascade()

Returns a context manager that defines a generation cascade.

Cascades are not required for most applications, but may be useful for certain advanced procedural generation techniques.

A cascade allows multiple random samples to be treated as part the same logical unit.

For example, generating a random position in space can be treated as a single “transaction” by grouping the fields into a single cascade:

>>> print(rrs.index)
10
>>> with rrs.cascade():
...     x = rrs.random()  # index is 10
...     y = rrs.random()  # index is the previously-generated random block
...     z = rrs.random()  # index is the previously generated random block
...
>>> print(rrs.index)
11

The use of cascades ensure that random values are repeatable and based only on the number of previously-generated values, not the type of the values themselves. This is often useful when creating procedurally-generated content using pre-defined seeds.

While cascading, subsequent calls to random generation functions use the result of the most recently generated random data rather than incrementing the index. When a cascade completes, the index is set to one more than the index when the cascade began.

class samplespace.repeatablerandom.RepeatableRandomSequenceState(_seed: Any, _hash_input: bytes, _index: int)

An object representing a RepeatableRandomSequence’s internal state.

as_dict()

Return the sequence state as a dictionary for serialization.

classmethod from_dict(as_dict)

Construct a new sequence state from a dictionary, as returned by as_dict().

Examples

>>> rrs = RepeatableRandomSequence()
>>>
>>> state_as_dict = rrs.getstate().as_dict()
>>> state_as_dict
{'seed': 0, 'hash_input': 'NMlqzcrbG7s=', 'index': 0}
>>>
>>> new_state = RepeatableRandomSequenceState.from_dict(state_as_dict)
>>> rrs.setstate(new_state)

Integer distributions

RepeatableRandomSequence.randrange(start: int, stop: Optional[int] = None, step: int = 1)int

Generate a random integer from range(start[, stop][, step]).

If only one argument is provided, samples from range(start).

Parameters
  • start (int) – The starting point for the range, inclusive. If stop is None, this becomes the endpoint for the range, exclusive. None None

  • stop (int, optional) – The end point for the range, exclusive.

  • step (int, default 1) – Steps between possible values. May not be 0.

Raises
RepeatableRandomSequence.randint(a: int, b: int)int

Generate a random integer in the range [a, b].

This is an alias for randrange(a, b + 1), included for compatibility with the builtin random module.

RepeatableRandomSequence.randbytes(num_bytes)bytes

Generate a sequence of random bytes.

The index will only increment once, regardless of the number of bytes in the sequence.

This method produces similar results to

with rrs.cascade():
    result = bytes([rrs.randrange(256) for _ in range(num_bytes])

but offers significantly-improved performance and does not discard excess random bits.

Returns

A bytes object with num_bytes random integers in [0, 255].

RepeatableRandomSequence.geometric(mean: float, include_zero: bool = False)int

Generate integers according to a geometric distribution.

If include_zero is False, returns integers from 1 to infinity according to \(\text{Pr}(x = k) = p {(1 - p)}^{k - 1}\) where \(p = \frac{1}{\mathit{mean}}\).

If include_zero is True, returns integers from 0 to infinity according to \(\text{Pr}(x = k) = p {(1 - p)}^{k}\) where \(p = \frac{1}{\mathit{mean} + 1}\).

Parameters
  • mean (float) – The desired mean value.

  • include_zero (bool) – Whether or not the distribution’s support includes zero.

Raises

ValueError – if mean is less than 1 if include_zero is False, or less than 0 if include_zero is True.

RepeatableRandomSequence.finitegeometric(s: float, n: int)

Generate a random integer according to a geometric-like distribution with exponent s and finite support {1, …, n}.

The finite geometric distribution is defined by the equation

\[\text{Pr}(x=k) = \frac{s^{k}}{\sum_{i=1}^{N} s^{i}}\]
Raises

ValueError – if n is not at least 1

RepeatableRandomSequence.zipfmandelbrot(s: float, q: float, n: int)

Generate a random integer according to a Zipf-Mandelbrot distribution with exponent s, offset q, and support {1, …, n}.

The Zipf-Mandelbrot distribution is defined by the equation

\[\text{Pr}(x=k) = \frac{(k + q)^{-s}}{\sum_{i=1}^{N} (i+q)^{-s}}\]
Raises

ValueError – if n is not at least 1

Categorical distributions

RepeatableRandomSequence.choice(sequence: Sequence)

Choose a single random element from within a sequence.

Raises

IndexError – if the sequence is empty.

RepeatableRandomSequence.choices(population: Sequence, weights: Optional[Sequence[float]] = None, *, cum_weights: Optional[Sequence[float]] = None, k: int = 1) → Sequence

Choose k elements from a population, with replacement.

Either relative (via weights) or cumulative (via cum_weights) weights may be specified. If no weights are specified, selections are made uniformly with equal probability.

Tip

Note that k == 0 is a valid input, which returns an empty list an advances the sequence once even though no elements are chosen.

Parameters
  • population (sequence) – The population to sample from, which may include duplicate elements.

  • weights (sequence[float], optional) – Relative weights for each element in the population. Need not sum to 1.

  • cum_weights (sequence[float], optional) – Cumulative weights for each element in the population, as calculated by something like list(accumulate(weights)).

  • k (int) – The number of elements to choose.

Raises
  • IndexError – The population is empty.

  • ValueError – The length of weights or cum_weights does not match the population size.

  • TypeError – Both weights and cum_weights are specified.

RepeatableRandomSequence.shuffle(sequence: Sequence)None

Shuffle a sequence in place.

The index is only incremented once.

RepeatableRandomSequence.sample(population, k: int) → Sequence

Choose k unique random elements from a population, without replacement.

Elements are returned in selection order, so all subsets of the returned list are valid samples.

If the population includes duplicate values, each occurrence is as distinct possible selection in the result.

Tip

Note that k == 0 is a valid input, which returns an empty list an advances the sequence once even though no elements are chosen.

Parameters
  • population (list, tuple, set, range) – The source population.

  • k (int) – The number of samples to choose, no more than the total number of elements in population.

Raises
RepeatableRandomSequence.chance(p: float)bool

Returns True with probability p, else False.

Alias for random() < p.

Continuous distributions

RepeatableRandomSequence.random()float

Return a random float in [0.0, 1.0).

RepeatableRandomSequence.uniform(a: float, b: float)float

Return a random float uniformly distributed in [a, b).

RepeatableRandomSequence.triangular(low: float = 0.0, high: float = 1.0, mode: Optional[float] = None)float

Sample from a triangular distribution with lower limit low, upper limit low, and mode mode.

The triangular distribution is defined by

\[\begin{split}\text{P}(x) = \begin{cases} 0 & \text{for } x \lt l, \\ \frac{2(x-l)}{(h-l)(m-l)} & \text{for }l\le x \lt h, \\ \frac{2}{h-l} & \text{for } x = m, \\ \frac{2(h-x)}{(h-l)(h-m)} & \text{for } m \lt x \le h, \\ 0 & \text{for } h \lt x \end{cases}\end{split}\]
Raises

ValueError – if the mode is not in [low, high].

RepeatableRandomSequence.uniformproduct(n: int)float

Sample from a distribution whose values are the product of N uniformly distributed variables.

This distribution has the following PDF

\[\begin{split}\text{P}(x) = \begin{cases} \frac{(-1)^{n-1} log^{n-1}(x)}{(n - 1)!} & \text{for } x \in [0, 1) \\ 0 & \text{otherwise} \end{cases}\end{split}\]
Raises

ValueError – if n is not at least 1.

RepeatableRandomSequence.gauss(mu: float, sigma: float)float

Sample from a Gaussian distribution with parameters mu and sigma.

The Gaussian, or Normal distribution is defined by

\[\text{P}(x) = \frac{1}{\sigma \sqrt{2 \pi}} e^{-\frac{1}{2} \left(\frac{x - \mu}{\sigma}\right)^2}\]

Tip

If multiple normally-distributed values are required, consider using gausspair() for improved performance.

RepeatableRandomSequence.gausspair(mu: float, sigma: float) → Tuple[float, float]

Return a pair of independent samples from a Gaussian distribution with parameters mu and sigma.

This method produces similar results to

with rrs.cascade():
    result = rrs.gauss(mu, sigma), rrs.gauss(mu, sigma)

but offers improved performance.

RepeatableRandomSequence.lognormvariate(mu: float, sigma: float)float

Sample from a log-normal distribution with parameters mu and sigma.

The logarithms of values sampled from a log-normal distribution are normally distributed with mean mu and standard deviation sigma. The distribution is defined by

\[\text{P}(x) = \frac{1}{x \sigma \sqrt{2 \pi}} \exp{\left(- \frac{(\ln{x} - \mu)^2}{2 \sigma ^2}\right)}\]
RepeatableRandomSequence.expovariate(lambd: float)float

Sample from an exponential distribution with rate lambd.

The probability density function for the exponential distribution is defined by \(\text{P}(x)= \lambda e^{-\lambda x}\) where \(x\ge 0\)

RepeatableRandomSequence.vonmisesvariate(mu: float, kappa: float)float

Sample from a von Mises distribution with parameters mu and kappa.

Samples from a von Mises distribution represent randomly-chosen angles clustered around a mean angle. This distribution is an approximation to the wrapped normal distribution and is defined by

\[\text{P}(x) = \frac{e^{\kappa \cos{(x - \mu)}}}{2 \pi I_0(\kappa)}\]

where \(I_0(\kappa)\) is the modified Bessel function of order 0.

Parameters
  • mu (float) – The mean angle, in radians, around which to cluster.

  • kappa (float) – The concentration parameter, which must be at least 0.

Returns

An angle in radians within [0, 2 pi)

RepeatableRandomSequence.gammavariate(alpha: float, beta: float)float

Sample from a gamma distribution with parameters alpha and beta. Not to be confused with math.gamma()!

The gamma distribution is defined as

\[\text{P}(x) = \frac{x^{\alpha - 1} e^{-\frac{x}{\beta}}} {\Gamma(\alpha) \beta^{\alpha}}\]

Caution

This implementation defines its parameters to match random.gammavariate(). The parametrization differs from most common definitions of the gamma distribution, as defined on Wikipedia, et al. Take care when setting alpha and beta!

Raises

ValueError – if either alpha or beta is not greater than 0.

RepeatableRandomSequence.betavariate(alpha: float, beta: float)float

Sample from a beta distribution with parameters alpha and beta.

The beta distribution is defined by

\[\text{P}(x) = x^{\alpha - 1} (1 - x)^{\beta - 1} \frac{\Gamma(\alpha + \beta)}{\Gamma(\alpha)\Gamma(\beta)}\]
Returns

A random, beta-distributed value in [0.0, 1.0]

Raises

ValueError – if either alpha or beta is not greater than 0.

RepeatableRandomSequence.paretovariate(alpha: float)float

Sample from a Pareto distribution with shape parameter alpha and minimum value 1.

The Pareto distribution has PDF

\[\text{P}(x) = \frac{\alpha}{x^{\alpha + 1}}\]
Raises

ValueError – if alpha is zero.

RepeatableRandomSequence.weibullvariate(alpha: float, beta: float)float

Sample from a Weibull distribution with scale parameter alpha and shape parameter beta.

The distribution is defined by

\[\text{P}(x) = \frac{\beta}{\alpha} \left(\frac{x}{\alpha}\right)^{k-1} e^{-(x/\alpha)^k}\]

where \(x \ge 0\).

Raises

ValueError – if alpha is zero.

RepeatableRandomSequence.normalvariate(mu: float, sigma: float)float

Sample from a normal distribution with mean mu and standard variation sigma.

Note

Unlike random.normalvariate(), this is an alias for gauss(mu, sigma). It is included for compatibility with the built-in random module.

Examples

Generating random values:

import samplespace

rrs = samplespace.RepeatableRandomSequence(seed=1234)

samples = [rrs.randrange(30) for _ in range(10)]
print(samples)
# Will always print:
# [21, 13, 28, 19, 16, 29, 28, 24, 29, 25]

Distinct seeds produce unique results:

import samplespace

# Each seed will generate a unique sequence of values
for seed in range(5):
    rrs = samplespace.RepeatableRandomSequence(seed=seed)
    samples = [rrs.random() for _ in range(5)]
    print('Seed: {0}\tResults: {1}'.format(
        seed,
        ' '.join('{:.3f}'.format(x) for x in samples)))

Results depend only on number of previous calls, not their type:

import samplespace

rrs = samplespace.RepeatableRandomSequence(seed=1234)

dummy = rrs.random()
x1 = rrs.random()

rrs.reset()
dummy = rrs.gauss(6.0, 2.0)
x2 = rrs.random()
assert x2 == x1

rrs.reset()
dummy = rrs.randrange(50)
x3 = rrs.random()
assert x3 == x1

rrs.reset()
dummy = list('abcdefg')
rrs.shuffle(dummy)
x4 = rrs.random()
assert x4 == x1

rrs.reset()
with rrs.cascade():
    dummy = [rrs.random() for _ in range(10)]
x5 = rrs.random()
assert x5 == x1

Replay random sequences using RepeatableRandomSequence.reset():

import samplespace

rrs = samplespace.RepeatableRandomSequence(seed=1234)

samples = [rrs.random() for _ in range(10)]
print(' '.join(samples))

# Using reset() returns the sequence to its initial state
rrs.reset()
samples2 = [rrs.random() for _ in range(10)]
print(' '.join(samples2))
assert samples == sample2

Replay random sequences using RepeatableRandomSequence.getstate()/ RepeatableRandomSequence.setstate():

import samplespace

rrs = samplespace.RepeatableRandomSequence(seed=12345)

# Generate some random values to advance the state
[rrs.random() for _ in range(100)]

# Save the state for later recall
state = rrs.getstate()
print(rrs.random())
# Will print 0.2736967629462168

# Generate some more values
[rrs.random() for _ in range(100)]

# Return the sequence to the saved state. The next value will match
# the value following when the state was saved.
rrs.setstate(state)
print(rrs.random())
# Will also print 0.2736967629462168

Replay random sequences using RepeatableRandomSequence.index():

import samplespace

rrs = samplespace.RepeatableRandomSequence(seed=5)

# Generate a sequence of values
samples = [rrs.randrange(10) for _ in range(15)]
print(samples)
# Will print
# [0, 2, 2, 7, 9, 4, 1, 5, 5, 6, 7, 1, 7, 6, 8]

# Rewind the sequence by 5
rrs.index -= 5

# Generate a new sequence, will overlap by 5 elements
samples = [rrs.randrange(10) for _ in range(15)]
print(samples)
# Will print
# [7, 1, 7, 6, 8, 6, 6, 6, 3, 7, 6, 9, 5, 2, 7]

Serialize sequence state as simple data types:

import samplespace
import samplespace.repeatablerandom
import json

rrs = samplespace.RepeatableRandomSequence(seed=12345)

# Generate some random values to advance the state
[rrs.random() for _ in range(100)]

# Save the state for later recall
# State can be serialzied to a dict and serialized as JSON
state = rrs.getstate()
state_as_dict = state.as_dict()
state_as_json = json.dumps(state_as_dict)
print(state_as_json)
# Prints {"seed": 12345, "hash_input": "gxzNfDj4Ypc=", "index": 100}

print(rrs.random())
# Will print 0.2736967629462168

# Generate some more values
[rrs.random() for _ in range(100)]

# Return the sequence to the saved state. The next value will match
# the value following when the state was saved.
new_state_as_dict = json.loads(state_as_json)
new_state = samplespace.repeatablerandom.RepeatableRandomSequenceState.from_dict(new_state_as_dict)
rrs.setstate(new_state)
print(rrs.random())
# Will also print 0.2736967629462168