Discover Your First Lenia Organisms
Running Lenia is easy.
Finding persistent, localized, non-trivial structures is the hard part.
This chapter turns that problem into a repeatable search pipeline. It follows Chan’s discovery playbook structurally — random soup generation, survival filtering, parameter tweaking, and, crucially, human judgment for what counts as interesting — while automating the parts that do not need eyes.
Define what counts as a candidate
We should not start with the vague objective:
Find something that looks alive.
Instead define measurable properties.
A useful first candidate might be:
survives for 1,000 steps
stays localized
keeps non-zero mass
continues changing
avoids filling the whole board
Each condition removes a different failure mode.
(“Organism,” “creature,” and “species” below are descriptive community terms in Chan’s own taxonomy — Orbium, Scutium, and some 400 cataloged siblings — not biological claims. The paper is explicit that self-replication, for one, is yet to be discovered in Lenia.)
Evaluate persistence
def survives(state, threshold=1e-3):
return state.sum() > threshold
Track it over a run:
def survival_time(state, kernel_f, config, max_steps=1000):
for step_index in range(max_steps):
state = lenia_step(state, kernel_f, config)
if not survives(state):
return step_index
return max_steps
Evaluate localization
def localization_score(state, threshold=0.05):
active = state > threshold
fraction = active.mean()
return float(1.0 - fraction)
A completely active board scores poorly.
A very small localized pattern scores highly.
But a single dying pixel would also score highly.
That is why objectives must be combined.
Measure sustained activity
def sustained_activity(history, tail=100):
values = [row["activity"] for row in history[-tail:]]
return float(np.mean(values))
This reads the metric log from run_lenia: Chapter 19’s tail-activity idea, adapted to continuous state (mean absolute change instead of fraction changed).
A static blob may be persistent but dynamically uninteresting.
A pattern that remains active without exploding is a stronger candidate.
Evaluate a seed
def evaluate_seed(initial, config, steps=1000):
kernel = build_kernel(config)
kernel_f = kernel_fft(kernel, initial.shape)
final, history = run_lenia(
initial.copy(),
kernel_f,
config,
steps=steps,
)
return {
"final_mass": float(final.sum()),
"active_fraction": active_fraction(final),
"activity": sustained_activity(history),
"final": final,
"history": history,
}
Now discovery can be automated.
Generate many initial conditions
def seed_bank(count, shape=(128, 128), patch=24, base_seed=1000):
for index in range(count):
yield random_seed(
shape=shape,
patch=patch,
seed=base_seed + index,
)
Evaluate them:
results = []
for index, initial in enumerate(seed_bank(100)):
result = evaluate_seed(initial, config)
result["seed_index"] = index
results.append(result)
(Verified on a 6-seed pilot at 200 steps: all persist with masses ≈ 3382–3392 and active fractions ≈ 0.22 — every seed persists on the reference configuration, which is why the next chapter varies parameters instead of seeds alone.)
Reject obvious failures first
Simulation is expensive.
Use staged evaluation:
100 steps
↓
reject dead / exploded
↓
500 steps
↓
reject unstable
↓
2,000 steps
↓
inspect survivors
This is the same principle used in many search systems:
cheap filter before expensive evaluation
Build a candidate score
For ranking only, we can combine several normalized properties:
def candidate_score(result):
mass_ok = min(result["final_mass"] / 100.0, 1.0)
localized = 1.0 - result["active_fraction"]
active = min(result["activity"] / 0.02, 1.0)
return 0.3 * mass_ok + 0.4 * localized + 0.3 * active
Do not confuse this with a scientific definition of life.
It is an engineering ranking function for one search task — carrying all of Part III’s score-vs-phenomenon warnings.
Keep diversity
If we simply keep the top twenty candidates, they may all be near-duplicates.
Compute simple descriptors:
def descriptors(result):
final = result["final"]
return np.array([
final.sum(),
final.mean(),
final.var(),
result["active_fraction"],
result["activity"],
])
Then prefer candidates far apart in descriptor space.
This is the beginning of novelty search and quality-diversity methods.
Save everything required to replay
For every promising candidate, save:
parameter config
initial seed
random seed
simulation length
metrics
final state
optional frames
A screenshot without lineage is not a scientific result.
Human judgment still matters
Automated metrics can remove obvious failures.
They cannot fully capture properties such as:
interesting symmetry
coherent locomotion
repeated appendages
collision behavior
regeneration
morphological novelty
This matches Chan’s experience directly: most Lenia species were found through interactive evolutionary computation — human eyes doing mutation and selection — with fully automatic exploration judged ineffective without pattern-recognition AI. The pipeline filters out the obviously dull; humans still name the interesting.
A productive workflow is:
automated search
↓
rank + diversify
↓
human inspection
↓
label interesting behaviors
↓
improve search objectives
That creates a feedback loop between computation and observation.
Make discovery visual
Save thumbnails for the best candidates:
def save_candidate_image(state, filename):
import matplotlib.pyplot as plt
plt.figure(figsize=(4, 4))
plt.imshow(state, cmap="viridis", vmin=0, vmax=1)
plt.axis("off")
plt.tight_layout()
plt.savefig(filename, dpi=150)
plt.close()
An atlas of discovered forms is often more useful than a terminal full of scores.
Discovery is now an engineering problem
We have converted:
Maybe tweak
muuntil something cool happens.
into:
candidate generator
↓
simulator
↓
measurements
↓
filters
↓
ranking
↓
diversity preservation
↓
replayable archive
As a discovery flow with its two gates made explicit — survival first, interest second:
flowchart LR
G[generate seeds] --> R[roll out]
R --> S{survives + localized?}
S -->|no| X[reject]
S -->|yes| I[inspect: human judgment]
I -->|interesting| K[retain + record lineage]
I -->|dull| X
That architecture scales far beyond Lenia.
Discovery methods differ in what they can find and what they cost:
| Method | What it finds | What it misses | Evidence status |
|---|---|---|---|
| random soup sampling | spontaneous persisters (Orbium class) | rare or parameter-sensitive forms | observed, cataloged |
| survival filtering | anything that lasts N steps | interesting-but-fragile transients | measured, threshold-dependent |
| parameter tweaking | neighbors of known forms | distant regions of space | exploratory, human-guided |
| human inspection | symmetry, locomotion, novelty | anything unrecognizable to eyes | judgment, not measurement |
Next: search the parameter space too
So far we held the Lenia rule fixed and varied initial conditions.
But interesting structures also depend strongly on:
mu
sigma
kernel radius
ring geometry
time step
In the next chapter we will search both the organism seed and the world parameters, and use the experimental machinery from Part III to keep that search reproducible instead of turning it into random parameter roulette.
Research
Chan, B. W.-C. — Lenia: Biology of Artificial Life (Complex Systems 28(3), 2019). The discovery playbook this chapter automates: random-soup generation (Orbium and kin emerged unprompted), survival/stability/novelty selection criteria, parameter tweaking and manual mutation operators — and the honest verdict that automatic exploration without pattern-recognition AI stays ineffective. Calibrate any “automated discovery” claim against it. https://arxiv.org/html/1812.05433v3
Lehman, J. & Stanley, K. O. — Abandoning Objectives (Evolutionary Computation 19(2), 2011). The foundation under the diversity section: fixed scores deceive, and distance-in-descriptor-space is the principled alternative to top-twenty cloning. Already used in Chapter 25; applied here to organisms. https://dl.acm.org/doi/10.1162/EVCO_a_00025