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

once per pair select two parents roll 1: crossover? crossover_rate two children one in each parent, in place then, for each child separately roll 2: mutate at all? mutation_rate roll 3: how many? uniform 1..=max_mutations Genome::mutate called that many times if yes
One roll per pair, then two rolls per child. All three live in one shared helper, so the two evolution strategies cannot drift apart on what a mutation means.
SettingAnswersRolled
crossover_rateDoes this pair recombine? Once per pair
mutation_rateDoes 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.

GenomeCrossoverOne 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.
The same count is not the same disruption. One edge-edit mutation changes one edit in a script of 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.

AxisQuestionOptions todayConfig
ScopeWhich individuals may this event touch at all? globalrandom_subset { size } [scope]
SelectionWhich of them become parents? besttournament { tournament_size } [selection]
ReplacementWhich 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:

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.

the population, most of it untouched green = in scope and best 路 amber = in scope and worst 路 grey = not in this event鈥檚 scope the two best breed crossover, then mutate children replace the two worst of that same scope Replacement is scope-local, not global, which is what makes the strategy self-elitist and diversity-preserving, and why the scope needs at least four distinct members.
One mating event. Everything outside the scope is untouched.

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.

A scope of at least four is required here, and only here. Two parents and the two individuals they replace must be distinct. Three still preserves the scope's best but makes the second parent one of the replaced; two breaks self-elitism outright. Generational has no such floor, and the same [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

GenerationalSteady-state
TurnoverEveryone but the elites, every generation Two individuals per event
Scoring per stepThe whole populationThe two new children only
ElitismExplicit, via elite_count Implicit, from scope-local replacement
Population-level parallelismAs wide as the population Two-way, regardless of cores
With a Python objectiveOne crossing per generation One crossing per event, a poor fit
Fitness comparisonsFair: everyone rescored under one seed Carries stale fitnesses; see below
Steady-state carries stale fitnesses, and this is a known accepted limitation. Only the two new children are scored per event, so an individual that is not replaced keeps the fitness it was born with, and a score can stand for thousands of events. The bias compounds in the worst available direction, since a lucky score makes an individual both likelier to be selected and less likely to be replaced. It stands because rescoring per event is exactly the 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.