Skip to content

Instantly share code, notes, and snippets.

@olooney
Last active October 18, 2025 14:10
Show Gist options
  • Save olooney/d98f8e862a11974f36b3620f517df006 to your computer and use it in GitHub Desktop.
Save olooney/d98f8e862a11974f36b3620f517df006 to your computer and use it in GitHub Desktop.
A Random Walk in ℤ⁵ - Vizualization with NumPy and Pillow
from PIL import Image
import numpy as np
from typing import Annotated, TypeAlias, Tuple
import numpy.typing as npt
# %%
# five dimensional integer vector
Z5 = Annotated[npt.NDArray[np.int_], (5,)]
RGB: TypeAlias = Annotated[npt.NDArray[np.uint8], (3,)]
RGBTensor: TypeAlias = Annotated[npt.NDArray[np.uint8], (..., ..., 3)]
# %%
def random_walk_z5(
width: int = 64,
height: int = 64,
strides: Tuple[int, int, int, int, int] = (1, 1, 3, 3, 3),
n_iterations: int = 100_000,
seed: int | None = None,
) -> Image.Image:
"""
Visualizes a random walk in ℤ⁵, the five dimensional space of integers,
by using two coordinates for position and the other three for color.
"""
# validate arguments
for n in (height, width, n_iterations) + strides:
assert isinstance(n, int)
assert n > 0
if seed is not None:
assert isinstance(seed, int)
# initialize the random number generator with the given seed.
rng: np.random.Generator = np.random.default_rng(seed)
# we'll stay inside this cube by wrapping
bounds: Z5 = np.array([height, width, 256, 256, 256], dtype=int)
# mutable vector state, starting at a random point
vector: Z5 = np.zeros(5, int)
vector[:] = rng.integers(0, bounds, dtype=int)
# image is initially black
image: RGBTensor = np.zeros((height, width, 3), dtype=np.uint8)
for iteration in range(n_iterations):
# take a step in a random direction along a random axis
axis = rng.integers(0, 5)
stride = strides[axis]
step = rng.choice((-stride, stride))
vector[axis] += step
vector %= bounds
# interpret vector as (y, x, red, green, blue) and plot one pixel
y: int = vector[0]
x: int = vector[1]
color: RGB = vector[2:].astype(np.uint8)
image[y, x] = color
return Image.fromarray(image)
# %%
# see a quick example scaled up x8
random_walk_z5().resize((512, 512), Image.Resampling.NEAREST)
# %%
# save to disk
image = random_walk_z5()
image.save("rwz5_64x64.png", format="PNG")
# %%
# a perfect cube of breathtaking symmetry
random_walk_z5(256, 256, strides=(1, 1, 1, 1, 1), seed=0, n_iterations=2**24)
# %%
# 37 is the most random number: https://www.youtube.com/watch?v=d6iQrh2TK98
# 100 million iterations all but guarentees few black pixels remain, but
# takes the better part of an hour to render.
random_walk_z5(512, 512, n_iterations=int(1e8), seed=37)
@olooney
Copy link
Author

olooney commented Oct 18, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment