Contribute

Extension Points

Seven things you can add, and they cost wildly different amounts. Knowing which axis you are on before you start saves the most time of anything on this page.

The Cost of Each Axis

Add a…Integration stepsBecause
Objective Seven steps, four required, but only one dispatch arm, which is what makes it the cheapest by a wide margin. In Python, no Rust at all. The objective is erased to a trait object before dispatch, so it never enters the strategy × genome match.
Genome Seven steps, six required, one of them a dispatch arm. The strategy match is generic over the genome, so evolvers are untouched.
Strategy Seven steps, three required, one of them an arm in the strategy match. The genome match is untouched, for the same reason in reverse.
Selection Scheme Six steps, four required. Selection is already an enum for exactly this reason, and it answers only "who breeds", so no strategy or genome is touched.
Crossover Operator Six steps, four required; one is a compatibility pair per genome. [crossover] is a shared section, so the operator has to declare which representations can honour it. The match is exhaustive on purpose: adding a variant stops it compiling until you do.
Mutation Operator Four steps, three required, per genome: the chain exists twice. What counts as "one mutation" differs by representation, so there is no shared enum. Adding one for edge-edit does not touch SDA.
Scope Six steps, four required. Who is eligible for one breeding event. Its parameters are its own, and that separation is why any scheme now pairs with any strategy.
Replacement Policy Three steps, the shortest chain here. Who gets overwritten, and steady-state's alone: generational rebuilds its population and never asks. A policy may draw (random does), so it shares the run's seeded stream.
validated config step 1: erase the objective Box<dyn Fitness> one arm per objective · touches nothing else step 2: strategy × genome 2 genome arms + 2 strategy arms a new genome or strategy adds one arm to one of the two not one per combination, because the second match is generic a running evolver
Erasing the objective first is what collapses dispatch from strategy × genome × objective down to strategy × genome, and it is why an objective is so much cheaper to add than anything else.

Why the Two Axes Are Not Symmetric

Genome cannot be a trait object. Its mutation and crossover methods are generic over the random number generator, crossover takes a mutable Self, cloning requires a known size, and its context is an associated type that differs per representation. Any one of those rules out dyn Genome. So strategy × genome stays a match.

Fitness has none of those problems: no generic methods, no Self in argument position, and it is thread-safe through its supertrait. So the objective is boxed before the evolver is built, and a new objective never touches dispatch at all.

Three things the boxing requires, each of which fails silently if missed. Relevant if you are working in this area rather than just adding an arm:

Two Front Doors, and Neither Is a Fallback

RouteFor
Config-driven Write a config, register a Python callable, call run. Most users, and any prototype. No Rust, no compilation.
Library Depend on get as a crate, implement Fitness for your own type, and drive an evolver directly. A hot native objective, without forking GET.

The library route needs no dispatch arm, because it does not go through dispatch. The evolver's run is generic over the objective, so a caller who already holds a concrete type instantiates the evolver with it directly. Dispatch exists to turn a config document into concrete types; a library consumer is not a config consumer, and the two doors are independent.

A user-supplied objective deliberately never gets a config variant. The alternative, a string-keyed registry that config could name, would move validation out of the parser, which is exactly the failure the single-validator design exists to prevent. Keeping user objectives out of the schema means nothing user-supplied is ever deserialized, so there is nothing new to validate.

What Dispatch Must Keep Public

Dispatch must not become the only way to construct a run. Narrowing any of these would kill the library route silently, with no compile error inside the crate:

Assembling a population and contexts by hand is genuinely awkward today, and that is a real cost of the library route, and it may deserve a builder later. It is not a reason to route user objectives through the config enum instead.

The One Rule That Is Not Optional

Score through express_and_score. The engine never calls Fitness::evaluate or evaluate_batch directly, anywhere. That single function is both the orientation point and the NaN gate, and a direct call bypasses both, producing values that compare backwards under a maximizing objective, and letting a NaN through that will take over the population. Neither failure announces itself. Any new evolution strategy scores its population through this function or it is wrong. Detail on Fitness & Output.

The Steps Are a Checklist, Not a Complete List

A marker chain names the sites every variant touches, not every site yours will. The compiler catches the exhaustive matches and nothing else, so a variant that adds something the shipped ones do not have can compile clean and still be incomplete.

The common case is a parameter. Scope::RandomSubset { size } needs size checked at load, so Config::validate_scope has an arm for it: a scope with no parameters would never have sent you there. If your variant carries a number, look for the Config::validate_* function covering its section and add the constraint, whether or not a marker mentions one.

Practical Notes for Any of Them