evonet 0.1.0a0.dev1__tar.gz → 0.1.0a0.dev2__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.0a0.dev1 → evonet-0.1.0a0.dev2}/PKG-INFO +38 -3
  2. evonet-0.1.0a0.dev2/README.md +47 -0
  3. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/activation.py +3 -4
  4. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/connection.py +4 -5
  5. evonet-0.1.0a0.dev2/evonet/core.py +154 -0
  6. evonet-0.1.0a0.dev1/evonet/types.py → evonet-0.1.0a0.dev2/evonet/enums.py +1 -0
  7. evonet-0.1.0a0.dev2/evonet/layer.py +10 -0
  8. evonet-0.1.0a0.dev2/evonet/mutation.py +137 -0
  9. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/neuron.py +21 -9
  10. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet.egg-info/PKG-INFO +38 -3
  11. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet.egg-info/SOURCES.txt +2 -1
  12. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/pyproject.toml +1 -1
  13. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/tests/test_activation.py +3 -3
  14. evonet-0.1.0a0.dev2/tests/test_core.py +75 -0
  15. evonet-0.1.0a0.dev1/README.md +0 -12
  16. evonet-0.1.0a0.dev1/evonet/core.py +0 -117
  17. evonet-0.1.0a0.dev1/evonet/mutation.py +0 -59
  18. evonet-0.1.0a0.dev1/tests/test_core.py +0 -42
  19. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/LICENSE +0 -0
  20. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/__init__.py +0 -0
  21. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/io.py +0 -0
  22. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/utils.py +0 -0
  23. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet/visualize.py +0 -0
  24. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet.egg-info/dependency_links.txt +0 -0
  25. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet.egg-info/requires.txt +0 -0
  26. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/evonet.egg-info/top_level.txt +0 -0
  27. {evonet-0.1.0a0.dev1 → evonet-0.1.0a0.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0a0.dev1
3
+ Version: 0.1.0a0.dev2
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -52,8 +52,43 @@ Dynamic: license-file
52
52
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
53
53
  [![Project Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/EvoLib/evo-net)
54
54
 
55
- **EvoNet** is a modular, evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
56
- It supports dynamic topologies, recurrent connections, and is optimized for mutation, crossover, and structural evolution.
55
+ **EvoNet** is a modular and evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
56
+ It supports dynamic topologies, recurrent connections, per-neuron activation, and structural evolution – with a strong emphasis on **clarity**, **transparency**, and **didactic value**.
57
+
58
+ ---
59
+
60
+ ## 🔧 Features
61
+
62
+ - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
63
+ - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
64
+ - **Topology-aware mutation system** – add/remove neurons and connections, mutate weights, change activations
65
+ - **Per-neuron activation functions** – configurable, extensible, evolvable
66
+ - **1-step recurrent state logic** – avoids multi-pass stabilization
67
+ - **Topology can grow at runtime** – with `add_neuron`, `add_connection`, `split_connection`
68
+ - **Debug-friendly architecture** – explicit IDs, labels, roles, directional graphs
69
+ - **Designed for evolutionary learning** – mutation, crossover, speciation ready
70
+ - **Lightweight & extensible** – pure Python, NumPy-based, no hard dependencies
71
+
72
+ ---
73
+
74
+ > ⚠️ **This project is in early development (alpha)**. Interfaces and structure may change.
75
+
76
+ ---
77
+
78
+ ## 🚀 Quick Example
79
+
80
+ ```python
81
+ from evonet.core import Nnet
82
+
83
+ net = Nnet()
84
+ net.add_layer() # Input
85
+ net.add_layer() # Output
86
+
87
+ net.add_neuron(layer_idx=0, activation="linear", lable="in")
88
+ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_layer=True)
89
+
90
+ print(net.calc([1.0]))
91
+ ```
57
92
 
58
93
  ## 🪪 License
59
94
 
@@ -0,0 +1,47 @@
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
+ This project is licensed under the [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
47
+
@@ -154,14 +154,13 @@ def softmax(values: Union[List[float], Tuple[float], np.ndarray]) -> np.ndarray:
154
154
  return exp_x / np.sum(exp_x)
155
155
 
156
156
 
157
- # --- Registry ---
158
-
159
-
160
- def random_function() -> str:
157
+ def random_function_name() -> str:
161
158
  """Returns a random activation function name from the registry."""
162
159
  return np.random.choice(list(ACTIVATIONS.keys()))
163
160
 
164
161
 
162
+ # Registry
163
+
165
164
  ACTIVATIONS: dict[str, Callable] = {
166
165
  "tanh": tanh,
167
166
  "ntanh": ntanh,
@@ -6,11 +6,9 @@ A connection links a source neuron to a target neuron with a weight. Supports op
6
6
  connection types for future use (e.g. inhibitory).
7
7
  """
8
8
 
9
- from __future__ import annotations
10
-
11
9
  from typing import TYPE_CHECKING
12
10
 
13
- from evonet.types import ConnectionType
11
+ from evonet.enums import ConnectionType
14
12
 
15
13
  if TYPE_CHECKING:
16
14
  from evonet.neuron import Neuron
@@ -30,12 +28,13 @@ class Connection:
30
28
 
31
29
  def __init__(
32
30
  self,
33
- source: Neuron,
34
- target: Neuron,
31
+ source: "Neuron",
32
+ target: "Neuron",
35
33
  weight: float = 1.0,
36
34
  delay: int = 0,
37
35
  conn_type: ConnectionType = ConnectionType.STANDARD,
38
36
  ) -> None:
37
+
39
38
  self.source = source
40
39
  self.target = target
41
40
  self.weight = weight
@@ -0,0 +1,154 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Core class for evolvable neural networks.
4
+
5
+ Manages neurons, connections, and forward computation. Prepares mutation, crossover, and
6
+ export interfaces.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+ from evonet.connection import Connection
14
+ from evonet.enums import ConnectionType, NeuronRole
15
+ from evonet.layer import Layer
16
+ from evonet.neuron import Neuron
17
+
18
+
19
+ class Nnet:
20
+ """
21
+ Evolvable neural network with explicit topology.
22
+
23
+ Attributes:
24
+ connections (list[Connection]): All directed, weighted edges.
25
+ input_neurons (list[Neuron]): Subset of neurons used as input nodes.
26
+ hidden_neurons (list[Neuron]): Subset of neurons used as hidden nodes.
27
+ output_neurons (list[Neuron]): Subset of neurons used as output nodes.
28
+ """
29
+
30
+ def __init__(self) -> None:
31
+ self.layers: list[Layer] = []
32
+
33
+ def add_layer(self, count: int = 1) -> int:
34
+ """
35
+ Add a layer to the network.
36
+
37
+ Parameter:
38
+ count (int): Number of layers to add
39
+ """
40
+
41
+ if count <= 0:
42
+ raise ValueError("Number of layers must be greater then zero")
43
+
44
+ for _ in range(count):
45
+ self.layers.append(Layer())
46
+
47
+ return len(self.layers) - 1
48
+
49
+ def add_neuron(
50
+ self,
51
+ layer_idx: int | None = None,
52
+ activation: str = "tanh",
53
+ bias: float = 0.0,
54
+ lable: str = "",
55
+ role: NeuronRole = NeuronRole.HIDDEN,
56
+ count: int = 1,
57
+ connect_layer: bool = True,
58
+ ) -> Neuron:
59
+
60
+ if layer_idx is None:
61
+ layer_idx = len(self.layers) - 1 # Add neuron to last layer
62
+
63
+ if layer_idx < 0:
64
+ raise ValueError(f"Expected positiv layerindex: got {layer_idx}")
65
+ if layer_idx >= len(self.layers):
66
+ raise ValueError("Layerindex out off scope: got {layer_idx}")
67
+
68
+ for _ in range(count):
69
+ neuron = Neuron(activation=activation, bias=bias)
70
+ neuron.role = role
71
+ neuron.lable = lable
72
+
73
+ self.layers[layer_idx].neurons.append(neuron)
74
+
75
+ if connect_layer and layer_idx > 0:
76
+ # Finde letzten nicht-leeren Layer vor diesem
77
+ for prev_idx in range(layer_idx - 1, -1, -1):
78
+ prev_layer = self.layers[prev_idx]
79
+ if prev_layer.neurons:
80
+ for prev_neuron in prev_layer.neurons:
81
+ self.add_connection(prev_neuron, neuron)
82
+ break
83
+ return neuron
84
+
85
+ def add_connection(
86
+ self,
87
+ source: Neuron,
88
+ target: Neuron,
89
+ weight: float | None = None,
90
+ conn_type: ConnectionType = ConnectionType.STANDARD,
91
+ ) -> None:
92
+
93
+ if weight is None:
94
+ weight = np.random.randn() * 0.5
95
+
96
+ conn = Connection(source, target, weight=weight, conn_type=conn_type)
97
+ source.outgoing.append(conn)
98
+ target.incoming.append(conn)
99
+
100
+ def reset(self) -> None:
101
+ """Resets all neurons."""
102
+ for layer in self.layers:
103
+ for neuron in layer.neurons:
104
+ neuron.reset()
105
+
106
+ def calc(self, input_values: list[float]) -> list[float]:
107
+
108
+ self.reset()
109
+
110
+ # Set Inputs
111
+ input_layer = self.layers[0]
112
+ assert len(input_layer.neurons) == len(input_values)
113
+ for idx, neuron in enumerate(input_layer.neurons):
114
+ neuron.input = float(input_values[idx])
115
+
116
+ # Recurrent
117
+ for layer in self.layers:
118
+ for neuron in layer.neurons:
119
+ for conn in neuron.incoming:
120
+ if conn.type.name.lower() == "recurrent":
121
+ neuron.input += conn.source.last_output * conn.weight
122
+ else:
123
+ neuron.input += conn.source.output * conn.weight
124
+
125
+ for layer in self.layers:
126
+ for neuron in layer.neurons:
127
+ total = neuron.input + neuron.bias
128
+ neuron.output = neuron.activation(total)
129
+ for conn in neuron.outgoing:
130
+ conn.target.input += conn.weight * neuron.output
131
+
132
+ # Return Output
133
+ return [n.output for n in self.layers[-1].neurons]
134
+
135
+ def get_all_neurons(self) -> list[Neuron]:
136
+ return [n for layer in self.layers for n in layer.neurons]
137
+
138
+ def get_all_connections(self) -> list[Connection]:
139
+ return [c for n in self.get_all_neurons() for c in n.outgoing]
140
+
141
+ def __repr__(self) -> str:
142
+ total_neurons = sum(len(layer.neurons) for layer in self.layers)
143
+ input_neurons = len(self.layers[0].neurons) if self.layers else 0
144
+ output_neurons = len(self.layers[-1].neurons) if len(self.layers) > 1 else 0
145
+ hidden_neurons = total_neurons - input_neurons - output_neurons
146
+
147
+ total_connections = len(self.get_all_connections())
148
+
149
+ return (
150
+ f"<Nnet | {len(self.layers)} layers, "
151
+ f"{total_neurons} neurons (I:{input_neurons} H:{hidden_neurons} "
152
+ f"O:{output_neurons}), "
153
+ f"{total_connections} connections "
154
+ )
@@ -14,3 +14,4 @@ class ConnectionType(Enum):
14
14
  INHIBITORY = auto()
15
15
  EXCITATORY = auto()
16
16
  MODULATORY = auto()
17
+ RECURRENT = auto()
@@ -0,0 +1,10 @@
1
+ # SPDX-License-Identifier: MIT
2
+ from evonet.neuron import Neuron
3
+
4
+
5
+ class Layer:
6
+ def __init__(self) -> None:
7
+ self.neurons: list[Neuron] = []
8
+
9
+ def add_neuron(self, neuron: Neuron) -> None:
10
+ self.neurons.append(neuron)
@@ -0,0 +1,137 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Basic mutation operations for evolvable neural networks.
4
+
5
+ Supports weight, bias, and structural mutations.
6
+ """
7
+
8
+ import random
9
+
10
+ import numpy as np
11
+
12
+ from evonet.activation import ACTIVATIONS, random_function_name
13
+ from evonet.connection import Connection
14
+ from evonet.core import Nnet
15
+ from evonet.enums import ConnectionType, NeuronRole
16
+ from evonet.neuron import Neuron
17
+
18
+
19
+ def mutate_activation(neuron: Neuron, std: float = 0.1) -> None:
20
+ neuron.activation_name = random_function_name()
21
+ neuron.activation = ACTIVATIONS[neuron.activation_name]
22
+
23
+
24
+ def mutate_activations(net: Nnet, probability: float = 1.0, std: float = 0.1) -> None:
25
+ for neuron in net.get_all_neurons():
26
+ if neuron.role != NeuronRole.INPUT and np.random.rand() < probability:
27
+ mutate_activation(neuron)
28
+
29
+
30
+ def mutate_weight(conn: Connection, std: float = 0.1) -> None:
31
+ conn.weight += np.random.normal(0, std)
32
+
33
+
34
+ def mutate_weights(net: Nnet, probability: float = 1.0, std: float = 0.1) -> None:
35
+ """Applies Gaussian noise to all connection weights."""
36
+
37
+ for conn in net.get_all_connections():
38
+ if np.random.rand() < probability:
39
+ mutate_weight(conn, std)
40
+
41
+
42
+ def mutate_bias(neuron: Neuron, std: float = 0.1) -> None:
43
+ neuron.bias += np.random.normal(0, std)
44
+
45
+
46
+ def mutate_biases(net: Nnet, probability: float = 1.0, std: float = 0.1) -> None:
47
+ """Applies Gaussian noise to all neuron biases (except input neurons)."""
48
+ for neuron in net.get_all_neurons():
49
+ if neuron.role != NeuronRole.INPUT and np.random.rand() < probability:
50
+ mutate_bias(neuron, std)
51
+
52
+
53
+ def add_random_connection(net: Nnet) -> None:
54
+ """Creates a new connection between two random Neuronen."""
55
+
56
+ all_neurons = net.get_all_neurons()
57
+ if len(all_neurons) < 2:
58
+ return
59
+
60
+ src = random.choice(all_neurons)
61
+ dst = random.choice(all_neurons)
62
+
63
+ if src == dst:
64
+ conn_type = ConnectionType.RECURRENT
65
+ else:
66
+ conn_type = ConnectionType.STANDARD
67
+
68
+ # Prüfen, ob Verbindung bereits existiert
69
+ if any(conn.target == dst for conn in src.outgoing):
70
+ return
71
+
72
+ net.add_connection(src, dst, conn_type=conn_type)
73
+
74
+
75
+ def remove_random_connection(net: Nnet) -> None:
76
+ """Entfernt zufällig eine bestehende Verbindung."""
77
+ all_connections = net.get_all_connections()
78
+ if not all_connections:
79
+ return
80
+
81
+ conn = random.choice(all_connections)
82
+ conn.source.outgoing.remove(conn)
83
+ conn.target.incoming.remove(conn)
84
+
85
+
86
+ def add_random_neuron(net: Nnet) -> None:
87
+ """Fügt ein neues Hidden-Neuron in ein zufälliges Layer ein und verbindet es mit
88
+ vorhandenen Neuronen."""
89
+ if len(net.layers) < 2:
90
+ return
91
+
92
+ # Ziel-Layer wählen (nicht Input, nicht Output)
93
+ candidate_layers = net.layers[1:-1]
94
+ if not candidate_layers:
95
+ return
96
+
97
+ layer = random.choice(candidate_layers)
98
+ net.add_neuron(
99
+ layer_idx=net.layers.index(layer),
100
+ activation="tanh",
101
+ role=NeuronRole.HIDDEN,
102
+ connect_layer=True,
103
+ )
104
+
105
+
106
+ def split_connection(net: Nnet, activation: str = "tanh", noise: float = 0.1) -> None:
107
+ """Add Neuron between connection."""
108
+ all_connections = net.get_all_connections()
109
+ if not all_connections:
110
+ return
111
+
112
+ conn = random.choice(all_connections)
113
+ src, dst = conn.source, conn.target
114
+
115
+ insert_idx = None
116
+ for idx, layer in enumerate(net.layers):
117
+ if src in layer.neurons:
118
+ insert_idx = idx + 1
119
+ break
120
+
121
+ if insert_idx is None or insert_idx >= len(net.layers):
122
+ return
123
+
124
+ new_neuron = net.add_neuron(
125
+ layer_idx=insert_idx,
126
+ role=NeuronRole.HIDDEN,
127
+ activation=activation,
128
+ connect_layer=False,
129
+ )
130
+
131
+ # Set new connections
132
+ src.outgoing.remove(conn)
133
+ dst.incoming.remove(conn)
134
+
135
+ weight = 1.0 + np.random.normal(0, noise)
136
+ net.add_connection(src, new_neuron, weight=weight)
137
+ net.add_connection(new_neuron, dst, weight=conn.weight)
@@ -6,12 +6,12 @@ Each neuron holds its activation function, input/output connections, bias value,
6
6
  cached output from the last forward pass.
7
7
  """
8
8
 
9
- from __future__ import annotations
10
-
11
9
  from typing import Callable
12
10
  from uuid import uuid4
13
11
 
14
12
  from evonet.activation import ACTIVATIONS
13
+ from evonet.connection import Connection
14
+ from evonet.enums import NeuronRole
15
15
 
16
16
 
17
17
  class Neuron:
@@ -19,32 +19,44 @@ class Neuron:
19
19
  Represents a single neuron in the network.
20
20
 
21
21
  Attributes:
22
- id (str): Unique identifier for tracking and crossover.
22
+ id (str): Unique identifier for tracking.
23
23
  activation_name (str): Name of the activation function.
24
24
  bias (float): Bias value added to incoming inputs.
25
25
  incoming (list): Incoming connections (to be filled externally).
26
26
  outgoing (list): Outgoing connections (to be filled externally).
27
27
  output (float): Cached result after activation.
28
+ lable (str): An optional lable
28
29
  """
29
30
 
30
- def __init__(self, activation: str = "tanh", bias: float = 0.0) -> None:
31
+ def __init__(
32
+ self, activation: str = "tanh", lable: str = "", bias: float = 0.0
33
+ ) -> None:
34
+
31
35
  if activation not in ACTIVATIONS:
32
36
  raise ValueError(f"Unknown activation function: '{activation}'")
33
37
  self.id: str = str(uuid4())
38
+ self.role: NeuronRole = NeuronRole.HIDDEN
34
39
  self.activation_name: str = activation
35
40
  self.activation: Callable[[float], float] = ACTIVATIONS[activation]
36
41
  self.bias: float = bias
37
- self.incoming: list = []
38
- self.outgoing: list = []
42
+ self.incoming: list[Connection] = []
43
+ self.outgoing: list[Connection] = []
44
+ self.input: float = 0.0
39
45
  self.output: float = 0.0
46
+ self.last_output: float = 0.0
47
+ self.lable = lable
40
48
 
41
49
  def reset(self) -> None:
42
- """Clears output before each forward pass."""
50
+ self.last_output = self.output
43
51
  self.output = 0.0
52
+ self.input = 0.0
44
53
 
45
54
  def __repr__(self) -> str:
46
55
  return (
47
- f"<Neuron id={self.id[:6]} "
56
+ f"Neuron id={self.id[:6]} "
48
57
  f"act={self.activation_name} "
49
- f"bias={self.bias:.2f}>"
58
+ f"role={self.role} "
59
+ f"bias={self.bias:.2f} "
60
+ f"input={self.input:0.5f} "
61
+ f"output={self.output:0.5f}"
50
62
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0a0.dev1
3
+ Version: 0.1.0a0.dev2
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -52,8 +52,43 @@ Dynamic: license-file
52
52
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
53
53
  [![Project Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/EvoLib/evo-net)
54
54
 
55
- **EvoNet** is a modular, evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
56
- It supports dynamic topologies, recurrent connections, and is optimized for mutation, crossover, and structural evolution.
55
+ **EvoNet** is a modular and evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
56
+ It supports dynamic topologies, recurrent connections, per-neuron activation, and structural evolution – with a strong emphasis on **clarity**, **transparency**, and **didactic value**.
57
+
58
+ ---
59
+
60
+ ## 🔧 Features
61
+
62
+ - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
63
+ - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
64
+ - **Topology-aware mutation system** – add/remove neurons and connections, mutate weights, change activations
65
+ - **Per-neuron activation functions** – configurable, extensible, evolvable
66
+ - **1-step recurrent state logic** – avoids multi-pass stabilization
67
+ - **Topology can grow at runtime** – with `add_neuron`, `add_connection`, `split_connection`
68
+ - **Debug-friendly architecture** – explicit IDs, labels, roles, directional graphs
69
+ - **Designed for evolutionary learning** – mutation, crossover, speciation ready
70
+ - **Lightweight & extensible** – pure Python, NumPy-based, no hard dependencies
71
+
72
+ ---
73
+
74
+ > ⚠️ **This project is in early development (alpha)**. Interfaces and structure may change.
75
+
76
+ ---
77
+
78
+ ## 🚀 Quick Example
79
+
80
+ ```python
81
+ from evonet.core import Nnet
82
+
83
+ net = Nnet()
84
+ net.add_layer() # Input
85
+ net.add_layer() # Output
86
+
87
+ net.add_neuron(layer_idx=0, activation="linear", lable="in")
88
+ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_layer=True)
89
+
90
+ print(net.calc([1.0]))
91
+ ```
57
92
 
58
93
  ## 🪪 License
59
94
 
@@ -6,10 +6,11 @@ evonet/__init__.py
6
6
  evonet/activation.py
7
7
  evonet/connection.py
8
8
  evonet/core.py
9
+ evonet/enums.py
9
10
  evonet/io.py
11
+ evonet/layer.py
10
12
  evonet/mutation.py
11
13
  evonet/neuron.py
12
- evonet/types.py
13
14
  evonet/utils.py
14
15
  evonet/visualize.py
15
16
  evonet.egg-info/PKG-INFO
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evonet"
7
- version = "0.1.0adev1"
7
+ version = "0.1.0adev2"
8
8
  description = "Evolvable neural network core for integration with EvoLib"
9
9
  authors = [
10
10
  { name = "EvoLib", email = "evolib@dismail.de" }
@@ -23,8 +23,8 @@ def test_softmax_sum_to_one() -> None:
23
23
  np.testing.assert_almost_equal(np.sum(output), 1.0, decimal=6)
24
24
 
25
25
 
26
- def test_random_function_exists_in_registry() -> None:
27
- """random_function must return a valid name in the registry."""
26
+ def test_random_function_name_exists_in_registry() -> None:
27
+ """random_function_name must return a valid name in the registry."""
28
28
  for _ in range(10):
29
- name = activation.random_function()
29
+ name = activation.random_function_name()
30
30
  assert name in activation.ACTIVATIONS
@@ -0,0 +1,75 @@
1
+ from evonet.core import Nnet
2
+ from evonet.enums import NeuronRole
3
+
4
+
5
+ def test_forward_pass_identity() -> None:
6
+ """Tests a minimal feedforward network with identity mapping."""
7
+ net = Nnet()
8
+ net.add_layer() # Input layer
9
+ net.add_layer() # Output layer
10
+
11
+ net.add_neuron(
12
+ layer_idx=0,
13
+ activation="linear",
14
+ role=NeuronRole.INPUT,
15
+ lable="in",
16
+ connect_layer=False,
17
+ )
18
+ net.add_neuron(
19
+ layer_idx=1,
20
+ activation="linear",
21
+ role=NeuronRole.OUTPUT,
22
+ lable="out",
23
+ connect_layer=False,
24
+ )
25
+
26
+ src = net.layers[0].neurons[0]
27
+ dst = net.layers[1].neurons[0]
28
+
29
+ net.add_connection(src, dst, weight=1.0)
30
+
31
+ # Gewicht prüfen
32
+ assert abs(src.outgoing[0].weight - 1.0) < 1e-6
33
+
34
+ result = net.calc([0.75])
35
+
36
+ assert isinstance(result, list)
37
+ assert len(result) == 1
38
+ assert abs(result[0] - 0.75) < 1e-6
39
+
40
+
41
+ def test_forward_pass_with_bias() -> None:
42
+ """Tests a simple net with bias on the output neuron."""
43
+ net = Nnet()
44
+ net.add_layer()
45
+ net.add_layer()
46
+
47
+ net.add_neuron(
48
+ layer_idx=0,
49
+ activation="linear",
50
+ role=NeuronRole.INPUT,
51
+ lable="in",
52
+ connect_layer=False,
53
+ )
54
+ net.add_neuron(
55
+ layer_idx=1,
56
+ activation="linear",
57
+ role=NeuronRole.OUTPUT,
58
+ bias=0.5,
59
+ lable="out",
60
+ connect_layer=False,
61
+ )
62
+
63
+ src = net.layers[0].neurons[0]
64
+ dst = net.layers[1].neurons[0]
65
+
66
+ net.add_connection(src, dst, weight=2.0)
67
+
68
+ # Gewicht prüfen
69
+ assert abs(src.outgoing[0].weight - 2.0) < 1e-6
70
+
71
+ result = net.calc([1.0])
72
+
73
+ assert isinstance(result, list)
74
+ assert len(result) == 1
75
+ assert abs(result[0] - 2.5) < 1e-6 # (1.0 * 2.0) + 0.5 = 2.5
@@ -1,12 +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, evolvable neural network core designed for integration with [EvoLib](https://github.com/EvoLib/evo-lib).
7
- It supports dynamic topologies, recurrent connections, and is optimized for mutation, crossover, and structural evolution.
8
-
9
- ## 🪪 License
10
-
11
- This project is licensed under the [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
12
-
@@ -1,117 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """
3
- Core class for evolvable neural networks.
4
-
5
- Manages neurons, connections, and forward computation. Prepares mutation, crossover, and
6
- export interfaces.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import numpy as np
12
-
13
- from evonet.connection import Connection
14
- from evonet.neuron import Neuron
15
- from evonet.types import NeuronRole
16
-
17
-
18
- class Nnet:
19
- """
20
- Evolvable neural network with explicit topology.
21
-
22
- Attributes:
23
- neurons (list[Neuron]): All neurons in the network.
24
- connections (list[Connection]): All directed, weighted edges.
25
- input_neurons (list[Neuron]): Subset of neurons used as input nodes.
26
- output_neurons (list[Neuron]): Subset of neurons used as output nodes.
27
- """
28
-
29
- def __init__(self) -> None:
30
- self.neurons: list[Neuron] = []
31
- self.connections: list[Connection] = []
32
- self.input_neurons: list[Neuron] = []
33
- self.output_neurons: list[Neuron] = []
34
-
35
- def add_neuron(self, neuron: Neuron, role: NeuronRole = NeuronRole.HIDDEN) -> None:
36
- """Adds a neuron to the network and assigns its functional role."""
37
- self.neurons.append(neuron)
38
- if role == NeuronRole.INPUT:
39
- self.input_neurons.append(neuron)
40
- elif role == NeuronRole.OUTPUT:
41
- self.output_neurons.append(neuron)
42
-
43
- def add_connection(self, conn: Connection) -> None:
44
- """Adds a connection and updates neuron references."""
45
- self.connections.append(conn)
46
- conn.target.incoming.append(conn)
47
- conn.source.outgoing.append(conn)
48
-
49
- def reset(self) -> None:
50
- """Resets output values of all neurons (before forward pass)."""
51
- for neuron in self.neurons:
52
- neuron.reset()
53
-
54
- def calc(self, input_values: list[float]) -> list[float]:
55
- """
56
- Forward pass through the network.
57
-
58
- Args:
59
- input_values: values to assign to input neurons
60
-
61
- Returns:
62
- outputs from output neurons (after activation)
63
- """
64
- assert len(input_values) == len(self.input_neurons), "Input size mismatch"
65
- self.reset()
66
-
67
- # Assign inputs
68
- for i, value in enumerate(input_values):
69
- self.input_neurons[i].output = float(value)
70
-
71
- # Topological forward computation (assumes acyclic)
72
- visited = set(self.input_neurons)
73
- queue = [n for n in self.neurons if n not in visited]
74
-
75
- while queue:
76
- progressed = False
77
- for neuron in queue[:]:
78
- if all(src.source in visited for src in neuron.incoming):
79
- total = sum(c.get_signal() for c in neuron.incoming) + neuron.bias
80
- neuron.output = neuron.activation(total)
81
- visited.add(neuron)
82
- queue.remove(neuron)
83
- progressed = True
84
- if not progressed:
85
- break # Prevent infinite loop
86
-
87
- return [n.output for n in self.output_neurons]
88
-
89
- def __repr__(self) -> str:
90
- return (
91
- f"<Nnet | {len(self.neurons)} neurons, "
92
- f"{len(self.connections)} connections>"
93
- )
94
-
95
- def get_weights(self) -> np.ndarray:
96
- """Returns all connection weights as a flat NumPy array."""
97
- import numpy as np
98
-
99
- return np.array([c.weight for c in self.connections], dtype=float)
100
-
101
- def set_weights(self, vector: np.ndarray) -> None:
102
- """Assigns connection weights from a flat NumPy array."""
103
- assert len(vector) == len(self.connections), "Weight vector length mismatch"
104
- for i, c in enumerate(self.connections):
105
- c.weight = float(vector[i])
106
-
107
- def get_biases(self) -> np.ndarray:
108
- """Returns all neuron biases as a flat NumPy array."""
109
- import numpy as np
110
-
111
- return np.array([n.bias for n in self.neurons], dtype=float)
112
-
113
- def set_biases(self, vector: np.ndarray) -> None:
114
- """Assigns neuron biases from a flat NumPy array."""
115
- assert len(vector) == len(self.neurons), "Bias vector length mismatch"
116
- for i, n in enumerate(self.neurons):
117
- n.bias = float(vector[i])
@@ -1,59 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- """
3
- Mutation operators for evolvable neural networks.
4
-
5
- Includes mutations for weights, biases, and (optionally) activation functions. Structure
6
- mutations will be implemented separately.
7
- """
8
-
9
- import random
10
-
11
- from evonet.core import Nnet
12
-
13
-
14
- def mutate_weights(
15
- net: Nnet,
16
- std: float = 0.1,
17
- mutation_rate: float = 1.0,
18
- weight_min: float = -5.0,
19
- weight_max: float = 5.0,
20
- ) -> None:
21
- """
22
- Applies Gaussian noise to connection weights.
23
-
24
- Args:
25
- net (Nnet): The network to mutate.
26
- std (float): Standard deviation of noise.
27
- mutation_rate (float): Probability per connection to mutate.
28
- weight_min (float): Lower bound for weights.
29
- weight_max (float): Upper bound for weights.
30
- """
31
- for conn in net.connections:
32
- if random.random() < mutation_rate:
33
- noise = random.gauss(0.0, std)
34
- conn.weight += noise
35
- conn.weight = max(min(conn.weight, weight_max), weight_min)
36
-
37
-
38
- def mutate_biases(
39
- net: Nnet,
40
- std: float = 0.1,
41
- mutation_rate: float = 1.0,
42
- bias_min: float = -5.0,
43
- bias_max: float = 5.0,
44
- ) -> None:
45
- """
46
- Applies Gaussian noise to neuron biases.
47
-
48
- Args:
49
- net (Nnet): The network to mutate.
50
- std (float): Standard deviation of noise.
51
- mutation_rate (float): Probability per neuron to mutate.
52
- bias_min (float): Lower bound for biases.
53
- bias_max (float): Upper bound for biases.
54
- """
55
- for neuron in net.neurons:
56
- if random.random() < mutation_rate:
57
- noise = random.gauss(0.0, std)
58
- neuron.bias += noise
59
- neuron.bias = max(min(neuron.bias, bias_max), bias_min)
@@ -1,42 +0,0 @@
1
- from evonet.connection import Connection
2
- from evonet.core import Nnet
3
- from evonet.neuron import Neuron
4
- from evonet.types import NeuronRole
5
-
6
-
7
- def test_forward_pass_identity() -> None:
8
- """Testet ein einfaches Netz mit einem Input und einem Output."""
9
- net = Nnet()
10
-
11
- # Neuronen erstellen
12
- n_input = Neuron(activation="linear")
13
- n_output = Neuron(activation="linear")
14
-
15
- # Netz aufbauen
16
- net.add_neuron(n_input, role=NeuronRole.INPUT)
17
- net.add_neuron(n_output, role=NeuronRole.OUTPUT)
18
- net.add_connection(Connection(n_input, n_output, weight=1.0))
19
-
20
- # Eingabe --> Ausgabe testen
21
- x = [0.75]
22
- y = net.calc(x)
23
-
24
- assert isinstance(y, list)
25
- assert len(y) == 1
26
- assert abs(y[0] - 0.75) < 1e-6
27
-
28
-
29
- def test_forward_pass_with_bias() -> None:
30
- """Testet Netz mit Bias am Output-Neuron."""
31
- net = Nnet()
32
-
33
- n_input = Neuron(activation="linear")
34
- n_output = Neuron(activation="linear", bias=0.5)
35
-
36
- net.add_neuron(n_input, role=NeuronRole.INPUT)
37
- net.add_neuron(n_output, role=NeuronRole.OUTPUT)
38
- net.add_connection(Connection(n_input, n_output, weight=2.0))
39
-
40
- y = net.calc([1.0])
41
-
42
- assert abs(y[0] - 2.5) < 1e-6 # (1.0 * 2.0) + 0.5 = 2.5
File without changes
File without changes