Skip to content

Commit 612230d

Browse files
Implement NEAT paper compliance fixes (Items A-K)
Add configurable options to match Stanley & Miikkulainen (2002) while preserving backward compatibility through defaults that match existing behavior. Changes include: - Fix 75% disable rule to replace (not layer on) inherited enabled value - Match connection genes by innovation number in distance function - Add separate excess coefficient, node gene flag, enable penalty config - Add canonical fitness sharing, proportional spawn, interspecies crossover - Add dynamic compatibility threshold adjustment - Prune dangling nodes after deletion mutations - Document complete distance formula and all new parameters All 552 tests pass (28 new). XOR smoke test verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bc0fb6d commit 612230d

13 files changed

+1176
-213
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## [Unreleased]
1010

1111
### Added
12+
- **Canonical fitness sharing option** via `fitness_sharing = canonical` in `[DefaultReproduction]` config section. Default `normalized` preserves existing behavior. (NEAT paper compliance)
13+
- **Interspecies crossover** via `interspecies_crossover_prob` parameter in `[DefaultReproduction]`. Default 0.0 (disabled). (NEAT paper compliance)
14+
- **Dynamic compatibility threshold** via `target_num_species` in `[DefaultSpeciesSet]`. Default `none` (static threshold). Related parameters: `threshold_adjust_rate`, `threshold_min`, `threshold_max`. (NEAT paper compliance)
15+
- **Separate excess gene coefficient** via `compatibility_excess_coefficient` in `[DefaultGenome]`. Defaults to `compatibility_disjoint_coefficient` (set to `auto`). (NEAT paper compliance)
16+
- **Configurable node gene distance** via `compatibility_include_node_genes` in `[DefaultGenome]`. Default True (current behavior). Set to False for canonical NEAT distance formula. (NEAT paper compliance)
17+
- **Configurable enabled-state penalty** via `compatibility_enable_penalty` in `[DefaultGenome]`. Default 1.0 (current behavior). Set to 0.0 for canonical NEAT distance formula. (NEAT paper compliance)
18+
- **Canonical spawn allocation** via `spawn_method = proportional` in `[DefaultReproduction]`. Default `smoothed` (current behavior). (NEAT paper compliance)
19+
20+
### Changed
21+
- **Distance function now matches genes by innovation number**, consistent with crossover behavior. Previously used tuple keys (endpoint pairs). This affects speciation when the same connection endpoints receive different innovation numbers in different generations (uncommon but possible). (NEAT paper compliance)
22+
- **Dangling nodes are now pruned** after `mutate_delete_node` and `mutate_delete_connection`. Hidden nodes that become disconnected from all outputs are automatically removed along with their connections. This reduces structural bloat in long evolution runs.
23+
24+
### Fixed
25+
- **75% disable rule** now matches the NEAT paper specification (Stanley & Miikkulainen, 2002, p. 111). Previously, the rule was applied after random attribute inheritance, producing an effective ~87.5% disable rate. Now correctly produces 75%. This may affect evolution dynamics in existing configurations.
26+
1227
- **GPU-accelerated evaluation** for CTRNN and Izhikevich spiking networks via optional CuPy dependency
1328
- `GPUCTRNNEvaluator` and `GPUIZNNEvaluator` in `neat.gpu.evaluator`
1429
- Batch-evaluates entire populations on GPU using padded tensor operations

docs/academic_research.rst

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ neat-python correctly implements the most complex and critical aspects of the NE
3131
* **Structural mutations** (add-node and add-connection) following standard NEAT practice
3232
* **75% disable rule** during crossover (with implementation note below)
3333

34-
However, neat-python contains several implementation choices that differ from the original 2002 NEAT paper. These design decisions that create a NEAT variant with different evolutionary dynamics. Researchers must understand these differences when publishing results.
34+
However, neat-python contains several implementation choices that differ from the original 2002 NEAT paper. Many of these differences are now configurable, allowing researchers to choose between neat-python's default behavior and canonical NEAT behavior.
3535

3636
Key Implementation Differences
3737
-------------------------------
@@ -41,7 +41,7 @@ High-Impact Differences
4141

4242
**1. Fitness Sharing Mechanism**
4343

44-
The most significant deviation affects selection pressure and speciation dynamics.
44+
The default behavior uses normalized fitness sharing, which differs from canonical NEAT.
4545

4646
*Canonical NEAT:*
4747

@@ -50,41 +50,60 @@ The most significant deviation affects selection pressure and speciation dynamic
5050
adjusted_fitness = raw_fitness / species_size
5151
# offspring_allocation proportional to sum(adjusted_fitness per species)
5252
53-
*neat-python implementation:*
53+
*neat-python default (normalized):*
5454

5555
.. code-block:: python
5656
57-
# From neat/reproduction.py
58-
# Species fitness is normalized to [0,1] based on population min/max
59-
af = (msf - min_fitness) / fitness_range
57+
af = (msf - min_fitness) / fitness_range # Normalized to [0, 1]
6058
61-
**Impact:** This creates rank-like selection pressure that scales with fitness variance. In low-variance populations, near-optimal species can be effectively eliminated. Species with many members are not penalized as in canonical NEAT, fundamentally altering the "protection of innovation" mechanism central to NEAT's design.
59+
**Impact:** Normalized sharing creates rank-like selection pressure. In low-variance populations, near-optimal species can be effectively eliminated.
6260

63-
**Example:** If Species A has fitness 100 and Species B has fitness 101, canonical NEAT allocates offspring nearly equally (~50/50), while neat-python gives Species B 100% of offspring (minus minimums), reducing Species A to minimum size.
61+
**Configurable:** Set ``fitness_sharing = canonical`` in ``[DefaultReproduction]`` for paper-faithful behavior. Default ``normalized`` preserves existing behavior.
6462

6563
**2. Genomic Distance Metric**
6664

67-
The distance metric used for speciation differs in three ways from the canonical formula δ = c₁·E/N + c₂·D/N + c₃·W̄:
65+
The distance metric used for speciation is now configurable to match the canonical formula δ = c₁·E/N + c₂·D/N + c₃·W̄.
6866

69-
1. Node genes are included in the distance calculation (normalized by max node count)
70-
2. Enabled/disabled state mismatch adds a +1 penalty per connection
71-
3. Per-connection distance combines weight differences with enabled state and disjoint penalties
67+
Connection genes are matched by **innovation number** (consistent with crossover). The full distance formula is:
7268

73-
*Location:* ``neat/genome.py`` lines 520-561, ``neat/genes.py`` lines 151-155
69+
.. math::
7470
75-
**Impact:** Speciation boundaries differ from canonical NEAT, structural mutations may be penalized twice, and cluster dynamics differ even with identical hyperparameters. Direct comparison to published NEAT results becomes problematic.
71+
\delta = \delta_{\text{nodes}} + \delta_{\text{connections}}
7672
77-
**3. Disable Rule Implementation**
73+
**Connection gene distance:**
7874

79-
The 75% disable rule is applied AFTER random attribute inheritance rather than INSTEAD of it, resulting in an effective disable rate of approximately 87.5% when one parent has the gene disabled:
75+
.. math::
8076
81-
.. code-block:: python
77+
\delta_{\text{connections}} = \frac{c_2 \cdot D + c_1 \cdot E + \sum_{i \in M} d_i}{N_c}
78+
79+
Where:
80+
81+
- *D* = number of disjoint connection genes (innovation within the other genome's range)
82+
- *E* = number of excess connection genes (innovation beyond the other genome's range)
83+
- *M* = set of homologous (matching) connection gene pairs
84+
- *d_i* = (|w₁ - w₂| + p · [e₁ ≠ e₂]) · c₃ for each matching pair, where *p* is ``compatibility_enable_penalty`` (default 1.0)
85+
- *N_c* = max number of connection genes in either genome
86+
- *c₁* = ``compatibility_excess_coefficient`` (defaults to ``compatibility_disjoint_coefficient`` if set to ``auto``)
87+
- *c₂* = ``compatibility_disjoint_coefficient``
88+
- *c₃* = ``compatibility_weight_coefficient``
89+
90+
**Node gene distance** (when ``compatibility_include_node_genes`` is True, the default):
91+
92+
.. math::
93+
94+
\delta_{\text{nodes}} = \frac{c_2 \cdot D_n + \sum_{i \in M_n} d_i^{(n)}}{N_n}
8295
83-
# Probability analysis:
84-
# Random inheritance: 50% chance of inheriting disabled state
85-
# Then 75% chance of disabling: 0.5 + (0.5 × 0.75) = 0.875
96+
Where node distance *d_i^(n)* includes bias difference, response difference, time_constant difference, and +1 penalties for activation and aggregation function mismatches, all scaled by c₃.
8697

87-
**Impact:** This reduces re-enabling of previously disabled connections compared to canonical NEAT and may affect network growth patterns and structural evolution.
98+
**To approximate canonical NEAT distance:**
99+
100+
- Set ``compatibility_include_node_genes = False``
101+
- Set ``compatibility_enable_penalty = 0.0``
102+
- Set ``compatibility_excess_coefficient`` explicitly if c₁ ≠ c₂
103+
104+
**3. Disable Rule Implementation**
105+
106+
The 75% disable rule now correctly matches the paper: when either parent has a gene disabled, the offspring's enabled state is determined by a fresh 75/25 coin flip (75% disabled, 25% enabled), replacing whatever value was randomly inherited.
88107

89108
Medium-Impact Differences
90109
^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -104,42 +123,43 @@ The original NEAT paper assumed fixed sigmoid activation and summation. This sig
104123

105124
neat-python uses "best-fit" clustering (place genome in species with closest representative) rather than the original "first-fit" approach (place in first compatible species). This is generally considered an improvement for stability and is less order-dependent.
106125

107-
**6. Static Compatibility Threshold**
126+
**6. Dynamic Compatibility Threshold**
108127

109-
The compatibility threshold for speciation is fixed rather than dynamically adjusted to maintain target species counts. This can cause species counts to drift over time.
128+
By default the compatibility threshold for speciation is fixed. Set ``target_num_species`` in ``[DefaultSpeciesSet]`` to enable dynamic adjustment toward a target species count (as described in the paper). Configure ``threshold_adjust_rate``, ``threshold_min``, and ``threshold_max`` to control the adjustment behavior.
110129

111130
**7. Initial Connectivity**
112131

113132
Multiple initialization topologies are available (``unconnected``, ``fs_neat_nohidden``, ``fs_neat``, ``full``, ``partial``, etc.). Canonical NEAT starts with minimal structure. Initial topology significantly affects convergence and solution quality.
114133

115134
**8. Spawn Allocation Smoothing**
116135

117-
Offspring counts are smoothed relative to previous species sizes with exact matching routines enforcing per-species minimums. This improves stability in practice but deviates from the paper.
136+
By default, offspring counts are smoothed relative to previous species sizes. Set ``spawn_method = proportional`` in ``[DefaultReproduction]`` for direct proportional allocation (canonical NEAT). Default ``smoothed`` preserves existing behavior.
118137

119138
Recommendations for Academic Use
120139
---------------------------------
121140

122141
For Strict NEAT Replication Studies
123142
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
124143

125-
If your research requires exact canonical NEAT behavior for comparison with other implementations or replication of published results:
144+
If your research requires exact canonical NEAT behavior for comparison with other implementations or replication of published results, all major differences are now configurable:
126145

127-
**1. Modify fitness sharing in reproduction logic**
146+
**1. Use canonical fitness sharing and spawn allocation**
128147

129-
The ``neat/reproduction.py`` module (lines 176-225) needs modification to implement canonical fitness sharing without normalization.
148+
Set ``fitness_sharing = canonical`` and ``spawn_method = proportional`` in ``[DefaultReproduction]``.
130149

131-
**2. Modify the distance metric**
150+
**2. Configure the distance metric for canonical NEAT**
132151

133-
Adjust ``neat/genome.py`` to match the canonical formula:
152+
Set ``compatibility_include_node_genes = False`` and ``compatibility_enable_penalty = 0.0`` in ``[DefaultGenome]``. Optionally set ``compatibility_excess_coefficient`` if c₁ ≠ c₂.
134153

135-
* Remove node gene contributions
136-
* Remove enabled/disabled penalty
137-
* Compute W̄ as pure average weight difference on matching connections
138-
* Expose c₁, c₂, c₃ as separate configuration parameters
154+
**3. Enable dynamic compatibility threshold**
139155

140-
**3. Configure for canonical search space**
156+
Set ``target_num_species`` in ``[DefaultSpeciesSet]`` to maintain a target species count.
141157

142-
Lock configuration to canonical NEAT parameters:
158+
**4. Enable interspecies crossover**
159+
160+
Set ``interspecies_crossover_prob`` to a small value (e.g., 0.001) in ``[DefaultReproduction]``.
161+
162+
**5. Lock activation and aggregation to canonical defaults**
143163

144164
.. code-block:: ini
145165
@@ -152,13 +172,7 @@ Lock configuration to canonical NEAT parameters:
152172
initial_connection = unconnected
153173
feed_forward = True
154174
155-
**4. Consider implementing dynamic compatibility threshold**
156-
157-
For species count stabilization, add threshold adjustment mechanisms.
158-
159-
**5. Run sensitivity analysis**
160-
161-
Evaluate the impact of the disable rate difference (75% vs 87.5%).
175+
See the "Configuration for Canonical Behavior" section below for a complete template.
162176

163177
For General NEAT-Variant Research
164178
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -209,7 +223,7 @@ Provide complete, runnable code including the specific neat-python version and a
209223
Configuration for Canonical Behavior
210224
-------------------------------------
211225

212-
If you need behavior closer to canonical NEAT without code modification, use this configuration template as a starting point:
226+
neat-python now supports canonical NEAT behavior through configuration alone. Use this template as a starting point:
213227

214228
.. code-block:: ini
215229
@@ -224,21 +238,25 @@ If you need behavior closer to canonical NEAT without code modification, use thi
224238
activation_default = sigmoid
225239
activation_mutate_rate = 0.0
226240
activation_options = sigmoid
227-
241+
228242
aggregation_default = sum
229243
aggregation_mutate_rate = 0.0
230244
aggregation_options = sum
231-
245+
232246
# Disable response evolution
233247
response_mutate_rate = 0.0
234248
response_replace_rate = 0.0
235-
249+
236250
# Minimal initial connectivity
237251
initial_connection = unconnected
238-
252+
239253
# Standard NEAT topology
240254
feed_forward = True
241-
255+
256+
# Canonical distance metric: no node genes, no enable penalty
257+
compatibility_include_node_genes = False
258+
compatibility_enable_penalty = 0.0
259+
242260
# Configure other mutation rates as needed for your problem
243261
bias_mutate_rate = 0.7
244262
bias_replace_rate = 0.1
@@ -251,17 +269,19 @@ If you need behavior closer to canonical NEAT without code modification, use thi
251269
252270
[DefaultSpeciesSet]
253271
compatibility_threshold = 3.0
272+
target_num_species = 10
254273
255274
[DefaultStagnation]
256275
species_fitness_func = max
257276
max_stagnation = 20
258277
species_elitism = 2
259278
260279
[DefaultReproduction]
261-
elitism = 2
262-
survival_threshold = 0.2
263-
264-
Note that even with this configuration, fitness sharing and distance metric differences remain. Complete canonical behavior requires code modification.
280+
elitism = 2
281+
survival_threshold = 0.2
282+
fitness_sharing = canonical
283+
spawn_method = proportional
284+
interspecies_crossover_prob = 0.001
265285
266286
Working with Deterministic Evolution
267287
-------------------------------------
@@ -380,13 +400,13 @@ See :doc:`customization` for guidance on extending the library.
380400
Summary
381401
-------
382402

383-
neat-python is a robust, well-maintained NEAT implementation suitable for serious academic research when used with awareness of its characteristics. The library correctly implements the most critical algorithmic components (innovation tracking, crossover, structural mutations) but differs from canonical NEAT in fitness sharing and distance calculation.
403+
neat-python is a robust, well-maintained NEAT implementation suitable for serious academic research. The library correctly implements the critical algorithmic components (innovation tracking, crossover, structural mutations) and now offers configuration options for canonical NEAT behavior.
384404

385-
**For canonical NEAT replication:** Code modifications and careful configuration are necessary.
405+
**For canonical NEAT replication:** All major differences from the paper are configurable without code modification. See the configuration template above.
386406

387-
**For NEAT-variant research:** The library is excellent as-is, provided implementation choices are documented in publications.
407+
**For NEAT-variant research:** The library is excellent with default settings, provided implementation choices are documented in publications.
388408

389-
**For all research:** Understanding these differences enables informed use and appropriate interpretation of results. The pure Python implementation and comprehensive test suite make verification and modification straightforward—a significant advantage for academic research.
409+
**For all research:** The pure Python implementation and comprehensive test suite make verification and modification straightforward—a significant advantage for academic research.
390410

391411
References
392412
----------

0 commit comments

Comments
 (0)