Advanced Extensions

Add a Replacement Policy

Choosing which members of a scope a breeding event's children overwrite. Three steps, the shortest chain in GET, and the only one that belongs to a single strategy.

Not part of the v0.9 release. This chain isn't finished and documented yet; only adding an objective is ready to use today. This note is removed once the chain is finished and documented.
This axis is steady-state's alone. Generational rebuilds its whole population every iteration and never asks who to overwrite, so a reader extending that strategy has no step in this chain to take. Every marker says so on the line: (for SteadyState), so a grep across all the chains at once does not hand you a step that is not yours.

The One Question a Policy Answers

A policy is handed the scope a breeding event drew and the fitnesses of everyone in it, and returns which positions the children take. It is the counterpart to selection: that answers who breeds, this who dies, and both work inside the same slice. Keeping them apart is what lets any selection scheme pair with any policy: a scheme no longer has to supply a replacement draw it has no theory for.

A policy may consume randomness. worst reads the scope's fitnesses and nothing else; random draws from the run's seeded stream. Either is reproducible from a seed, but adding or removing a drawing policy changes what every later draw in the run sees, so runs are only comparable against others using the same policy.

Self-Elitism Rests Here

Worst takes the scope's least fit members, never its best. That is what makes steady-state self-elitist, and note it is a property of the policy, not of the selection scheme, which is why it holds whichever scheme picked the parents. Random gives that guarantee up by design: it can draw the scope's best, so a run's best fitness can fall.

What the User Ends Up Writing

[evolution]
type              = "steady_state"
num_mating_events = 5000
replacement       = { type = "random" }   # or "worst", the default

It is read from the strategy's own table rather than a section of its own, because it belongs to whichever strategy displaces individuals, the same way elite_count belongs to generational. Absent means worst.

The Three Steps

git grep -n "ADD A REPLACEMENT STEP" get/src    # 3 steps, 6 sites
StepWhereWhat
1get/src/evolver/replacement.rs:15 the variant, and any parameters it needs
2get/src/evolver/replacement.rs:61 the arm naming who the children overwrite; the match is exhaustive
3get/src/config.rs:95, get/src/dispatch.rs:698, get/src/py_config.rs:170 and :641 what a user names under replacement, the arm building it, and the Python mirror

Step 3 is one step across four sites rather than four steps, because none of them is a decision: once the variant and its arm exist, the rest is naming it in each of the three places a name can arrive from. The Python half is optional; leaving it out makes the policy TOML-only.

Steps 1 and 2: The Engine Side

Both are in get/src/evolver/replacement.rs. Add the variant, then the arm that names the victims:

// step 1: evolver/replacement.rs:15
Tournament { size: usize },

// step 2: evolver/replacement.rs:61
Replacement::Tournament { size } => {
    // draw `size` members, overwrite the least fit of them
}

The signature you are filling in is:

fn pick<R>(&self, scope: &[usize], fitnesses: &[f64], count: usize, rng: &mut R) -> Vec<usize>
where
    R: Rng + ?Sized
Two contracts, and the second is the expensive one.

Say at the variant what your policy gives up. Worst is what makes a strategy self-elitist; Random can overwrite the scope's best and says so in its own doc comment. Leaving that to be discovered later is how a run quietly stops improving.

Step 3: The Three Places a Name Arrives From

// step 3: config.rs:95
Tournament { size: usize },

// step 3: dispatch.rs:698
ReplacementConfig::Tournament { size } => Replacement::Tournament { size: *size },

// step 3: py_config.rs:170   (optional)
#[pyo3(constructor = (size))]
Tournament { size: usize },

// step 3: py_config.rs:641   (optional, the matching arm)
PyReplacementConfig::Tournament { size } => {
    table.insert("type".to_string(), Value::String("tournament".to_string()));
    table.insert("size".to_string(), integer("size", *size)?);
}

The first two are one change split across two files and neither compiles without the other. The second two are the Python mirror, and nothing checks them: a policy missing its mirror compiles clean and passes every test, and is simply absent from the Python API.

Testing It

pick takes a scope, a fitness slice and an RNG, and returns a list of indices, with no evolver, nothing else to set up:

#[test]
fn tournament_returns_population_indices_from_the_scope() {
    let scope = [3, 7, 11, 15];
    let fitnesses = [0.0; 16];
    let mut rng = ChaCha8Rng::seed_from_u64(7);

    let victims = Replacement::Tournament { size: 2 }.pick(&scope, &fitnesses, 2, &mut rng);

    assert_eq!(victims.len(), 2);
    assert!(victims.iter().all(|v| scope.contains(v)));
}

The existing test beside it, the_worst_are_returned_worst_first, is the one to read before writing yours: it pins the ordering contract that the children's slot assignment depends on. If your policy draws, seed a fixed RNG so the test is reproducible, and read random_returns_distinct_members_of_the_scope for the distinctness assertion every drawing policy owes: two victims that coincide mean one child overwrites the other.

The Limit Worth Knowing Before You Start

A policy can only read what the engine records per individual. Today that is a genome and a fitness and nothing else. So Oldest, or anything keyed on lineage, tenure or distance from a parent, is not a variant plus an arm: it needs that quantity recorded first, which is a change to the engine rather than to this enum. Check what you need exists before writing the variant.

The Other Two Axes

git grep -n "ADD A SCOPE STEP" get/src config.example.toml          # 6 steps, 7 sites
git grep -n "ADD A SELECTION STEP" get/src config.example.toml      # 6 steps, 7 sites

See Add a Scope and Add a Selection Scheme.