Genetic Engine
The GeneticEngine is the core component. Once built, it manages the entire evolutionary process, including population management, fitness evaluation, and genetic operations. The engine itself is essentially a large iterator that produces Generation objects representing each step or epoch.
Engine Defaults
Every engine is created through a fluent builder. Only two things are required — an encoding (the codec) and a fitness function; everything else has a sensible default you override only when you need to.
| Setting | Default |
|---|---|
| Encoding / genome | — (required) |
| Fitness function | — (required) |
| Objective | maximize, single |
| Population size | 100 |
| Offspring selector | Roulette |
| Survivor selector | Tournament (k=3) |
| Offspring fraction | 0.8 |
| Alterers | UniformCrossover(0.5) + UniformMutator(0.1) |
| Diversity | off |
| Executor | Serial |
| Stopping limits | none — runs until you stop it |
| Events | none |
So a minimal engine with just a codec and a fitness function will do the following: maximizes a single objective over a population of 100, breeding 80% offspring each generation with uniform crossover and mutation, selecting offspring by roulette and survivors by tournament, running single-threaded. From there you change only what your problem needs.
Life of an epoch
Each time the engine advances one step, it runs a fixed pipeline of steps. Two of them are conditional — Front only runs for multi-objective problems, and Speciate only when you've configured a diversity measure:
flowchart TD
S[Next epoch] --> E1[Evaluate — score unscored individuals]
E1 --> R[Recombine — select survivors, breed offspring via crossover + mutation]
R --> F[Filter — replace individuals past max_age or with invalid genomes]
F --> E2[Evaluate — re-score the individuals whose genomes changed]
E2 --> MO{multi-objective?}
MO -->|yes| FR[Front — update the Pareto front]
MO -->|no| DV{diversity configured?}
FR --> DV
DV -->|yes| SP[Speciate — cluster the population into species by distance]
DV -->|no| AU[Metrics — collect generation's metrics]
SP --> AU
AU --> G[Repeat until stopping limit reached]
G --> S
The engine evaluates twice per generation. The first pass ranks the current population so selection has scores to work with. The second pass re-scores every individual whose genome changed in between — the offspring produced by crossover and mutation (modifying a genome invalidates its old score) and any replacements introduced by Filter — so each emitted epoch is fully scored.
This section is organized as
| Page | Covers |
|---|---|
| Runtime | how the engine actually advances — the Engine trait, EngineRuntime, run() vs the iterator, the control interface |
| Generations | the Generation and GenerationView types — what you get back each epoch, and what it costs to get it |
| Limits | every built-in stopping condition, and how to combine them |
| Metrics | the statistics collected every generation |
| Exprs | the expression DSL behind dynamic rates and expression-based limits |
| Example | a few small, focused examples — including when you actually need the iterator instead of run() |
Best Practices
- Population size: 100-500 covers most problems; go larger only if you have the fitness-evaluation budget for it.
- Executor: enable parallel execution for expensive fitness functions before tuning anything else — it's usually the biggest lever.
- Diversity: reach for species-based diversity only once you've seen premature convergence — it's opt-in and not free.
- Rates: experiment with mutation/crossover rates, and prefer an
Expr-driven rate over a fixed one if the right rate changes as the run progresses. - Observability: use logging, checkpointing, and the control interface for long or interactive runs.
Common Pitfalls
- No stopping limit: an engine with no limit attached runs forever in Rust (you must
break/returnor attach one) and raises immediately in Python (at least oneLimitis mandatory there). - Reusing a consumed Rust engine:
.iter()consumes theGeneticEngine— once you've built anEngineRuntimefrom it, that engine value is gone.run(closure)doesn't consume it, so it's safe to call more than once. - Assuming Python's
.run()resumes: Python'sEngineis a reusable builder, not a live engine — every.run()call constructs a fresh engine from scratch. See Runtime for the Rust/Python distinction. - Materializing a
Generationyou don't need: a stop condition that has to inspect the actual per-generation result (a custom callback, or an actual loop over each generation) forces a fresh clone every generation. If you only want the final result, drive the stop condition withLimits instead and skip the per-generation cost entirely — see Runtime for the Rust/Python calls that take each path.