Skip to content

Latest commit

 

History

History
189 lines (132 loc) · 14.8 KB

File metadata and controls

189 lines (132 loc) · 14.8 KB

Progress Log — 2026-03-02

Port Lorenz CTRNN example from Julia NeatEvolution to neat-python

Summary

Ported the Lorenz attractor CTRNN prediction example from the Julia NeatEvolution library to neat-python, then ran the full experimental protocol (6 conditions × 6 trials) and wrote up results.

Files Created

  • examples/lorenz-ctrnn/evolve_lorenz_ctrnn.py — Main script, faithful port of Julia lorenz_ctrnn.jl
  • examples/lorenz-ctrnn/config-ctrnn — NEAT config in INI format, adapted from Julia TOML
  • examples/lorenz-ctrnn/README.md — Example documentation
  • NEAT-PYTHON-LORENZ.md — Full experimental writeup (counterpart to ~/NEAT/NEAT-LORENZ.md)

Key Design Decisions

Fixed time constant (tau=1.0): neat-python's CTRNN uses a single time constant for all nodes, passed at network creation time. The Julia version evolves per-node time constants in [0.01, 5.0]. Used the Julia initialization mean (1.0) as the fixed value. Alternatives considered: (a) modifying neat-python's CTRNN to support per-node time constants — rejected as out of scope for a port; (b) using RecurrentNetwork instead — rejected because the task specifically calls for CTRNN; (c) different fixed values — 1.0 is the best compromise for the Lorenz system's timescales.

Parallel evaluation: Added --workers flag using neat.ParallelEvaluator since the user explicitly requested it. Module-level globals for training data work on Linux (fork-based multiprocessing). The Julia version is single-threaded.

Config overrides via temp file: For mode-dependent config changes (num_inputs, num_outputs, aggregation_options), the script reads the base config with configparser, modifies values, writes a temp file, and loads it with neat.Config. This matches the Julia approach of round-tripping through a temp TOML file.

Data pipeline in pure Python: No numpy dependency, consistent with neat-python's zero-dependency design. Lists of lists for data storage. Data is small enough (800 × 6 values) that this has negligible performance impact.

Experimental Results

Ran the same protocol as the Julia experiment: 3 input modes (base, products, product-agg) × 2 output configs (3-output, z-only) × 6 trials each.

Performance is substantially worse than Julia across all conditions due to the fixed time constant:

  • x correlation: 0.49–0.71 (Python) vs 0.84–0.96 (Julia)
  • z-only base: 0.32–0.44 vs 0.83–0.86
  • z-only products: 0.51–0.61 vs 0.94

Qualitative findings from Julia still hold:

  • Fitness dilution confirmed: z-only always outperforms 3-output z
  • Pre-computed products help: z-only products (0.51–0.61) > base (0.32–0.44)

New finding: product-agg fails in Python. z-only product-agg (0.29–0.40) is worse than base (0.32–0.44), opposite of Julia where it matched products exactly (0.94). Analysis: product aggregation's optimization difficulty requires per-node time constants as a compensating mechanism. Without them, the additional search-space complexity is a net negative.

Intermittent OverflowError: ~1 in 9 product-agg runs crash with float64 overflow in neat-python's variance calculation when genomes with extremely poor fitness cause (v - mean)^2 to overflow. Pre-existing neat-python issue, not introduced by this port.

Next Steps

  • Consider adding per-node time constant support to neat-python's CTRNN (separate effort) Done — see below.
  • The product-agg failure result extends the Julia findings and is worth investigating further

Per-node time constants for CTRNN + Version 2.0.0

Summary

Added time_constant as an evolvable per-node attribute of DefaultNodeGene, removed the fixed time_constant parameter from CTRNN.create(), and bumped the version to 2.0.0. This was the dominant bottleneck identified in the Lorenz CTRNN experiment — the fixed time constant degraded correlation by ~2x and MSE by 2-3x compared to the Julia NeatEvolution implementation, which evolves per-node time constants.

Files Modified

Core library (4 files):

  • neat/config.py — Relaxed ConfigParameter.interpret() to return self.default when a parameter has a non-None default but is missing from config, instead of raising RuntimeError. This allows the new time_constant attribute to have inert defaults that are used silently by feedforward/recurrent configs.
  • neat/genes.py — Added FloatAttribute('time_constant', init_mean=1.0, init_stdev=0.0, mutate_rate=0.0, ...) to DefaultNodeGene._gene_attributes; updated distance() to include abs(self.time_constant - other.time_constant).
  • neat/ctrnn/__init__.pyCTRNN.create(genome, config) signature changed (removed time_constant param); now reads node.time_constant from each genome node gene.
  • neat/__init__.py — Version bumped to '2.0.0'.

Build config: pyproject.toml — version bumped to "2.0.0".

Tests (4 files):

  • tests/test_ctrnn.py — Set time_constant on hand-built nodes, removed third arg from create(), assertions now compare against node.time_constant.
  • tests/test_simple_run.py — Removed third arg from 2 CTRNN.create() calls.
  • tests/test_export.py — Removed time_constant=1.0 kwarg from CTRNN.create().
  • tests/test_genes.py — Added time_constant to all hand-constructed DefaultNodeGene instances (4 tests).

Examples (6 files):

  • examples/lorenz-ctrnn/config-ctrnn — Added time_constant_* params matching Julia config (init_mean=1.0, init_stdev=0.5, range [0.01, 5.0], mutate_rate=0.5).
  • examples/lorenz-ctrnn/evolve_lorenz_ctrnn.py — Removed TIME_CONSTANT constant and all references, updated CTRNN.create() calls.
  • examples/lorenz-ctrnn/README.md — Updated to reflect per-node time constants.
  • examples/single-pole-balancing/config-ctrnn — Added time_constant_* params (init_mean=0.01, preserving original fixed value).
  • examples/single-pole-balancing/evolve-ctrnn.py — Removed time_const variable, updated create() and advance() calls.
  • examples/single-pole-balancing/test-ctrnn.py — Removed third arg from CTRNN.create().

Documentation (2 files):

  • docs/module_summaries.rst — Updated CTRNN.create() signature and added versionchanged:: 2.0.0 note.
  • docs/network_export.rst — Updated CTRNN example code.

Key Design Decisions

Config defaults relaxation (v1.0 → v2.0 policy change): Adding FloatAttribute('time_constant') to DefaultNodeGene means all ~35 config files with [DefaultGenome] would need 8 new parameters — even feedforward configs that never use the time constant. Instead, ConfigParameter.interpret() was changed to return self.default silently when a parameter has a non-None default but is missing from config. Safety analysis: all existing FloatAttribute._config_items have default=None (bias, response, weight params), so this change has zero visible effect on existing configs. Only the new time_constant FloatAttribute and init_type (default='gaussian') have non-None defaults, and init_type is already present in all configs. Alternative rejected: making time_constant optional with a sentinel value — this would have required special-casing in BaseGene.init_attributes() and mutate().

Inert defaults for feedforward/recurrent: The time_constant attribute defaults are specifically chosen so it has no effect when not configured: init_stdev=0.0 means all nodes get exactly init_mean=1.0, mutate_rate=0.0 means it never changes, and equal time constants contribute zero to genetic distance. This means feedforward and recurrent configs work identically to before without any changes.

Breaking API change (v2.0.0): CTRNN.create() signature changed from create(genome, config, time_constant) to create(genome, config). Any code calling with the old signature will get a clear TypeError: create() takes 2 positional arguments but 3 were given. This is the correct versioning: a breaking public API change warrants a major version bump.

Coverage Verification

Coverage was checked before and after the change:

  • Before (v1.1.0): 96% total (2099 stmts, 87 miss)
  • After (v2.0.0): 96% total (2099 stmts, 84 miss) — 3 fewer misses
  • neat/config.py improved from 89% to 91% because the old multi-line error branch (dead code for non-None defaults) was replaced by a single return self.default that gets exercised.
  • No coverage dropped in any file.

Test Results

510 passed, 3 skipped, 0 failures.

Next Steps

  • Run the Lorenz experiment protocol (Step 11 from the plan) with per-node time constants to measure the improvement over the fixed time constant baseline Done — see below.

Lorenz CTRNN v2.0 Experiment Results

Summary

Ran the full Lorenz experiment protocol (6 conditions × 3 trials each) with per-node evolvable time constants. Wrote detailed results to NEAT-PYTHON2-LORENZ.md. The v1.0 hypothesis — that the fixed time constant was the dominant bottleneck — is confirmed: every condition improves substantially.

Overflow Fix

All 6 initial runs crashed with OverflowError: (34, 'Numerical result out of range') at the line total_se += (output[i] - target) ** 2. Root cause: with per-node time constants initialized from N(1.0, 0.5) and min_value=0.01, some nodes get small tau values. With DATA_DT=0.1 and tau=0.01, the Euler update factor dt/tau=10 drives the explicit Euler integration into instability — state values oscillate with exponentially growing amplitude, producing large finite values that overflow float64 when squared.

Fix applied to examples/lorenz-ctrnn/evolve_lorenz_ctrnn.py line 220: extended the existing NaN/Inf guard to also catch large finite values:

if any(math.isnan(v) or math.isinf(v) or abs(v) > 1e10 for v in output):
    return PENALTY_FITNESS

Alternative considered: clamping tau to ensure dt/tau < 1 (i.e., tau_min >= 0.1). Rejected because the evolutionary penalty approach is more principled — it lets evolution discover the useful range naturally without biasing the dynamics.

Key Results (v1.0 → v2.0 improvement)

Z-only mode (focused fitness signal):

Mode v1.0 z corr v2.0 z corr v1.0 MSE v2.0 MSE Julia z corr
base 0.32–0.44 0.64–0.66 0.16–0.17 0.107–0.109 0.83–0.86
products 0.51–0.61 0.78–0.84 0.15 0.056–0.073 0.94
product-agg 0.29–0.40 0.57–0.65 0.16–0.17 0.108–0.127 0.94

3-output mode (best single-variable correlation):

Mode v1.0 best corr v2.0 best corr Julia best corr
base x: 0.49–0.71 x: 0.81–0.85 x: 0.84–0.96
products x: 0.00–0.72 x/z: 0.76–0.92 x: 0.84–0.96
product-agg x: 0.54–0.71 x: 0.80–0.85 x: 0.84–0.96

Key Findings

  1. Per-node time constants improve z-only correlation by 50–80% and reduce MSE by 30–60% across all conditions.
  2. 3-output x correlation now matches Julia (v2.0: 0.81–0.92 vs Julia: 0.84–0.96), demonstrating per-node tau was the primary bottleneck for single-variable accuracy.
  3. Products z-only MSE reaches 0.056 in the best trial, within Julia's base-condition range (0.055–0.069).
  4. Product-agg partially rehabilitated: no longer worse than base (as in v1.0), but still substantially underperforms products (0.57–0.65 vs 0.78–0.84). Julia has both at 0.94.
  5. Fitness dilution still dominates in 3-output mode — every trial learns exactly one variable well (0.76–0.92) while the other two stay at zero. Per-node tau doesn't help because the bottleneck is the fitness signal, not network expressiveness.
  6. Roughly half the gap to Julia is closed. Remaining gap likely from speciation/reproduction algorithm differences and product-agg optimization difficulty.

Files Created/Modified

  • NEAT-PYTHON2-LORENZ.md — Full experimental writeup with all trial-level results, analysis, and cross-version comparison
  • examples/lorenz-ctrnn/evolve_lorenz_ctrnn.py — Added overflow guard for numerically unstable CTRNN outputs (large finite values from dt/tau >> 1)

CTRNN changes writeup and docs reorganization

Summary

Wrote CTRNN-CHANGES.md, a formal document explaining the per-node time constant change for academic users: original v1.0 behavior, the problem it caused, the v2.0 fix, numerical stability considerations, quantitative improvement, and migration guide. Generated a PDF version (CTRNN-CHANGES.pdf) via pandoc + xelatex with LaTeX math, booktabs tables, syntax-highlighted code, and running headers.

Moved all Lorenz experiment documentation from the repo root into examples/lorenz-ctrnn/docs/:

  • NEAT-PYTHON-LORENZ.md — v1.0 experiment results (moved)
  • NEAT-PYTHON2-LORENZ.md — v2.0 experiment results (moved)
  • CTRNN-CHANGES.md — formal writeup of per-node time constant change (new)
  • CTRNN-CHANGES.pdf — PDF rendering of the above (new)

Key Decisions

  • Formal tone for CTRNN-CHANGES.md: targeted at academic users who need to understand exactly what changed in the CTRNN implementation and why. Includes the state update equation in proper notation, the numerical stability constraint (dt/tau < ~1 for explicit Euler), and a migration guide with code examples.
  • PDF via pandoc + xelatex: used a separate LaTeX preamble file for fancyhdr, titlesec, booktabs styling rather than embedding complex multi-line strings in YAML front matter (which pandoc 3.1.3 struggles with).
  • Docs directory under lorenz-ctrnn example: keeps the experiment-specific documentation co-located with the example code rather than cluttering the repo root.

Test publish v2.0.0 on TestPyPI and README update

Summary

Built and published neat-python 2.0.0 to TestPyPI. Verified the install in a temporary venv: version string, CTRNN.create() signature, DefaultNodeGene attributes, and config defaults relaxation all check out. Added a "What's New in 2.0" section to README.md.

TestPyPI publish

  • Built with python3 -m build: sdist (124 KB) + wheel (53 KB)
  • twine check passed on both artifacts
  • Published to https://test.pypi.org/project/neat-python/2.0.0/
  • Test install in a fresh venv confirmed: version 2.0.0, CTRNN.create(genome, config) signature, time_constant in DefaultNodeGene attributes, config defaults relaxation returns default for missing params with non-None defaults
  • Note: setuptools emits a deprecation warning about project.license as a TOML table — should switch to a SPDX string before 2027-02-18

README update

  • Added "What's New in 2.0" section between "About" and "Features"
  • Three paragraphs: what changed (per-node evolvable time constants), the breaking API change (CTRNN.create signature), and a link to CTRNN-CHANGES.pdf for full details
  • Deliberately kept brief — the PDF has the formal writeup for academic users

Files Modified

  • README.md — added "What's New in 2.0" section