Randomness in Python¶
In Python, the random module implements random number
generators for sampling a bunch of different distributions. Technically, these are
pseudo-random, meaning the numbers created only seem random, but it will take an astronomically
long time before the generators start repeating themselves.
To start using random, place the following at the top of
your script:
import random
Note
While some of these can be recreated with Grasshopper nodes, some cannot be, and some cannot be recreated without more effort than it would take to create a simple script node with a few lines of code.
Using Seeds¶
It’s important to note that these random number generators are deterministic, meaning
if you know the initial configuration, you can predict exactly which numbers will be
produced after n steps. For us, this is useful because it means we can recreate
the same design if we start with the same seed. Setting the seed of the random
number generator completely determines what numbers will be generated. If you play
a lot of Rogue-likes, this is the same concept that you’ve seen with the seed of a run.
To set the seed used globally in the random module, you can
use the random.seed() function:
- seed(a=None, version=2)
Initialize internal state from a seed.
The only supported seed types are None, int, float, str, bytes, and bytearray.
None or no argument seeds from current time or from an operating system specific randomness source if available.
If a is an int, all bits are used.
For version 2 (the default), all of the bits are used if a is a str, bytes, or bytearray. For version 1 (provided for reproducing random sequences from older versions of Python), the algorithm for str and bytes generates a narrower range of seeds.
Within Grasshopper, it’s practical for us to be able to provide a seed to a Python 3
script node via a slider, so that we can have an easy way outside of our script to set
the seed. To do this, add an input type-hinted to int, and pass the value to
random.seed():
import random
seed: int # provided by an input pin
random.seed(seed)
Note
If you don’t call random.seed() before using the random
number generator, the seed will be set as if you passed None to random.seed().
Generating Random Numbers¶
The functions provided by the random module are all based
on the random.random() function. This generates a floating
point number in the range [0.0, 1.0) with a uniform distribution.
import random
x = random.random() # x is now a pseudo-random floating point number in the range [0.0, 1.0)
Before talking about the functions that can make what you want to do easier, let’s take
a look at how we can use the output of random.random() to
create a bunch of different pseudo-random results for us.
Generating a Different Uniform Distribution¶
If you want to generate numbers in a different range, you’ll need to transform the
output of random.random(), x with simple arithmetic. To increase
the high end of the range from 1 to any number a, you’ll need to multiply x
by a. To shift the start and endpoints of the range with the same number b, you’ll
need to add b to x.
How would you generate an arbitrary uniform distribution, [low, high)?
Answer
import random
def uniform(low: float, high: float) -> float:
x = random.random()
x *= high - low # stretch the range
x += low # shift the range
return x
This behavior is provided by the random.uniform() function.
- uniform(a, b)
Get a random number in the range [a, b) or [a, b] depending on rounding.
The mean (expected value) and variance of the random variable are:
E[X] = (a + b) / 2 Var[X] = (b - a) ** 2 / 12
Note
This is something that can be done easily with the Random component in Grasshopper.
Generating Integers¶
The random.random() function returns floating-point numbers,
but if you want random integers, you’ll need to convert a generated floating-point number
to an integer using int(). If you do this directly on
the output of random.random(), you’ll always get 0, because converting
a float to an int in Python just floors the floating-point number.
How would you sample an arbitrary integer distribution, [low, low + 1, low + 2, …, high]?
Answer
import random
def randint(low: int, high: int) -> int:
x = random.random()
x *= high - low + 1 # Add 1 because flooring floats in the range [0, a) yields ints in the range [0, a - 1]
x += low # Shift
x = int(x) # Floor
return x
This behavior is provided by the random.randint() function.
- randint(a, b)
Return random integer in range [a, b], including both end points.
Warning
This is not something you can do well with Grasshopper, even though the random component can be set to generate integer numbers. This component uses rounding to convert to integers, which means you’re actually not getting a uniform distribution of integers when you use this component.
Generating an Arbitrary Distribution¶
Arbitrary real-valued distributions can be sampled if you can convert from the standard uniform distribution to your target distribution. Transformations of this sort will often look like a real-valued function \(f:\: \mathbb{R} \rightarrow \mathbb{R}\).
Any real-valued distribution can be converted to a discrete integer-valued distribution
using int(), but be careful to note that this
conversion floors floating-point numbers, which has implications for the distribution
of integers created.
Random Sequence Operations¶
Some operations you might want to perform on sequences can be supplemented with the
random module. Namely, you might want to pick one or
more items from the sequence at random.
Picking a Random Item¶
Very Useful!
To take a single random item from a sequence, you effectively want to choose a random integer index to use to subscript the sequence. The index you’ll use will need to be at least 0 and up to, but not including, the length of the sequence.
How would you pick a random item from a given sequence?
Answer
import random
from typing import Any, Sequence
def choice(sequence: Sequence[Any]) -> Any:
x = random.random()
x *= len(sequence) # Scale the range to [0, len(sequence))
x = int(x) # Convert to int distribution [0, len(sequence) - 1]
return sequence[x]
This behavior is provided by the random.choice() function.
- choice(seq)
Choose a random element from a non-empty sequence.
Warning
This can be simulated with Grasshopper to create an index and sample a list, but again, the default behavior for converting real numbers to integers is by rounding, so you won’t get a uniform distribution of selected items.
Picking Multiple Items with Replacement¶
To select multiple random items from a sequence with replacement, you basically just have to repeatedly pick a single random item. Because we’re replacing the items that we take, we don’t mind if we take multiple of the same thing.
How would you pick n items from a given sequence, with replacement?
Answer
import random
from typing import Any, List, Sequence
def choices(sequence: Sequence[Any], n: int) -> List[Any}:
out = []
# Using _ as the iteration variable is just a way to say we don't really
# care about the variable
for _ in range(n):
x = random.random()
x *= len(sequence)
x = int(x)
out.append(sequence[x])
return out
This behavior is provided by the random.choices() function.
- choices(population, weights=None, *, cum_weights=None, k=1)
Return a k sized list of population elements chosen with replacement.
If the relative weights or cumulative weights are not specified, the selections are made with equal probability.
Picking Multiple Items Without Replacement¶
Picking multiple items from a sequence without replacement is a bit trickier, because we need to make sure that we don’t have any repeats. A naive approach might be to create a set of indices that have already been sampled and check that you haven’t already checked the number that you drew at random:
import random
def sample(sequence: Sequence[Any], n: int) -> List[Any]
out = []
sampled = set()
while len(out) < n:
idx = random.randint(len(sequence))
if idx in sampled:
continue
set.add(idx)
out.append(sequence[idx])
return out
What’s the issue with this approach?
Answer
It’s possible you might sample values for idx that you’ve already sampled many
different times in a row, and you may end up looping a lot.
A smarter approach might be to create a companion list of indices that haven’t been sampled yet and remove items from this list as you sample your sequence. How might you do this?
Answer
import random
from typing import Any, List, Sequence
def sample(sequence: Sequence[Any], n: int) -> List[Any}:
out = []
indices = list(range(len(sequence))) # Convert to a list because we need a sequence, not a range
for _ in range(n):
idx = random.choice(indices) # Pick an index
indices.remove(idx) # Remove it from the available indices
out.append(sequence[idx]) # Add the picked item to the output
return out
This behavior is provided by the random.sample() function.
- sample(population, k, *, counts=None)
Chooses k unique random elements from a population sequence.
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 hashable or unique. If the population contains repeats, then each occurrence is a possible selection in the sample.
Repeated elements can be specified one at a time or with the optional counts parameter. For example:
sample([‘red’, ‘blue’], counts=[4, 2], k=5)
is equivalent to:
sample([‘red’, ‘red’, ‘red’, ‘red’, ‘blue’, ‘blue’], k=5)
To choose a sample from a range of integers, use range() for the population argument. This is especially fast and space efficient for sampling from a large population:
sample(range(10000000), 60)
Important
This function can be used to create a shuffled version of a sequence:
import random
from typing import Any, List, Sequence
def shuffled(sequence: Sequence[Any]) -> List[Any]
return random.sample(sequence, len(sequence))
To shuffle a sequence in place (meaning you change the sequence itself without
returning anything), you can use the random.shuffle()
function.
Warning
To the best of my knowledge, sampling without replacement and shuffling a sequence cannot be recreated with Grasshopper!