neat/mutation

Root orchestration for NEAT mutation operations.

Mutation in NEAT: More Than Weight Perturbation

In a fixed-topology network, mutation only adjusts weights. NEAT expands this to include structural mutations — adding nodes by splitting existing connections and adding direct connections between previously unlinked nodes. These structural changes are the engine of topology evolution.

The challenge structural mutation introduces is alignment: when two genomes with different topologies produce offspring, which genes should be crossed over? NEAT solves this with innovation numbers — every new gene (node or connection) that appears anywhere in the population during a generation receives a globally unique innovation number. When a node-split mutation fires, instead of inventing a new id, the controller first checks whether the same split was already performed by another genome this generation. If so, both genomes reuse the same innovation number. This keeps crossover alignment correct even across topologically diverse parents. See Stanley and Miikkulainen, Evolving Neural Networks through Augmenting Topologies, for the historical-markings mechanism and its role in enabling meaningful crossover between structurally different genomes.

Operator Families

Mutation operators fall into two broad families:

What This Boundary Owns

This root file answers four controller-facing questions:

The helper folders own the narrower mechanics:

A practical reading order:

  1. mutate() — where the whole-population pass begins,
  2. selectMutationMethod() — how one operator is resolved per genome,
  3. mutateAddNodeReuse() and mutateAddConnReuse() — the two structural-growth paths,
  4. ensureMinHiddenNodes() and ensureNoDeadEnds() — topology repair.
flowchart TD
  classDef base fill:#001522,stroke:#0fb5ff,color:#9fdcff,stroke-width:1.5px;
  classDef accent fill:#0f1f33,stroke:#00e5ff,color:#d8f6ff,stroke-width:2px;
  classDef op fill:#001522,stroke:#ff9a2e,color:#ffe6cc,stroke-width:1.5px;

  Start["Generation ready for structure edits"]:::accent --> Mutate["mutate()\nwalk every genome"]:::base
  Mutate --> Select["selectMutationMethod()\nresolve operator for this genome"]:::base
  Select --> Operator{"Structural or parametric?"}:::accent
  Operator -->|"ADD_NODE"| AddNode["mutateAddNodeReuse()\nreuse or allocate split innovations"]:::op
  Operator -->|"ADD_CONN"| AddConn["mutateAddConnReuse()\nreuse or allocate connection innovations"]:::op
  Operator -->|"MOD_WEIGHT etc."| Param["Parametric operator\nadjust weights / bias / gating"]:::base
  Operator -->|"repair needed"| Repair["ensureMinHiddenNodes()\nensureNoDeadEnds()"]:::base
  AddNode --> Population["Genome structure updated in place"]:::accent
  AddConn --> Population
  Param --> Population
  Repair --> Population
  Population --> Bookkeeping["Innovation tables · caches · operator stats stay aligned"]:::base

neat/mutation/mutation.ts

DEFAULT_CONNECTION_WEIGHT

Default connection weight used when mutation must create a structural edge from scratch.

This keeps bootstrap connections and split in-edges deterministic at the mutation boundary before later weight mutations or evaluation passes tune the value more precisely.

DEFAULT_GENE_ID

Default gene id used when mutation needs a stable fallback for node metadata.

The value is intentionally simple because it acts as compatibility padding, not as a semantic innovation marker.

DEFAULT_INNOVATION_ID

Default innovation id used when a connection lacks recorded innovation metadata.

This fallback prevents root mutation helpers from depending on missing ids while the real innovation-tracking paths decide whether to reuse or allocate new structural records.

ensureMinHiddenNodes

ensureMinHiddenNodes(
  network: GenomeWithMetadata,
  multiplierOverride: number | undefined,
): Promise<void>

Ensure the network has a minimum number of hidden nodes and connectivity.

This repair helper runs after structural edits when the controller wants to keep a mutated genome above a minimum hidden-capacity floor. It is less about exploration than about preserving a usable topology budget so later mutation, evaluation, and selection steps do not inherit a trivially underbuilt graph.

The helper may add hidden nodes, wire missing edges, normalize feed-forward node ordering, and rebuild cached connection structures, so callers should treat it as a topology-maintenance pass rather than a tiny invariant check. Those structural edits now stay on the canonical add-node/add-connection identity paths so repair work cannot drift from the innovation tracker or the explicit topology-policy bridge.

Parameters:

Returns: Promise that resolves after hidden-node and connectivity repairs have completed.

ensureNoDeadEnds

ensureNoDeadEnds(
  network: GenomeWithMetadata,
): void

Ensure there are no dead-end nodes (input/output isolation) in the network.

Mutation can produce temporarily awkward graphs, especially after structural growth or pruning-like simplification. This repair pass reconnects stranded input, output, or hidden nodes so the genome remains a sensible candidate for later evaluation and does not carry obviously broken topology into the next controller stage. Repair connections now reuse the canonical add-connection identity path, and feed-forward runs normalize hidden/output ordering before reconnecting edges so maintenance work still respects the innovation tracker and the explicit topology-policy bridge.

Parameters:

Returns: Nothing. The network may gain repair connections in place.

mutate

mutate(): Promise<void>

Mutate every genome in the population according to configured policies.

This is the controller's whole-population mutation pass. It does not decide one fixed operator up front for the entire generation. Instead it walks the current population, lets the lower-level flow helpers resolve each genome's effective mutation policy, and then applies whatever operator mix is valid for that specific genome at that moment.

In practice, this is where several otherwise separate mutation concerns meet: adaptive per-genome rates, structural innovation reuse, optional extra structural exploration, and post-edit repair work. That is why the root mutation chapter exists at all: the controller needs one place where those per-genome edits stay coherent before later stages such as evaluation or speciation inspect the changed population.

Educational notes:

Returns: Promise that resolves after every genome has gone through the mutation flow for this pass.

Example:

// Called after evaluation and before the next generation is consumed.
neat.mutate();

mutateAddConnReuse

mutateAddConnReuse(
  genome: GenomeWithMetadata,
): void

Add a connection between two previously unconnected nodes, reusing a stable innovation id per exact directed edge when possible.

Notes on behavior:

Steps:

This is the connection-growth companion to mutateAddNodeReuse(). Its main controller value is innovation consistency: if two genomes discover the same directed structural edit across time, the mutation system tries to keep that edit comparable for later crossover and speciation rather than treating it as a completely unrelated event.

Parameters:

Returns: Nothing. The genome may gain one new connection and the controller innovation map may be consulted or extended.

mutateAddNodeReuse

mutateAddNodeReuse(
  genome: GenomeWithMetadata,
): Promise<void>

Split a randomly chosen enabled connection and insert a hidden node.

This routine attempts to reuse a historical "node split" innovation record so that genomes splitting the same historically marked connection during one mutation window share the same inserted node id and replacement connection innovations. This preservation of innovation information is important for NEAT-style speciation and genome alignment.

Use this helper when the controller wants a structural growth mutation that stays compatible with prior history. The important state change is not only the new hidden node inside one genome, but also the possible update to the controller's split-innovation table when this exact split event has never been seen before.

Method steps (high-level):

Parameters:

Returns: Promise that resolves after the split has either reused an existing innovation record or created a new one.

Example:

neat._mutateAddNodeReuse(genome);

normalizeRepairNodeOrderForFeedForward

normalizeRepairNodeOrderForFeedForward(
  network: GenomeWithMetadata,
  allowRecurrent: boolean | undefined,
): void

Normalize node ordering for feed-forward maintenance repairs.

Some legacy or bootstrap paths can leave hidden nodes after the output tail. That ordering is awkward whenever the current mutation policy is effectively feed-forward, because the mutation chapter interprets hidden-to-output repairs through node order. This helper keeps repair decisions conservative by reestablishing the standard input-hidden-output ordering before any repair shelf is evaluated when recurrent growth is not currently allowed.

Parameters:

Returns: void

selectMutationMethod

selectMutationMethod(
  genome: GenomeWithMetadata,
  rawReturnForTest: boolean,
): Promise<MutationMethod | MutationMethod[] | null>

Select a mutation method respecting structural constraints and adaptive controllers.

This is the policy gateway between "a genome is eligible for mutation" and "here is the concrete operator to apply." The selector folds together legacy fast-feed-forward behavior, phased-complexity biasing, adaptive operator weighting, structural safety checks, and optional bandit-style operator choice so the rest of the mutation flow can work with one resolved answer.

Mirrors legacy implementation from neat.ts to preserve test expectations. rawReturnForTest retains historical behavior where the full FFW array is returned for identity checks in tests.

Parameters:

Returns: Resolved mutation method, legacy FFW array for compatibility tests, or null when no operator should run.

Example:

const method = await neat.selectMutationMethod(genome, false);
// The result already reflects policy gates such as phased complexity and
// structural limits, not just a random sample from the raw configured pool.
Generated from source JSDoc • GitHub