evonet 0.1.0.dev25__tar.gz → 0.1.0.dev27__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.
Files changed (28) hide show
  1. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/PKG-INFO +1 -1
  2. evonet-0.1.0.dev27/evonet/connection.py +124 -0
  3. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/core.py +23 -3
  4. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/mutation.py +3 -0
  5. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/serialization.py +10 -1
  6. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet.egg-info/PKG-INFO +1 -1
  7. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/pyproject.toml +2 -2
  8. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/tests/test_recurrent_dynamics.py +86 -0
  9. evonet-0.1.0.dev25/evonet/connection.py +0 -70
  10. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/LICENSE +0 -0
  11. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/README.md +0 -0
  12. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/__init__.py +0 -0
  13. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/activation.py +0 -0
  14. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/core_mit_plot_simple.py +0 -0
  15. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/enums.py +0 -0
  16. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/io.py +0 -0
  17. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/layer.py +0 -0
  18. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/neuron.py +0 -0
  19. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/utils.py +0 -0
  20. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet/visualize.py +0 -0
  21. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet.egg-info/SOURCES.txt +0 -0
  22. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet.egg-info/dependency_links.txt +0 -0
  23. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet.egg-info/requires.txt +0 -0
  24. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/evonet.egg-info/top_level.txt +0 -0
  25. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/setup.cfg +0 -0
  26. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/tests/test_activation.py +0 -0
  27. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/tests/test_core.py +0 -0
  28. {evonet-0.1.0.dev25 → evonet-0.1.0.dev27}/tests/test_nnet_io_and_forward.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0.dev25
3
+ Version: 0.1.0.dev27
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -0,0 +1,124 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Connection between neurons in an evolvable neural network.
4
+
5
+ A connection links a source neuron to a target neuron and transmits a weighted signal.
6
+ Supports optional connection types for specialized behaviors (e.g. inhibitory,
7
+ recurrent).
8
+ """
9
+
10
+
11
+ from collections import deque
12
+ from typing import TYPE_CHECKING, Deque, Optional
13
+
14
+ from evonet.enums import ConnectionType
15
+
16
+ if TYPE_CHECKING:
17
+ from evonet.neuron import Neuron
18
+
19
+
20
+ class Connection:
21
+ """
22
+ Represents a directed, weighted connection between two neurons.
23
+
24
+ Attributes:
25
+ source (Neuron): The source neuron (presynaptic).
26
+ target (Neuron): The target neuron (postsynaptic).
27
+ weight (float): Multiplicative weight of the transmitted signal.
28
+ delay (int): Delay in discrete time steps (used for recurrent edges).
29
+ type (ConnectionType): Type of connection (e.g. standard, recurrent).
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ source: "Neuron",
35
+ target: "Neuron",
36
+ weight: float = 1.0,
37
+ delay: int = 0,
38
+ conn_type: ConnectionType = ConnectionType.STANDARD,
39
+ ) -> None:
40
+
41
+ if delay < 0:
42
+ raise ValueError("delay must be >= 0")
43
+
44
+ # Normalize: recurrent implies delay >= 1
45
+ if conn_type is ConnectionType.RECURRENT and delay == 0:
46
+ delay = 1
47
+
48
+ self.source = source
49
+ self.target = target
50
+ self.weight = weight
51
+ self.delay = delay
52
+ self.type = conn_type
53
+
54
+ self._history: Optional[Deque[float]] = None
55
+ if self.type is ConnectionType.RECURRENT:
56
+ self._history = deque(maxlen=self.delay)
57
+
58
+ def set_delay(self, delay: int) -> None:
59
+ if self.type is not ConnectionType.RECURRENT:
60
+ raise ValueError("set_delay is only valid for recurrent connections")
61
+ if delay <= 0:
62
+ delay = 1
63
+ self.delay = int(delay)
64
+ self._history = deque(maxlen=self.delay)
65
+
66
+ def push_source_output(self, value: float) -> None:
67
+ """
68
+ Push the current source output into the delay buffer.
69
+
70
+ This should be called once per time step after the network computed outputs.
71
+ """
72
+ if self._history is None:
73
+ return
74
+ self._history.append(float(value))
75
+
76
+ def delayed_source_output(self) -> float:
77
+ """
78
+ Return the delayed source output.
79
+
80
+ Semantics:
81
+ - delay == 0 -> no delay buffer used (caller decides what to do)
82
+ - delay > 0 -> returns output[t-delay] if available else 0.0
83
+ """
84
+ if self.delay == 0:
85
+ return 0.0
86
+
87
+ if self._history is None:
88
+ return 0.0
89
+
90
+ if len(self._history) < self.delay:
91
+ return 0.0
92
+
93
+ return float(self._history[0])
94
+
95
+ def reset_buffer(self) -> None:
96
+ """Clear the internal delay history buffer."""
97
+ if self._history is not None:
98
+ self._history.clear()
99
+
100
+ def get_signal(self) -> float:
101
+ """
102
+ Return the weighted signal from the source neuron.
103
+
104
+ Returns:
105
+ float: source.output × weight
106
+ """
107
+
108
+ return self.source.output * self.weight
109
+
110
+ def __repr__(self) -> str:
111
+ """
112
+ Return a concise string representation of the connection.
113
+
114
+ Example:
115
+ <Conn abc123 -> def456 w=0.85 d=3 type=standard>
116
+ """
117
+
118
+ type_str = self.type.name.lower()
119
+ return (
120
+ f"<Conn {self.source.id[:6]} "
121
+ f"-> {self.target.id[:6]} "
122
+ f"d={self.delay} "
123
+ f"w={self.weight:.2f} type={type_str}>"
124
+ )
@@ -318,7 +318,7 @@ class Nnet:
318
318
  # --------------------------------------------------------------
319
319
  # Create connections
320
320
  # --------------------------------------------------------------
321
- for src, dst, ctype in forced + sampled:
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,15 @@ class Nnet:
349
351
  if weight is None:
350
352
  weight = 0.0
351
353
 
352
- conn = Connection(source, target, weight=weight, conn_type=conn_type)
354
+ # Normalize recurrent delays
355
+ if conn_type is ConnectionType.RECURRENT and delay <= 0:
356
+ delay = 1
357
+ if conn_type is not ConnectionType.RECURRENT and delay < 0:
358
+ delay = 0
359
+
360
+ conn = Connection(
361
+ source, target, weight=weight, conn_type=conn_type, delay=delay
362
+ )
353
363
  source.outgoing.append(conn)
354
364
  target.incoming.append(conn)
355
365
 
@@ -361,6 +371,11 @@ class Nnet:
361
371
  for neuron in layer.neurons:
362
372
  neuron.reset(full=full)
363
373
 
374
+ if full:
375
+ # Clear delayed history buffers on connections.
376
+ for conn in self.get_all_connections():
377
+ conn.reset_buffer()
378
+
364
379
  def calc(self, input_values: list[float]) -> list[float]:
365
380
  """
366
381
  Perform a forward pass through the network.
@@ -393,7 +408,7 @@ class Nnet:
393
408
  for n in layer.neurons:
394
409
  for c in n.incoming:
395
410
  if c.type is ConnectionType.RECURRENT:
396
- c.target.input += c.weight * c.source.last_output
411
+ n.input += c.weight * c.delayed_source_output()
397
412
 
398
413
  # Feed-forward by layers: activate first, then propagate non-recurrent edges
399
414
  for layer in self.layers:
@@ -426,6 +441,11 @@ class Nnet:
426
441
  if c.type is not ConnectionType.RECURRENT:
427
442
  c.target.input += c.weight * n.output
428
443
 
444
+ # Update recurrent delay buffers once per time step.
445
+ for c in self.get_all_connections():
446
+ if c.type is ConnectionType.RECURRENT:
447
+ c.push_source_output(c.source.output)
448
+
429
449
  return [n.output for n in self.layers[-1].neurons]
430
450
 
431
451
  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=ConnectionType[c_info["type"]],
107
+ conn_type=conn_type,
108
+ delay=delay,
100
109
  )
101
110
 
102
111
  return net
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0.dev25
3
+ Version: 0.1.0.dev27
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evonet"
7
- version = "0.1.0.dev25"
7
+ version = "0.1.0.dev27"
8
8
  description = "Evolvable neural network core for integration with EvoLib"
9
9
  authors = [
10
10
  { name = "EvoLib", email = "evolib@dismail.de" }
@@ -48,7 +48,7 @@ include = ["evonet*"]
48
48
  # Tool-Konfigurationen
49
49
  [tool.black]
50
50
  line-length = 88
51
- target-version = ["py311"]
51
+ target-version = ["py312"]
52
52
 
53
53
  [tool.isort]
54
54
  profile = "black"
@@ -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,88 @@ 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)
184
+
185
+
186
+ def test_recurrent_delay_zero_normalizes_to_one() -> None:
187
+ net, n_in, n_hid, _ = _mk_linear_ih_h_o()
188
+ c = net.add_connection(
189
+ n_hid, n_hid, weight=1.0, conn_type=ConnectionType.RECURRENT, delay=0
190
+ )
191
+ assert c is not None
192
+ assert c.delay == 1
@@ -1,70 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """
3
- Connection between neurons in an evolvable neural network.
4
-
5
- A connection links a source neuron to a target neuron and transmits a weighted signal.
6
- Supports optional connection types for specialized behaviors (e.g. inhibitory,
7
- recurrent).
8
- """
9
-
10
-
11
- from typing import TYPE_CHECKING
12
-
13
- from evonet.enums import ConnectionType
14
-
15
- if TYPE_CHECKING:
16
- from evonet.neuron import Neuron
17
-
18
-
19
- class Connection:
20
- """
21
- Represents a directed, weighted connection between two neurons.
22
-
23
- Attributes:
24
- source (Neuron): The source neuron (presynaptic).
25
- target (Neuron): The target neuron (postsynaptic).
26
- weight (float): Multiplicative weight of the transmitted signal.
27
- delay (int): Optional delay in time steps (not yet implemented).
28
- type (ConnectionType): Type of connection (e.g. standard, recurrent,
29
- inhibitory).
30
- """
31
-
32
- def __init__(
33
- self,
34
- source: "Neuron",
35
- target: "Neuron",
36
- weight: float = 1.0,
37
- delay: int = 0,
38
- conn_type: ConnectionType = ConnectionType.STANDARD,
39
- ) -> None:
40
-
41
- self.source = source
42
- self.target = target
43
- self.weight = weight
44
- self.delay = delay
45
- self.type: ConnectionType = conn_type
46
-
47
- def get_signal(self) -> float:
48
- """
49
- Return the weighted signal from the source neuron.
50
-
51
- Returns:
52
- float: source.output × weight
53
- """
54
-
55
- return self.source.output * self.weight
56
-
57
- def __repr__(self) -> str:
58
- """
59
- Return a concise string representation of the connection.
60
-
61
- Example:
62
- <Conn abc123 -> def456 w=0.85 type=standard>
63
- """
64
-
65
- type_str = self.type.name.lower()
66
- return (
67
- f"<Conn {self.source.id[:6]} "
68
- f"-> {self.target.id[:6]} "
69
- f"w={self.weight:.2f} type={type_str}>"
70
- )
File without changes
File without changes
File without changes
File without changes