Generative art experiments#

Danielle Navarro delivered an exceptionally captivating presentation on their generative art as part of the 2023 Ihaka lecture series. I’ve been enthusiastic about integrating procedural generation into my games, as I find the concept of harnessing randomness to craft something extraordinary truly fascinating. Generative art provides a remarkable avenue for delving into this very concept. Here, I’d like to share a few of the experiments I ventured into following Danielle’s enlightening talk.

1. Random walk#

Hide code cell source
# import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import os

def hide_axes(ax):
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.xaxis.set_ticks_position('none')
    ax.yaxis.set_ticks_position('none')
    ax.set_xticklabels([])
    ax.set_yticklabels([])


OUTDIR = "random_walk"
os.makedirs(OUTDIR, exist_ok=True)

def next_step():
    direction = np.random.uniform(-1, 1, size=1)
    distance = np.random.normal(0, 5, size=1)
    return direction * distance


def generate_random_walk(n_steps, n_smooth):
    pts = np.zeros((2, n_steps))
    for i in range(1, n_steps):
        pts[0, i] = pts[0, i-1] + next_step()
        pts[1, i] = pts[1, i-1] + next_step()
    # smooth xy coordinates
    pts[0, :] = np.convolve(pts[0, :], np.ones(n_smooth)/n_smooth, mode='same')
    pts[1, :] = np.convolve(pts[1, :], np.ones(n_smooth)/n_smooth, mode='same')
    return pts


def plot_walk(n_steps, n_walkers, seed, colormap=plt.cm.Blues_r, alpha=0.5):
    n_smooth = int(n_steps * 0.05)
    np.random.seed(seed)
    fig = plt.figure(figsize=(5,5), facecolor=(0, 0, 0))
    for i in range(n_walkers):
        pts = generate_random_walk(n_steps, n_smooth)[:, 0:n_steps-n_smooth]
        point_numbers = list(range(len(pts[0])))
        plt.scatter(pts[0, :], pts[1, :], c=point_numbers, cmap=colormap, edgecolors='none', s=1, alpha=alpha)

    plt.gca().set_facecolor('black')
    plt.axis('off')
    return fig
    
# fig = plot_walk(n_steps=5000, n_walkers=10, seed=0)
# fig.savefig(f"{OUTDIR}/1.png")
# fig = plot_walk(n_steps=5000, n_walkers=100, seed=1, colormap=plt.cm.Reds_r, alpha=0.05)
# fig.savefig(f"{OUTDIR}/2.png")
# fig = plot_walk(n_steps=100000, n_walkers=20, seed=2, colormap='winter', alpha=0.5)
# fig.savefig(f"{OUTDIR}/3.png")

2. N-body#

a) All bodies are the same mass#

Hide code cell source
# Some utils
import matplotlib.pyplot as plt
# import imageio
import os
import glob
import re
from datetime import datetime
import numpy as np
from tqdm.auto import trange, tqdm
import warnings

from matplotlib import rcParams

import colorsys
from matplotlib.colors import ColorConverter, LinearSegmentedColormap

rcParams.update({"xtick.major.pad": "7.0"})
rcParams.update({"xtick.major.size": "7.5"})
rcParams.update({"xtick.major.width": "1.5"})
rcParams.update({"xtick.minor.pad": "7.0"})
rcParams.update({"xtick.minor.size": "3.5"})
rcParams.update({"xtick.minor.width": "1.0"})
rcParams.update({"ytick.major.pad": "7.0"})
rcParams.update({"ytick.major.size": "7.5"})
rcParams.update({"ytick.major.width": "1.5"})
rcParams.update({"ytick.minor.pad": "7.0"})
rcParams.update({"ytick.minor.size": "3.5"})
rcParams.update({"ytick.minor.width": "1.0"})
rcParams.update({"font.size": 20})
rcParams.update({"xtick.top": True})
rcParams.update({"ytick.right": True})
rcParams.update({"xtick.direction": "in"})
rcParams.update({"ytick.direction": "in"})


OUTDIR = 'orbit_out'
os.makedirs(OUTDIR, exist_ok=True)


def collect_runtimes(func, n_vals, n_trials=2, kwargs={}) -> np.ndarray:
    """
    Collect runtimes for a function with different input sizes

    Parameters
    ----------
    func : function
        Function to run
    n_vals : list
        List of input sizes
    n_trials : int, optional
        Number of trials to run, by default 2
    kwargs : dict, optional
        Keyword arguments to pass to func, by default {}

    Returns
    -------
    np.ndarray
        Array of runtimes with shape (len(n_vals), n_trials)
    """
    warnings.filterwarnings("error")
    runtimes = np.zeros((len(n_vals), n_trials))
    for npart_i in trange(len(n_vals), desc="Collecting runtimes"):
        for trial_i in range(n_trials):
            start = datetime.now()
            n = n_vals[npart_i]
            try:
                func(N=n, **kwargs)
                runtimes[npart_i, trial_i] = get_runtime(start)
            except RuntimeWarning:
                runtimes[npart_i, trial_i] = np.nan
    return runtimes


def get_runtime(start: datetime):
    return (datetime.now() - start).total_seconds()


def make_gif(im_regex, outname, duration=0.1):
    imgs = glob.glob(im_regex)
    imgs = sorted(imgs, key=lambda x: int(re.findall(r"\d+", x)[0]))
    frames = [imageio.imread_v2(f) for f in imgs]
    imageio.mimsave(outname, frames, duration=duration)
    for f in imgs:
        os.remove(f)


def remove_spines(ax):
    """Remove all spines and ticks from an axis"""
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.set_yticks([])
    ax.set_xticks([])


def scale_color_brightness(color, scale_l=1.0):
    rgb = ColorConverter.to_rgb(color)
    # convert rgb to hls
    h, l, s = colorsys.rgb_to_hls(*rgb)
    # manipulate h, l, s values and return as rgb
    return colorsys.hls_to_rgb(h, min(1, l * scale_l), s=s)


def make_colormap(color):
    rgb = [
        (i, scale_color_brightness(color, scale_l=i + 1)) for i in np.linspace(0, 1, 30)
    ]
    cmap = LinearSegmentedColormap.from_list(f"custom_{color}", colors=rgb, N=256)
    cmap = cmap.reversed()
    return cmap

from datetime import datetime
from tqdm.auto import trange
from typing import List, Union
import numpy as np
import matplotlib.pyplot as plt
import os
import random

G = 1.0  # Gravitational Constant
OUTDIR = 'orbit_out/'


def plot_particles(positions: Union[List, np.ndarray], n_time_total: int = 0, color='tab:blue'):
    """Plot the positions of particles in 2D

    Parameters
    ----------
    positions : Union[List, np.ndarray]
        List of positions of particles. Should be of shape (n_particles, {xyz}, n_time).
    n_time_total : int, optional
        Total number of time steps (used for plotting the trail), by default 0
    color : str, optional
        Color of the particles, by default 'tab:blue'
    """

    n_part, _, n_time = positions.shape
    if n_time_total == 0:
        n_time_total = n_time

    fig = plt.figure(figsize=(4, 4), dpi=80)
    ax = fig.gca()

    # plot the particle orbits
    idx_end = np.argmax(np.where(np.sum(positions, axis=(0, 1)) != 0)[0])
    idx_start = np.max([int(idx_end - 0.1 * n_time_total), 0])
    nidx = idx_end - idx_start

    max_size = 10
    ax.scatter(
        positions[:, 0, idx_end], positions[:, 1, idx_end],
        s=max_size, color=color, ec='k', lw=0.5
    )

    # plot the trail
    if nidx > 1:
        ms = np.geomspace(1e-4, max_size, nidx)
        # set ms < 0.05 to 0
        mask = ms < 0.05
        ms[mask] = 0

        # colors = np.array([make_colormap('tab:blue', 'white')(i) for i in np.linspace(0, 1, nidx)])
        ax.scatter(
            positions[:, 0, idx_start:idx_end], positions[:, 1, idx_start:idx_end],
            s=[ms] * n_part, zorder=-10,
            c=[ms] * n_part,
            cmap=make_colormap(color),

        )

    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)
    ax = plt.gca()
    remove_spines(ax)
    ax.set_aspect('equal', 'box')
    # remove white border around figure
    fig.tight_layout(pad=0)
    return fig


def plot_particle_gif(pos, outdir, dur):
    os.makedirs(outdir, exist_ok=True)
    n_part, _, n_time = pos.shape
    for i in trange(10, n_time, 10, desc='Making gif'):
        fig = plot_particles(pos[:, :, 0:i], n_time_total=n_time, color='tab:blue')
        # add textbox in top left corner
        ax = plt.gca()
        ax.text(
            0.05, 0.95,
            f't={i:003d}', transform=ax.transAxes,
            fontsize=14, verticalalignment='top',
            fontstyle='italic',
            alpha=0.5,
        )
        fig.savefig(f'{outdir}/orbit_{i:003d}.png')
        plt.close(fig)
    make_gif(f'{outdir}/orbit_*.png', f'{outdir}/orbit.gif', duration=dur)
    print(f'Gif saved to {outdir}/orbit.gif')


def newtonian_acceleration_np(pos, mass, G, softening):
    # positions r = [x,y,z] for all particles
    x = pos[:, 0:1]
    y = pos[:, 1:2]
    z = pos[:, 2:3]

    # matrix that stores all pairwise particle separations: r_j - r_i
    dx = x.T - x
    dy = y.T - y
    dz = z.T - z

    # matrix that stores 1/r^3 for all particle pairwise particle separations
    inv_r3 = (dx ** 2 + dy ** 2 + dz ** 2 + softening ** 2)
    inv_r3[inv_r3 > 0] = inv_r3[inv_r3 > 0] ** (-1.5)

    # pack together the acceleration components
    return G * np.hstack((
        np.matmul(dx * inv_r3, mass),
        np.matmul(dy * inv_r3, mass),
        np.matmul(dz * inv_r3, mass)
    ))


def nbody_runner_np(
        N: int = 5,
        tEnd: float = 10.0,
        dt: float = 0.01,
        softening: float = 0.1,
        random_seed: int = 17,
        max_runtime: int = 5,
        verbose: bool = True,
):
    """ N-body simulation """

    # Initialisation
    np.random.seed(random_seed)
    mass = 20.0 * np.ones((N, 1)) / N  # total mass of particles is 20
    pos = np.random.randn(N, 3)  # randomly selected positions and velocities
    vel = np.random.randn(N, 3)
    t = 0
    Nt = int(np.ceil(tEnd / dt))
    runtime_start = datetime.now()
    vel -= np.mean(mass * vel, 0) / np.mean(mass)
    acc = newtonian_acceleration_np(pos, mass, G, softening)
    pos_save = np.zeros((N, 3, Nt + 1))
    pos_save[:, :, 0] = pos

    # Simulation loop
    for i in trange(Nt, desc="Running simulation", disable=not verbose):
        vel += acc * dt / 2.0
        pos += vel * dt
        acc = newtonian_acceleration_np(pos, mass, G, softening)
        vel += acc * dt / 2.0
        t += dt
        pos_save[:, :, i + 1] = pos
        runtime = get_runtime(runtime_start)
        if runtime > max_runtime:
            warnings.warn(f"Runtime exceeded {max_runtime} seconds. Stopping simulation.")
            break
    return pos_save




import matplotlib.collections as mcoll

def colorline(
    x, y, z=None, cmap=plt.get_cmap('copper'), norm=plt.Normalize(0.0, 1.0),
        linewidth=3, alpha=1.0):
    # Default colors equally spaced on [0,1]:
    if z is None:
        z = np.linspace(0.0, 1.0, len(x))

    # Special case if a single number:
    if not hasattr(z, "__iter__"):  # to check for numerical input -- this is a hack
        z = np.array([z])

    z = np.asarray(z)

    segments = make_segments(x, y)
    lc = mcoll.LineCollection(segments, array=z, cmap=cmap, norm=norm,
                              linewidth=linewidth, alpha=alpha)

    ax = plt.gca()
    ax.add_collection(lc)

    return lc


def make_segments(x, y):
    points = np.array([x, y]).T.reshape(-1, 1, 2)
    segments = np.concatenate([points[:-1], points[1:]], axis=1)
    return segments



def plot_simple_orbits(seed, N=2, tEnd=3, dt=0.001, spacing=0.001, nlin=30):
    fig = plt.figure(figsize=(10,10), facecolor='black')
    positions = nbody_runner_np(N=N, random_seed=seed, tEnd=tEnd, verbose=False, dt=dt)

    max_lim_x, min_lim_x = np.max(positions[:, 0, :]), np.min(positions[:, 0, :])
    max_lim_y, min_lim_y = np.max(positions[:, 1, :]), np.min(positions[:, 1, :])

    pts = np.linspace(0, 1, positions.shape[2])
    
    plt.xlim(min_lim_x, max_lim_x)
    plt.ylim(min_lim_y, max_lim_y)
    for i, plnts in enumerate(positions):
        # decrease alpha over time
        # alpha = np.linspace(0.1, 1, len(i))
        # color between white and blue
        colorbar = make_colormap(f'C{i}')

#         colorline(plnts[0][1:-1], plnts[1][1:-1], pts, cmap=colorbar, linewidth=2)

        for _ in reversed(range(nlin)):
            colorline(plnts[0][1:-1]+spacing*_, plnts[1][1:-1]+spacing*_, pts, cmap=colorbar, linewidth=2*0.1*_, alpha=0.01*_)

    # turn off axis
#     plt.suptitle(seed, color='white')
    plt.xlim(min_lim_x, max_lim_x)
    plt.ylim(min_lim_y, max_lim_y)
    plt.axis('off')
    # get max x
    # get max y
    # recompute the ax.dataLim
    plt.axis('auto')
    plt.box(False)
    plt.gca().set_facecolor('black')
    plt.tight_layout()
    return fig

# 

# plot_simple_orbits(91)
# plot_particle_gif(positions, outdir=f'orbits.gif', dur=3)

# for i in range(10):
#     fig = plot_simple_orbits(i,spacing=0.005, nlin=50)
#     fig.savefig(f"{OUTDIR}/2body_{i:002d}.png" )
#     plt.close(fig)
# for i in range(200,250):
#     fig = plot_simple_orbits(i, N=3,spacing=0.005)
#     fig.savefig(f"{OUTDIR}/3body_{i:002d}.png")
#     plt.close(fig)

b) One central ‘black hole’#

Hide code cell source
MIN_V, MAX_V  = 3,7

## 2 Body newtonian gravity, 1 body fixed in the center
def twobody_runner_np(
        N: int = 5,
        tEnd: float = 10.0,
        dt: float = 0.01,
        softening: float = 0.1,
        random_seed: int = 17,
        max_runtime: int = 5,
        verbose: bool = True,
):
    """ N-body simulation """

    # Initialisation
    np.random.seed(random_seed)
    
    
    # First body (mass = 100, velocity = [0, 0, 0], position = [0, 0, 0])
    mass = np.array([np.concatenate((np.array([100.0]), 0.1 * np.ones(N - 1)))]).T  # First body has 100 times the mass
    pos = np.zeros((N, 3))  # All positions along the Z axis are 0
    vel = np.zeros((N, 3))  # All velocities along the Z axis are 0
    t = 0
    Nt = int(np.ceil(tEnd / dt))
    runtime_start = datetime.now()
    
    # Calculate tangential velocity for other bodies
    angular_velocity = np.sqrt(G * mass[0] / 10)  # Tangential velocity formula
    for i in range(1, N):
        R = np.random.uniform(1.5,3)
        theta = np.random.uniform(0, 2*np.pi)
        pos[i, 0] =  np.sin(theta) * R
        pos[i, 1] = np.cos(theta) * R

        
        theta = np.arctan2(pos[i, 1], pos[i, 0])  # Angle relative to x-axis
        # print(theta)
        vel_magnitude = np.random.uniform(MIN_V, MAX_V) # Tangential velocity magnitude
        vel[i, 0] = -vel_magnitude * np.sin(theta)  # Tangential velocity components
        vel[i, 1] = vel_magnitude * np.cos(theta)


    acc = newtonian_acceleration_np(pos, mass, G, softening)
    pos_save = np.zeros((N, 3, Nt + 1))
    pos_save[:, :, 0] = pos

    # Simulation loop
    for i in trange(Nt, desc="Running simulation", disable=not verbose):
        vel += acc * dt / 2.0
        pos += vel * dt
        acc = newtonian_acceleration_np(pos, mass, G, softening)
        vel += acc * dt / 2.0
        t += dt
        pos_save[:, :, i + 1] = pos
        runtime = get_runtime(runtime_start)
        if runtime > max_runtime:
            warnings.warn(f"Runtime exceeded {max_runtime} seconds. Stopping simulation.")
            break

    for j in range(1, N):
        if np.linalg.norm(pos[j]) > 0. :
            pos[j] = np.array([np.nan, np.nan, np.nan])
 
    return pos_save



def plot_2body_orbits(seed, N=2, tEnd=3, dt=0.001, spacing=0.001, nlin=30, alpha_rev=False):
    fig = plt.figure(figsize=(10,10), facecolor='black')
    positions = twobody_runner_np(N=N, random_seed=seed, tEnd=tEnd, verbose=False, dt=dt)


    
    max_lim_x, min_lim_x = np.max(positions[:, 0, :]), np.min(positions[:, 0, :])
    max_lim_y, min_lim_y = np.max(positions[:, 1, :]), np.min(positions[:, 1, :])

    pts = np.linspace(0, 1, positions.shape[2])
    
    plt.xlim(min_lim_x, max_lim_x)
    plt.ylim(min_lim_y, max_lim_y)
    for i, plnts in enumerate(positions):
        if i ==0 :
            continue
        # decrease alpha over time
        # alpha = np.linspace(0.1, 1, len(i))
        # color between white and blue
        colorbar = make_colormap(f'C{i}')

#         colorline(plnts[0][1:-1], plnts[1][1:-1], pts, cmap=colorbar, linewidth=2)

        for _ in range(nlin):
            a = 0.01*_
            if alpha_rev:
                a = 1 - a
            colorline(plnts[0][1:-1]+spacing*_, plnts[1][1:-1]+spacing*_, pts, cmap=colorbar, linewidth=2*0.1*_, alpha=a)

    # turn off axis
#     plt.suptitle(seed, color='white')
    plt.xlim(min_lim_x, max_lim_x)
    plt.ylim(min_lim_y, max_lim_y)
    plt.axis('off')
    # get max x
    # get max y
    # recompute the ax.dataLim
    plt.axis('auto')
    plt.box(False)
    plt.gca().set_facecolor('black')
    plt.tight_layout()
    return fig


# OUTDIR = 'black_hole' 
# os.makedirs(OUTDIR, exist_ok=True)
# fig = plot_2body_orbits(1, N=2, tEnd=15, spacing=0.01)
# fig.savefig(f"{OUTDIR}/1.png")
# MIN_V, MAX_V  = 6,10
# fig = plot_2body_orbits(5, N=2, tEnd=50, spacing=0.5, dt=0.01, nlin=6, alpha_rev=True)
# fig.savefig(f"{OUTDIR}/2.png")
# fig = plot_2body_orbits(21, N=2, tEnd=50, spacing=0.1, dt=0.1, nlin=6, alpha_rev=True)
# fig.savefig(f"{OUTDIR}/3.png")

3. Math functions#

Hide code cell source
! pip install samila==1.1 -q

import matplotlib.pyplot as plt
import random
from samila import GenerativeImage, Projection, Marker
from samila import Projection



# import numpy as np
# import matplotlib.pyplot as plt

# # Create a grid of x and y values
# x = np.linspace(-2, 2, 400)
# y = np.linspace(-2, 2, 400)
# X, Y = np.meshgrid(x, y)

# # Calculate the function values for each point on the grid
# result = np.random.uniform(-1, 1, size=X.shape) * X**2 - np.sin(Y**2) + np.abs(Y - X)

# # Create a contour plot
# plt.contourf(X, Y, result, levels=20, cmap='viridis')
# plt.colorbar()

# # Add labels and a title
# plt.xlabel('x')
# plt.ylabel('y')
# plt.title('Contour Plot of the Function')

# # Show the plot
# plt.show()


OUTDIR = '3d_fx' 
os.makedirs(OUTDIR, exist_ok=True)

np.random.seed(0)

def f1(x, y):
    result = random.uniform(-1,1) * x**2  - np.sin(y**2) + np.abs(y-x)
    return result

def f2(x, y):
    result = random.uniform(-0.5,0.5) * y**3 - np.cos(x**2) + np.sqrt(2*x)
    return result

def f3(x, y):
    result = random.uniform(-1,1) * x*y + np.tan(x*y)
    return result

def f4(x,y, b=.1, a=0.3,c=0.3,d=.4):
    return (np.sqrt((np.sqrt((x*np.sin(b) + a*np.cos(b))**2))))

g2 = GenerativeImage(f2, f1)
g2.generate(seed=5)
# g2.plot(cmap="winter", color="darkorange", alpha=0.5, bgcolor="black",)
# plt.savefig(f"{OUTDIR}/1.png")
g2.plot(cmap="winter", color="darkorange", projection=Projection.POLAR, marker=Marker.CIRCLE,alpha=1,bgcolor="black")
plt.savefig(f"{OUTDIR}/2_highres.png", dpi=300)
# g2 = GenerativeImage(f4, f1)
# g2.generate(seed=5)
# g2.plot(cmap="winter", color="darkorange", alpha=0.5, bgcolor="black",)
# plt.savefig(f"{OUTDIR}/3.png")
DEPRECATION: devscripts 2.20.2ubuntu2 has a non-standard version number. pip 24.0 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of devscripts or contact the author to suggest that they release a version with a conforming version number. Discussion can be found at https://github.com/pypa/pip/issues/12063

/tmp/ipykernel_182490/1897044965.py:44: RuntimeWarning: invalid value encountered in sqrt
  result = random.uniform(-0.5,0.5) * y**3 - np.cos(x**2) + np.sqrt(2*x)
_images/27e03d144a735ce86e8a988ca8f270fcd76062b2d2ffcd027aa91cca0f3b6821.png