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.
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.
| Step | Where | What |
|---|---|---|
| 1 | get/src/evolver/common.rs:27 |
A variant on enum Crossover, plus any parameters it reads from the
config. |
| 2 | get/src/evolver/common.rs:52 |
The arm in Crossover::recombine that performs it. |
| 3a | get/src/config.rs:131 |
The mirrored variant on enum CrossoverConfig, the one whose name becomes
type = "my_crossover". |
| 3b | get/src/config.rs:692 |
The one that gets forgotten. One (operator, genome) pair per
representation your operator can honour. |
| 4 | get/src/dispatch.rs:643 |
The arm mapping your CrossoverConfig onto the engine variant. |
| 5 | get/src/py_config.rs:218 and :706 |
Optional. The Python-side variant, and its matching arm. |
| 6 | config.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).