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.
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/PKG-INFO +41 -16
- evonet-0.1.0.dev26/README.md +72 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/connection.py +47 -5
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/core.py +29 -4
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/mutation.py +7 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/neuron.py +35 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/serialization.py +10 -1
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/PKG-INFO +41 -16
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/pyproject.toml +1 -1
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/tests/test_recurrent_dynamics.py +77 -0
- evonet-0.1.0.dev24/README.md +0 -47
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/LICENSE +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/__init__.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/activation.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/core_mit_plot_simple.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/enums.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/io.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/layer.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/utils.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet/visualize.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/SOURCES.txt +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/dependency_links.txt +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/requires.txt +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/evonet.egg-info/top_level.txt +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/setup.cfg +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/tests/test_activation.py +0 -0
- {evonet-0.1.0.dev24 → evonet-0.1.0.dev26}/tests/test_core.py +0 -0
- {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.
|
|
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
|
[](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
|
|
54
55
|
[](LICENSE)
|
|
55
56
|
[](https://github.com/EvoLib/evo-net)
|
|
56
57
|
|
|
57
|
-
**EvoNet** is a modular and evolvable neural network core designed for integration
|
|
58
|
-
|
|
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
|
-
- **
|
|
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
|
|
67
|
-
|
|
68
|
-
- **
|
|
69
|
-
- **
|
|
70
|
-
- **
|
|
71
|
-
- **
|
|
72
|
-
|
|
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
|
-
> ⚠️ **
|
|
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",
|
|
90
|
-
net.add_neuron(layer_idx=1, activation="linear", bias=0.5,
|
|
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
|
+
[](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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
|
|
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
|
)
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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.
|
|
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
|
[](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
|
|
54
55
|
[](LICENSE)
|
|
55
56
|
[](https://github.com/EvoLib/evo-net)
|
|
56
57
|
|
|
57
|
-
**EvoNet** is a modular and evolvable neural network core designed for integration
|
|
58
|
-
|
|
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
|
-
- **
|
|
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
|
|
67
|
-
|
|
68
|
-
- **
|
|
69
|
-
- **
|
|
70
|
-
- **
|
|
71
|
-
- **
|
|
72
|
-
|
|
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
|
-
> ⚠️ **
|
|
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",
|
|
90
|
-
net.add_neuron(layer_idx=1, activation="linear", bias=0.5,
|
|
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
|
|
|
@@ -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)
|
evonet-0.1.0.dev24/README.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# EvoNet
|
|
2
|
-
[](https://github.com/EvoLib/evo-net/actions/workflows/ci.yml)
|
|
3
|
-
[](LICENSE)
|
|
4
|
-
[](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
|
|
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
|