Learn
Variation & Selection
How children are made, and who they replace. Three probability rolls cover the first; a breeding event's three independent choices cover the second.
The Three Rolls
| Setting | Answers | Rolled |
|---|---|---|
crossover_rate | Does this pair recombine? | Once per pair |
mutation_rate | Does this child mutate at all? | Once per child |
max_mutations |
If it does, how many, drawn uniformly from
1..=max_mutations. |
Once per child |
So mutation_rate = 0.2, max_mutations = 4 means 80% of children are untouched, and
the other 20% take 1, 2, 3 or 4 mutations with equal probability. It does not mean each
gene mutates with probability 0.2, a different and much more common convention, and confusing
the two will cost you a run.
Genome::mutate applies exactly one mutation per call. The engine calls it in
a loop, so max_mutations means the same thing for every representation. A genome
that rolled its own count would make the setting meaningless for itself and nothing would report
it.
What One Mutation Is, and Which Operator Runs
Crossover recombines in place, leaving one child in each parent, and both are kept. The
operator is selected under [crossover] and applies to whichever representation is
running, because both genomes ship the same two-point operator. Mutation is the other way
round: its operator is chosen per genome, under [genome], because the two
representations' mutations share no shape at all.
| Genome | Crossover | One mutation is |
|---|---|---|
| edge-edit | Two-point over genes | Reroll one gene, opcode drawn from the configured operation mix, payload redrawn. |
| SDA | Two-point over states | Redraw init_char, one transition target, or one response.
init_char_mutation_rate (default 0.04) picks
init_char versus the rest;
transition_vs_response_rate (default 0.5) splits the
remainder. |
gene_length. One SDA mutation changes one table entry, and because the
automaton feeds on its own output, a single changed response can alter every character after it.
SDA is correspondingly more disruptive at the same max_mutations.
Both rolls live in one shared helper used by both strategies, so they cannot drift apart on what
a mutation means. Both rates are validated in [0, 1] before a run starts.
A Breeding Event: Scope, Selection, Replacement
A breeding event is three independent choices, each its own config section and each one variant to extend. Every combination is legal; there is deliberately no scheme-by-strategy rejection table.
| Axis | Question | Options today | Config |
|---|---|---|---|
Scope | Which individuals may this event touch at all? | global 路 random_subset { size } |
[scope] |
Selection | Which of them become parents? | best 路 tournament { tournament_size } |
[selection] |
Replacement | Which of them do the children overwrite? | worst |
[evolution] replacement |
Each axis owns its own parameters and neither reads the other:
size belongs to [scope], tournament_size to
[selection]. Ordering is by fitness with ties broken by lower index, so an
event's outcome depends only on which indices were drawn, not on the order the generator produced
them. Arity is fixed at 2 parents, 2 children, 2 replaced.
random_subset draws distinct individuals; tournament samples with
replacement. The distinctness is not stylistic: "the worst two members" means nothing over a
multiset. The consequence: under a global scope two picks can be the same individual, so
crossover between a genome and its own clone does nothing but mutate.
The Two Strategies
Both are points in that space, and the difference is how much of the population turns over.
Generational
The whole population is replaced each generation: score everyone, log a row, copy the
elite_count best forward unchanged, then fill every remaining slot with children of
pairs drawn from the configured scope. Replacement does not arise, because the population is rebuilt, so
elite_count is this strategy's own knob. The usual configuration is
global scope with tournament selection, but the scope is read from
config like any other, not fixed by the strategy. Three rules are easy to get wrong:
- Elites are rescored every generation, like everyone else. Under a stochastic objective an elite's recorded fitness moves while its genome does not, and that is correct: the new number is a fresh sample, and freezing the old one would let a lucky draw persist forever.
- Odd slot counts lose one child. Crossover yields two, but
population_size - elite_countmay be odd; the last pair contributes one and the other is discarded. - Generation 0 is the initial population, logged before any breeding, so both strategies' histories share an axis.
Steady-state
One mate-and-replace event at a time: each event draws the configured scope, selects two
parents within it, and applies the configured replacement policy to that same scope. Most of the
population simply persists. Replacement is this strategy's own knob; generational has no use for
one. The usual configuration is random_subset { size } with best and
worst, which is what "tournament selection with tournament-local replacement"
decomposes into, and everything below describes that default.
Scope-local replacement makes the strategy self-elitist (the scope's best is never among
the replaced, so no explicit elitism is needed) and diversity-preserving, since a globally
poor individual survives until it is drawn. It is also cheap: O(k log k) per
event rather than an O(population) scan, which matters at 100,000 events. Replacement
is unconditional: a child takes its slot even if it scores worse than what it displaces.
[scope] size rejected here is accepted there.
Logging cadence: the starting population as iteration 0, then one row per
population_size events, a "generation equivalent", so
history.len() == num_mating_events / population_size + 1. Logging every event would
give a 100,000-row history.
Choosing Between Them
| Generational | Steady-state | |
|---|---|---|
| Turnover | Everyone but the elites, every generation | Two individuals per event |
| Scoring per step | The whole population | The two new children only |
| Elitism | Explicit, via elite_count |
Implicit, from scope-local replacement |
| Population-level parallelism | As wide as the population | Two-way, regardless of cores |
| With a Python objective | One crossing per generation | One crossing per event, a poor fit |
| Fitness comparisons | Fair: everyone rescored under one seed | Carries stale fitnesses; see below |
O(population) work
that scope-local replacement exists to avoid. Prefer generational when the objective is
stochastic and the comparison needs to be fair, which, with any of the three SIR objectives,
it is.
Adding a Strategy, or a Scheme
A new scope, selection scheme, replacement policy or crossover operator is one enum variant plus
one dispatch arm, and works with every strategy. A new strategy is one config variant, one match
arm, and an implementation of Evolver, whose one non-optional rule is to score
through express_and_score. Recipes on
Add a Strategy and
Add a Selection Scheme.