Skip to content

Commit 56653d4

Browse files
Fix two double-buffer bugs in CTRNN advance method
Bug 1: The exponential Euler update applied decay to ovalues (the stale buffer from two steps ago) instead of ivalues (the current state). Each buffer was only updated every other step, so the decay term used state from two timesteps ago rather than one. Fix: decay * ivalues[node_key] instead of decay * ovalues[node_key] Bug 2: The return statement read from values[1 - self.active], which is the pre-update buffer (ivalues), not the just-written buffer (ovalues). This returned output lagging one step behind the actual state. Fix: return values[self.active] (the buffer that was last written to) Both bugs existed in the original forward Euler code and were carried over in the exponential Euler conversion. They were found by the GPU numerical equivalence tests, which use single-buffer in-place updates and thus don't have the double-buffer issue. Also fix test_response_parameter_effect to use small inputs/weights that avoid tanh saturation, which was masking the response difference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb26b8d commit 56653d4

File tree

3 files changed

+14
-10
lines changed

3 files changed

+14
-10
lines changed

neat/ctrnn/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ def advance(self, inputs, advance_time, time_step=None):
8080
s = ne.aggregation(node_inputs)
8181
z = ne.activation(ne.bias + ne.response * s)
8282
decay = math.exp(-dt / ne.time_constant)
83-
ovalues[node_key] = decay * ovalues[node_key] + (1.0 - decay) * z
83+
ovalues[node_key] = decay * ivalues[node_key] + (1.0 - decay) * z
8484

8585
self.time_seconds += dt
8686

87-
ovalues = self.values[1 - self.active]
87+
ovalues = self.values[self.active]
8888
return [ovalues[i] for i in self.output_nodes]
8989

9090
@staticmethod

tests/test_ctrnn.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ def test_basic_two_neuron_dynamics():
5252
# the linear decay term exactly: u(t+h) = decay*u(t) + (1-decay)*z
5353
# where decay = exp(-h/tau).
5454
reference = {
55-
0: (0.0, 0.0),
56-
100: (0.3087387628, 0.7906422519),
57-
500: (0.7965631789, 0.4505240611),
58-
1149: (0.4952237403, 0.1974657233),
59-
1249: (0.2539079366, 0.7016150055),
55+
0: (0.0108918618, 0.0268364041),
56+
100: (0.8036336977, 0.6300463990),
57+
500: (0.2062530810, 0.3471592629),
58+
1149: (0.1922239059, 0.4050205901),
59+
1249: (0.7685324686, 0.3325289787),
6060
}
6161
tol = 1e-6
6262
for idx, (exp0, exp1) in reference.items():

tests/test_gpu.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,17 @@ def test_response_parameter_effect(self):
476476
from neat.gpu._cupy_backend import evaluate_ctrnn_batch
477477

478478
config = _make_ctrnn_config()
479-
g1 = _make_simple_ctrnn_genome(config, genome_id=1, response=1.0)
480-
g2 = _make_simple_ctrnn_genome(config, genome_id=2, response=3.0)
479+
# Use small weights and inputs to avoid tanh saturation, which would
480+
# mask the effect of different response values.
481+
g1 = _make_simple_ctrnn_genome(config, genome_id=1, response=0.5,
482+
w_in1=0.3, w_in2=0.1, bias=0.0)
483+
g2 = _make_simple_ctrnn_genome(config, genome_id=2, response=3.0,
484+
w_in1=0.3, w_in2=0.1, bias=0.0)
481485
genomes = [(1, g1), (2, g2)]
482486

483487
dt = 0.005
484488
num_steps = 50
485-
inputs_np = np.tile(np.array([1.0, 0.5], dtype=np.float32),
489+
inputs_np = np.tile(np.array([0.2, 0.1], dtype=np.float32),
486490
(num_steps, 1))
487491

488492
packed = pack_ctrnn_population(genomes, config)

0 commit comments

Comments
 (0)