evonet 0.1.0.dev24__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 (28) hide show
  1. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/PKG-INFO +41 -16
  2. evonet-0.1.0.dev26/README.md +72 -0
  3. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/connection.py +47 -5
  4. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/core.py +29 -4
  5. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/mutation.py +7 -0
  6. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/neuron.py +35 -0
  7. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/serialization.py +10 -1
  8. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/PKG-INFO +41 -16
  9. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/pyproject.toml +1 -1
  10. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/tests/test_recurrent_dynamics.py +77 -0
  11. evonet-0.1.0.dev24/README.md +0 -47
  12. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/LICENSE +0 -0
  13. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/__init__.py +0 -0
  14. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/activation.py +0 -0
  15. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/core_mit_plot_simple.py +0 -0
  16. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/enums.py +0 -0
  17. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/io.py +0 -0
  18. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/layer.py +0 -0
  19. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/utils.py +0 -0
  20. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/visualize.py +0 -0
  21. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/SOURCES.txt +0 -0
  22. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/dependency_links.txt +0 -0
  23. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/requires.txt +0 -0
  24. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/top_level.txt +0 -0
  25. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/setup.cfg +0 -0
  26. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/tests/test_activation.py +0 -0
  27. {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/tests/test_core.py +0 -0
  28. {evonet-0.1.0.dev24 → 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.dev24
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
@@ -50,30 +50,54 @@ Requires-Dist: myst-parser; extra == "docs"
50
50
  Dynamic: license-file
51
51
 
52
52
  # EvoNet
53
+
53
54
  [![Code Quality & Tests](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml/badge.svg)](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
54
55
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
55
56
  [![Project Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/EvoLib/evo-net)
56
57
 
57
- **EvoNet** is a modular and evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
58
- It supports dynamic topologies, recurrent connections, per-neuron activation, and structural evolution – with a strong emphasis on **clarity**, **transparency**, and **didactic value**.
58
+ **EvoNet** is a modular and evolvable neural network core designed for integration
59
+ with [EvoLib](https://github.com/EvoLib/evo-lib).
60
+ It supports dynamic topologies, recurrent connections, per-neuron activation, and
61
+ structural evolution, with a strong emphasis on clarity and explicit behaviour.
62
+
63
+ ---
64
+
65
+ ## Scope
66
+
67
+ EvoNet is not a state-of-the-art or general-purpose deep learning framework.
68
+
69
+ It does not aim to compete with libraries such as PyTorch, TensorFlow, or JAX in terms
70
+ of performance, scalability, or training algorithms. Backpropagation, GPU acceleration,
71
+ and highly optimised tensor operations are outside the scope of this project.
72
+
73
+ Instead, EvoNet is designed for evolutionary algorithms, structural mutation, and
74
+ exploratory research, with a focus on transparent and explicit implementations rather
75
+ than performance optimisation or feature completeness.
76
+
77
+ EvoNet should be understood as a conceptual and experimental model, not as a
78
+ production-grade neural-network engine.
59
79
 
60
80
  ---
61
81
 
62
82
  ## Features
63
83
 
64
- - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
84
+ - **Explicit, layer-based topology** with support for skip connections, cycles,
85
+ and recurrent paths
65
86
  - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
66
- - **Topology-aware mutation system** – add/remove neurons and connections, mutate weights, change activations
67
- - **Per-neuron activation functions** – configurable, extensible, evolvable
68
- - **1-step recurrent state logic** – avoids multi-pass stabilization
69
- - **Topology can grow at runtime** – with `add_neuron`, `add_connection`, `split_connection`
70
- - **Debug-friendly architecture** – explicit IDs, labels, roles, directional graphs
71
- - **Designed for evolutionary learning** – mutation, crossover, speciation ready
72
- - **Lightweight & extensible** – pure Python, NumPy-based, no hard dependencies
87
+ - **Topology-aware mutation operations**:
88
+ add/remove neurons and connections, mutate weights, change activations
89
+ - **Per-neuron activation functions**, configurable and evolvable
90
+ - **Explicit 1-step recurrent state model**, without iterative stabilisation passes
91
+ - **Runtime topology growth**, e.g. via `add_neuron` and `add_connection`
92
+ - **Debug-friendly architecture** with explicit neuron IDs, labels, roles,
93
+ and directional graphs
94
+ - **Designed for evolutionary integration**, not gradient-based training
95
+ - **Lightweight and extensible**: pure Python, NumPy-based, no hard dependencies
73
96
 
74
97
  ---
75
98
 
76
- > ⚠️ **This project is in early development (alpha)**. Interfaces and structure may change.
99
+ > ⚠️ **Project status: Alpha**
100
+ > Interfaces, APIs, and internal structure may change as the project evolves.
77
101
 
78
102
  ---
79
103
 
@@ -83,14 +107,15 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
83
107
  from evonet.core import Nnet
84
108
 
85
109
  net = Nnet()
86
- net.add_layer() # Input
87
- net.add_layer() # Output
110
+ net.add_layer() # Input layer
111
+ net.add_layer() # Output layer
88
112
 
89
- net.add_neuron(layer_idx=0, activation="linear", lable="in")
90
- net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_layer=True)
113
+ net.add_neuron(layer_idx=0, activation="linear", label="in")
114
+ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, label="out")
91
115
 
92
116
  print(net.calc([1.0]))
93
117
  ```
118
+ ---
94
119
 
95
120
  ## License
96
121
 
@@ -0,0 +1,72 @@
1
+ # EvoNet
2
+
3
+ [![Code Quality & Tests](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml/badge.svg)](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5
+ [![Project Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/EvoLib/evo-net)
6
+
7
+ **EvoNet** is a modular and evolvable neural network core designed for integration
8
+ with [EvoLib](https://github.com/EvoLib/evo-lib).
9
+ It supports dynamic topologies, recurrent connections, per-neuron activation, and
10
+ structural evolution, with a strong emphasis on clarity and explicit behaviour.
11
+
12
+ ---
13
+
14
+ ## Scope
15
+
16
+ EvoNet is not a state-of-the-art or general-purpose deep learning framework.
17
+
18
+ It does not aim to compete with libraries such as PyTorch, TensorFlow, or JAX in terms
19
+ of performance, scalability, or training algorithms. Backpropagation, GPU acceleration,
20
+ and highly optimised tensor operations are outside the scope of this project.
21
+
22
+ Instead, EvoNet is designed for evolutionary algorithms, structural mutation, and
23
+ exploratory research, with a focus on transparent and explicit implementations rather
24
+ than performance optimisation or feature completeness.
25
+
26
+ EvoNet should be understood as a conceptual and experimental model, not as a
27
+ production-grade neural-network engine.
28
+
29
+ ---
30
+
31
+ ## Features
32
+
33
+ - **Explicit, layer-based topology** with support for skip connections, cycles,
34
+ and recurrent paths
35
+ - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
36
+ - **Topology-aware mutation operations**:
37
+ add/remove neurons and connections, mutate weights, change activations
38
+ - **Per-neuron activation functions**, configurable and evolvable
39
+ - **Explicit 1-step recurrent state model**, without iterative stabilisation passes
40
+ - **Runtime topology growth**, e.g. via `add_neuron` and `add_connection`
41
+ - **Debug-friendly architecture** with explicit neuron IDs, labels, roles,
42
+ and directional graphs
43
+ - **Designed for evolutionary integration**, not gradient-based training
44
+ - **Lightweight and extensible**: pure Python, NumPy-based, no hard dependencies
45
+
46
+ ---
47
+
48
+ > ⚠️ **Project status: Alpha**
49
+ > Interfaces, APIs, and internal structure may change as the project evolves.
50
+
51
+ ---
52
+
53
+ ## Quick Example
54
+
55
+ ```python
56
+ from evonet.core import Nnet
57
+
58
+ net = Nnet()
59
+ net.add_layer() # Input layer
60
+ net.add_layer() # Output layer
61
+
62
+ net.add_neuron(layer_idx=0, activation="linear", label="in")
63
+ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, label="out")
64
+
65
+ print(net.calc([1.0]))
66
+ ```
67
+ ---
68
+
69
+ ## License
70
+
71
+ MIT License - see [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
72
+
@@ -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
  )
@@ -117,6 +117,8 @@ class Nnet:
117
117
  label: str = "",
118
118
  role: NeuronRole = NeuronRole.HIDDEN,
119
119
  count: int = 1,
120
+ dynamics_name: str = "standard",
121
+ dynamics_params: dict[str, float] | None = None,
120
122
  connection_init: Literal["random", "zero", "near_zero", "none"] = "zero",
121
123
  recurrent: Optional[set[RecurrentKind]] = None,
122
124
  connection_scope: Literal["adjacent", "crosslayer"] = "adjacent",
@@ -176,6 +178,10 @@ class Nnet:
176
178
  neuron = Neuron(activation=activation, bias=bias)
177
179
  neuron.role = role
178
180
  neuron.label = label
181
+
182
+ neuron.dynamics_name = dynamics_name
183
+ neuron.dynamics_params = dict(dynamics_params) if dynamics_params else {}
184
+
179
185
  target_layer.neurons.append(neuron)
180
186
  new_neurons.append(neuron)
181
187
 
@@ -312,7 +318,7 @@ class Nnet:
312
318
  # --------------------------------------------------------------
313
319
  # Create connections
314
320
  # --------------------------------------------------------------
315
- for src, dst, ctype in forced + sampled:
321
+ for src, dst, ctype in sampled:
316
322
  self.add_connection(src, dst, weight=weight, conn_type=ctype)
317
323
 
318
324
  return new_neurons
@@ -323,6 +329,7 @@ class Nnet:
323
329
  target: Neuron,
324
330
  weight: float | None = None,
325
331
  conn_type: ConnectionType = ConnectionType.STANDARD,
332
+ delay: int = 0,
326
333
  ) -> Connection | None:
327
334
  """
328
335
  Create a directed connection between two neurons.
@@ -332,6 +339,7 @@ class Nnet:
332
339
  target (Neuron): Target neuron.
333
340
  weight (float | None): Initial weight. If None, 0.0 is used.
334
341
  conn_type (ConnectionType): Type of connection (e.g. standard, recurrent).
342
+ delay (int): Delay in discrete time steps.
335
343
 
336
344
  Returns:
337
345
  Connection: Added Connection.
@@ -343,7 +351,9 @@ class Nnet:
343
351
  if weight is None:
344
352
  weight = 0.0
345
353
 
346
- 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
+ )
347
357
  source.outgoing.append(conn)
348
358
  target.incoming.append(conn)
349
359
 
@@ -355,6 +365,11 @@ class Nnet:
355
365
  for neuron in layer.neurons:
356
366
  neuron.reset(full=full)
357
367
 
368
+ if full:
369
+ # Clear delayed history buffers on connections.
370
+ for conn in self.get_all_connections():
371
+ conn.reset_buffer()
372
+
358
373
  def calc(self, input_values: list[float]) -> list[float]:
359
374
  """
360
375
  Perform a forward pass through the network.
@@ -387,7 +402,12 @@ class Nnet:
387
402
  for n in layer.neurons:
388
403
  for c in n.incoming:
389
404
  if c.type is ConnectionType.RECURRENT:
390
- 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
391
411
 
392
412
  # Feed-forward by layers: activate first, then propagate non-recurrent edges
393
413
  for layer in self.layers:
@@ -412,7 +432,7 @@ class Nnet:
412
432
  for n in layer.neurons:
413
433
  if n.activation_name != "softmax":
414
434
  total = n.input + n.bias
415
- n.output = n.activation(total)
435
+ n.output = n.compute_output(total)
416
436
 
417
437
  # Propagate to targets (exclude recurrent edges)
418
438
  for n in layer.neurons:
@@ -420,6 +440,11 @@ class Nnet:
420
440
  if c.type is not ConnectionType.RECURRENT:
421
441
  c.target.input += c.weight * n.output
422
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
+
423
448
  return [n.output for n in self.layers[-1].neurons]
424
449
 
425
450
  def get_all_neurons(self) -> list[Neuron]:
@@ -273,6 +273,8 @@ def add_random_neuron(
273
273
  connection_scope: Literal["adjacent", "crosslayer"] = "adjacent",
274
274
  connection_density: float = 1.0,
275
275
  max_connections: int = 2**63 - 1,
276
+ dynamics_name: str = "standard",
277
+ dynamics_params: dict[str, float] = {},
276
278
  ) -> Neuron | None:
277
279
  """
278
280
  Insert a new hidden neuron into a random layer.
@@ -298,6 +300,9 @@ def add_random_neuron(
298
300
  if len(net.layers) == 2:
299
301
  net.insert_layer(1)
300
302
 
303
+ if dynamics_params is None:
304
+ dynamics_params = {}
305
+
301
306
  # Choose target layer (not input, not output)
302
307
  candidate_layers = net.layers[1:-1]
303
308
  if not candidate_layers:
@@ -313,6 +318,8 @@ def add_random_neuron(
313
318
  connection_scope=connection_scope,
314
319
  connection_density=connection_density,
315
320
  max_connections=max_connections,
321
+ dynamics_name=dynamics_name,
322
+ dynamics_params=dynamics_params,
316
323
  )[0]
317
324
 
318
325
  return new_neuron
@@ -55,6 +55,41 @@ class Neuron:
55
55
  self.last_output: float = 0.0
56
56
  self.label = label
57
57
 
58
+ # Neuron dynamics (stateful behaviour over time)
59
+ self.dynamics_name: str = "standard"
60
+ self.dynamics_params: dict[str, float] = {}
61
+
62
+ def compute_output(self, total_input: float) -> float:
63
+ """
64
+ Compute the neuron's output given the total input.
65
+
66
+ This method defines the neuron's local output dynamics and serves as the single
67
+ extension point for experimental neuron behaviour (e.g. leaky integration,
68
+ gating, oscillators).
69
+
70
+ By default, this applies the activation function directly.
71
+ """
72
+
73
+ # Default / standard dynamics
74
+ if self.dynamics_name == "standard":
75
+ return self.activation(total_input)
76
+
77
+ # Leaky integrator dynamics
78
+ if self.dynamics_name == "leaky":
79
+ alpha = self.dynamics_params.get("alpha", 1.0)
80
+
81
+ # Safety: keep alpha in [0, 1]
82
+ if alpha < 0.0:
83
+ alpha = 0.0
84
+ elif alpha > 1.0:
85
+ alpha = 1.0
86
+
87
+ raw = self.activation(total_input)
88
+ return (1.0 - alpha) * self.last_output + alpha * raw
89
+
90
+ # Fallback: unknown dynamics --> behave like standard
91
+ return self.activation(total_input)
92
+
58
93
  def reset(self, full: bool = False) -> None:
59
94
  """
60
95
  Clear current neuron state for a new forward pass.
@@ -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.dev24
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
@@ -50,30 +50,54 @@ Requires-Dist: myst-parser; extra == "docs"
50
50
  Dynamic: license-file
51
51
 
52
52
  # EvoNet
53
+
53
54
  [![Code Quality & Tests](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml/badge.svg)](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
54
55
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
55
56
  [![Project Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/EvoLib/evo-net)
56
57
 
57
- **EvoNet** is a modular and evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
58
- It supports dynamic topologies, recurrent connections, per-neuron activation, and structural evolution – with a strong emphasis on **clarity**, **transparency**, and **didactic value**.
58
+ **EvoNet** is a modular and evolvable neural network core designed for integration
59
+ with [EvoLib](https://github.com/EvoLib/evo-lib).
60
+ It supports dynamic topologies, recurrent connections, per-neuron activation, and
61
+ structural evolution, with a strong emphasis on clarity and explicit behaviour.
62
+
63
+ ---
64
+
65
+ ## Scope
66
+
67
+ EvoNet is not a state-of-the-art or general-purpose deep learning framework.
68
+
69
+ It does not aim to compete with libraries such as PyTorch, TensorFlow, or JAX in terms
70
+ of performance, scalability, or training algorithms. Backpropagation, GPU acceleration,
71
+ and highly optimised tensor operations are outside the scope of this project.
72
+
73
+ Instead, EvoNet is designed for evolutionary algorithms, structural mutation, and
74
+ exploratory research, with a focus on transparent and explicit implementations rather
75
+ than performance optimisation or feature completeness.
76
+
77
+ EvoNet should be understood as a conceptual and experimental model, not as a
78
+ production-grade neural-network engine.
59
79
 
60
80
  ---
61
81
 
62
82
  ## Features
63
83
 
64
- - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
84
+ - **Explicit, layer-based topology** with support for skip connections, cycles,
85
+ and recurrent paths
65
86
  - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
66
- - **Topology-aware mutation system** – add/remove neurons and connections, mutate weights, change activations
67
- - **Per-neuron activation functions** – configurable, extensible, evolvable
68
- - **1-step recurrent state logic** – avoids multi-pass stabilization
69
- - **Topology can grow at runtime** – with `add_neuron`, `add_connection`, `split_connection`
70
- - **Debug-friendly architecture** – explicit IDs, labels, roles, directional graphs
71
- - **Designed for evolutionary learning** – mutation, crossover, speciation ready
72
- - **Lightweight & extensible** – pure Python, NumPy-based, no hard dependencies
87
+ - **Topology-aware mutation operations**:
88
+ add/remove neurons and connections, mutate weights, change activations
89
+ - **Per-neuron activation functions**, configurable and evolvable
90
+ - **Explicit 1-step recurrent state model**, without iterative stabilisation passes
91
+ - **Runtime topology growth**, e.g. via `add_neuron` and `add_connection`
92
+ - **Debug-friendly architecture** with explicit neuron IDs, labels, roles,
93
+ and directional graphs
94
+ - **Designed for evolutionary integration**, not gradient-based training
95
+ - **Lightweight and extensible**: pure Python, NumPy-based, no hard dependencies
73
96
 
74
97
  ---
75
98
 
76
- > ⚠️ **This project is in early development (alpha)**. Interfaces and structure may change.
99
+ > ⚠️ **Project status: Alpha**
100
+ > Interfaces, APIs, and internal structure may change as the project evolves.
77
101
 
78
102
  ---
79
103
 
@@ -83,14 +107,15 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
83
107
  from evonet.core import Nnet
84
108
 
85
109
  net = Nnet()
86
- net.add_layer() # Input
87
- net.add_layer() # Output
110
+ net.add_layer() # Input layer
111
+ net.add_layer() # Output layer
88
112
 
89
- net.add_neuron(layer_idx=0, activation="linear", lable="in")
90
- net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_layer=True)
113
+ net.add_neuron(layer_idx=0, activation="linear", label="in")
114
+ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, label="out")
91
115
 
92
116
  print(net.calc([1.0]))
93
117
  ```
118
+ ---
94
119
 
95
120
  ## License
96
121
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evonet"
7
- version = "0.1.0.dev24"
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)
@@ -1,47 +0,0 @@
1
- # EvoNet
2
- [![Code Quality & Tests](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml/badge.svg)](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
3
- [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
4
- [![Project Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/EvoLib/evo-net)
5
-
6
- **EvoNet** is a modular and evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
7
- It supports dynamic topologies, recurrent connections, per-neuron activation, and structural evolution – with a strong emphasis on **clarity**, **transparency**, and **didactic value**.
8
-
9
- ---
10
-
11
- ## Features
12
-
13
- - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
14
- - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
15
- - **Topology-aware mutation system** – add/remove neurons and connections, mutate weights, change activations
16
- - **Per-neuron activation functions** – configurable, extensible, evolvable
17
- - **1-step recurrent state logic** – avoids multi-pass stabilization
18
- - **Topology can grow at runtime** – with `add_neuron`, `add_connection`, `split_connection`
19
- - **Debug-friendly architecture** – explicit IDs, labels, roles, directional graphs
20
- - **Designed for evolutionary learning** – mutation, crossover, speciation ready
21
- - **Lightweight & extensible** – pure Python, NumPy-based, no hard dependencies
22
-
23
- ---
24
-
25
- > ⚠️ **This project is in early development (alpha)**. Interfaces and structure may change.
26
-
27
- ---
28
-
29
- ## Quick Example
30
-
31
- ```python
32
- from evonet.core import Nnet
33
-
34
- net = Nnet()
35
- net.add_layer() # Input
36
- net.add_layer() # Output
37
-
38
- net.add_neuron(layer_idx=0, activation="linear", lable="in")
39
- net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_layer=True)
40
-
41
- print(net.calc([1.0]))
42
- ```
43
-
44
- ## License
45
-
46
- MIT License - see [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
47
-
File without changes
File without changes
File without changes