An artificial chemistry simulator designed to trigger an evolutionary process. Molecules are graph objects (proteins) that interact with each other through chemical reactions, producing new molecules. The seed for evolution is a self-replicating system, and random mutations introduced by certain reactions provide the variation necessary for natural selection.
Technically, molecules encode proteins which fold into Petri nets that drive chemical reactions in a simulated environment.
The project has three components:
- Engine (
engine/) — OCaml CLI that handles simulation: parsing molecules, building Petri nets, and running reactions. Outputs JSON to stdout. - Django app (
django/) — Web application for managing experiments, running simulations (calls the OCaml binary as a subprocess), and exposing a REST API. Also provides a Typer-based CLI (cli.py) for running experiments from the command line. - Cytoscape visualization (
cytoscape/) — JavaScript graph visualization using webcola for layout, bundled with webpack.
Django calls the yaac OCaml binary via subprocess.Popen (see django/experiment/engine.py). The engine reads JSON input from CLI arguments and writes JSON results to stdout. Django parses the output and stores snapshots/logs in SQLite.
Grafana connects to the Django SQLite database for experiment monitoring dashboards.
engine/ # OCaml simulation engine
bin/yaac.ml # CLI entry point (cmdliner via ppx_subliner)
bacterie/ # Simulation runtime (reactions, environments, molecule sets)
base_chemistry/ # Core types: molecules, proteins, Petri nets, places, transitions
libs/ # Utility libraries (config, numerics, random, sets)
jlog/ # Custom JSON-capable logging library
tests/ # Alcotest tests and fixture states
bact_states/ # JSON fixture files for initial states
dune-project # OCaml package definition
django/ # Django web application
manage.py # Django management
cli.py # Typer CLI entry point (experiment management)
alife/ # Django project settings
settings.py
urls.py
experiment/ # Main Django app
models.py # Experiment, BactSnapshot, Log, InitialState
engine.py # YaacWrapper — subprocess interface to OCaml engine
views.py # REST API (DRF) and template views
urls.py
admin.py
fixtures/
subcommansds/ # Typer subcommands
experiment.py # list, run, clear experiments
templates/ # HTML templates
static/ # Static assets
Dockerfile # Archlinux-based container
pyproject.toml # Python dependencies (uv)
cytoscape/ # Graph visualization
src/ # JS source (index.mjs, cytoscape-cola.mjs, defaults.mjs)
webpack.config.mjs
package.json # webcola dependency, webpack build
docs/ # Documentation
experiments.md # Experiment log (summaries and links to reports)
experiments/ # Detailed experiment reports
plans/ # Implementation plans
docker-compose.yaml # Django + Grafana services
grafana/ # Grafana data (SQLite datasource on Django DB)
Requires opam. Compatible with OCaml 5.x (tested with 5.4.0).
cd engine
opam switch create . ocaml-base-compiler.5.4.0 --yes
eval $(opam env)
opam pin add pringo git+https://github.com/sapristi/pringo.git --yes
opam install . --deps-only --with-test --yes
dune build # Build the yaac binary
dune runtest # Run alcotest testsThe binary is built at engine/_build/default/bin/yaac.exe. Key CLI subcommands:
from-mol— Parse molecule string, return protein + Petri net JSONfrom-prot— Build Petri net from protein JSONeval— Run N reaction steps from an initial stateload-signature— Expand a compact bacterie signature to full statereactions— List available reactions from a stateacid-examples— List example acid types
Use --log-level (-l) to control logging. Set JSON_LOG=1 env var for JSON log output.
Requires Python >= 3.10. Uses uv for dependency management.
cd django
uv sync # Install dependencies
uv run ./manage.py migrate # Create/update database
ln -sf ../engine/_build/default/bin/yaac.exe yaac # Symlink engine binary
uv run ./cli.py load-initial-states # Load fixtures from engine test data
uv run ./manage.py runserver # Start dev server (default: localhost:8000)The Typer CLI for experiment management:
cd django
uv run ./cli.py experiment list # List experiments
uv run ./cli.py experiment run <id> <nb_reacs> # Run experiment
uv run ./cli.py experiment run <id> <nb_reacs> --reset # Run from initial state
uv run ./cli.py experiment clear <id> # Remove snapshots
uv run ./cli.py load-initial-states # Load fixtures from engine test dataImportant: The yaac binary must be accessible at ./yaac from the Django working directory (the symlink step above handles this).
docker-compose up # Starts Django (port 8000) and Grafana (port 3000)cd cytoscape
pnpm install
pnpm build # Webpack bundleMolecules are strings of atoms (A-F). They are parsed left-to-right into a list of acids, which form a protein that folds into a Petri net.
| Prefix | Suffix | Acid | Notes |
|---|---|---|---|
AAA |
— | Place | Petri net place |
BAA |
<id>DDF |
Regular_iarc | Standard input arc |
BC |
<atom><id>DDF |
Filter_iarc | Guards on atom at cursor |
BAB |
<id>DDF |
Filter_empty_iarc | Guards on cursor past end |
BAC |
<id>DDF |
No_token_iarc | Fires only when place is EMPTY |
BAD |
<id>DDF |
Copy_iarc | Reads acid at cursor → [original, acid_token] |
BBA |
<id>DDF |
Split_iarc | Cuts token at cursor → [left, right] |
CAA |
<id>DDF |
Regular_oarc | Standard output arc |
CBA |
<id>DDF |
Merge_oarc | Inserts source into dest |
CCA |
<id>DDF |
Move_fw_oarc | Advances cursor |
ABA |
<pattern>DDF |
Grab_ext | Place grabs matching molecules |
ABB |
— | Release_ext | Place releases tokens externally |
ABC |
— | Init_token_ext | Place starts with empty token |
ABD |
— | Copy_grabbed_ext | Place with internal copy buffer for CopyGrabbed reaction |
BAE |
<id>DDF |
Copy_done_iarc | Fires when copy buffer complete; produces [template, copy] |
EEE |
— | Stop_interpretation | Rest of molecule is inert |
- Transition IDs:
max_group_length = 6, lazy match(.{1,6}?) - Grab patterns:
F(X)F= single char capture,FF= wildcard.*?, anchored^...$
- areactants: active molecules (with Petri nets) as
[mol, pnets_list]pairs - ireactants: inert molecules as
{mol, qtt, ambient}dicts
- PPX:
ppx_deriving_yojsonfor JSON serialization,ppx_sublinerfor CLI argument parsing from type definitions - Key dependencies:
containers(stdlib extension),zarith(arbitrary precision),pringo(PRNG),alcotest(testing) - Module layout:
base_chemistrydefines core domain types (molecules, proteins, Petri nets);bacterieimplements runtime simulation;libshas general utilities;jlogis a custom structured logging library - CLI pattern: Subcommands are defined as modules with a
paramstype (derived withsubliner), adocstring, and ahandlefunction
- Models (
experiment/models.py):Experimentholds initial state as JSON;BactSnapshotstores simulation state at checkpoints;Logstores per-step statistics;InitialStatestores reusable starting configurations - Engine integration (
experiment/engine.py):YaacWrapperclass invokes the OCaml binary,StatLogCollectorcaptures structured log output during runs - API: Django REST Framework viewsets for experiments and snapshots
- CLI: Typer app in
cli.pywith subcommands insubcommansds/
Always keep README.md up to date when making changes that affect project structure, build instructions, conventions, or workflow. Ask the user before adding new sections. This file is a symlink — that's expected, edit it normally.
All commands run from django/.
uv run ./cli.py experiment list # List experiments
uv run ./cli.py experiment info <id> # Detailed experiment info
uv run ./cli.py experiment create <initial_state_id> # Create from an InitialState
uv run ./cli.py experiment create <id> --name "my exp" # With custom name
uv run ./cli.py experiment run <id> <nb_reacs> # Run reactions (continues from last snapshot)
uv run ./cli.py experiment run <id> <nb_reacs> --reset # Run from initial state
uv run ./cli.py experiment run <id> 1000 --stats-period 50 --snapshot-period 500
uv run ./cli.py experiment stats <id> # Show log stats as table
uv run ./cli.py experiment stats <id> --last 10 --csv # Last 10 entries as CSV
uv run ./cli.py experiment compare <id1> <id2> # Side-by-side comparison
uv run ./cli.py experiment clear <id> # Remove all snapshotsNever run two concurrent continuations on the same experiment. Each run resumes from the last snapshot and saves new snapshots at fixed intervals. Concurrent runs will start from the same snapshot and crash with a UNIQUE constraint error when both try to save at the same reaction count.
ireactants.nb_species/ireactants.total_nb— inactive molecule diversity and countareactants.nb_species/areactants.total_nb— active (functional Petri net) molecule diversity and countreactions.breaks/collisions/grabs/transitions.nb_reactions— cumulative reaction counts by type
Save reports to docs/experiments/YYYYMMDDTHHmm-<topic>.md (compact datetime to the minute). Include:
- Header: full datetime (e.g.
2026-03-08 14:30), experiment ID, initial state name, parameters - Summary table of key stats over time
- Analysis of observed phases/trends
- Footer: initial state JSON (in
<details>block), date, and git commit hash
After writing a report, add a summary entry to docs/experiments.md with the experiment IDs, names, key findings, and a link to the full report.
- Engine changes: Edit OCaml code in
engine/, build withdune build, test withdune runtest - Django changes: Edit Python code in
django/, restart dev server. Use./cli.pyfor experiment operations - Visualization: Edit JS in
cytoscape/src/, rebuild withpnpm build - Full integration: Ensure the
yaacbinary is built and accessible from the Django working directory, then run the Django server