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.
Files changed (27) hide show
  1. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/PKG-INFO +1 -1
  2. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/connection.py +47 -5
  3. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/core.py +22 -3
  4. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/mutation.py +3 -0
  5. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/serialization.py +10 -1
  6. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/PKG-INFO +1 -1
  7. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/pyproject.toml +1 -1
  8. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_recurrent_dynamics.py +77 -0
  9. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/LICENSE +0 -0
  10. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/README.md +0 -0
  11. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/__init__.py +0 -0
  12. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/activation.py +0 -0
  13. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/core_mit_plot_simple.py +0 -0
  14. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/enums.py +0 -0
  15. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/io.py +0 -0
  16. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/layer.py +0 -0
  17. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/neuron.py +0 -0
  18. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/utils.py +0 -0
  19. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet/visualize.py +0 -0
  20. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/SOURCES.txt +0 -0
  21. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/dependency_links.txt +0 -0
  22. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/requires.txt +0 -0
  23. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/evonet.egg-info/top_level.txt +0 -0
  24. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/setup.cfg +0 -0
  25. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_activation.py +0 -0
  26. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/tests/test_core.py +0 -0
  27. {evonet-0.1.0.dev25 → evonet-0.1.0.dev26}/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.dev26
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -8,7 +8,8 @@ recurrent).
8
8
  """
9
9
 
10
10
 
11
- from typing import TYPE_CHECKING
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): Optional delay in time steps (not yet implemented).
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 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,9 @@ 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
+ 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
- c.target.input += c.weight * c.source.last_output
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=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.dev26
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.dev26"
8
8
  description = "Evolvable neural network core for integration with EvoLib"
9
9
  authors = [
10
10
  { name = "EvoLib", email = "evolib@dismail.de" }
@@ -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