A minimal evolutionary simulation where each organism maintains a full joint probability distribution over local neighborhood (X), internal energy (H), and action (O). Behavior is sampled from that joint; offspring inherit a mutated, renormalized joint. The environment is a one-dimensional circular world with food spawning, metabolic decay, and collision death.
Useful for experimenting with how selection pressure reshapes high-dimensional categorical policies without hand-specifying neural nets or explicit fitness functions beyond survival and reproduction.
- State space: 27 neighborhood encodings (
3³spots: empty / food / occupied), 11 discrete energy levels (0–10), 3 actions (move left, stay, move right). - Organism: Position, energy
H, and a27 × 11 × 3joint array (normalized). Actions are drawn asP(O | X, H)from the appropriate slice. - World:
Worldwraps toroidally; each tick organisms choose moves; collisions (multiple movers to one cell, or moving into someone who stays) remove organisms; eating increasesH; periodic decay lowersH; reproduction requires energy above a threshold and places offspring in an empty neighbor slot with a mutated joint. - Outputs: Time series in
logs.csv(population, energy stats, births/deaths intervals, KL divergence vs. uniform joint), periodic.npyjoint snapshots, and matplotlib plots underoutputs/plots/after each run.
Python 3.10+ recommended (uses int | None style hints). Dependencies are listed in joint_sim/requirements.txt:
numpy,scipy,matplotlib,pandaspygame— optional; used byjoint_sim/visualizer.py
Install and run from the joint_sim directory (imports assume that working directory):
cd joint_sim
python3 -m venv .venv && source .venv/bin/activate # optional
pip install -r requirements.txt
python3 run.pyBy default this uses values from joint_sim/config.py (100_000 ticks, world size 1000, etc.). Override with CLI flags or edit Config dataclasses in code.
Analysis (population curve, KL vs. uniform, marginals and other plots) runs automatically after the simulation. To regenerate plots from existing logs without rerunning:
python3 run.py --analyzeOptional ASCII world printout during runs:
python3 run.py --visualize --progress-interval 2000Optional Pygame window:
python3 visualizer.py| Flag | Role |
|---|---|
--ticks |
Simulation length |
--seed |
RNG seed |
--world-size |
Cells on the ring |
--initial-food, --food-rate |
Initial food count and spawn rate per tick |
--population |
Starting organism count |
--repro-threshold |
Minimum H to reproduce |
--decay-interval |
Ticks between -1 energy metabolic steps |
--mutation-rate, --mutation-sigma |
Per-entry mutation probability and Gaussian noise scale |
--output-dir |
Base output directory (default outputs) |
--log-interval |
Tick spacing for CSV / snapshot writes |
--quiet, --progress-interval, --visualize |
Console noise and ASCII preview |
Example long run matching a typical exploratory profile:
python3 run.py --ticks 100000 --population 50 --food-rate 10 --initial-food 400 \
--decay-interval 15 --progress-interval 5000 \
--mutation-rate 0.01 --mutation-sigma 0.02For a tabular rundown of hyperparameters and intuition, see hyperparameter_reference.txt at the repo root.
Re-analyzed from joint_sim/outputs/ on 2026-05-31 (python3 run.py --analyze).
python3 run.py --ticks 500000 --decay-interval 15 --progress-interval 10000 \
--mutation-rate 0.02 --mutation-sigma 0.03Other settings from joint_sim/config.py defaults: world 1000, initial population 50, food spawn 10/tick, initial food 400, repro threshold H ≥ 8, log interval 100 ticks.
| Target | 500 000 ticks |
| Actual (simulator counter) | 369 482 ticks (Ctrl+C interrupt) |
| Logged max tick | 369 400 (CSV rows every 100 ticks) |
| Completion | ~74% of planned length |
| Wall clock | ~2.9 hours (first → last snapshot file timestamps: 17:21 → 20:14 on 2026-04-30) |
| Throughput | ~35 ticks/sec effective over the full run |
Post-hoc analysis ran on interrupt; all artifacts remain under joint_sim/outputs/.
| Metric | Value |
|---|---|
| Ticks simulated (logged max) | 369 400 |
| Final / peak / min population | 49 / 99 / 8 |
| Mean population (whole run) | 57.7 |
| Final KL (pop‑mean joint ∥ uniform) | 0.420 |
| Max KL (over run) | 1.103 (at tick 148 100, during bottleneck) |
| Mean KL (whole run) | 0.416 |
| % of run with KL > 0.3 | 97.2% |
| Total births | 639 137 |
| Total starvation deaths | 289 (0.05%) |
| Total collision deaths | 638 849 (99.95%) |
Final mean / median H |
5.67 / 6.0 |
| Final organisms saved | 54 (.npy in final_joints/) |
| Joint snapshots | 3 695 |
| Lineage traces | 30 |
Verdict: supported.
| Evidence | Detail |
|---|---|
| Fast departure from uniform | KL rises from 0.003 (tick 0) to > 0.1 by tick 100, > 0.5 by tick 500. |
| Sustained structure | KL stays elevated for 97% of the run (mean 0.42); not a transient early artifact. |
| Peak under selection pressure | Max KL 1.10 coincides with population bottleneck (pop = 8 at tick 148 100), when survivors carry the most peaked joints. |
| Individual joints are highly structured | Per-organism KL at end: mean 2.34 (σ 0.05, CV 2.3%) — far above uniform, and survivors converged to similar policies. |
| Lower entropy than uniform | Final joint entropy 4.45 vs uniform 6.79 (891-dimensional simplex). |
| Population persisted | Recovered from bottleneck 8 → 49 by run end; no extinction. |
Caveat: high KL proves concentrated mass, not necessarily “optimal foraging.” Pair with conditional slices below.
| Phase | Tick range | Mean pop | Mean KL | Notes |
|---|---|---|---|---|
| Early | 0 – 10 k | 58.2 | 0.40 | Structure appears within first ~500 ticks |
| Mid | 100 – 200 k | 55.0 | 0.44 | Peak KL 1.10 near tick 148 k |
| Late | 300 k+ | 59.7 | 0.40 | Post-bottleneck recovery; KL dips then stabilizes |
Qualitative shifts in P(O | X, H) — see outputs/plots/conditional_slices.png:
| Context | Hypothesis | Result |
|---|---|---|
Food to left, H=5 |
↑ move left | Supported — LEFT 0.31 → 0.49 (+0.17) |
Food center, H=3 |
↑ stay | Not supported — STAY 0.36 → 0.06; RIGHT rose to 0.69 |
Surrounded, H=8 |
↑ stay | Not supported — STAY 0.34 → 0.19; RIGHT 0.65 |
Food right, H=2 |
↑ move right | Not supported — RIGHT 0.33 → 0.10; LEFT 0.80 |
Read: the joint is clearly non-uniform and context-dependent, but simple “move toward food / stay on food” stories only partially hold. Collision-dominated selection may favor movement patterns that reduce crowding over naive foraging heuristics.
- Mortality: 99.95% collision, 0.05% starvation — crowded ring, movement is the main selective filter.
- Energy: mean
H5.0 → 5.7 (modest rise); median stays 6 — metabolic state stable, not the primary evolution signal. - Food: world food count 400 → 909 (mean 870); environment is food-rich relative to population.
From joint_sim/:
python3 run.py --analyzePlots written to outputs/plots/: population_over_time, kl_divergence_over_time, conditional_slices, h_marginal_comparison, diversity_histogram.
Artifacts under joint_sim/outputs/ (logs.csv, joint_snapshots/, plots/, final_joints/, lineages/) are regenerated every run. They are listed in .gitignore so bulky or machine-specific traces are not committed. To reproduce plots from an existing CSV: python3 run.py --analyze (run from joint_sim). If you committed outputs/ before adding the ignore file, stop tracking it with git rm -r --cached joint_sim/outputs and commit (local files stay on disk).
| Path | Purpose |
|---|---|
joint_sim/run.py |
CLI entrypoint: simulate → log → analyze |
joint_sim/config.py |
Dataclass configs + StateSpace constants |
joint_sim/organism.py |
Joint representation, sampling, reproduction / mutation |
joint_sim/world.py |
Circular grid, food, sensing, collisions helpers |
joint_sim/simulation.py |
Tick loop |
joint_sim/logger.py |
CSV + snapshot + KL metrics |
joint_sim/analysis.py |
Matplotlib plots from logs and snapshots |
joint_sim/visualizer.py |
Live pygame view |
joint_sim/tests/ |
Unit tests (pytest style) |
From joint_sim:
pip install pytest
pytest tests/AI-assisted coding tools were used to implement the simulation code and tooling that runs these experiments. The experiments themselves — including the hypotheses, design choices, parameter sweeps, and interpretation of results — were ideated, iterated, and designed by me.
No license file is included in this repository; add one if you plan to publish or accept contributions.