← Cellular Automata From First Principles

Grow a Target From One Seed

Now we can pose the central morphogenesis problem:

Start from one cell and learn local rules that construct a target pattern.

Note what this chapter is not: it is not asynchronous scheduling (that is the next chapter), nor persistence or regeneration training (later chapters own those objectives). One objective — growth to target — trained and honestly evaluated.

Prepare a target

Assume an RGBA image has been loaded into a tensor:

# shape: [1, 4, H, W], values in [0, 1]
target = load_target("target.png").to(DEVICE)

The NCA state contains more channels than the image, so only the visible channels are compared:

def target_loss(x, target):
    return F.mse_loss(x[:, :4], target)

Randomize rollout length

Training at one exact step count encourages brittle timing tricks.

Instead sample a horizon (the canonical [64, 96) range):

steps = torch.randint(64, 97, ()).item()
x = make_seed(size=target.shape[-1], channels=16)

for _ in range(steps):
    x = model(x)

Now the model must approach a useful region of state space across a range of times.

Train the local rule

optimizer = torch.optim.Adam(model.parameters(), lr=2e-3)

for iteration in range(8000):
    x = make_seed(size=target.shape[-1], channels=16)
    steps = torch.randint(64, 97, ()).item()

    for _ in range(steps):
        x = model(x)

    loss = target_loss(x, target)

    optimizer.zero_grad()
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    optimizer.step()

(The gradient clipping mirrors the paper’s gradient-normalization remedy for late-training instability spikes. Verified at small scale: nonzero gradients, updating weights, decreasing loss — pipeline evidence. Convergence here would still not imply generalization; the guardrail travels with the code.)

The training loop as a cycle — and what each element does and does not buy:

    flowchart LR
    S[seed state] --> R[rollout N steps]
    R --> L[compare visible to target]
    L --> B[backprop through time]
    B --> U[update rule weights]
    U --> S
  
Training elementRoleDoes not establish
seed startfixed origin, tests growthrobustness to other starts
randomized horizonprevents timing trickspersistence past horizon
visible-only MSEshapes the outputhidden-state sanity
gradient clippingsurvives instability spikesconvergence quality
8000 iterationsthorough optimizationgeneralization

Measured on a small CPU run (8 channels, 40 iterations): loss 0.133 → 0.115, falling but far from solved — the curve below shows a working pipeline, not a competent model:

Training loss over 40 iterations on a ring target: decreasing, unsolved, honest about scale

Why growth from one seed is difficult

Every cell executes the same local rule, yet different regions must eventually play different roles.

The system has to create its own positional information through local interactions.

That is the real problem:

identical rule
+ local communication
+ recurrent hidden state
        ↓
spatial differentiation

Do not judge only the final frame

Record the entire trajectory:

frames = []
x = make_seed()

for step in range(128):
    x = model(x)
    if step % 4 == 0:
        frames.append(x[:, :4].detach().cpu())

Rendered as an animation, this trajectory is more informative than the final image: the process of becoming is what the rule was trained to produce.

A low final loss can still hide a bad dynamical system

Check what happens after the training horizon:

for _ in range(500):
    x = model(x)

Does the organism:

persist?
explode?
decay?
drift?
keep growing?

A model that reaches the target and then destroys it has learned growth, not homeostasis.

Separate growth from persistence

This distinction gives us three different capabilities:

growing      = reach the target
persistent   = remain near the target
regenerating = return after damage

They should be tested separately — and trained separately, in the chapters that own them. This chapter trains the first; persistence and regeneration objectives arrive later, because a growth-trained model earns neither automatically.

Before training persistence and regeneration, however, we need to remove another unrealistic assumption: that every cell updates at exactly the same instant.

In the next chapter we randomize the update schedule.


Research

  • Mordvintsev, A., Randazzo, E., Niklasson, E. & Levin, M. — Growing Neural Cellular Automata (Distill, 2020). The training regime formalized here — seed start, randomized [64, 96) horizon, pixel-wise L2 on RGBA, backpropagation through time — plus the three-regime ladder (growing, persistent, regenerating models) that assigns this chapter the first rung and the later chapters the rest. https://doi.org/10.23915/distill.00023