.. index:: ! continuous-time
.. index:: recurrent
.. index:: ! ctrnn
The default :term:`continuous-time` :term:`recurrent` neural network (CTRNN) :py:mod:`implementation <ctrnn>` 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 :term:`activation function` of neuron i.
- \beta_i is the :term:`bias` of neuron i.
- A_i is the set of indices of neurons that provide input to neuron i.
- w_{ij} is the :term:`weight` of the :term:`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.
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
sumaggregation 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
ValueErrorat evaluation time.