evonet 0.1.0a0.dev2__tar.gz → 0.1.0.dev4__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 (30) hide show
  1. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/PKG-INFO +6 -5
  2. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/README.md +4 -4
  3. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/activation.py +29 -8
  4. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/connection.py +25 -9
  5. evonet-0.1.0.dev4/evonet/core.py +472 -0
  6. evonet-0.1.0.dev4/evonet/enums.py +50 -0
  7. evonet-0.1.0.dev4/evonet/layer.py +26 -0
  8. evonet-0.1.0.dev4/evonet/mutation.py +278 -0
  9. evonet-0.1.0.dev4/evonet/neuron.py +92 -0
  10. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/PKG-INFO +6 -5
  11. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/SOURCES.txt +3 -1
  12. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/requires.txt +1 -0
  13. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/pyproject.toml +3 -2
  14. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/tests/test_core.py +4 -4
  15. evonet-0.1.0.dev4/tests/test_nnet_io_and_forward.py +139 -0
  16. evonet-0.1.0.dev4/tests/test_recurrent_dynamics.py +103 -0
  17. evonet-0.1.0a0.dev2/evonet/core.py +0 -154
  18. evonet-0.1.0a0.dev2/evonet/enums.py +0 -17
  19. evonet-0.1.0a0.dev2/evonet/layer.py +0 -10
  20. evonet-0.1.0a0.dev2/evonet/mutation.py +0 -137
  21. evonet-0.1.0a0.dev2/evonet/neuron.py +0 -62
  22. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/LICENSE +0 -0
  23. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/__init__.py +0 -0
  24. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/io.py +0 -0
  25. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/utils.py +0 -0
  26. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet/visualize.py +0 -0
  27. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/dependency_links.txt +0 -0
  28. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/evonet.egg-info/top_level.txt +0 -0
  29. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/setup.cfg +0 -0
  30. {evonet-0.1.0a0.dev2 → evonet-0.1.0.dev4}/tests/test_activation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0a0.dev2
3
+ Version: 0.1.0.dev4
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -38,6 +38,7 @@ Requires-Dist: numpy>=1.24
38
38
  Requires-Dist: pyyaml>=6.0
39
39
  Requires-Dist: pandas>=2.3.0
40
40
  Requires-Dist: pydantic<3.0,>=2.7
41
+ Requires-Dist: graphviz>=0.20.1
41
42
  Provides-Extra: dev
42
43
  Requires-Dist: mypy; extra == "dev"
43
44
  Requires-Dist: types-PyYAML; extra == "dev"
@@ -57,7 +58,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
57
58
 
58
59
  ---
59
60
 
60
- ## 🔧 Features
61
+ ## Features
61
62
 
62
63
  - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
63
64
  - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
@@ -75,7 +76,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
75
76
 
76
77
  ---
77
78
 
78
- ## 🚀 Quick Example
79
+ ## Quick Example
79
80
 
80
81
  ```python
81
82
  from evonet.core import Nnet
@@ -90,7 +91,7 @@ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_
90
91
  print(net.calc([1.0]))
91
92
  ```
92
93
 
93
- ## 🪪 License
94
+ ## License
94
95
 
95
- This project is licensed under the [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
96
+ MIT License - see [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
96
97
 
@@ -8,7 +8,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
8
8
 
9
9
  ---
10
10
 
11
- ## 🔧 Features
11
+ ## Features
12
12
 
13
13
  - **Layer-based but flexible** – allows skip connections, cycles, and recurrent paths
14
14
  - **Typed neuron roles and connection types** (`NeuronRole`, `ConnectionType`)
@@ -26,7 +26,7 @@ It supports dynamic topologies, recurrent connections, per-neuron activation, an
26
26
 
27
27
  ---
28
28
 
29
- ## 🚀 Quick Example
29
+ ## Quick Example
30
30
 
31
31
  ```python
32
32
  from evonet.core import Nnet
@@ -41,7 +41,7 @@ net.add_neuron(layer_idx=1, activation="linear", bias=0.5, lable="out", connect_
41
41
  print(net.calc([1.0]))
42
42
  ```
43
43
 
44
- ## 🪪 License
44
+ ## License
45
45
 
46
- This project is licensed under the [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
46
+ MIT License - see [MIT License](https://github.com/EvoLib/evo-net/tree/main/LICENSE).
47
47
 
@@ -2,12 +2,13 @@
2
2
  """
3
3
  Activation functions for evolvable neural networks.
4
4
 
5
- Provides scalar functions such as ReLU, Tanh, Sigmoid, as well as a softmax
6
- implementation and a function registry (ACTIVATIONS).
5
+ Includes standard nonlinearities (ReLU, Tanh, Sigmoid), linear and threshold functions,
6
+ modern smooth activations (Swish, Mish), and a softmax utility.
7
7
 
8
- These functions are stateless and operate on scalars unless otherwise noted.
8
+ All functions are scalar-based and stateless, unless noted otherwise.
9
9
  """
10
10
 
11
+ import random
11
12
  from typing import Callable, List, Tuple, Union
12
13
 
13
14
  import numpy as np
@@ -70,7 +71,7 @@ def gaussian(x: Scalar) -> float:
70
71
  return np.exp(-(x**2))
71
72
 
72
73
 
73
- # --- Threshold Functions ---
74
+ # Threshold Functions
74
75
 
75
76
 
76
77
  def binary(x: Scalar) -> float:
@@ -83,7 +84,7 @@ def signum(x: Scalar) -> float:
83
84
  return 1.0 if x > 0 else -1.0 if x < 0 else 0.0
84
85
 
85
86
 
86
- # --- Linear Variants ---
87
+ # Linear Variants
87
88
 
88
89
 
89
90
  def linear(x: Scalar) -> float:
@@ -154,9 +155,29 @@ def softmax(values: Union[List[float], Tuple[float], np.ndarray]) -> np.ndarray:
154
155
  return exp_x / np.sum(exp_x)
155
156
 
156
157
 
157
- def random_function_name() -> str:
158
- """Returns a random activation function name from the registry."""
159
- return np.random.choice(list(ACTIVATIONS.keys()))
158
+ def random_function_name(activations: list[str] | None = None) -> str:
159
+ """
160
+ Return a random activation function name from the registry.
161
+
162
+ Args:
163
+ activations (list[str] | None): Optional subset of allowed function names.
164
+ If None, all registered activation names are considered.
165
+
166
+ Raises:
167
+ ValueError: If any name is not in the activation registry.
168
+
169
+ Returns:
170
+ str: Randomly selected activation function name.
171
+ """
172
+
173
+ if activations is None:
174
+ activations = list(ACTIVATIONS.keys())
175
+
176
+ invalid = set(activations) - set(ACTIVATIONS.keys())
177
+ if invalid:
178
+ raise ValueError(f"Invalid activation functions: {invalid}")
179
+
180
+ return random.choice(activations)
160
181
 
161
182
 
162
183
  # Registry
@@ -1,11 +1,13 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  """
3
- Connection between neurons in evolvable neural network.
3
+ Connection between neurons in an evolvable neural network.
4
4
 
5
- A connection links a source neuron to a target neuron with a weight. Supports optional
6
- connection types for future use (e.g. inhibitory).
5
+ A connection links a source neuron to a target neuron and transmits a weighted signal.
6
+ Supports optional connection types for specialized behaviors (e.g. inhibitory,
7
+ recurrent).
7
8
  """
8
9
 
10
+
9
11
  from typing import TYPE_CHECKING
10
12
 
11
13
  from evonet.enums import ConnectionType
@@ -21,9 +23,10 @@ class Connection:
21
23
  Attributes:
22
24
  source (Neuron): The source neuron (presynaptic).
23
25
  target (Neuron): The target neuron (postsynaptic).
24
- weight (float): Multiplicative weight of the signal.
25
- delay (int): Optional delay in steps (not yet used).
26
- type (ConnectionType): Type of the connection (e.g. excitatory).
26
+ 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).
27
30
  """
28
31
 
29
32
  def __init__(
@@ -42,13 +45,26 @@ class Connection:
42
45
  self.type: ConnectionType = conn_type
43
46
 
44
47
  def get_signal(self) -> float:
45
- """Computes the weighted signal from the source neuron."""
48
+ """
49
+ Return the weighted signal from the source neuron.
50
+
51
+ Returns:
52
+ float: source.output × weight
53
+ """
54
+
46
55
  return self.source.output * self.weight
47
56
 
48
57
  def __repr__(self) -> str:
58
+ """
59
+ Return a concise string representation of the connection.
60
+
61
+ Example:
62
+ <Conn abc123 -> def456 w=0.85 type=standard>
63
+ """
64
+
49
65
  type_str = self.type.name.lower()
50
66
  return (
51
- f"<Conn {self.source.id[:4]} "
52
- f"-> {self.target.id[:4]} "
67
+ f"<Conn {self.source.id[:6]} "
68
+ f"-> {self.target.id[:6]} "
53
69
  f"w={self.weight:.2f} type={type_str}>"
54
70
  )
@@ -0,0 +1,472 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ Core class for evolvable neural networks.
4
+
5
+ Manages neurons, layers, and connections with explicit topology. Supports forward passes
6
+ with optional recurrent connections across time steps, mutation/crossover hooks, and
7
+ export interfaces.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import graphviz
13
+ import numpy as np
14
+
15
+ from evonet.connection import Connection
16
+ from evonet.enums import ConnectionType, NeuronRole
17
+ from evonet.layer import Layer
18
+ from evonet.neuron import Neuron
19
+
20
+
21
+ class Nnet:
22
+ """
23
+ Evolvable neural network with explicit layered topology.
24
+
25
+ Attributes:
26
+ layers (list[Layer]): Ordered list of network layers.
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ self.layers: list[Layer] = []
31
+
32
+ @property
33
+ def num_weights(self) -> int:
34
+ """Number of connections in the network (no allocation)."""
35
+ return len(self.get_all_connections())
36
+
37
+ @property
38
+ def num_biases(self) -> int:
39
+ """
40
+ Return the number of trainable biases (excludes input neurons).
41
+
42
+ Inputs are feature holders and have no trainable bias in this design.
43
+ """
44
+ count = 0
45
+ for layer in self.layers:
46
+ for neuron in layer.neurons:
47
+ if neuron.role is not NeuronRole.INPUT:
48
+ count += 1
49
+ return count
50
+
51
+ @property
52
+ def num_params(self) -> int:
53
+ """Total parameter count = weights + biases."""
54
+ return self.num_weights + self.num_biases
55
+
56
+ def add_layer(self, count: int = 1) -> int:
57
+ """
58
+ Append one or more empty layers to the network.
59
+
60
+ Args:
61
+ count (int): Number of layers to add (must be > 0).
62
+
63
+ Returns:
64
+ int: Index of the last added layer.
65
+
66
+ Raises:
67
+ ValueError: If count is not positive.
68
+ """
69
+
70
+ if count <= 0:
71
+ raise ValueError("Number of layers must be greater then zero")
72
+
73
+ for _ in range(count):
74
+ self.layers.append(Layer())
75
+
76
+ return len(self.layers) - 1
77
+
78
+ def insert_layer(self, index: int) -> None:
79
+ """
80
+ Insert an empty layer at a given index.
81
+
82
+ Args:
83
+ index (int): Position to insert the new layer (0 = before input).
84
+
85
+ Raises:
86
+ ValueError: If index is out of bounds.
87
+ """
88
+
89
+ if not (0 <= index <= len(self.layers)):
90
+ raise ValueError(f"insert_layer: index {index} out of bounds.")
91
+ self.layers.insert(index, Layer())
92
+
93
+ def add_neuron(
94
+ self,
95
+ layer_idx: int | None = None,
96
+ activation: str = "tanh",
97
+ bias: float = 0.0,
98
+ label: str = "",
99
+ role: NeuronRole = NeuronRole.HIDDEN,
100
+ count: int = 1,
101
+ connect_layer: bool = True,
102
+ ) -> Neuron:
103
+ """
104
+ Add one or more neurons to a specified layer.
105
+
106
+ Args:
107
+ layer_idx (int | None): Index of the target layer. If None, uses last layer.
108
+ activation (str): Activation function name.
109
+ bias (float): Initial bias value.
110
+ label (str): Optional label for visualization/debugging.
111
+ 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.
114
+
115
+ Returns:
116
+ Neuron: Reference to the last added neuron.
117
+
118
+ Raises:
119
+ ValueError: If layer index is invalid.
120
+ """
121
+
122
+ if layer_idx is None:
123
+ layer_idx = len(self.layers) - 1 # Add neuron to last layer
124
+
125
+ if layer_idx < 0:
126
+ raise ValueError(f"Expected positiv layerindex: got {layer_idx}")
127
+ if layer_idx >= len(self.layers):
128
+ raise ValueError(f"Layer index out of bounds: {layer_idx}")
129
+
130
+ for _ in range(count):
131
+ neuron = Neuron(activation=activation, bias=bias)
132
+ neuron.role = role
133
+ 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
155
+
156
+ def add_connection(
157
+ self,
158
+ source: Neuron,
159
+ target: Neuron,
160
+ weight: float | None = None,
161
+ conn_type: ConnectionType = ConnectionType.STANDARD,
162
+ ) -> None:
163
+ """
164
+ Create a directed connection between two neurons.
165
+
166
+ Args:
167
+ source (Neuron): Source neuron.
168
+ target (Neuron): Target neuron.
169
+ weight (float | None): Initial weight. If None, random value is used.
170
+ conn_type (ConnectionType): Type of connection (e.g. standard, recurrent).
171
+ """
172
+
173
+ if weight is None:
174
+ weight = np.random.randn() * 0.5
175
+
176
+ conn = Connection(source, target, weight=weight, conn_type=conn_type)
177
+ source.outgoing.append(conn)
178
+ target.incoming.append(conn)
179
+
180
+ def reset(self) -> None:
181
+ """Reset all neurons (clears input, output, and caches)."""
182
+ for layer in self.layers:
183
+ for neuron in layer.neurons:
184
+ neuron.reset()
185
+
186
+ def calc(self, input_values: list[float]) -> list[float]:
187
+ """
188
+ Perform a forward pass through the network.
189
+
190
+ Args:
191
+ input_values (list[float]): Input vector (must match input layer size).
192
+
193
+ Returns:
194
+ list[float]: Output values from the last layer.
195
+
196
+ Raises:
197
+ AssertionError: If input size does not match input layer.
198
+ """
199
+
200
+ self.reset()
201
+
202
+ # Set inputs
203
+ input_layer = self.layers[0]
204
+ assert len(input_layer.neurons) == len(input_values)
205
+ for i, n in enumerate(input_layer.neurons):
206
+ n.input = float(input_values[i])
207
+
208
+ # Preload recurrent contributions from previous time step (last_output)
209
+ for layer in self.layers:
210
+ for n in layer.neurons:
211
+ for c in n.incoming:
212
+ if c.type is ConnectionType.RECURRENT:
213
+ c.target.input += c.weight * c.source.last_output
214
+
215
+ # Feed-forward by layers: activate first, then propagate non-recurrent edges
216
+ for layer in self.layers:
217
+ # Activate all neurons in this layer
218
+ for n in layer.neurons:
219
+ total = n.input + n.bias
220
+ n.output = n.activation(total)
221
+
222
+ # Propagate to targets (exclude recurrent edges)
223
+ for n in layer.neurons:
224
+ for c in n.outgoing:
225
+ if c.type is not ConnectionType.RECURRENT:
226
+ c.target.input += c.weight * n.output
227
+
228
+ return [n.output for n in self.layers[-1].neurons]
229
+
230
+ def get_all_neurons(self) -> list[Neuron]:
231
+ """Return all neurons in all layers (flattened)."""
232
+ return [n for layer in self.layers for n in layer.neurons]
233
+
234
+ def get_all_connections(self) -> list[Connection]:
235
+ """Return all outgoing connections in the network."""
236
+ return [c for n in self.get_all_neurons() for c in n.outgoing]
237
+
238
+ def __repr__(self) -> str:
239
+ total_neurons = sum(len(layer.neurons) for layer in self.layers)
240
+ input_neurons = len(self.layers[0].neurons) if self.layers else 0
241
+ output_neurons = len(self.layers[-1].neurons) if len(self.layers) > 1 else 0
242
+ hidden_neurons = total_neurons - input_neurons - output_neurons
243
+
244
+ total_connections = len(self.get_all_connections())
245
+
246
+ return (
247
+ f"<Nnet | {len(self.layers)} layers, "
248
+ f"{total_neurons} neurons (I:{input_neurons} H:{hidden_neurons} "
249
+ f"O:{output_neurons}), "
250
+ f"{total_connections} connections "
251
+ )
252
+
253
+ def print_graph(
254
+ self,
255
+ name: str,
256
+ engine: str = "dot",
257
+ labels_on: bool = True,
258
+ colors_on: bool = True,
259
+ thickness_on: bool = False,
260
+ fillcolors_on: bool = False,
261
+ ) -> None:
262
+ """
263
+ Render a visual representation of the network using Graphviz.
264
+
265
+ Args:
266
+ name (str): Output file name (without extension).
267
+ engine (str): Graphviz layout engine (e.g., 'dot', 'neato').
268
+ labels_on (bool): Whether to show edge weights as labels.
269
+ colors_on (bool): Whether to color edges by sign.
270
+ thickness_on (bool): Whether to scale edge thickness by weight.
271
+ fillcolors_on (bool): Whether to color neurons by role.
272
+ """
273
+
274
+ if not self.layers:
275
+ print("No layers to visualize.")
276
+ return
277
+
278
+ dot = graphviz.Digraph(name=name, format="png", engine=engine)
279
+ dot.graph_attr.update(
280
+ bgcolor="white",
281
+ rankdir="LR",
282
+ overlap="prism",
283
+ sep="15",
284
+ ratio="fill",
285
+ splines="spline",
286
+ size="6.68,5!",
287
+ dpi="200",
288
+ )
289
+ dot.node_attr.update(
290
+ shape="circle", style="filled", fixedsize="shape", width="1.8"
291
+ )
292
+ dot.edge_attr.update(arrowsize="0.8")
293
+
294
+ # Add neurons with coordinates (x = layer, y = index)
295
+ for layer_idx, layer in enumerate(self.layers):
296
+ for neuron_idx, neuron in enumerate(layer.neurons):
297
+ if neuron.role.name == "INPUT":
298
+ fillcolor = "lightblue" if fillcolors_on else "white"
299
+ elif neuron.role.name == "OUTPUT":
300
+ fillcolor = "orange" if fillcolors_on else "white"
301
+ else:
302
+ fillcolor = "lightgreen" if fillcolors_on else "white"
303
+
304
+ label = (
305
+ f"{neuron.label or neuron.role.name}({layer_idx})\n"
306
+ f"In: {neuron.input:.3f}\n"
307
+ f"Out: {neuron.output:.3f}\n"
308
+ f"LastOut: {neuron.last_output:.3f}\n"
309
+ f"Bias: {neuron.bias:.3f}\n"
310
+ f"{neuron.activation_name}"
311
+ )
312
+
313
+ pos = f"{layer_idx},{-neuron_idx}!"
314
+ dot.node(
315
+ name=neuron.id,
316
+ label=label,
317
+ fillcolor=fillcolor,
318
+ pos=pos,
319
+ )
320
+
321
+ # Add edges
322
+ for conn in self.get_all_connections():
323
+ label = f"{conn.weight:.2f}" if labels_on else ""
324
+ color = (
325
+ "green"
326
+ if colors_on and conn.weight >= 0
327
+ else "red" if colors_on else "black"
328
+ )
329
+ penwidth = (
330
+ str(max(1, min(5, abs(conn.weight * 5)))) if thickness_on else "1"
331
+ )
332
+ style = "dashed" if conn.type.name == "RECURRENT" else "solid"
333
+
334
+ dot.edge(
335
+ conn.source.id,
336
+ conn.target.id,
337
+ label=label,
338
+ color=color,
339
+ penwidth=penwidth,
340
+ style=style,
341
+ )
342
+
343
+ dot.render(name, cleanup=True)
344
+
345
+ def _build_index_map(self) -> dict[Neuron, tuple[int, int]]:
346
+ """Build a mapping from neuron -> (layer_idx, neuron_idx) for O(1) lookups."""
347
+ index_map: dict[Neuron, tuple[int, int]] = {}
348
+ for layer_idx, layer in enumerate(self.layers):
349
+ for neuron_idx, neuron in enumerate(layer.neurons):
350
+ index_map[neuron] = (layer_idx, neuron_idx)
351
+ return index_map
352
+
353
+ def get_weights(self) -> np.ndarray:
354
+ """
355
+ Return all connection weights as a flat vector in a deterministic order.
356
+
357
+ Returns:
358
+ np.ndarray: 1D array of connection weights.
359
+
360
+ Order key:
361
+ (src_layer_idx, src_neuron_idx, dst_layer_idx,
362
+ dst_neuron_idx, connection_type)
363
+ """
364
+ conns = self.get_all_connections()
365
+ if not conns:
366
+ return np.empty(0, dtype=float)
367
+
368
+ index_map = self._build_index_map()
369
+
370
+ def sort_key(c: Connection) -> tuple[int, int, int, int, int]:
371
+ src_layer_idx, src_neuron_idx = index_map[c.source]
372
+ dst_layer_idx, dst_neuron_idx = index_map[c.target]
373
+ return (
374
+ src_layer_idx,
375
+ src_neuron_idx,
376
+ dst_layer_idx,
377
+ dst_neuron_idx,
378
+ int(c.type.value),
379
+ )
380
+
381
+ conns_sorted = sorted(conns, key=sort_key)
382
+ return np.array([c.weight for c in conns_sorted], dtype=float)
383
+
384
+ def set_weights(self, flat: np.ndarray) -> None:
385
+ """
386
+ Set all connection weights from a flat vector using the same deterministic order
387
+ as `get_weights()`.
388
+
389
+ Args:
390
+ flat (np.ndarray): Flat array of weights (must match number of connections).
391
+
392
+ Raises:
393
+ ValueError: If the length of the array does not match.
394
+ """
395
+ flat = np.asarray(flat, dtype=float).ravel()
396
+
397
+ conns = self.get_all_connections()
398
+ if not conns and flat.size == 0:
399
+ return
400
+
401
+ index_map = self._build_index_map()
402
+
403
+ def sort_key(c: Connection) -> tuple[int, int, int, int, int]:
404
+ src_layer_idx, src_neuron_idx = index_map[c.source]
405
+ dst_layer_idx, dst_neuron_idx = index_map[c.target]
406
+ return (
407
+ src_layer_idx,
408
+ src_neuron_idx,
409
+ dst_layer_idx,
410
+ dst_neuron_idx,
411
+ int(c.type.value),
412
+ )
413
+
414
+ conns_sorted = sorted(conns, key=sort_key)
415
+
416
+ if flat.size != len(conns_sorted):
417
+ raise ValueError(
418
+ f"Length mismatch for weights: expected {len(conns_sorted)}, "
419
+ f"got {flat.size}."
420
+ )
421
+
422
+ for weight_value, conn in zip(flat, conns_sorted):
423
+ conn.weight = float(weight_value)
424
+
425
+ def get_biases(self) -> np.ndarray:
426
+ """
427
+ Return all trainable biases as a flat vector (excluding input neurons).
428
+
429
+ Returns:
430
+ np.ndarray: Bias vector.
431
+
432
+ Order:
433
+ (layer_index, neuron_index) over all non-input neurons.
434
+ """
435
+
436
+ if not self.layers:
437
+ return np.empty(0, dtype=float)
438
+
439
+ biases: list[float] = []
440
+ for _, layer in enumerate(self.layers):
441
+ for _, neuron in enumerate(layer.neurons):
442
+ if neuron.role is not NeuronRole.INPUT:
443
+ biases.append(neuron.bias)
444
+ return np.asarray(biases, dtype=float)
445
+
446
+ def set_biases(self, flat: np.ndarray) -> None:
447
+ """
448
+ Set all neuron biases (excluding input neurons) from a flat vector using the
449
+ same ordering as `get_biases()`.
450
+
451
+ Args:
452
+ flat (np.ndarray): Bias values in deterministic order.
453
+
454
+ Raises:
455
+ ValueError: If length does not match the number of biases.
456
+ """
457
+ flat = np.asarray(flat, dtype=float).ravel()
458
+
459
+ # Collect non-input neurons in deterministic order
460
+ targets: list[Neuron] = []
461
+ for _, layer in enumerate(self.layers):
462
+ for _, neuron in enumerate(layer.neurons):
463
+ if neuron.role is not NeuronRole.INPUT:
464
+ targets.append(neuron)
465
+
466
+ if flat.size != len(targets):
467
+ raise ValueError(
468
+ f"Length mismatch for biases: expected {len(targets)}, got {flat.size}."
469
+ )
470
+
471
+ for b, n in zip(flat, targets):
472
+ n.bias = float(b)