"""
Innovation tracking for NEAT.
This module implements the innovation numbering system described in:
Stanley, K. O., & Miikkulainen, R. (2002). Evolving neural networks through
augmenting topologies. Evolutionary computation, 10(2), 99-127.
From the paper (p. 108):
"Whenever a new gene appears (through structural mutation), a global innovation
number is incremented and assigned to that gene."
"By keeping a list of the innovations that occurred in the current generation,
it is possible to ensure that when the same structure arises more than once
through independent mutations in the same generation, each identical mutation
is assigned the same innovation number."
"""
[docs]
class InnovationTracker:
"""
Tracks innovation numbers for structural mutations in NEAT.
This class maintains:
1. A global counter that increments across all generations
2. A generation-specific dictionary for deduplication of identical mutations
within the same generation
Innovation numbers are assigned when:
- A new connection is added between two nodes
- A connection is split by adding a node (creates two new connections)
- Initial connections are created in the starting population
The tracker ensures that if multiple genomes independently make the same
structural mutation in one generation, they receive the same innovation number.
This enables proper gene alignment during crossover.
"""
def __init__(self, start_number=0):
"""
Initialize the innovation tracker.
Args:
start_number: The initial value for the global counter (default: 0).
The first innovation will be start_number + 1.
"""
self.global_counter = start_number
# Maps (input_node, output_node, mutation_type) -> innovation_number
# This is cleared at the start of each generation
self.generation_innovations = {}
[docs]
def get_innovation_number(self, input_node, output_node, mutation_type='add_connection'):
"""
Get or assign an innovation number for a structural mutation.
If this exact mutation (same nodes and type) has already occurred in the
current generation, returns the existing innovation number. Otherwise,
increments the global counter and assigns a new innovation number.
Args:
input_node: The input node ID for the connection
output_node: The output node ID for the connection
mutation_type: Type of mutation:
- 'add_connection': A new connection was added
- 'add_node_in': Connection from original input to new node
- 'add_node_out': Connection from new node to original output
- 'initial_connection': Connection in initial population
Returns:
int: The innovation number for this structural mutation
Example:
>>> tracker = InnovationTracker()
>>> # First genome adds connection 1->2
>>> inn1 = tracker.get_innovation_number(1, 2, 'add_connection')
>>> print(inn1)
1
>>> # Second genome also adds connection 1->2 in same generation
>>> inn2 = tracker.get_innovation_number(1, 2, 'add_connection')
>>> print(inn2)
1
>>> # Different connection gets different number
>>> inn3 = tracker.get_innovation_number(1, 3, 'add_connection')
>>> print(inn3)
2
"""
key = (input_node, output_node, mutation_type)
# Check if this innovation already occurred this generation
if key in self.generation_innovations:
return self.generation_innovations[key]
# New innovation - increment counter and record it
self.global_counter += 1
innovation_number = self.global_counter
self.generation_innovations[key] = innovation_number
return innovation_number
[docs]
def reset_generation(self):
"""
Clear generation-specific tracking at the start of a new generation.
This method should be called at the beginning of each generation's
reproduction phase. It clears the generation_innovations dictionary
but preserves the global_counter so innovation numbers never repeat.
From the paper (p. 108):
"By keeping a list of the innovations that occurred in the current
generation..."
Example:
>>> tracker = InnovationTracker()
>>> inn1 = tracker.get_innovation_number(1, 2)
>>> print(inn1)
1
>>> tracker.reset_generation() # Start new generation
>>> # Same mutation in new generation gets NEW innovation number
>>> inn2 = tracker.get_innovation_number(1, 2)
>>> print(inn2)
2
"""
self.generation_innovations.clear()
[docs]
def get_current_innovation_number(self):
"""
Get the current (most recently assigned) innovation number.
Returns:
int: The current value of the global counter
"""
return self.global_counter
def __repr__(self):
return (f"InnovationTracker(global_counter={self.global_counter}, "
f"generation_innovations={len(self.generation_innovations)} tracked)")
def __getstate__(self):
"""
Prepare tracker for pickling (checkpoint save).
Returns a dictionary containing the state to be pickled.
"""
return {
'global_counter': self.global_counter,
'generation_innovations': self.generation_innovations.copy()
}
def __setstate__(self, state):
"""
Restore tracker from pickled state (checkpoint restore).
Args:
state: Dictionary containing the pickled state
"""
self.global_counter = state['global_counter']
self.generation_innovations = state['generation_innovations']