← Cellular Automata From First Principles

Multi-Kernel and Multi-Channel Lenia

Our Lenia implementation has one scalar field:

A(x, y)

and one kernel:

K

That is enough for rich behavior.

But it also forces every local process to share the same spatial scale and the same state variable.

A richer model can use multiple channels:

A0(x, y)
A1(x, y)
A2(x, y)

and multiple kernels linking them.

This chapter builds a pedagogical multi-channel formulation in Lenia’s spirit — a connection graph with per-edge kernels and growth parameters. Canonical multi-kernel, multi-channel Lenia is Chan’s “Expanded Universe” (ALIFE 2020) direction; the book’s version is simpler and says so, keeping the graph inspectable rather than matching the canonical matrix formulation.


Represent state as channels

import numpy as np

channels = 3
state = np.zeros((channels, 128, 128), dtype=np.float64)

Now each cell has a vector state:

[cell_0, cell_1, cell_2]

The channels do not need literal biological meanings.

They are interacting local fields.


A connection describes influence

We can describe one interaction as:

from dataclasses import dataclass


@dataclass(frozen=True)
class Connection:
    source: int
    target: int
    kernel: np.ndarray
    mu: float
    sigma: float
    weight: float = 1.0

This says:

read source channel
      ↓
apply this spatial kernel
      ↓
apply this growth response
      ↓
add weighted change to target channel

That is a small local network.


Compute several influences

def multi_channel_step(state, connections, dt=0.1):
    delta = np.zeros_like(state)

    for connection in connections:
        kernel_f = kernel_fft(connection.kernel, state.shape[1:])

        potential = np.fft.ifft2(
            np.fft.fft2(state[connection.source]) * kernel_f
        ).real

        response = gaussian_growth(
            potential,
            connection.mu,
            connection.sigma,
        )

        delta[connection.target] += connection.weight * response

    return np.clip(state + dt * delta, 0.0, 1.0)

The growth call reuses Chapter 30’s owner — no fourth name for the same bump. The kernel transform reuses Chapter 31’s. (Verified: two-channel cross-coupled runs stay finite; inhibition visibly suppresses its target channel while excitation saturates its own — coupling does work, in both directions.)

For repeated simulation we should precompute each kernel FFT rather than rebuilding it every step — each connection costs a forward/inverse FFT pair per step, so cost scales with connections, not just grid size.


Cross-channel interaction

Suppose:

channel 0 encourages channel 1
channel 1 suppresses channel 0

We can express that with two connections.

The result can create feedback loops:

A grows B
B suppresses A
A falls
B loses support
B falls
A can recover

Local feedback creates temporal structure as well as spatial structure.


Multiple spatial scales

Different kernels can operate at different radii:

short-range excitation
long-range inhibition

This is a recurring pattern in self-organizing systems.

For example:

short_kernel = ring_kernel(radius=8, ring_center=0.4, ring_width=0.12)
long_kernel = ring_kernel(radius=20, ring_center=0.6, ring_width=0.18)

Now a cell can respond differently to nearby and distant activity.


Think in terms of a graph

With several channels and connections, the rule can be visualized as a graph — edges, not prose, carry the topology:

    flowchart LR
    A0[channel 0] -->|K0 self| A0
    A0 -->|K1 excite| B1[channel 1]
    B1 -->|K2 inhibit| A0
  

Each edge carries:

kernel
growth parameters
weight

That is much easier to inspect than one giant function containing all interactions. Connection patterns and their dynamical roles:

PatternConstructionDynamical role
self-loop (0→0)kernel on own channelself-maintenance of one field
excitation (0→1, +weight)cross-kernel, positiverecruit another field
inhibition (1→0, −weight)cross-kernel, negativesuppress, bound growth
feedback pairexcite + inhibit looposcillation, regulation

Precompute an execution plan

@dataclass
class PreparedConnection:
    source: int
    target: int
    kernel_f: np.ndarray
    mu: float
    sigma: float
    weight: float


def prepare_connections(connections, shape):
    return [
        PreparedConnection(
            source=c.source,
            target=c.target,
            kernel_f=kernel_fft(c.kernel, shape),
            mu=c.mu,
            sigma=c.sigma,
            weight=c.weight,
        )
        for c in connections
    ]

The simulation loop should execute prepared data, not repeatedly reconstruct model structure.

This is the same distinction between configuration and runtime representation that appears in larger software systems.


Visualize channels separately

import matplotlib.pyplot as plt

for channel in range(state.shape[0]):
    plt.figure(figsize=(4, 4))
    plt.imshow(state[channel], vmin=0, vmax=1)
    plt.title(f"channel {channel}")
    plt.axis("off")
    plt.show()

Also create composites:

rgb = np.moveaxis(state[:3], 0, -1)
plt.imshow(np.clip(rgb, 0, 1))
plt.axis("off")
plt.show()

A combined image can hide important internal dynamics, so always retain per-channel inspection.


Search becomes structural

Our earlier parameter search varied numbers.

Now we might also vary:

number of channels
number of connections
source/target topology
kernel radii
connection weights

The search space is no longer just numerical.

It includes architecture.

That is a useful preview of neural cellular automata, where the local update rule itself will become learned.


Avoid unnecessary biological claims

Multiple channels can produce behaviors that look tissue-like or organism-like.

That does not mean each channel corresponds to a chemical, cell type or biological pathway.

Keep the interpretation disciplined, with this part’s guardrails stated plainly:

observed:
multiple interacting local fields produce persistent morphology

not automatically established:
biological equivalence

more channels
  ≠ more intelligence

more parameters
  ≠ better model

richer morphology
  ≠ richer behavior

Artificial life becomes more interesting when we are precise about what has actually emerged.


Richer systems create a new question

A pattern may survive indefinitely under perfect conditions.

But is it robust?

What happens if we:

delete part of it
inject noise
change parameters slightly
collide it with another structure

Persistence under no disturbance is a weak test.

In the next chapter we will turn damage and recovery into measurable experiments and separate genuine robustness from lucky stability.


Research

  • Chan, B. W.-C. — Lenia and Expanded Universe (ALIFE 2020). The canonical reference for this chapter’s direction: higher dimensions, multiple kernels, and multiple channels as the generalization of single-kernel Lenia. Read it to see what the book’s connection-graph simplification stands in for — and what the full formulation adds. https://arxiv.org/abs/2005.03742

  • Chan, B. W.-C. — Lenia: Biology of Artificial Life (Complex Systems 28(3), 2019). The base system being generalized: single-kernel update, growth mapping, and the species catalog that multi-channel work extends. The fidelity baseline from Chapter 31, still the standard here. https://arxiv.org/html/1812.05433v3