evonet 0.1.0.dev4__tar.gz → 0.1.0.dev6__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 (25) hide show
  1. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/PKG-INFO +1 -1
  2. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/core.py +77 -35
  3. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/mutation.py +71 -10
  4. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/neuron.py +14 -7
  5. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet.egg-info/PKG-INFO +1 -1
  6. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/pyproject.toml +1 -1
  7. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/tests/test_recurrent_dynamics.py +17 -14
  8. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/LICENSE +0 -0
  9. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/README.md +0 -0
  10. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/__init__.py +0 -0
  11. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/activation.py +0 -0
  12. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/connection.py +0 -0
  13. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/enums.py +0 -0
  14. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/io.py +0 -0
  15. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/layer.py +0 -0
  16. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/utils.py +0 -0
  17. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet/visualize.py +0 -0
  18. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet.egg-info/SOURCES.txt +0 -0
  19. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet.egg-info/dependency_links.txt +0 -0
  20. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet.egg-info/requires.txt +0 -0
  21. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/evonet.egg-info/top_level.txt +0 -0
  22. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/setup.cfg +0 -0
  23. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/tests/test_activation.py +0 -0
  24. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/tests/test_core.py +0 -0
  25. {evonet-0.1.0.dev4 → evonet-0.1.0.dev6}/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.dev4
3
+ Version: 0.1.0.dev6
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -9,11 +9,13 @@ export interfaces.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ from typing import Optional
13
+
12
14
  import graphviz
13
15
  import numpy as np
14
16
 
15
17
  from evonet.connection import Connection
16
- from evonet.enums import ConnectionType, NeuronRole
18
+ from evonet.enums import ConnectionType, NeuronRole, RecurrentKind
17
19
  from evonet.layer import Layer
18
20
  from evonet.neuron import Neuron
19
21
 
@@ -99,24 +101,23 @@ class Nnet:
99
101
  role: NeuronRole = NeuronRole.HIDDEN,
100
102
  count: int = 1,
101
103
  connect_layer: bool = True,
102
- ) -> Neuron:
104
+ recurrent: Optional[set[RecurrentKind]] = None,
105
+ ) -> list[Neuron]:
103
106
  """
104
- Add one or more neurons to a specified layer.
107
+ Add one or more neurons to the network.
105
108
 
106
109
  Args:
107
- layer_idx (int | None): Index of the target layer. If None, uses last layer.
110
+ layer_idx (int | None): Target layer index. Defaults to last layer.
108
111
  activation (str): Activation function name.
109
112
  bias (float): Initial bias value.
110
- label (str): Optional label for visualization/debugging.
113
+ label (str): Optional label.
111
114
  role (NeuronRole): Role of the neuron (INPUT, HIDDEN, OUTPUT).
112
- count (int): Number of neurons to add.
113
- connect_layer (bool): Whether to auto-connect to adjacent layers.
115
+ count (int): Number of neurons to add (default: 1).
116
+ connect_layer (bool): If True, auto-connect to adjacent layers.
117
+ recurrent (set[RecurrentKind] | None): Optional recurrent connection types.
114
118
 
115
119
  Returns:
116
- Neuron: Reference to the last added neuron.
117
-
118
- Raises:
119
- ValueError: If layer index is invalid.
120
+ list[Neuron]: List of added neurons.
120
121
  """
121
122
 
122
123
  if layer_idx is None:
@@ -127,31 +128,67 @@ class Nnet:
127
128
  if layer_idx >= len(self.layers):
128
129
  raise ValueError(f"Layer index out of bounds: {layer_idx}")
129
130
 
130
- for _ in range(count):
131
+ target_layer = self.layers[layer_idx]
132
+ new_neurons: list[Neuron] = []
133
+
134
+ # Create neurons without connections
135
+ for i in range(count):
131
136
  neuron = Neuron(activation=activation, bias=bias)
132
137
  neuron.role = role
133
138
  neuron.label = label
134
-
135
- self.layers[layer_idx].neurons.append(neuron)
136
-
137
- if connect_layer and layer_idx > 0:
138
- # Finde letzten nicht-leeren Layer vor diesem
139
- for prev_idx in range(layer_idx - 1, -1, -1):
140
- prev_layer = self.layers[prev_idx]
141
- if prev_layer.neurons:
142
- for prev_neuron in prev_layer.neurons:
143
- self.add_connection(prev_neuron, neuron)
144
- break
145
-
146
- # connect to next non-empty layer
147
- if connect_layer and role == NeuronRole.HIDDEN:
148
- for next_idx in range(layer_idx + 1, len(self.layers)):
149
- next_layer = self.layers[next_idx]
150
- if next_layer.neurons:
151
- for next_neuron in next_layer.neurons:
152
- self.add_connection(neuron, next_neuron)
153
- break
154
- return neuron
139
+ target_layer.neurons.append(neuron)
140
+ new_neurons.append(neuron)
141
+
142
+ # Connect to previous layer (feedforward)
143
+ if connect_layer and layer_idx > 0:
144
+ for prev_neuron in self.layers[layer_idx - 1].neurons:
145
+ for n in new_neurons:
146
+ self.add_connection(prev_neuron, n)
147
+
148
+ # Connect to next layer (feedforward)
149
+ if (
150
+ connect_layer
151
+ and role == NeuronRole.HIDDEN
152
+ and layer_idx < len(self.layers) - 1
153
+ ):
154
+
155
+ for next_neuron in self.layers[layer_idx + 1].neurons:
156
+ for n in new_neurons:
157
+ self.add_connection(n, next_neuron)
158
+
159
+ # Recurrent connections
160
+ if recurrent:
161
+ if RecurrentKind.DIRECT in recurrent:
162
+ for n in new_neurons:
163
+ if n.role == NeuronRole.HIDDEN:
164
+ self.add_connection(n, n, conn_type=ConnectionType.RECURRENT)
165
+
166
+ if RecurrentKind.LATERAL in recurrent:
167
+ # All neurons in this layer (old + new)
168
+ full_layer = list(self.layers[layer_idx].neurons)
169
+
170
+ for src in full_layer:
171
+ for dst in new_neurons:
172
+ if src is not dst:
173
+ self.add_connection(
174
+ src, dst, conn_type=ConnectionType.RECURRENT
175
+ )
176
+
177
+ if RecurrentKind.INDIRECT in recurrent:
178
+ for src in new_neurons:
179
+ for lower_layer in self.layers[1:layer_idx]:
180
+ for dst in lower_layer.neurons:
181
+ self.add_connection(
182
+ src, dst, conn_type=ConnectionType.RECURRENT
183
+ )
184
+ for higher_layer in self.layers[layer_idx + 1 :]:
185
+ for src in higher_layer.neurons:
186
+ for dst in new_neurons:
187
+ self.add_connection(
188
+ src, dst, conn_type=ConnectionType.RECURRENT
189
+ )
190
+
191
+ return new_neurons
155
192
 
156
193
  def add_connection(
157
194
  self,
@@ -177,11 +214,11 @@ class Nnet:
177
214
  source.outgoing.append(conn)
178
215
  target.incoming.append(conn)
179
216
 
180
- def reset(self) -> None:
217
+ def reset(self, full: bool = False) -> None:
181
218
  """Reset all neurons (clears input, output, and caches)."""
182
219
  for layer in self.layers:
183
220
  for neuron in layer.neurons:
184
- neuron.reset()
221
+ neuron.reset(full=full)
185
222
 
186
223
  def calc(self, input_values: list[float]) -> list[float]:
187
224
  """
@@ -197,6 +234,11 @@ class Nnet:
197
234
  AssertionError: If input size does not match input layer.
198
235
  """
199
236
 
237
+ # Save LAST OUTPUT
238
+ for layer in self.layers:
239
+ for neuron in layer.neurons:
240
+ neuron.last_output = neuron.output
241
+
200
242
  self.reset()
201
243
 
202
244
  # Set inputs
@@ -10,7 +10,7 @@ Includes:
10
10
 
11
11
  import random
12
12
  from collections.abc import Collection
13
- from typing import Optional
13
+ from typing import Literal, Optional, Union, cast
14
14
 
15
15
  import numpy as np
16
16
 
@@ -20,6 +20,8 @@ from evonet.core import Nnet
20
20
  from evonet.enums import ConnectionType, NeuronRole, RecurrentKind
21
21
  from evonet.neuron import Neuron
22
22
 
23
+ ALL_ACTIVATIONS = "all"
24
+
23
25
 
24
26
  def mutate_activation(neuron: Neuron, activations: list[str] | None = None) -> None:
25
27
  """
@@ -34,19 +36,78 @@ def mutate_activation(neuron: Neuron, activations: list[str] | None = None) -> N
34
36
 
35
37
 
36
38
  def mutate_activations(
37
- net: Nnet, probability: float = 1.0, activations: list[str] | None = None
39
+ net: Nnet,
40
+ probability: float = 1.0,
41
+ activations: Optional[list[str]] = None,
42
+ layers: Optional[dict[int, Union[list[str], Literal["all"]]]] = None,
38
43
  ) -> None:
39
44
  """
40
- Mutate the activation functions of non-input neurons in the network.
45
+ Mutate the activation functions of neurons with optional global or layer-specific
46
+ control.
47
+
48
+ Modes:
49
+ A) Default: Hidden layers are mutated using all available activation functions.
50
+ B) With `activations`: Hidden layers use a restricted set of activation
51
+ functions.
52
+ C) With `layers`: Only specified layers are mutated, each with their own
53
+ activation sets. This mode overrides the `activations` argument.
41
54
 
42
55
  Args:
43
- net (Nnet): The target network.
44
- probability (float): Mutation probability per neuron.
45
- activations (list[str] | None): Optional subset of allowed activation functions.
56
+ net (Nnet): The target neural network.
57
+ probability (float): Mutation probability per neuron (must be in [0, 1]).
58
+ activations (list[str] | None): Optional list of allowed activation functions
59
+ (used in mode B).
60
+ layers (dict[int, list[str] | Literal["all"]] | None):
61
+ Optional per-layer activation sets (mode C).
62
+ If specified, overrides the `activations` argument.
63
+
64
+ Notes:
65
+ - Input and output layers are not mutated unless explicitly included
66
+ in `layers`.
67
+ - The function relies on `numpy.random.rand()` for random number generation.
68
+ - The `mutate_activation` function is called to perform the actual mutation of
69
+ a neuron's activation function.
70
+
71
+ Raises:
72
+ ValueError: If `probability` is not in [0, 1] or
73
+ if `layers` contains invalid layer indices.
74
+
75
+ Example:
76
+ >>> net = Nnet(...) # Assume a neural network with 3 layers
77
+ >>> mutate_activations(net, probability=0.5, activations=["relu", "sigmoid"])
78
+ >>> mutate_activations(net, layers={1: ["relu"], 2: "all"}, probability=0.3)
46
79
  """
47
- for neuron in net.get_all_neurons():
48
- if neuron.role != NeuronRole.INPUT and np.random.rand() < probability:
49
- mutate_activation(neuron, activations)
80
+
81
+ if not 0 <= probability <= 1:
82
+ raise ValueError("Probability must be between 0 and 1.")
83
+
84
+ if layers is not None:
85
+ max_layer = len(net.layers)
86
+ invalid_layers = [i for i in layers if i < 0 or i >= max_layer]
87
+ if invalid_layers:
88
+ raise ValueError(
89
+ f"Invalid layer indices: {invalid_layers}."
90
+ f"Must be between 0 and {max_layer - 1}."
91
+ )
92
+
93
+ for layer_idx, layer in enumerate(net.layers):
94
+ if layers is not None:
95
+ if layer_idx not in layers:
96
+ continue
97
+
98
+ if layers[layer_idx] != ALL_ACTIVATIONS:
99
+ layer_activations = cast(list[str], layers[layer_idx])
100
+ else:
101
+ layer_activations = None # None: mutate_activation uses all activations
102
+
103
+ elif layer_idx == 0 or layer_idx == len(net.layers) - 1:
104
+ continue
105
+ else:
106
+ layer_activations = activations
107
+
108
+ for neuron in layer.neurons:
109
+ if np.random.rand() < probability:
110
+ mutate_activation(neuron, layer_activations)
50
111
 
51
112
 
52
113
  def mutate_weight(conn: Connection, std: float = 0.1) -> None:
@@ -267,7 +328,7 @@ def split_connection(net: Nnet, activation: str = "tanh", noise: float = 0.1) ->
267
328
  role=NeuronRole.HIDDEN,
268
329
  activation=activation,
269
330
  connect_layer=False,
270
- )
331
+ )[0]
271
332
 
272
333
  # Set new connections
273
334
  src.outgoing.remove(conn)
@@ -55,18 +55,25 @@ class Neuron:
55
55
  self.last_output: float = 0.0
56
56
  self.label = label
57
57
 
58
- def reset(self) -> None:
58
+ def reset(self, full: bool = False) -> None:
59
59
  """
60
- Reset the neuron's state for a new forward pass.
60
+ Clear current neuron state for a new forward pass.
61
61
 
62
- Sets:
63
- - last_output = output
64
- - output = 0.0
65
- - input = 0.0
62
+ Args:
63
+ full (bool): If True, also clears `last_output` (used for recurrent memory).
64
+
65
+ This method:
66
+ - Always resets `input` and `output`.
67
+ - Only resets `last_output` if `full=True`.
68
+
69
+ Use `full=True` if the neuron should forget its prior state
70
+ (e.g. after fitness evaluation).
66
71
  """
67
- self.last_output = self.output
72
+
68
73
  self.output = 0.0
69
74
  self.input = 0.0
75
+ if full:
76
+ self.last_output = 0.0
70
77
 
71
78
  def __repr__(self) -> str:
72
79
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0.dev4
3
+ Version: 0.1.0.dev6
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "evonet"
7
- version = "0.1.0.dev4"
7
+ version = "0.1.0.dev6"
8
8
  description = "Evolvable neural network core for integration with EvoLib"
9
9
  authors = [
10
10
  { name = "EvoLib", email = "evolib@dismail.de" }
@@ -1,10 +1,11 @@
1
1
  import numpy as np
2
2
 
3
3
  from evonet.core import Nnet
4
- from evonet.enums import NeuronRole, ConnectionType
4
+ from evonet.enums import ConnectionType, NeuronRole
5
+ from evonet.neuron import Neuron
5
6
 
6
7
 
7
- def _mk_linear_ih_h_o() -> tuple[Nnet, object, object, object]:
8
+ def _mk_linear_ih_h_o() -> tuple[Nnet, Neuron, Neuron, Neuron]:
8
9
  """
9
10
  Build a minimal 3-layer net (I-H-O), all linear activations, no auto-connections.
10
11
 
@@ -13,18 +14,20 @@ def _mk_linear_ih_h_o() -> tuple[Nnet, object, object, object]:
13
14
  net = Nnet()
14
15
  net.add_layer(3) # input, hidden, output
15
16
 
16
- n_in = net.add_neuron(layer_idx=0,
17
- role=NeuronRole.INPUT,
18
- activation="linear",
19
- connect_layer=False)
20
- n_hid = net.add_neuron(layer_idx=1,
21
- role=NeuronRole.HIDDEN,
22
- activation="linear",
23
- connect_layer=False)
24
- n_out = net.add_neuron(layer_idx=2,
25
- role=NeuronRole.OUTPUT,
26
- activation="linear",
27
- connect_layer=False)
17
+ neurons: list[Neuron] = net.add_neuron(
18
+ layer_idx=0, role=NeuronRole.INPUT, activation="linear", connect_layer=False
19
+ )
20
+ n_in = neurons[0]
21
+
22
+ neurons = net.add_neuron(
23
+ layer_idx=1, role=NeuronRole.HIDDEN, activation="linear", connect_layer=False
24
+ )
25
+ n_hid = neurons[0]
26
+
27
+ neurons = net.add_neuron(
28
+ layer_idx=2, role=NeuronRole.OUTPUT, activation="linear", connect_layer=False
29
+ )
30
+ n_out = neurons[0]
28
31
 
29
32
  return net, n_in, n_hid, n_out
30
33
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes