Advanced Extensions

Add a Crossover Operator

Recombining two parents into two children. Six edits, and one of them is the step people miss: [crossover] is a shared section, so a new operator has to declare which genomes can honour it.

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.

Where the Operator Sits

Crossover is an enum in get/src/evolver/common.rs. By the time it is called, the engine has already rolled crossover_rate and decided this pair breeds, the operator only chooses how. It receives two genomes and an RNG, and modifies both in place; both children are kept.

What the User Ends Up Writing

A GET run is configured by a TOML document. See the Python: TOML File for its full shape, and Python: Config Objects for how to run one. Adding an operator called MyCrossover is what makes this valid:

[crossover]
type       = "my_crossover"
some_param = 0.5

You never write the string "my_crossover" anywhere in the code. It is derived from your variant's name, converted to snake_case. Name the variant and the config name follows; the keys under it are your struct's field names, the same way.

This is not specific to the command-line route. The same document is read by the CLI and by the Rust library, and a Python caller builds the same section through a config object instead of typing it. Skipping the optional Python step below is what makes an operator TOML-only.

The Six Steps

Every location carries a literal marker in the source, so you can jump to any step by searching its number. The line numbers below were correct when this page was written; if one has moved, the marker is the anchor that has not.

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

Each step is named at more than one site, because a marker points forward to the next; go by the step numbers rather than the number of lines the grep prints.

StepWhereWhat
1get/src/evolver/common.rs:27 A variant on enum Crossover, plus any parameters it reads from the config.
2get/src/evolver/common.rs:52 The arm in Crossover::recombine that performs it.
3aget/src/config.rs:131 The mirrored variant on enum CrossoverConfig, the one whose name becomes type = "my_crossover".
3bget/src/config.rs:692 The one that gets forgotten. One (operator, genome) pair per representation your operator can honour.
4get/src/dispatch.rs:643 The arm mapping your CrossoverConfig onto the engine variant.
5get/src/py_config.rs:218 and :706 Optional. The Python-side variant, and its matching arm.
6config.example.toml:179 Optional, and the step people skip. A commented-out [crossover] block. The shipped example is what a user copies from.

Steps 1 and 2: The Engine Side

// step 1: evolver/common.rs:27
MyCrossover { some_param: f64 },

// step 2: evolver/common.rs:52
Crossover::MyCrossover { some_param } => { /* recombine `first` and `second` */ }

The shipped TwoPoint arm is not a template. It is the trait call first.crossover(second, rng), because two-point is what Genome::crossover already means for every representation. A second operator will not be, and that is precisely the point at which Genome grows a second method. Genome::crossover takes no context deliberately, so an operator needing per-representation behaviour adds a trait method rather than branching on the genome inside your arm.

Step 3: The Config Variant, and the Compatibility Pairs

Two sites in one file, and the second is the one that gets forgotten. [crossover] names an operator for whichever genome is selected, and the two are chosen independently, so nothing but this match can catch an illegal combination:

// step 3a: config.rs:131
MyCrossover { some_param: f64 },

// step 3b: config.rs:692
(CrossoverConfig::MyCrossover { .. }, GenomeConfig::Sda(_)) => Ok(()),
(CrossoverConfig::MyCrossover { .. }, GenomeConfig::EdgeEdit(_)) =>
    Err(invalid("crossover", "edge-edit cannot honour my_crossover")),

You cannot forget 3b, by construction. The match is written out in full rather than using a catch-all: every pairing is spelled out even though every pairing is currently legal, so adding a variant stops it compiling. That forces whoever adds an operator to state which genomes can honour it, instead of a user discovering the answer part-way through a run.

Step 4: Dispatch

// step 4: dispatch.rs:643
CrossoverConfig::MyCrossover { some_param } => {
    Crossover::MyCrossover { some_param: *some_param }
}

Steps 3a and 4 are one change split across two files, the same shape the selection chain has.

Steps 5 and 6: Python and the Example, Both Optional

Leaving step 5 out costs nothing elsewhere; the operator is then configurable from a TOML file but absent from the Python API. The variant goes at get/src/py_config.rs:218 and its matching arm at :706.

Step 6 is a commented-out [crossover] block in config.example.toml:179. It is the step people skip, and it costs nothing mechanically: the example file is simply what a user copies from, so an operator missing from it is one most people never find. The test suite parses and validates that file, so a block that does not parse fails the build rather than a user's afternoon.

Testing It

Crossover is testable in isolation: build two known parents, recombine under a fixed seed, and assert the children are what the operator promises. The property worth pinning is whatever your operator claims to preserve: two-point preserves everything outside the swapped band, on both sides.

Then add the keys to config.example.toml, which the test suite parses and validates so a broken example fails the build.

If You Wanted Mutation Instead

Mutation is a separate chain and it branches per genome; see Add a Mutation Operator. Grepping its markers without narrowing returns two interleaved chains, one marked (for EdgeEdit) and one (for SDA).