Advanced Extensions
Add a Mutation Operator
Four edits, but this chain exists twice, once per genome, and the two halves share no code. Find your half first; a step from the other one is a different enum in a different file and does nothing for your representation.
Read this before you grep. There are four numbered steps for edge-edit and four for SDA, interleaved. The grep prints more lines than that, because each step is also named where a neighbouring marker points forward to it, so read the step numbers, not the line count. Every marker names its half, so narrow the search to yours:
git grep -nE "ADD A MUTATION STEP . \(for EdgeEdit\)" get/src config.example.toml # 4 steps, 7 sites
git grep -nE "ADD A MUTATION STEP . \(for SDA\)" get/src config.example.toml # 4 steps, 7 sites
git grep -n "ADD A MUTATION STEP" get/src config.example.toml # both halves: 8 steps, 14 sites
Unlike crossover, mutation is per representation: there is no shared
Mutation enum and no compatibility match to extend. Adding an operator for one
genome does not touch the other at all.
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. Mutation is configured as
an inline table inside the genome's own section, because the operator belongs to the
representation; SDA's goes under its [genome] the same way, alongside
redraw_one:
[genome]
type = "edge_edit"
gene_length = 256
mutation = { type = "my_mutation", some_param = 0.5 }
You never write the string "my_mutation" 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 Contract, Whichever Half You Are On
Exactly one mutation per call. The engine's max_mutations counts calls, not
changes, so a variant that applies several makes that setting meaningless for the
representation, and nothing reports the disagreement. The run completes, the numbers look
plausible, and the mutation rate the user configured is not the one they got. This is the single
most expensive thing to get wrong on this page.
Edge-edit: The Four Steps
| Step | Where | What |
|---|---|---|
| 1 | get/src/genomes/genome.rs:108 |
A variant on enum EdgeEditMutation, plus any parameters it reads. |
| 2 | get/src/genomes/edge_edit.rs:235 |
The arm in EdgeEditGenome::mutate that performs it. |
| 3 | get/src/config.rs:207 and
get/src/dispatch.rs:656 |
The mirrored variant on enum EdgeEditMutationConfig, and the arm mapping it
onto the engine enum. |
| 4 | get/src/py_config.rs:723, :744 and
config.example.toml:192 |
Optional. The Python-side variant, its matching arm, and a
mutation line in the example config. |
// step 1: genomes/genome.rs:108
MyMutation { some_param: f64 },
// step 2: genomes/edge_edit.rs:235
EdgeEditMutation::MyMutation { some_param } => {
// exactly one change to `self.genes`
}
// step 3a: config.rs:207
MyMutation { some_param: f64 },
// step 3b: dispatch.rs:656
EdgeEditMutationConfig::MyMutation { some_param } => {
EdgeEditMutation::MyMutation { some_param: *some_param }
}
The shipped variant is RerollGene: reroll one gene, its opcode drawn from the
operation mix. That is what edge-edit did before the operator became selectable, which is why it
is the default.
SDA: The Four Steps
| Step | Where | What |
|---|---|---|
| 1 | get/src/genomes/genome.rs:121 |
A variant on enum SdaMutation, plus any parameters it reads. |
| 2 | get/src/genomes/sda.rs:405 |
The arm in SdaGenome::mutate that performs it. |
| 3 | get/src/config.rs:249 and
get/src/dispatch.rs:670 |
The mirrored variant on enum SdaMutationConfig, and the arm mapping it onto
the engine enum. |
| 4 | get/src/py_config.rs:760, :781 and
config.example.toml:199 |
Optional. The Python-side variant, its matching arm, and a
mutation line in the example config. |
// step 1: genomes/genome.rs:121
MyMutation { some_param: f64 },
// step 2: genomes/sda.rs:405
SdaMutation::MyMutation { some_param } => self.my_mutation(context, rng),
// step 3a: config.rs:249
MyMutation { some_param: f64 },
// step 3b: dispatch.rs:670
SdaMutationConfig::MyMutation { some_param } => {
SdaMutation::MyMutation { some_param: *some_param }
}
The shipped variant is RedrawOne: redraw one transition's target state, or that
transition's response, chosen by the two rates on SdaContext. As with edge-edit, it
is what SDA did before the operator became selectable, and it is the default.
SDA's arm delegates; edge-edit's does the work inline. That is a style difference in the shipped code, not a rule. Either shape is fine as long as the call performs exactly one mutation.
Both Halves, and What They Do Not Share
The two chains are deliberately independent. There is no shared enum to unify them, because what counts as "one mutation" differs by representation (a gene for edge-edit, a transition for SDA), and a shared operator would have to branch on the genome to mean anything. If you want an operator available to both representations, you add it twice, once per chain, and the two arms will not look alike.
Testing It
The property to pin is the contract: mutate a known genome under a fixed seed and assert that exactly one unit changed. That is the assertion that catches the failure nothing else reports. Then assert whatever your operator specifically promises.
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 Crossover Instead
Crossover is a single shared chain rather than one per genome, and it has a compatibility match that mutation does not; see Add a Crossover Operator.