Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Project — Chapter 89: Cellular Automata

Prerequisites: ch062 (Piecewise), ch076 (Dynamical Systems), ch063 (Step Functions)

Concepts: Rule-based dynamics, 1D/2D CA, Conway’s Game of Life, emergent behavior

Output: Full cellular automaton simulator for 1D elementary CA and 2D Game of Life

Difficulty: Intermediate | ~50 minutes


Stage 1 — 1D Elementary Cellular Automata

import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-whitegrid')

def apply_rule(state, rule_num):
    """Apply Wolfram elementary CA rule (0-255) to a 1D state."""
    rule = np.array([(rule_num >> i) & 1 for i in range(8)], dtype=np.uint8)
    n = len(state)
    new_state = np.zeros(n, dtype=np.uint8)
    for i in range(n):
        left = state[(i-1) % n]
        center = state[i]
        right = state[(i+1) % n]
        neighborhood = 4*left + 2*center + right
        new_state[i] = rule[neighborhood]
    return new_state

def run_ca_1d(rule_num, n_cells=150, n_steps=100, seed_single=True):
    """Run 1D CA for n_steps. Start with single cell or random."""
    grid = np.zeros((n_steps, n_cells), dtype=np.uint8)
    if seed_single:
        grid[0, n_cells//2] = 1
    else:
        np.random.seed(0)
        grid[0] = np.random.randint(0, 2, n_cells)
    for t in range(1, n_steps):
        grid[t] = apply_rule(grid[t-1], rule_num)
    return grid

fig, axes = plt.subplots(2, 4, figsize=(16, 8))
rules = [30, 90, 110, 184, 60, 45, 18, 54]
for ax, rule in zip(axes.flat, rules):
    grid = run_ca_1d(rule)
    ax.imshow(grid, cmap='binary', aspect='auto', interpolation='nearest')
    ax.set_title(f'Rule {rule}', fontsize=10); ax.axis('off')

plt.suptitle('Wolfram Elementary Cellular Automata (256 possible rules)', fontsize=13, fontweight='bold')
plt.tight_layout(); plt.show()
<Figure size 1600x800 with 8 Axes>

Stage 2 — Conway’s Game of Life

# Conway's Game of Life: 2D CA with 4 rules
# 1. Underpopulation: live cell < 2 neighbors → dies
# 2. Survival: live cell 2-3 neighbors → lives
# 3. Overpopulation: live cell > 3 neighbors → dies
# 4. Reproduction: dead cell == 3 neighbors → becomes alive

def gol_step(grid):
    """One step of Conway's Game of Life."""
    from scipy.signal import convolve2d
    kernel = np.ones((3,3), dtype=int); kernel[1,1] = 0
    # Count neighbors using convolution (manual if scipy unavailable)
    # Manual version:
    neighbors = np.zeros_like(grid, dtype=int)
    rows, cols = grid.shape
    for dr in [-1, 0, 1]:
        for dc in [-1, 0, 1]:
            if dr == 0 and dc == 0: continue
            neighbors += np.roll(np.roll(grid, dr, axis=0), dc, axis=1)
    
    new_grid = np.zeros_like(grid)
    # Survival
    new_grid[(grid == 1) & ((neighbors == 2) | (neighbors == 3))] = 1
    # Reproduction
    new_grid[(grid == 0) & (neighbors == 3)] = 1
    return new_grid

# Initialize with glider and random region
np.random.seed(42)
SIZE = 80
grid = (np.random.random((SIZE, SIZE)) > 0.75).astype(int)
# Add glider
glider = [(0,1),(1,2),(2,0),(2,1),(2,2)]
for r, c in glider:
    grid[5+r, 5+c] = 1

n_frames = 8
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
current = grid.copy()
for ax, _ in zip(axes.flat, range(n_frames)):
    ax.imshow(current, cmap='binary', interpolation='nearest')
    ax.set_title(f't={_*5}'); ax.axis('off')
    for __ in range(5):
        current = gol_step(current)

plt.suptitle("Conway's Game of Life: Evolution", fontsize=13, fontweight='bold')
plt.tight_layout(); plt.show()
<Figure size 1600x800 with 8 Axes>

Stage 3 — Population Analysis

# Track live cell count over time in Game of Life
np.random.seed(7)
SIZE = 100
grid = (np.random.random((SIZE, SIZE)) > 0.7).astype(int)

populations = [grid.sum()]
for _ in range(300):
    grid = gol_step(grid)
    populations.append(grid.sum())

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(populations, color='steelblue', linewidth=1.5)
axes[0].set_title("Game of Life: Live Cell Count Over Time")
axes[0].set_xlabel('Generation'); axes[0].set_ylabel('Live cells')

# Equilibrium analysis
tail = populations[200:]
axes[1].hist(tail, bins=30, color='steelblue', alpha=0.7, edgecolor='white')
axes[1].axvline(np.mean(tail), color='crimson', linestyle='--', linewidth=2, label=f'Mean={np.mean(tail):.0f}')
axes[1].set_title('Live Cell Distribution (Equilibrium)')
axes[1].set_xlabel('Live cells'); axes[1].legend()
plt.suptitle('Game of Life Population Analysis', fontsize=13, fontweight='bold')
plt.tight_layout(); plt.show()
<Figure size 1400x500 with 2 Axes>

Results & Reflection

What was built: Wolfram’s 256 elementary CA rules, Conway’s Game of Life simulator, and population equilibrium analysis.

Extensions: 1) Find still lifes, oscillators, and spaceships in GoL. 2) Implement Brian’s Brain (3-state CA). 3) Apply CA to forest fire modeling.