neat/selection/core

Contracts for the bounded parent-selection mechanics chapter.

The selection core only needs a small runtime seam to answer one practical question repeatedly during evolution: given an evaluated population, which genome should parent the next child? These contracts keep that seam narrow so the mechanics chapter can focus on guard rails, strategy dispatch, and strategy-specific choice flow rather than broader controller policy.

Read the chapter in this order:

neat/selection/core/selection.types.ts

GenomeWithScore

Genome with a fitness score and arbitrary additional metadata.

The selection core deliberately narrows its view of a genome to one decisive field: the score that earlier evaluation work produced. That keeps this boundary reusable across parent selection, ordering, and summary reads without forcing the selection chapter to understand topology, telemetry, lineage, or other controller-owned metadata.

Keep the contract loose when you add new genome-side fields elsewhere in the controller. If a selection helper can still do its job from score plus opaque metadata, the extra fields belong outside this boundary.

NeatLikeWithSelection

NEAT-like instance extended with selection-specific state and helpers.

This host contract captures the minimum runtime surface the mechanics need in order to preserve stable controller assumptions.

Those assumptions are:

The result is a small, practical seam for selection behavior and tests rather than another public API surface.

SelectionContext

Shared state passed through the internal selection strategies.

This bundles the stable inputs that every strategy needs once dispatch has already decided which algorithm to run. It lets POWER, FITNESS_PROPORTIONATE, and TOURNAMENT share one context shape without each helper repeatedly pulling data off the host.

Read this as the per-selection call frame: one host, one population view, one resolved selection-options object, and one RNG source.

SelectionOptions

Selection strategy settings used by the NEAT controller.

These options are intentionally small because the public selection story is already taught at the root selection chapter and exposed through the stable Neat wrappers. The core layer only needs the knobs that alter actual parent choice mechanics: which strategy runs, how strongly POWER biases toward the front, and how TOURNAMENT sizes and probabilistic winner walks behave.

Keeping this contract narrow prevents the core chapter from turning into a second facade for broader controller policy.

neat/selection/core/selection.core.ts

Selection mechanics used by the NEAT controller.

This is the narrow mechanics layer beneath the controller-facing selection chapter. The root selection helpers answer high-level questions such as "who is the current champion?" or "which parent should breed next?" This core chapter explains how those answers stay stable.

Three responsibilities live here:

  1. evaluation guards make sure score-dependent reads do not quietly operate on an unevaluated population,
  2. ordering guards restore descending-score order before strategies that depend on best-first traversal,
  3. strategy helpers implement the distinct parent-selection flows for POWER, FITNESS_PROPORTIONATE, and TOURNAMENT.

Keeping that work in core/ preserves a clean split. The root selection chapter stays readable and controller-facing, the facade chapter keeps the stable Neat wrappers thin, and this layer owns the exact mechanics and fallback rules those higher surfaces rely on.

The exported constants also belong to that story rather than standing alone: they define the shared defaults, sentinels, and traversal anchors that keep summary reads, ordering guards, roulette scans, and tournament walks aligned with one another.

Read the chapter in this order: start with the evaluation and ordering guards, then the strategy dispatcher, then the strategy-specific helpers for sorted bias, shifted-fitness roulette, and tournament sampling.

flowchart TD
  Caller[Root selection helper or Neat facade]
  Guards[Evaluation and ordering guards]
  Dispatch[selectParentByStrategy]
  Power[POWER\nbest-first bias]
  Fitness[FITNESS_PROPORTIONATE\nshifted roulette]
  Tournament[TOURNAMENT\nsample and walk bracket]
  Parent[Chosen parent]

  Caller --> Guards
  Guards --> Dispatch
  Dispatch --> Power
  Dispatch --> Fitness
  Dispatch --> Tournament
  Power --> Parent
  Fitness --> Parent
  Tournament --> Parent

calculateFitnessTotals

calculateFitnessTotals(
  population: GenomeWithScore[],
): { totalFitness: number; minFitnessShift: number; }

Compute the total fitness and minimal shift used by roulette selection.

Fitness-proportionate selection must work even when a population contains negative scores. This helper records the most-negative value, converts that into a uniform upward shift, and returns the adjusted total that the later threshold scan uses. In other words, it prepares the roulette space so every genome still gets a measurable slice even when raw scores dip below zero.

Parameters:

Returns: Aggregate fitness totals with the negative-score shift.

calculateTotalScore

calculateTotalScore(
  population: GenomeWithScore[],
): number

Calculate the total fitness across the population.

This is the simplest "treat the generation as one pool" fold used by summary reads. Missing scores are interpreted with the shared selection fallback so total-score calculations stay aligned with the rest of the chapter. This helper is not itself a parent-selection strategy, but it lives in the same mechanics layer because the controller's summary reads should speak the same score semantics as the parent-selection pipeline.

Parameters:

Returns: Sum of all scores with missing scores treated as zero.

DEFAULT_POWER

Default power exponent for POWER selection when none is configured.

A value of 1 keeps POWER selection as a direct index-bias curve without adding extra front-loading beyond the strategy's normal rank preference. That makes this the mildest built-in pressure setting: strong enough to prefer the front of the sorted population, but not so aggressive that the champion becomes nearly inevitable on every draw.

DEFAULT_SCORE

Default score when a genome has no explicit score.

Selection uses one shared fallback so summaries, sorting, and threshold scans all interpret unevaluated or missing scores consistently. That matters for chapter coherence as much as runtime behavior: every inspection helper and parent-selection guard speaks the same "missing score" language instead of inventing its own local default.

DEFAULT_TOURNAMENT_PROBABILITY

Default tournament win probability when none is configured.

This keeps the top sampled participant favored while still allowing weaker entrants to remain reachable later in the tournament walk. Read it as the tournament counterpart to selection pressure: a balanced default that keeps the bracket competitive instead of turning every mini-tournament into a guaranteed top-seed march.

DEFAULT_TOURNAMENT_SIZE

Default tournament size when none is configured.

The built-in bracket stays intentionally small so tournament selection keeps some competitive pressure without collapsing into near-deterministic champion picks. In practice this means the default strategy samples just enough local competition to reward strong genomes while still letting non-champion genomes remain reachable.

ensurePopulationEvaluated

ensurePopulationEvaluated(
  internal: NeatLikeWithSelection,
): void

Ensure population scores exist by running evaluation if needed.

Selection treats score production as an upstream responsibility, but the controller still needs a safe guard at the point where score-dependent reads actually happen. This helper preserves that assumption: callers can ask for fittest genomes, averages, or parents without manually remembering whether evaluation already ran this generation.

Parameters:

Returns: Nothing. Evaluation is triggered only when the population is unevaluated.

ensurePopulationSortedDescending

ensurePopulationSortedDescending(
  internal: NeatLikeWithSelection,
): void

Ensure the population is sorted descending by score when out of order.

Several selection reads assume best-first order but should not pay the cost of sorting when the population is already in the expected shape. This helper preserves that cheap guard by checking only the leading edge before calling the shared in-place sort hook.

Parameters:

Returns: Nothing. Sorting only runs when the first two scores are out of order.

ensurePopulationSortedDescendingForPower

ensurePopulationSortedDescendingForPower(
  selectionContext: SelectionContext,
): void

Ensure the population is sorted descending by score for POWER selection.

POWER selection is the only built-in strategy that depends on the population already being rank-ordered before index sampling. The narrower guard lives here instead of in the root chapter so the strategy can preserve that rule without forcing unrelated selection paths to sort first.

Parameters:

Returns: Nothing. Sorting only runs when the first two entries are out of order.

FIRST_INDEX

First element index used by guards, fallbacks, and best-first reads.

Selection logic names this index explicitly because the front of the population has semantic meaning: it is where champion reads and sorted-bias strategies begin.

getRandomPopulationMember

getRandomPopulationMember(
  selectionContext: SelectionContext,
): GenomeWithScore

Select a random population member using the configured RNG.

This is the small shared fallback used by roulette misses and suppressed tournament overflow. Centralizing it here keeps every selection path tied to the same controller RNG stream. It also makes the fallback semantics explicit instead of hiding ad hoc random picks inside individual strategies.

Parameters:

Returns: Randomly chosen genome from the current population.

INITIAL_CUMULATIVE_FITNESS

Initial cumulative fitness value for roulette threshold scans.

Roulette-style selection accumulates shifted fitness as it walks the population. This zero point keeps that running threshold explicit and aligned with the rest of the selection fallback semantics.

INITIAL_MOST_NEGATIVE_SCORE

Initial most-negative score sentinel for shifted-fitness scans.

FITNESS_PROPORTIONATE selection may need to lift negative scores into a usable roulette space, and this sentinel marks the baseline from which that most-negative search starts.

INITIAL_TOTAL_FITNESS

Initial accumulator value for generation-wide score folds.

Summary helpers begin from this neutral total so whole-population averages and other folds remain explicit about their starting score semantics.

LAST_ELEMENT_INDEX

Index used with at() when checking the tail of the population.

The evaluation guard only needs the final genome to answer one practical question: has this generation already been scored all the way through?

LAST_INDEX_OFFSET

Offset for retrieving the last element via length arithmetic.

This keeps tail access readable in places where explicit length math is more portable than at() for the surrounding helper shape.

LOOP_INDEX_INCREMENT

Loop step used by explicit tournament and threshold walks.

Naming the increment makes the small index-based scans read like deliberate traversal code instead of scattered magic numbers.

pickByShiftedThreshold

pickByShiftedThreshold(
  population: GenomeWithScore[],
  selectionThreshold: number,
  minFitnessShift: number,
): GenomeWithScore | undefined

Pick the first genome whose shifted cumulative fitness exceeds the threshold.

Read this as the second half of roulette selection. Once the threshold has been sampled, the helper walks the population once, expanding a cumulative shifted-fitness window until the threshold lands inside one genome's slice. This keeps the selection flow linear and deterministic for a fixed RNG draw.

Parameters:

Returns: The chosen genome when a threshold crossing occurs.

pickTournamentWinner

pickTournamentWinner(
  selectionContext: SelectionContext,
  sortedParticipants: GenomeWithScore[],
): GenomeWithScore

Select a winner from sorted tournament participants.

After participants are sorted best-first, this helper walks the list from the front and gives each participant a chance to win immediately. The configured probability therefore controls how often the top entrant wins outright versus how often weaker entrants remain reachable later in the walk. This is what makes tournament selection tunable instead of purely greedy.

Parameters:

Returns: The chosen tournament winner.

resolveTournamentOverflow

resolveTournamentOverflow(
  selectionContext: SelectionContext,
): GenomeWithScore

Resolve what happens when tournament size exceeds population size.

Oversized tournaments usually indicate a configuration mistake, so the default behavior is to fail loudly. Tests and a few tolerant call sites can opt into the host-level suppression flag when "best effort" random fallback is more useful than a hard error. That separation keeps the normal controller path strict while still leaving a narrow escape hatch for compatibility scenarios.

Parameters:

Returns: A fallback parent genome.

sampleTournamentParticipants

sampleTournamentParticipants(
  selectionContext: SelectionContext,
  tournamentSize: number,
): GenomeWithScore[]

Sample tournament participants with possible repeats.

Repeats are allowed because this helper models independent random draws from the current population rather than a unique bracket seeding pass. That keeps the implementation small and preserves the controller's existing stochastic behavior. The result is a lightweight temporary bracket, not a durable roster object.

Parameters:

Returns: Sampled participants.

SECOND_INDEX

Second element index used by the cheap leading-edge ordering guard.

Comparing the first two genomes is enough for the root helpers' fast "probably already sorted" check, so this constant marks the smallest useful comparison boundary.

selectParentByFitnessProportionate

selectParentByFitnessProportionate(
  selectionContext: SelectionContext,
): GenomeWithScore

Select a parent using roulette-wheel fitness proportionate selection.

This path turns the current population into a weighted threshold scan. When some genomes have negative scores, the helper first shifts the whole fitness space upward so every participant still occupies a non-negative span on the roulette wheel. That makes this strategy the most score-sensitive branch in the chapter: it reacts to relative score magnitude rather than only rank or sampled bracket ordering.

Parameters:

Returns: The chosen parent genome.

selectParentByPower

selectParentByPower(
  selectionContext: SelectionContext,
): GenomeWithScore

Select a parent by power-law distribution on the sorted population.

POWER selection assumes best-first ordering and then biases random choice toward the front of that ranking. Lower random samples stay near the leading genomes, while the configured exponent controls how quickly the chance falls away from the champion. Read this as the lightest built-in strategy: it does not inspect absolute score gaps, only the current descending order.

Parameters:

Returns: The chosen parent genome.

selectParentByStrategy

selectParentByStrategy(
  internal: NeatLikeWithSelection,
): GenomeWithScore

Select a parent genome according to the configured selection strategy.

This is the single dispatch point shared by the controller-facing selection helpers. It resolves the active strategy once, builds a compact {@link SelectionContext}, and then hands off to the concrete algorithm.

The fallback to the first population entry is deliberate. When callers have configured an unknown strategy name, selection still returns a deterministic candidate instead of failing unexpectedly deep inside crossover or mutation flow.

Parameters:

Returns: A genome chosen according to the active selection strategy.

Example:

const parent = selectParentByStrategy(neat);
const strategyName = neat.options.selection?.name;

selectParentByTournament

selectParentByTournament(
  selectionContext: SelectionContext,
): GenomeWithScore

Select a parent by tournament selection.

Tournament selection samples a temporary bracket, orders it by descending score, then walks from strongest to weakest using the configured win probability. This gives the controller a middle ground between pure best-first bias and fully score-proportional roulette. The sampled bracket is intentionally local: the strategy asks "who wins this small contest?" rather than "how does the whole population distribute weight?"

Parameters:

Returns: The chosen parent genome.

neat/selection/core/selection.core.errors.ts

Raised when tournament selection is asked to sample more entries than exist.

SelectionTournamentOverflowError

Raised when tournament selection is asked to sample more entries than exist.

Generated from source JSDoc • GitHub