Architecture

biota is a distributed quality-diversity search platform for Flow-Lenia. Two subsystems share the same vectorized PyTorch runtime and the same Ray cluster: a MAP-Elites search that fills a behavioral archive, and an ecosystem dispatch that runs selected archive creatures on shared canvases. Both run on a homelab 3-node RTX 5060 Ti cluster.

archive capacity
1,024 centroids
archive type
CVT-MAP-Elites
descriptor library
18 built-in axes

MAP-Elites runs a loop: sample parameters, simulate a creature, measure its behavior, insert it into the matching archive cell if it beats whatever is already there. After thousands of rollouts the archive fills with creatures covering the behavioral space as broadly as possible — not optimizing toward one solution, but toward diversity.

The archive is a CVT-MAP-Elites archive. Before search begins, a calibration phase runs a small number of rollouts and fits k-means centroids to the observed descriptor distribution. Archive cells are Voronoi regions around those centroids — dense where creatures naturally live in behavioral space, sparse where they do not. Each cell holds the highest-quality creature found in its region.

biota CVT archive The biota archive uses CVT-MAP-Elites. A calibration phase fits k-means centroids to the observed distribution of survivor descriptors, placing cells where creatures actually live in behavioral space. Each centroid holds the highest-quality creature found in its Voronoi region. Small grey dots show calibration survivors; large circles show CVT cells colored by quality (magma scale, bright = high quality). fast slow velocity compact spread gyradius high entropy (sharp) low entropy (smooth) spectral entropy occupied — best creature in this Voronoi cell empty — search has not reached this region yet calibration survivor

The driver owns the archive and the search loop. Each Ray task evaluates B creatures as a single (B, H, W) vectorized forward pass on one GPU. Workers are stateless; nothing persistent lives on the cluster between tasks. --workers N controls how many batches are in flight simultaneously: one means synchronous MAP-Elites with a maximally fresh archive, higher values trade freshness for throughput.

biota search loop Data flow: the driver process owns the MAP-Elites archive and search loop. It dispatches batches of B parameter sets to stateless Ray workers that simulate B Flow-Lenia creatures in a single vectorized forward pass, compute three behavioral descriptors, and score quality. B results return to the driver for archive placement. DRIVER PROCESS MAP-Elites archive 1,024 CVT centroids any 3 from 18 descriptors one best creature per cell search loop mutate · select · queue result handler score · place · checkpoint select parent params insert or replace RAY WORKERS — stateless, local or cluster, CPU or GPU — one task evaluates B rollouts Flow-Lenia rollout B × 300 steps, one fwd pass state (B, H, W) · PyTorch CPU or GPU · mass-conserving --batch-size B · --workers N descriptors 3 behavioral axes velocity · gyradius · entropy signal · spatial · +14 more quality filter · score persistence · mass smoothness B params B results
biota CVT-MAP-Elites algorithm Two-phase algorithm. Phase 1: calibration. M random creatures are rolled out and their descriptors are recorded. K-means fits k centroids to the survivor distribution, placing archive cells where creatures actually live in behavioral space. Phase 2: search. The MAP-Elites loop samples a parent from occupied cells, mutates its parameters to produce a child, rolls it out, and finds the nearest CVT centroid via cKDTree lookup. If the child's quality beats the current occupant (or the cell is empty), it replaces it. PHASE 1 — CALIBRATION random rollouts M creatures uniform params survivors only k-means k centroids 1,024 cells fitted to survivor distribution cKDTree for O(log k) PHASE 2 — SEARCH LOOP MAP-Elites archive k CVT cells · one best creature each select parent select · mutate random parent or random params Gaussian perturbation of param vector child params Flow-Lenia rollout B creatures · descriptors · quality nearest centroid cKDTree · O(log k) lookup quality > occupant ? yes → replace no → discard centroid positions

Quality metric

Every rollout passes three hard filters before it can enter the archive. First, the conserved quantity (mass + signal for signal runs, mass alone otherwise) must stay within [0.5, 2.0] of its initial value — creatures that explode or die are rejected. Second, the mean bounding-box fraction over the trailing window must be below 0.6 — creatures that spread across the entire grid are rejected. Third, the three behavioral descriptors measured over adjacent 50-step windows must not drift by more than 20% of the observed descriptor range — creatures that are still changing shape or speed are rejected.

Survivors are ranked by a quality score that creates genuine selection pressure within the viable population:

quality score (non-signal) q = 0.6 · compactness + 0.4 · stability
quality score (signal) q = 0.5 · compactness + 0.3 · stability + 0.2 · signal activity
compactness  —  two-point minimum compactness = min( compact(stateT/2), compact(stateT) )
where compact(s) = mass inside bounding box / total mass

stability  —  continuous drift score stability = clip( 1 − drift / 0.2, 0, 1 )
where drift = max descriptor change across adjacent 50-step windows

signal activity  —  signal runs only signal activity = clip( final_signal_mass / initial_signal_mass, 0, 1 )

The two-point compactness term is the key addition over a naive single-snapshot metric. Almost all viable Flow-Lenia solitons score above 0.95 compactness at the final step, making a single-point measurement nearly constant across the population and providing no selection pressure. By taking the minimum of the midpoint and final compactness, creatures that peak early and gradually become diffuse score below creatures that maintain their structure throughout — exactly the property that matters for long ecosystem runs.

The stability term converts the binary persistent filter into a continuous reward: a creature that barely passes (drift = 0.19) scores near zero, while a rock-stable creature (drift = 0.01) scores near 1.0. This ensures the archive prioritises behaviorally consistent creatures over lucky borderline survivors.

Ecosystem dispatch

Once the archive is populated, biota ecosystem takes specific archive cells and runs them on a shared grid to see how creatures interact. A homogeneous run spawns N copies of one species. A heterogeneous run mixes two or more species, each with its own full parameter set — kernel radii, growth windows, weights — using species-indexed LocalizedFlowLenia: per-cell species ownership tracks which lineage owns the local mass, blends growth fields by ownership, and advects with the flow.

After the simulation, a suite of spatial observables is computed from the captured snapshots without re-running the simulation. For heterogeneous runs: patch count per species over time, interface area and center-of-mass distance per species pair, and spatial entropy per species. For homogeneous runs: patch count over time, spatial entropy, and patch size distribution. Interaction coefficients between species are gated to snapshot windows where species actually co-occur, so they measure contact dynamics rather than spatial separation. A temporal outcome classifier assigns per-species labeled windows — coexistence, exclusion, merger, or fragmentation for heterogeneous runs; stable isolation, full merger, partial clustering, cannibalism, or fragmentation for homogeneous runs — and derives a dominant run-level label shown as a badge. The ecosystem viewer renders all charts alongside the animated GIF.

Ecosystem dispatch is Ray-correct in both directions: each experiment is a self-contained payload. The driver loads creatures from its local archive and ships them as part of the task input; workers simulate and render to bytes; the driver materializes outputs to its own local filesystem. No shared filesystem is assumed at any step, so experiments run correctly on real multi-node clusters without NFS or rsync setup.

biota ecosystem dispatch Data flow: the driver loads N experiments from YAML, resolves creatures from the local archive, and dispatches each experiment as a self-contained Ray task carrying configs and pre-loaded creatures. Workers simulate a shared-canvas ecosystem with species-indexed FlowLenia, render all outputs to bytes, and return (result, artifacts) as a single payload. The driver materializes the artifacts locally. No shared filesystem assumed between driver and workers. DRIVER PROCESS experiments.yaml N experiment configs species · grid · steps spawn · border · patch homo or heterogeneous archive resolve creatures per-source cell lookup RolloutResult objects driver-local filesystem output_root materialize artifacts config · summary · gif trajectory · frames driver-local filesystem for each RAY WORKERS — one task per experiment, self-contained payload, no shared filesystem spawn Poisson disk placement per-source patch species ownership init shared canvas tensor simulate species-indexed FlowLenia localized kernels per species ownership advects w/ mass T steps · snapshots render bytes, no disk I/O gif · per-frame PNG trajectory.npy summary.json config + creatures result + artifacts
biota ecosystem outcome taxonomy Six outcome labels produced by the temporal classifier. Heterogeneous runs: coexistence, exclusion, merger, fragmentation. Homogeneous runs: stable isolation, cannibalism. HETEROGENEOUS RUNS HOMOGENEOUS RUNS coexistence both stable s1 stable s2 stable both species maintain mass and territory signal mediates spacing exclusion one wins s1 ↑ s2 ↓ one species steadily outcompetes and eliminates the other fragmentation patches form s1 ~ s2 ~ patches form and break under competitive or signal-mediated pressure stable isolation copies separate mass stable copies remain spatially separate and stable throughout the run ← time → merger boundaries dissolve s1 s2 → blended species boundaries dissolve — mass intermixes into one cannibalism copies absorb copies mass ↓ copies absorb each other — some grow at others' expense ← time →

Signal field

An optional chemical communication layer adds a shared (H, W, 16) signal field to the simulation. When --signal-field is passed to biota search, eight additional parameters become searchable per creature across 16 independent channels:

per-creature signal parameters emission_vector (C,) in [0, 1] # how emitted signal distributes across channels
receptor_profile (C,) in [−1, 1] # channel weights; negative = inhibitory (GABA-like)
emission_rate scalar in [0.001, 0.05] # fraction of G&sup+; converted to signal per step
decay_rates (C,) in [0, 0.9] # per-channel dissipation; low = long-range gradients
signal_kernel_r / a / b / w # spatial diffusion kernel (per-creature)

One simulation step: (1) convolve mass to get growth G(H,W); (2) convolve signal field with signal kernel; (3) compute reception: dot(convolved_signal, receptor_profile) — negative receptor weights are inhibitory; (4) apply alpha_coupling: G × (1 + α × reception) clamped to [0,∞) — positive α amplifies growth where signal is favorable, including inside other species’ territory (the cross-species predation pathway); negative α is chemorepulsion; (5) modulate emission rate via beta_modulation: rate × (1 + β × mean(reception)) — positive β = quorum sensing, negative β = feedback inhibition; (6) emit G&sup+ × rate_eff × emission_vector, draining mass into signal field; (7) reintegrate mass via Flow-Lenia advection; (8) decay signal per-channel at decay_rates. Total conserved: mass + signal.

biota signal field mechanics MASS FIELD A(H, W) t convolve fft(A) × fK GROWTH G G(H, W) α COUPLING G × (1 + α·recep) clamp(min=0) EMISSION G⁺ × rate_eff × ev MASS FIELD A(H, W) t+1 SIGNAL FIELD S(H, W, C) t convolve fft(S) × fK_sig RECEPTION dot(S̃, rp) → recep β MODULATION rate×(1+β·mean(recep)) clip to [0, 0.1] DECAY S × (1 − dr) SIGNAL FIELD S(H, W, C) t+1 SIGNAL PARAMETERS ev emission_vector (C,) [0,1] rp receptor_profile (C,) [−1,1] neg=inhibitory α alpha_coupling [−1,1] chemotaxis / repulsion β beta_modulation [−1,1] quorum / inhibition rate, decay_rates, signal_kernel_* — static per-creature params conservation Σ A(t+1) + Σ S(t+1) ≤ Σ A(t) + Σ S(t) α reception → growth multiplier β mean(reception) → modulate rate emission drains mass, adds to signal field decay is the only leak — Σ(mass + signal) is conserved modulo decay
biota inter-species signal coupling Signal coupling between two species is determined by dot products between emission vectors and receptor profiles. dot(emission_A, receptor_B) governs how A's chemical output affects B's growth, and vice versa. Both positive: mutual chemotaxis. Both negative: mutual chemorepulsion. Mixed: asymmetric pursuit. Near-zero: chemically blind. Alpha_coupling scales all coupling effects. SPECIES A emission_vector ev_A = [0.8, 0.1, …] receptor_profile rp_A = [−0.6, 0.3, …] SPECIES B emission_vector ev_B = [0.1, 0.7, …] receptor_profile rp_B = [0.2, −0.8, …] dot(ev_A, rp_B) → A→B dot(ev_B, rp_A) → B→A α scales both A emits → B receives B emits → A receives INTERACTION TYPES — sign(A→B) × sign(B→A) chemotaxis both positive A→B > 0, B→A > 0 species attract compete for space chemorepulsion both negative A→B < 0, B→A < 0 species repel partition territory pursuit mixed signs one positive, one negative pursuer + prey blind near zero |A→B| ≈ 0 or |B→A| ≈ 0 spatial only α > 0 (chemotaxis): signal reception amplifies growth in saturated cells → species drifts toward signal source α < 0 (chemorepulsion): signal reception suppresses growth → species drifts away from signal source no mass is transferred — signal biases the flow field, total ecosystem mass is conserved

Searches with --signal-field automatically use 800 steps (vs 500 for standard) so emission and reception dynamics have time to build meaningful gradients. The quality metric gains a signal activity term: clip(final_signal_mass / initial_signal_mass, 0, 1) weighted at 0.2, rewarding creatures that maintain or build up signal mass rather than letting the background field decay. A creature mass floor of 0.2 × initial_mass is enforced as a hard filter. In ecosystem runs, all species share one signal field: each species emits and decays by its own parameters, ownership-weighted. Signal archives and non-signal archives cannot be mixed in a single ecosystem run.

The ecosystem viewer exposes signal-specific observables: total signal mass history, signal mass fraction per step (signal / (mass + signal)), receptor alignment per species per snapshot (dot(receptor_profile, mean signal received in territory)), and an emission-reception compatibility matrix (dot(emission_vector[i], receptor_profile[j])) showing which species pairs have chemically compatible signal profiles.

Behavioral descriptors

Three scalars measured from each rollout index the archive's three axes. The active set is chosen per run — any three from the built-in library of eighteen (15 general + 3 signal-only).

velocity
Mean COM displacement per step over the trailing 50 steps. Separates drifters from stationary creatures.
gyradius
Mass-weighted RMS distance from the center of mass. Separates compact dots from spread-out rings and lattices.
spectral entropy
Shannon entropy of the radially-averaged FFT spectrum. Separates smooth blobs from sharp-edged structured creatures.
oscillation
Variance of bounding-box area over the trace tail. Separates pulsing, breathing creatures from rigid translators.
compactness
Fraction of total mass inside the bounding box. Separates tight, well-defined creatures from diffuse scattered ones.
mass asymmetry
Directional bias of COM motion. Separates straight-line movers from orbiters and creatures with circular trajectories.
PNG compressibility
Compressed-to-raw size ratio of the final state. Low = smooth and boring, high = noisy, middle = structured and interesting.
rotational symmetry
Angular variance of mass around the center of mass. Low = rings and disks, high = dumbbells, L-shapes, asymmetric gliders.
persistence score
Maximum descriptor drift across the trace tail. Low = behaviorally stable over time, high = the creature is still changing.
displacement ratio
Total displacement divided by total path length over the trace tail. Zero = pure orbiter, one = perfect straight-line glider.
angular velocity
Mean absolute angular speed of COM motion over the trace tail. Separates rotors and orbiters from translators and stationary creatures.
growth gradient
Mass-weighted mean spatial gradient magnitude at the final step. Low = smooth symmetric creature, high = labyrinthine internal structure.
morphological instability
Variance of gyradius over the trace tail. Low = rigid stable form, high = creature constantly reshapes or fragments over time.
activity
Mean absolute gyradius change per step. Measures internal work rate — static creatures score near zero, pulsing or morphing ones score high.
spatial entropy
Shannon entropy of mass distribution over a coarse spatial grid. Low = compact localized mass, high = diffuse or multi-body spread.
signal field variance signal only
Spatial variance of the total signal field (summed across channels) at the end of the rollout. High = signal is concentrated and localized near the creature body. Low = diffused evenly or barely accumulated. Captures the creature’s chemical footprint structure. Zero for non-signal rollouts.
signal mass ratio signal only
Final signal field mass divided by initial signal mass. Measures how much chemical substance has accumulated relative to the background field. A pure listener scores near 1 (field unchanged); a broadcaster scores high (field built up). Zero for non-signal rollouts.
dominant channel fraction signal only
Fraction of total signal mass carried by the channel with the highest accumulation. High = chemical specialist (strongly emits one channel). Low = generalist (spreads evenly across channels). Captures the effective emission_vector direction. Returns 1/C for non-signal rollouts.

What is biota

biota searches the parameter space of Flow-Lenia — a continuous cellular automaton where matter is conserved by construction. Rather than finding a single best creature, it fills a behavioral grid where each cell holds the most compact creature with a particular phenotypic fingerprint. The result is a structured catalog of qualitatively distinct life-forms.

Each creature is a soliton: a stable, self-maintaining pattern that persists indefinitely given the right parameters. Mass conservation is what makes them viable across a wide parameter range — without it, most settings produce explosion or collapse within a few steps.

Why this project

biota started as a side project to get hands-on with PyTorch and Ray without doing yet another deep learning thing, and grew into a full Flow-Lenia research platform with behavioral search, heterogeneous ecosystem simulation, and this static atlas. It runs on consumer-grade hardware in a home lab — nothing about the approach requires a data center.

If any of it is useful to you, or you want to point out something I got wrong, feel free to open an issue on GitHub.

Further reading

8 runs
45 runs
20260419-225450-544-quad-complex-morpho-chaos-torus
hetsignal fragmentation
10 creatures · 4 species · 640x640 grid · 15000 steps · 531.9s
4 archives torus mass turnover 0.000%/step
20260419-225210-081-hetero-177-vs-340
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 209.3s
2 archives mass turnover 0.000%/step
20260419-225201-209-signal-dots-vs-rings
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 227.6s
2 archives torus mass turnover 0.000%/step
20260419-225113-279-signal-smiley-vs-spotted
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 204.2s
2 archives mass turnover 0.000%/step
20260419-224838-271-homo-spatial-dotgrid-packed
fragmentation
30 creatures · 640x640 grid · 12000 steps · 143.9s
07-torus-spatial torus mass turnover 0.000%/step
20260419-224837-370-spatial-dotgrid-vs-labyrinth
het fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 202.0s
2 archives torus mass turnover 0.000%/step
20260419-224703-576-trio-repulsion-torus-dense
hetsignal fragmentation
12 creatures · 3 species · 512x512 grid · 15000 steps · 279.1s
3 archives torus mass turnover 0.000%/step
20260419-224535-358-competition-176-vs-180-chaos
hetsignal fragmentation
20 creatures · 2 species · 512x512 grid · 12000 steps · 167.6s
2 archives mass turnover 0.000%/step
20260419-224233-484-pursuit-340-vs-180-dense
hetsignal fragmentation
15 creatures · 2 species · 512x512 grid · 12000 steps · 167.6s
2 archives mass turnover 0.000%/step
20260419-223936-511-quad-complex-morpho
hetsignal fragmentation
8 creatures · 4 species · 640x640 grid · 15000 steps · 508.3s
4 archives mass turnover 0.000%/step
20260419-223926-181-trio-177-248-340
hetsignal fragmentation
10 creatures · 3 species · 640x640 grid · 15000 steps · 431.6s
3 archives mass turnover 0.000%/step
20260419-223851-005-hetero-177-vs-231
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 209.2s
2 archives mass turnover 0.000%/step
20260419-223705-000-homo-231-chaos
signal full_merger
30 creatures · 512x512 grid · 15000 steps · 143.5s
05-torus-signal-morpho mass turnover 0.000%/step
20260419-223509-108-hetero-177-vs-248
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 209.4s
2 archives mass turnover 0.000%/step
20260419-223434-278-homo-248-packed
signal partial_clustering
20 creatures · 512x512 grid · 15000 steps · 144.3s
05-torus-signal-morpho mass turnover 0.004%/step
20260419-223235-580-homo-231-packed
signal cannibalism
20 creatures · 512x512 grid · 15000 steps · 146.2s
05-torus-signal-morpho mass turnover 0.000%/step
20260419-223135-231-homo-177-torus-packed
signal fragmentation
25 creatures · 512x512 grid · 15000 steps · 161.6s
05-torus-signal-morpho torus mass turnover 0.000%/step
20260419-222950-068-homo-177-chaos
signal fragmentation
30 creatures · 512x512 grid · 15000 steps · 145.9s
05-torus-signal-morpho mass turnover 0.000%/step
20260419-222856-311-homo-177-packed
signal fragmentation
25 creatures · 512x512 grid · 15000 steps · 141.7s
05-torus-signal-morpho mass turnover 0.000%/step
20260419-222841-020-signal-morpho-penta-chaos
hetsignal fragmentation
8 creatures · 5 species · 640x640 grid · 15000 steps · 599.1s
5 archives mass turnover 0.000%/step
20260419-222546-188-cross-mandala-vs-dots
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 231.1s
2 archives torus mass turnover 0.000%/step
20260419-222520-139-cross-smiley-vs-brain
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 203.5s
2 archives mass turnover 0.000%/step
20260419-222439-639-signal-brain-vs-circuit
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 227.1s
2 archives torus mass turnover 0.000%/step
20260419-222246-134-homo-smiley-packed
signal stable_isolation
25 creatures · 640x640 grid · 12000 steps · 170.1s
08-torus-signal-specialist mass turnover 0.002%/step
20260419-222059-591-signal-mandala-vs-lattice
hetsignal fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 208.0s
2 archives mass turnover 0.000%/step
20260419-221932-544-repulsion-340-vs-46-landscape-dense
hetsignal fragmentation
12 creatures · 2 species · 256x768 grid · 15000 steps · 183.4s
2 archives mass turnover 0.000%/step
20260419-221821-646-spatial-trio-chaos
het fragmentation
12 creatures · 3 species · 640x640 grid · 15000 steps · 394.8s
3 archives torus mass turnover 0.000%/step
20260419-221729-343-spatial-lattice-vs-rings
het fragmentation
12 creatures · 2 species · 512x512 grid · 15000 steps · 200.8s
2 archives torus mass turnover 0.000%/step
20260419-221542-167-homo-180-specimen
signal fragmentation
40 creatures · 640x640 grid · 15000 steps · 212.4s
06-wall-signal-accumulation mass turnover 0.003%/step
20260419-221453-441-homo-180-cannibalism-dense
signal fragmentation
30 creatures · 512x512 grid · 15000 steps · 144.7s
06-wall-signal-accumulation mass turnover 0.003%/step
20260419-221227-013-homo-46-packed
signal cannibalism
30 creatures · 512x640 grid · 12000 steps · 137.7s
06-wall-signal-accumulation mass turnover 0.005%/step
20260419-221008-799-homo-340-torus-packed
signal stable_isolation
25 creatures · 512x512 grid · 12000 steps · 130.8s
05-torus-signal-morpho torus mass turnover 0.007%/step
20260419-220806-033-homo-340-packed
signal stable_isolation
25 creatures · 512x512 grid · 12000 steps · 115.4s
05-torus-signal-morpho mass turnover 0.007%/step
20260419-220742-204-penta-all-modes-dense
hetsignal fragmentation
10 creatures · 5 species · 640x640 grid · 15000 steps · 592.8s
5 archives mass turnover 0.000%/step
20260419-220443-485-penta-chaos
hetsignal fragmentation
10 creatures · 5 species · 640x640 grid · 15000 steps · 611.0s
5 archives mass turnover 0.000%/step
20260419-220304-208-trio-mixed-chaos
hetsignal fragmentation
15 creatures · 3 species · 512x512 grid · 15000 steps · 257.4s
3 archives mass turnover 0.000%/step
20260419-215909-318-quad-repulsion-torus-landscape
hetsignal fragmentation
10 creatures · 4 species · 320x768 grid · 15000 steps · 313.1s
4 archives torus mass turnover 0.000%/step
20260419-215856-266-quad-all-modes-dense
hetsignal fragmentation
10 creatures · 4 species · 640x640 grid · 15000 steps · 513.3s
4 archives mass turnover 0.000%/step
20260419-215827-794-trio-repulsion-dense
hetsignal fragmentation
12 creatures · 3 species · 512x512 grid · 15000 steps · 257.2s
3 archives mass turnover 0.000%/step
20260419-215556-444-competition-176-vs-180-dense
hetsignal fragmentation
15 creatures · 2 species · 512x512 grid · 12000 steps · 166.6s
2 archives mass turnover 0.000%/step
20260419-215538-013-pursuit-340-vs-155-torus-landscape
hetsignal fragmentation
12 creatures · 2 species · 256x768 grid · 15000 steps · 201.5s
2 archives torus mass turnover 0.000%/step
20260419-215531-442-pursuit-340-vs-180-chaos
hetsignal fragmentation
20 creatures · 2 species · 512x512 grid · 12000 steps · 162.2s
2 archives mass turnover 0.000%/step
20260419-215155-310-repulsion-340-vs-46-torus-dense
hetsignal fragmentation
15 creatures · 2 species · 512x512 grid · 15000 steps · 227.2s
2 archives torus mass turnover 0.000%/step
20260419-215155-310-repulsion-340-vs-46-chaos
hetsignal fragmentation
20 creatures · 2 species · 512x512 grid · 15000 steps · 201.3s
2 archives mass turnover 0.000%/step
20260419-215155-240-repulsion-340-vs-46-dense
hetsignal fragmentation
15 creatures · 2 species · 512x512 grid · 15000 steps · 208.6s
2 archives mass turnover 0.000%/step

Run your own atlas

biota is a self-contained CLI tool. You can run it on a laptop for quick iteration or across a Ray cluster for full-scale searches. The only hard dependencies are Python 3.12, PyTorch, and Ray.

Install

# Clone and install
git clone https://github.com/rkv0id/biota
cd biota
uv sync

Quick start

1
Run a search
The dev preset (64×64 grid, 200 steps) is fast enough for a laptop. No Ray needed.
uv run biota search --preset dev --budget 200
2
Build the viewer
Generates a self-contained HTML file for each run, plus an index.
python scripts/build_index.py
open runs/index.html

GPU and cluster

On Apple Silicon pass --device mps --batch-size 32 for a meaningful speedup. On a CUDA cluster, use --batch-size 64 --workers N where N is the number of nodes. The standard preset (192×192, 300 steps) at B=64 on an RTX 5060 Ti cluster runs 500 rollouts in about 97 seconds.

# Apple Silicon
uv run biota search --preset standard --budget 500 \
    --device mps --batch-size 32

# Single CUDA GPU, no Ray
uv run biota search --preset standard --budget 500 \
    --device cuda --batch-size 64

# Multi-node Ray cluster, custom descriptor axes
uv run biota search --ray-address head:6379 \
    --preset standard --budget 2000 \
    --device cuda --batch-size 64 --workers 3 \
    --descriptors oscillation,compactness,png_compressibility

# Signal field: adds emission, receptor, and kernel parameters to the search
uv run biota search --ray-address head:6379 \
    --preset standard --budget 2000 \
    --device cuda --batch-size 64 --workers 3 \
    --signal-field

Choosing descriptors

Pass --descriptors with three comma-separated names to control which behavioral axes the archive uses. With 18 built-ins (15 general + 3 signal-only) there are 816 possible three-axis configurations. You can also supply your own via --descriptor-module path/to/file.py — the file must define a list named DESCRIPTORS containing Descriptor objects.

# Default axes
uv run biota search --descriptors velocity,gyradius,spectral_entropy

# Shape and complexity axes
uv run biota search --descriptors oscillation,compactness,png_compressibility

Ecosystem experiments

Once you have an archive, define one or more ecosystem experiments in a YAML config. A homogeneous run spawns multiple copies of one creature; a heterogeneous run mixes creatures from different archive cells.

# experiments.yaml
experiments:
  - name: self-interaction
    grid: 256
    steps: 5000
    snapshot_every: 50
    border: torus
    output_format: gif
    spawn: {seed: 42, min_dist: 80, patch: 48}
    sources:
      - run: <your-run-id>
        creature_id: <your-creature-id>
        n: 4

  - name: two-species
    grid: 256
    steps: 5000
    snapshot_every: 50
    border: torus
    output_format: gif
    spawn: {seed: 42, min_dist: 100, patch: 48}
    sources:
      - run: <your-run-id>
        creature_id: <your-creature-id>
        n: 2
      - run: <your-run-id>
        creature_id: <another-creature-id>
        n: 2
# Run experiments, then rebuild the atlas to include ecosystem results
biota ecosystem --config experiments.yaml --device cuda
python scripts/build_index.py --output-dir archive --ecosystem-dir ecosystem --publish

More detail

Rami Kader
Machine learning engineer. Interested in cellular automata, artificial life, evolutionary computing, and distributed systems.