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 element | Role | Does not establish |
|---|---|---|
| seed start | fixed origin, tests growth | robustness to other starts |
| randomized horizon | prevents timing tricks | persistence past horizon |
| visible-only MSE | shapes the output | hidden-state sanity |
| gradient clipping | survives instability spikes | convergence quality |
| 8000 iterations | thorough optimization | generalization |
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:

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