I recently came across this interesting post and I wanted to replicate the whole thing in python within a jupyter notebook.

Let's start by correctly generating random positions within a disk (the original post missed the right way to do it as explained here). We then plot them in polar coordinates.

import numpy as np

npoints = 1000
radius = 10

r = np.sqrt(np.random.rand(npoints)) * radius
theta = np.random.rand(npoints) * 2 * np.pi

import matplotlib.pyplot as plt

fig, ax = plt.subplots(subplot_kw={"projection": "polar"}, figsize=(8, 8))
ax.plot(theta, r, "o", alpha=0.5);

Given this set of initial point, we will generate random trajectories using Perlin noise. Despite this kind of noise seems not to be coded within numpy or scipy, there are python packages to do it and we will install and use the following one

!pip install perlin-noise

Let's write a function to return a bunch of trajectories cutting each of them when the next point is outside the planet.

from perlin_noise import PerlinNoise


def generate_noisy_planet(npoints=1000, radius=10, step=0.1, seed=None):
    np.random.seed(seed)
    noise = PerlinNoise(octaves=4, seed=seed)

    trajectories = []
    for i in range(npoints):
        r = np.sqrt(np.random.rand()) * radius
        theta = np.random.rand() * 2 * np.pi
        x, y = r * np.cos(theta), r * np.sin(theta)

        trajectory = []
        while np.hypot(x, y) < radius:
            trajectory += [[x, y]]
            n = noise([x / 100, y / 100])
            fx = np.sin if n > 0.5 else np.cos
            fy = np.cos if n > 0.5 else np.sin
            x += fx(n * 2 * np.pi) * step
            y += fy(n * 2 * np.pi) * step

        trajectories += [np.array(trajectory)]
    return trajectories

Finally let's plot them in cartesian coordinates

trajectories = generate_noisy_planet()

fig, ax = plt.subplots(figsize=(8, 8))
ax.axis("equal")
ax.axis("off")
cmap = plt.get_cmap("Greys", len(trajectories))
for i, trajectory in enumerate(trajectories):
    plt.plot(trajectory[:, 0], trajectory[:, 1], color=cmap(i))

And here are some nice examples of noisy planets

seeds = [20, 52, 666]

cmaps = ["Reds", "Greens", "Blues"]
fig, axes = plt.subplots(ncols=3, figsize=(24, 8))

for i, seed in enumerate(seeds):
    axes[i].axis("equal")
    axes[i].axis("off")
    trajectories = generate_noisy_planet(seed=seed)
    cmap = plt.get_cmap(cmaps[i], len(trajectories))
    for j, trajectory in enumerate(trajectories):
        axes[i].plot(*trajectory.T, color=cmap(j))

Just like the original post, we finally create a gif file for the latest evil planet (seed=666)

import os

tmp_fig = "/tmp/planets"
os.makedirs(tmp_fig, exist_ok=True)

plt.figure(figsize=(8, 8))
min_time = 0
max_time = np.max([len(trajectory) for trajectory in trajectories])  
for t in range(min_time, max_time):
    plt.axis(2 * [-radius, +radius])
    plt.axis("off")
    for i, trajectory in enumerate(trajectories):
        tlim = min(t, len(trajectory))
        plt.plot(trajectory[:tlim, 0], trajectory[:tlim, 1], color=cmap(i))
    plt.savefig(os.path.join(tmp_fig, "planet_{:03d}.png".format(t)))
    plt.clf();

and the process conversion using ImageMagick

import subprocess

subprocess.run(
    [
        "convert",
        "-background",
        "white",
        "-alpha",
        "remove",
        "-layers",
        "optimize",
        "-delay",
        "10",
        "-loop",
        "0",
        tmp_fig + "/*.png",
        "./images/animation.gif",
    ]
);