evonet 0.1.0.dev25__tar.gz → 0.1.0.dev26__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/PKG-INFO +1 -1
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/connection.py +47 -5
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/core.py +22 -3
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/mutation.py +3 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/serialization.py +10 -1
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/PKG-INFO +1 -1
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/pyproject.toml +1 -1
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_recurrent_dynamics.py +77 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/LICENSE +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/README.md +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/__init__.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/activation.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/core_mit_plot_simple.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/enums.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/io.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/layer.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/neuron.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/utils.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/visualize.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/SOURCES.txt +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/dependency_links.txt +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/requires.txt +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/top_level.txt +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/setup.cfg +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_activation.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_core.py +0 -0
- {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_nnet_io_and_forward.py +0 -0
|
@@ -8,7 +8,8 @@ recurrent).
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from collections import deque
|
|
12
|
+
from typing import TYPE_CHECKING, Deque, Optional
|
|
12
13
|
|
|
13
14
|
from evonet.enums import ConnectionType
|
|
14
15
|
|
|
@@ -24,9 +25,8 @@ class Connection:
|
|
|
24
25
|
source (Neuron): The source neuron (presynaptic).
|
|
25
26
|
target (Neuron): The target neuron (postsynaptic).
|
|
26
27
|
weight (float): Multiplicative weight of the transmitted signal.
|
|
27
|
-
delay (int):
|
|
28
|
-
type (ConnectionType): Type of connection (e.g. standard, recurrent
|
|
29
|
-
inhibitory).
|
|
28
|
+
delay (int): Delay in discrete time steps (used for recurrent edges).
|
|
29
|
+
type (ConnectionType): Type of connection (e.g. standard, recurrent).
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
def __init__(
|
|
@@ -44,6 +44,47 @@ class Connection:
|
|
|
44
44
|
self.delay = delay
|
|
45
45
|
self.type: ConnectionType = conn_type
|
|
46
46
|
|
|
47
|
+
self._history: Optional[Deque[float]] = None
|
|
48
|
+
if self.delay > 0:
|
|
49
|
+
self._history = deque(maxlen=self.delay)
|
|
50
|
+
|
|
51
|
+
if self.delay < 0:
|
|
52
|
+
raise ValueError("delay must be >= 0")
|
|
53
|
+
|
|
54
|
+
def push_source_output(self, value: float) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Push the current source output into the delay buffer.
|
|
57
|
+
|
|
58
|
+
This should be called once per time step after the network computed outputs.
|
|
59
|
+
"""
|
|
60
|
+
if self._history is None:
|
|
61
|
+
return
|
|
62
|
+
self._history.append(float(value))
|
|
63
|
+
|
|
64
|
+
def delayed_source_output(self) -> float:
|
|
65
|
+
"""
|
|
66
|
+
Return the delayed source output.
|
|
67
|
+
|
|
68
|
+
Semantics:
|
|
69
|
+
- delay == 0 -> no delay buffer used (caller decides what to do)
|
|
70
|
+
- delay > 0 -> returns output[t-delay] if available else 0.0
|
|
71
|
+
"""
|
|
72
|
+
if self.delay == 0:
|
|
73
|
+
return 0.0
|
|
74
|
+
|
|
75
|
+
if self._history is None:
|
|
76
|
+
return 0.0
|
|
77
|
+
|
|
78
|
+
if len(self._history) < self.delay:
|
|
79
|
+
return 0.0
|
|
80
|
+
|
|
81
|
+
return float(self._history[0])
|
|
82
|
+
|
|
83
|
+
def reset_buffer(self) -> None:
|
|
84
|
+
"""Clear the internal delay history buffer."""
|
|
85
|
+
if self._history is not None:
|
|
86
|
+
self._history.clear()
|
|
87
|
+
|
|
47
88
|
def get_signal(self) -> float:
|
|
48
89
|
"""
|
|
49
90
|
Return the weighted signal from the source neuron.
|
|
@@ -59,12 +100,13 @@ class Connection:
|
|
|
59
100
|
Return a concise string representation of the connection.
|
|
60
101
|
|
|
61
102
|
Example:
|
|
62
|
-
<Conn abc123 -> def456 w=0.85 type=standard>
|
|
103
|
+
<Conn abc123 -> def456 w=0.85 d=3 type=standard>
|
|
63
104
|
"""
|
|
64
105
|
|
|
65
106
|
type_str = self.type.name.lower()
|
|
66
107
|
return (
|
|
67
108
|
f"<Conn {self.source.id[:6]} "
|
|
68
109
|
f"-> {self.target.id[:6]} "
|
|
110
|
+
f"d={self.delay} "
|
|
69
111
|
f"w={self.weight:.2f} type={type_str}>"
|
|
70
112
|
)
|
|
@@ -318,7 +318,7 @@ class Nnet:
|
|
|
318
318
|
# --------------------------------------------------------------
|
|
319
319
|
# Create connections
|
|
320
320
|
# --------------------------------------------------------------
|
|
321
|
-
for src, dst, ctype in
|
|
321
|
+
for src, dst, ctype in sampled:
|
|
322
322
|
self.add_connection(src, dst, weight=weight, conn_type=ctype)
|
|
323
323
|
|
|
324
324
|
return new_neurons
|
|
@@ -329,6 +329,7 @@ class Nnet:
|
|
|
329
329
|
target: Neuron,
|
|
330
330
|
weight: float | None = None,
|
|
331
331
|
conn_type: ConnectionType = ConnectionType.STANDARD,
|
|
332
|
+
delay: int = 0,
|
|
332
333
|
) -> Connection | None:
|
|
333
334
|
"""
|
|
334
335
|
Create a directed connection between two neurons.
|
|
@@ -338,6 +339,7 @@ class Nnet:
|
|
|
338
339
|
target (Neuron): Target neuron.
|
|
339
340
|
weight (float | None): Initial weight. If None, 0.0 is used.
|
|
340
341
|
conn_type (ConnectionType): Type of connection (e.g. standard, recurrent).
|
|
342
|
+
delay (int): Delay in discrete time steps.
|
|
341
343
|
|
|
342
344
|
Returns:
|
|
343
345
|
Connection: Added Connection.
|
|
@@ -349,7 +351,9 @@ class Nnet:
|
|
|
349
351
|
if weight is None:
|
|
350
352
|
weight = 0.0
|
|
351
353
|
|
|
352
|
-
conn = Connection(
|
|
354
|
+
conn = Connection(
|
|
355
|
+
source, target, weight=weight, conn_type=conn_type, delay=delay
|
|
356
|
+
)
|
|
353
357
|
source.outgoing.append(conn)
|
|
354
358
|
target.incoming.append(conn)
|
|
355
359
|
|
|
@@ -361,6 +365,11 @@ class Nnet:
|
|
|
361
365
|
for neuron in layer.neurons:
|
|
362
366
|
neuron.reset(full=full)
|
|
363
367
|
|
|
368
|
+
if full:
|
|
369
|
+
# Clear delayed history buffers on connections.
|
|
370
|
+
for conn in self.get_all_connections():
|
|
371
|
+
conn.reset_buffer()
|
|
372
|
+
|
|
364
373
|
def calc(self, input_values: list[float]) -> list[float]:
|
|
365
374
|
"""
|
|
366
375
|
Perform a forward pass through the network.
|
|
@@ -393,7 +402,12 @@ class Nnet:
|
|
|
393
402
|
for n in layer.neurons:
|
|
394
403
|
for c in n.incoming:
|
|
395
404
|
if c.type is ConnectionType.RECURRENT:
|
|
396
|
-
|
|
405
|
+
if c.delay > 0:
|
|
406
|
+
# Use delayed history if configured
|
|
407
|
+
n.input += c.weight * c.delayed_source_output()
|
|
408
|
+
else:
|
|
409
|
+
# default: 1-step recurrence via last_output
|
|
410
|
+
n.input += c.weight * c.source.last_output
|
|
397
411
|
|
|
398
412
|
# Feed-forward by layers: activate first, then propagate non-recurrent edges
|
|
399
413
|
for layer in self.layers:
|
|
@@ -426,6 +440,11 @@ class Nnet:
|
|
|
426
440
|
if c.type is not ConnectionType.RECURRENT:
|
|
427
441
|
c.target.input += c.weight * n.output
|
|
428
442
|
|
|
443
|
+
# Update recurrent delay buffers once per time step.
|
|
444
|
+
for c in self.get_all_connections():
|
|
445
|
+
if c.type is ConnectionType.RECURRENT and c.delay > 0:
|
|
446
|
+
c.push_source_output(c.source.output)
|
|
447
|
+
|
|
429
448
|
return [n.output for n in self.layers[-1].neurons]
|
|
430
449
|
|
|
431
450
|
def get_all_neurons(self) -> list[Neuron]:
|
|
@@ -300,6 +300,9 @@ def add_random_neuron(
|
|
|
300
300
|
if len(net.layers) == 2:
|
|
301
301
|
net.insert_layer(1)
|
|
302
302
|
|
|
303
|
+
if dynamics_params is None:
|
|
304
|
+
dynamics_params = {}
|
|
305
|
+
|
|
303
306
|
# Choose target layer (not input, not output)
|
|
304
307
|
candidate_layers = net.layers[1:-1]
|
|
305
308
|
if not candidate_layers:
|
|
@@ -49,12 +49,15 @@ def to_dict(net: Nnet) -> dict[str, Any]:
|
|
|
49
49
|
"bias": n.bias,
|
|
50
50
|
"role": n.role.name,
|
|
51
51
|
"label": n.label,
|
|
52
|
+
"dynamics_name": n.dynamics_name,
|
|
53
|
+
"dynamics_params": n.dynamics_params,
|
|
52
54
|
"incoming": [
|
|
53
55
|
{
|
|
54
56
|
"source": c.source.id,
|
|
55
57
|
"target": c.target.id,
|
|
56
58
|
"weight": c.weight,
|
|
57
59
|
"type": c.type.name, # store enum as string
|
|
60
|
+
"delay": c.delay,
|
|
58
61
|
}
|
|
59
62
|
for c in n.incoming
|
|
60
63
|
],
|
|
@@ -81,6 +84,8 @@ def from_dict(data: dict[str, Any]) -> Nnet:
|
|
|
81
84
|
bias=n_info["bias"],
|
|
82
85
|
role=NeuronRole[n_info["role"]],
|
|
83
86
|
label=n_info.get("label", ""),
|
|
87
|
+
dynamics_name=n_info.get("dynamics_name", "standard"),
|
|
88
|
+
dynamics_params=n_info.get("dynamics_params", {}),
|
|
84
89
|
connection_init="none",
|
|
85
90
|
)[0]
|
|
86
91
|
n.id = n_info["id"]
|
|
@@ -92,11 +97,15 @@ def from_dict(data: dict[str, Any]) -> Nnet:
|
|
|
92
97
|
for c_info in n_info["incoming"]:
|
|
93
98
|
src = neuron_map[c_info["source"]]
|
|
94
99
|
dst = neuron_map[c_info["target"]]
|
|
100
|
+
delay = int(c_info.get("delay", 0))
|
|
101
|
+
conn_type = ConnectionType[c_info["type"]]
|
|
102
|
+
|
|
95
103
|
net.add_connection(
|
|
96
104
|
src,
|
|
97
105
|
dst,
|
|
98
106
|
weight=c_info["weight"],
|
|
99
|
-
conn_type=
|
|
107
|
+
conn_type=conn_type,
|
|
108
|
+
delay=delay,
|
|
100
109
|
)
|
|
101
110
|
|
|
102
111
|
return net
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import pytest
|
|
2
3
|
|
|
3
4
|
from evonet.core import Nnet
|
|
4
5
|
from evonet.enums import ConnectionType, NeuronRole
|
|
@@ -104,3 +105,79 @@ def test_recurrent_back_edge_output_to_hidden_linear() -> None:
|
|
|
104
105
|
actual_outputs.append(y)
|
|
105
106
|
|
|
106
107
|
assert np.allclose(actual_outputs, expected_outputs, atol=1e-12)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_recurrent_delay_two_steps_linear_chain() -> None:
|
|
111
|
+
"""
|
|
112
|
+
A minimal sequence test for recurrent delay buffers.
|
|
113
|
+
|
|
114
|
+
Network:
|
|
115
|
+
input -> hidden -> output
|
|
116
|
+
hidden has a recurrent self-loop with delay=2
|
|
117
|
+
|
|
118
|
+
Expected behaviour (linear activations, weights=1):
|
|
119
|
+
t0 input=1 -> hidden=1, output=1
|
|
120
|
+
t1 input=0 -> hidden=0, output=0 (delay=2 not yet filled)
|
|
121
|
+
t2 input=0 -> hidden=1, output=1 (uses hidden output from t0)
|
|
122
|
+
"""
|
|
123
|
+
net = Nnet()
|
|
124
|
+
net.add_layer() # 0 input
|
|
125
|
+
net.add_layer() # 1 hidden
|
|
126
|
+
net.add_layer() # 2 output
|
|
127
|
+
|
|
128
|
+
n_in = net.add_neuron(
|
|
129
|
+
layer_idx=0, activation="linear", role=NeuronRole.INPUT, connection_init="none"
|
|
130
|
+
)[0]
|
|
131
|
+
n_h = net.add_neuron(
|
|
132
|
+
layer_idx=1, activation="linear", role=NeuronRole.HIDDEN, connection_init="none"
|
|
133
|
+
)[0]
|
|
134
|
+
n_out = net.add_neuron(
|
|
135
|
+
layer_idx=2, activation="linear", role=NeuronRole.OUTPUT, connection_init="none"
|
|
136
|
+
)[0]
|
|
137
|
+
|
|
138
|
+
net.add_connection(n_in, n_h, weight=1.0, conn_type=ConnectionType.STANDARD)
|
|
139
|
+
net.add_connection(n_h, n_out, weight=1.0, conn_type=ConnectionType.STANDARD)
|
|
140
|
+
|
|
141
|
+
# Recurrent self-loop with delay=2
|
|
142
|
+
net.add_connection(
|
|
143
|
+
n_h, n_h, weight=1.0, conn_type=ConnectionType.RECURRENT, delay=2
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
y0 = net.calc([1.0])[0]
|
|
147
|
+
y1 = net.calc([0.0])[0]
|
|
148
|
+
y2 = net.calc([0.0])[0]
|
|
149
|
+
|
|
150
|
+
assert y0 == pytest.approx(1.0)
|
|
151
|
+
assert y1 == pytest.approx(0.0)
|
|
152
|
+
assert y2 == pytest.approx(1.0)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_reset_full_clears_delay_history() -> None:
|
|
156
|
+
"""Full reset must clear delay buffers, otherwise episodes leak memory."""
|
|
157
|
+
net = Nnet()
|
|
158
|
+
net.add_layer()
|
|
159
|
+
net.add_layer()
|
|
160
|
+
net.add_layer()
|
|
161
|
+
|
|
162
|
+
n_in = net.add_neuron(
|
|
163
|
+
layer_idx=0, activation="linear", role=NeuronRole.INPUT, connection_init="none"
|
|
164
|
+
)[0]
|
|
165
|
+
n_h = net.add_neuron(
|
|
166
|
+
layer_idx=1, activation="linear", role=NeuronRole.HIDDEN, connection_init="none"
|
|
167
|
+
)[0]
|
|
168
|
+
n_out = net.add_neuron(
|
|
169
|
+
layer_idx=2, activation="linear", role=NeuronRole.OUTPUT, connection_init="none"
|
|
170
|
+
)[0]
|
|
171
|
+
|
|
172
|
+
net.add_connection(n_in, n_h, weight=1.0, conn_type=ConnectionType.STANDARD)
|
|
173
|
+
net.add_connection(n_h, n_out, weight=1.0, conn_type=ConnectionType.STANDARD)
|
|
174
|
+
net.add_connection(
|
|
175
|
+
n_h, n_h, weight=1.0, conn_type=ConnectionType.RECURRENT, delay=2
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
_ = net.calc([1.0])[0] # fills history partially
|
|
179
|
+
net.reset(full=True)
|
|
180
|
+
|
|
181
|
+
# After full reset, history is gone -> delay contribution must be 0.0
|
|
182
|
+
y = net.calc([0.0])[0]
|
|
183
|
+
assert y == pytest.approx(0.0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|