Advanced Extensions
Add a Selection Scheme
Choosing which members of a scope become parents. Six edits across five files, and no strategy or genome is touched: selection is one of three independent axes, and it answers only this question.
The One Question a Scheme Answers
Selection takes a scope, a list of population indices someone else chose, and returns
count parent indices drawn from it. It does not decide who is eligible
([scope] does that) and it does not decide who dies
([replacement] does that). That separation is why every scheme works with every
strategy, and why there is no scheme-by-strategy compatibility check to extend.
Three contracts the signature does not enforce. Nothing fails to compile if you break one, and two of the three fail silently.
- Compare through the ranking helper, never a direction. Fitnesses arrive already oriented, so a scheme never asks whether bigger or smaller wins. One that decides for itself runs the search backwards on half the objectives and looks merely unconverged.
- Take every random value from the RNG you are handed. Reaching for a thread RNG instead produces a run that cannot be reproduced from its seed, which is the one guarantee the whole seeding chain exists to give.
- Draw only from the scope. It is a list of indices someone else chose; an index from outside it is a bug the assertion at the top of the draw will catch, and only in a debug build.
Drawing with replacement is allowed: the tournament does it, so the same individual can be both parents. That is deliberate, not an oversight to correct in a new scheme.
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. Selection is one section
of it. Adding a scheme called Roulette is what makes this valid:
[selection]
type = "roulette"
pressure = 1.8
You never write the string "roulette" anywhere in the code. It is derived
from your variant's name, converted to snake_case, so Roulette becomes
roulette, MyScheme becomes my_scheme. 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 two optional Python steps below is what makes a scheme 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 SELECTION STEP" get/src config.example.toml # 6 steps, 7 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:75 |
A variant on enum Selection, carrying any parameters the scheme reads out
of the config. |
| 2 | get/src/evolver/common.rs:159 |
The arm in Selection::pick that actually chooses parents. |
| 3 | get/src/config.rs:112 |
The mirrored variant on enum SelectionConfig. This is the one whose name
becomes type = "roulette". |
| 4 | get/src/dispatch.rs:714 |
The arm mapping your config variant onto the engine variant. Steps 3 and 4 are one change split across two files. |
| 5 | get/src/py_config.rs:203 and :689 |
Optional. The Python-side variant, and its matching arm. |
| 6 | config.example.toml:173 |
Optional. A commented-out [selection] block naming your scheme:
only the scheme's own parameters, since the scope sizes itself. |
Steps 1 and 2: The Engine Side
Both are in get/src/evolver/common.rs. Add the variant, then the arm that performs
it:
// step 1: evolver/common.rs:75
Roulette { pressure: f64 },
// step 2: evolver/common.rs:159
Selection::Roulette { pressure } => {
let mut parents = Vec::with_capacity(count);
for _ in 0..count {
parents.push(spin(scope, fitnesses, *pressure, rng));
}
parents
}
Three contracts your arm must honour, none of which the compiler checks:
- Pick only from
scope. Returning a population index that is not in the scope silently ignores the[scope]section the user configured. - Compare through
rank. It orders better-first with ties broken by lower index, which is what keeps a run reproducible when two individuals score identically. Comparing the rawf64yourself reintroduces the tie. - Take every random value from
rng. Any other source of randomness breaks seeded reproducibility, and it breaks it in the way that is hardest to notice: the run still works, it just cannot be repeated.
Steps 3 and 4: The Config Side
One change, two files. The config variant, and the arm that maps it onto the engine variant:
// step 3: config.rs:112
Roulette { pressure: f64 },
// step 4: dispatch.rs:714
SelectionConfig::Roulette { pressure } => {
Selection::Roulette { pressure: *pressure }
}
If your scheme has parameters that can be wrong (a pressure that must be positive, a size that
must be at least one), constrain them in validate_evolution_and_selection, in
get/src/config.rs. Only your own parameters: there is no scheme-by-strategy check to
extend there.
Steps 5 and 6: Python, and Optional
Leaving these out costs nothing elsewhere; the scheme is then configurable from a TOML file but
absent from the Python API. Step 5 is the mirrored variant on PySelectionConfig
(get/src/py_config.rs:203) and its matching arm (:689). Write
your own parameters and no other block's: the arm is building the
[selection] table alone. Step 6 is a commented-out block in
config.example.toml:173.
Step 5 is the one nothing checks. A scheme missing its Python mirror does not fail to compile and does not fail a test; it is simply absent from the Python API, and the first person to notice is a user who cannot find it.
Testing It
Selection is unusually easy to test directly: it is a pure function from a scope, a fitness slice and an RNG to a list of indices. Assert that every returned index is in the scope, that a fixed seed gives a fixed answer, and that a population with one obviously-best member selects it more often than chance.
Step 6 is the other half of that: the commented-out block you add to
config.example.toml is parsed and validated by the test suite, so a block that does
not parse fails the suite rather than a user's afternoon.
The Other Two Axes
If what you actually want is to change who is eligible or who gets overwritten, you
want a different chain: [scope] and [replacement] are their own
sections and their own marker families:
git grep -n "ADD A SCOPE STEP" get/src config.example.toml # 6 steps, 7 sites
git grep -n "ADD A REPLACEMENT STEP" get/src # 3 steps, 6 sites
See Add a Scope and Add a Replacement Policy.