Continuous-time recurrent neural network implementation

The default continuous-time recurrent neural network (CTRNN) implementation in neat-python is modeled as a system of ordinary differential equations, with neuron potentials as the dependent variables.

\(\tau_i \frac{d y_i}{dt} = -y_i + f_i\left(\beta_i + \sum\limits_{j \in A_i} w_{ij} y_j\right)\)

Where:

  • \(\tau_i\) is the time constant of neuron \(i\).

  • \(y_i\) is the potential of neuron \(i\).

  • \(f_i\) is the activation function of neuron \(i\).

  • \(\beta_i\) is the bias of neuron \(i\).

  • \(A_i\) is the set of indices of neurons that provide input to neuron \(i\).

  • \(w_{ij}\) is the weight of the connection from neuron \(j\) to neuron \(i\).

The time evolution of the network is computed using the exponential Euler (ETD1) method, which integrates the linear decay term exactly:

\(y_i(t+\Delta t) = e^{-\Delta t / \tau_i} \cdot y_i(t) + \left(1 - e^{-\Delta t / \tau_i}\right) \cdot z_i\)

where \(z_i = f_i\left(\beta_i + \rho_i \sum\limits_{j \in A_i} w_{ij} y_j\right)\) is the activated output and \(\rho_i\) is the response multiplier of neuron \(i\).

This method is unconditionally stable for the linear decay part regardless of the ratio \(\Delta t / \tau_i\). Forward Euler (used in versions prior to 2.0) required \(\Delta t < 2 \tau_i\) for stability, which was problematic when evolving per-node time constants — nodes with small \(\tau_i\) relative to the integration timestep would produce divergent trajectories.

Note

The exponential Euler method holds the nonlinear term \(z_i\) constant over each timestep (the same assumption as forward Euler). The accuracy advantage comes from exactly integrating the linear decay \(-y_i / \tau_i\), which is the dominant term for stiff systems where \(\tau_i \ll \Delta t\).

GPU-Accelerated Evaluation

For large populations, CTRNN evaluation can be accelerated on GPU using the optional neat.gpu module. This requires CuPy (install via pip install 'neat-python[gpu]').

The GPU evaluator uses the same exponential Euler integration method as the CPU implementation. Variable-topology genomes are packed into fixed-size padded tensors and evaluated in a single batched operation across the entire population.

from neat.gpu.evaluator import GPUCTRNNEvaluator

def input_fn(t, dt):
    """Return input values at time t. Shape: [num_inputs]."""
    return [math.sin(2 * math.pi * t), math.cos(2 * math.pi * t)]

def fitness_fn(output_trajectory):
    """Compute fitness from output trajectory. Shape: [num_steps, num_outputs]."""
    return float(output_trajectory[-1, 0])

evaluator = GPUCTRNNEvaluator(dt=0.01, t_max=1.0,
                               input_fn=input_fn, fitness_fn=fitness_fn)
winner = population.run(evaluator.evaluate, n=300)

Constraints:

  • Only sum aggregation is supported (required for batched matrix-vector multiply).

  • Supported activation functions: sigmoid, tanh, relu, identity, clamped, elu, softplus, sin, gauss, abs, square.

  • Genomes using unsupported aggregation or activation functions will raise ValueError at evaluation time.