neat/mutation/add-node

Add-node mutation helpers.

This chapter owns the "split one connection into two" mechanics used by the root mutation flow when preserving node-split innovation reuse.

Add-node growth is the more identity-sensitive of the two structural mutation paths. Adding a node is not just "insert one hidden unit." The controller is trying to remember whether the exact same source-to-target split has already happened elsewhere so that later crossover and speciation can recognize the resulting structure as the same historical innovation rather than an unrelated accident.

The lifecycle in this chapter is therefore deliberate:

  1. ensure there is at least one connection worth splitting,
  2. choose one enabled connection,
  3. derive a stable split key from the source and target genes,
  4. either reuse an existing split record or assign a brand-new one,
  5. insert the new hidden node while preserving output ordering.

Read this chapter from top to bottom when debugging structural growth by connection splitting. The early helpers prepare a valid split target. The middle helpers explain how one split becomes a reusable innovation record. The final helpers explain where the new node and edges land in the genome.

flowchart TD
  Seed[Genome enters add-node path] --> Bootstrap{Connection available to split?}
  Bootstrap -->|no| Connect[Seed one input to output edge]
  Bootstrap -->|yes| Enabled[Collect enabled connections]
  Connect --> Enabled
  Enabled --> Choose[Choose one connection to split]
  Choose --> SplitKey[Build stable split descriptor]
  SplitKey --> Record{Existing split record?}
  Record -->|yes| Reuse[Reuse stored node and innovation ids]
  Record -->|no| NewRecord[Create node and assign fresh innovations]
  Reuse --> Insert[Insert hidden node and replacement edges]
  NewRecord --> Insert

neat/mutation/add-node/mutation.add-node.ts

applySplitWithExistingRecord

applySplitWithExistingRecord(
  genomeToEdit: GenomeWithMetadata,
  connectionToSplit: ConnectionWithMetadata,
  splitDescriptor: { splitKey: string; originalWeight: number; },
  splitRecord: { newNodeGeneId: number; inInnov: number; outInnov: number; },
  NodeClass: new (type: "input" | "output" | "hidden") => unknown,
): void

Apply a split using an existing innovation record.

This is the preferred path when the same structural split has already been observed elsewhere in the population history. Reusing the stored node gene id and edge innovation ids preserves historical identity, which makes later alignment-based operations treat equivalent splits as equivalent structure.

Parameters:

Returns: void

applySplitWithNewRecord

applySplitWithNewRecord(
  genomeToEdit: GenomeWithMetadata,
  connectionToSplit: ConnectionWithMetadata,
  splitDescriptor: { splitKey: string; originalWeight: number; },
  NodeClass: new (type: "input" | "output" | "hidden") => unknown,
  internal: NeatControllerForMutation,
): void

Apply a split and create a new innovation record.

This path handles genuinely novel structural growth. It inserts a fresh hidden node, assigns new innovations to the replacement edges, and records the resulting identity under the split key so future genomes can reuse it.

Parameters:

Returns: void

assignInnovationsForNewSplit

assignInnovationsForNewSplit(
  newNode: NodeWithMetadata,
  splitConnections: { incomingConnection?: ConnectionWithMetadata | undefined; outgoingConnection?: ConnectionWithMetadata | undefined; },
  internal: NeatControllerForMutation,
): { newNodeGeneId: number; inInnov: number; outInnov: number; }

Assign new innovations for a split and build the innovation record.

New split records are the durable memory that turns a one-off structural edit into reusable innovation history. This helper assigns the next global innovation ids to the replacement edges and packages those ids together with the new node gene id so later equivalent splits can be recognized quickly.

Parameters:

Returns: innovation record for the split

buildSplitDescriptor

buildSplitDescriptor(
  connectionToSplit: ConnectionWithMetadata,
): { splitKey: string; originalWeight: number; }

Build the split descriptor used for innovation lookup and connection creation.

The descriptor is the compact identity packet for a split. Its key captures which source and target genes were separated, while its preserved weight lets the outgoing replacement edge inherit the old signal strength.

Parameters:

Returns: split descriptor

chooseConnectionForSplit

chooseConnectionForSplit(
  enabledConnectionsList: ConnectionWithMetadata[],
  internal: NeatControllerForMutation,
): ConnectionWithMetadata | null

Choose a random enabled connection to split.

Once the candidate shelf is built, the split path keeps selection light: one RNG draw chooses the connection whose history may now branch into a hidden node insertion.

Parameters:

Returns: selected connection or null

collectEnabledConnections

collectEnabledConnections(
  genomeToInspect: GenomeWithMetadata,
): ConnectionWithMetadata[]

Collect all enabled connections from a genome.

Split mutations only operate on live structural edges. Disabled connections remain historical artifacts and should not become split candidates because doing so would grow new structure from topology the runtime is not currently using.

Parameters:

Returns: enabled connections list

connectSplitEdges

connectSplitEdges(
  genomeToEdit: GenomeWithMetadata,
  connectionToSplit: ConnectionWithMetadata,
  newNode: NodeWithMetadata,
  originalWeight: number,
): { incomingConnection?: ConnectionWithMetadata | undefined; outgoingConnection?: ConnectionWithMetadata | undefined; }

Create the incoming and outgoing split connections.

A split replaces one edge with two edges. The incoming edge starts with the chapter's default bootstrap weight, while the outgoing edge preserves the original connection weight so the pre-split signal can still pass forward in a comparable way.

Parameters:

Returns: incoming/outgoing connection handles

disconnectOriginalConnection

disconnectOriginalConnection(
  genomeToEdit: GenomeWithMetadata,
  connectionToRemove: ConnectionWithMetadata,
): void

Disconnect the original connection before inserting the split node.

The add-node mutation is modeled as a real split, not as a parallel bypass. Removing the original edge first preserves the intended NEAT-style topology change: the signal must now pass through the new hidden node.

Parameters:

Returns: void

ensureBootstrapConnection

ensureBootstrapConnection(
  genomeToSeed: GenomeWithMetadata,
  internal: NeatControllerForMutation,
): void

Ensure the genome has at least one connection by linking input to output.

A connection split only makes sense when a genome already has an edge to cut. This helper is the bootstrap escape hatch for extremely sparse genomes. It seeds the smallest possible forward connection so the add-node path can keep behaving like a split-based structural mutation instead of bailing out immediately.

Parameters:

Returns: void

findFirstNodeByType

findFirstNodeByType(
  genomeToSearch: GenomeWithMetadata,
  nodeType: "input" | "output" | "hidden",
): NodeWithMetadata | undefined

Find the first node of a given type.

The add-node bootstrap path only needs a minimal node lookup strategy, so this helper stays intentionally simple and deterministic.

Parameters:

Returns: the first matching node or undefined

resolveInsertIndex

resolveInsertIndex(
  genomeToEdit: GenomeWithMetadata,
  targetNode: NodeWithMetadata,
): number

Resolve the insertion index for a new node, keeping outputs at the end.

Node order matters in this codebase because output nodes are expected to stay grouped at the tail of the genome node list. This helper preserves that local invariant while still placing the new hidden node near the split target.

Parameters:

Returns: insertion index

Generated from source JSDoc • GitHub