evonet 0.1.0.dev8__tar.gz → 0.1.0.dev10__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.dev8 → evonet-0.1.0.dev10}/PKG-INFO +2 -1
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/activation.py +1 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/core.py +76 -4
- evonet-0.1.0.dev10/evonet/core_mit_plot_simple.py +645 -0
- evonet-0.1.0.dev10/evonet/serialization.py +171 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet.egg-info/PKG-INFO +2 -1
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet.egg-info/SOURCES.txt +2 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet.egg-info/requires.txt +1 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/pyproject.toml +3 -2
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/LICENSE +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/README.md +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/__init__.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/connection.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/enums.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/io.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/layer.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/mutation.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/neuron.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/utils.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet/visualize.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet.egg-info/dependency_links.txt +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/evonet.egg-info/top_level.txt +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/setup.cfg +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/tests/test_activation.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/tests/test_core.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/tests/test_nnet_io_and_forward.py +0 -0
- {evonet-0.1.0.dev8 → evonet-0.1.0.dev10}/tests/test_recurrent_dynamics.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.dev10
|
|
4
4
|
Summary: Evolvable neural network core for integration with EvoLib
|
|
5
5
|
Author-email: EvoLib <evolib@dismail.de>
|
|
6
6
|
License: MIT License
|
|
@@ -39,6 +39,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
39
39
|
Requires-Dist: pandas>=2.3.0
|
|
40
40
|
Requires-Dist: pydantic<3.0,>=2.7
|
|
41
41
|
Requires-Dist: graphviz>=0.20.1
|
|
42
|
+
Requires-Dist: matplotlib
|
|
42
43
|
Provides-Extra: dev
|
|
43
44
|
Requires-Dist: mypy; extra == "dev"
|
|
44
45
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
@@ -9,11 +9,13 @@ export interfaces.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
from typing import Literal, Optional
|
|
13
14
|
|
|
14
15
|
import graphviz
|
|
15
16
|
import numpy as np
|
|
16
17
|
|
|
18
|
+
from evonet.activation import softmax as softmax_vec
|
|
17
19
|
from evonet.connection import Connection
|
|
18
20
|
from evonet.enums import ConnectionType, NeuronRole, RecurrentKind
|
|
19
21
|
from evonet.layer import Layer
|
|
@@ -274,10 +276,28 @@ class Nnet:
|
|
|
274
276
|
|
|
275
277
|
# Feed-forward by layers: activate first, then propagate non-recurrent edges
|
|
276
278
|
for layer in self.layers:
|
|
279
|
+
|
|
280
|
+
# Apply softmax to all neurons with activation_name == "softmax"
|
|
281
|
+
softmax_neurons = [
|
|
282
|
+
n for n in layer.neurons if n.activation_name == "softmax"
|
|
283
|
+
]
|
|
284
|
+
if softmax_neurons:
|
|
285
|
+
if len(softmax_neurons) >= 2:
|
|
286
|
+
# Normal softmax behaviour
|
|
287
|
+
totals = [n.input + n.bias for n in softmax_neurons]
|
|
288
|
+
probabilities = softmax_vec(totals)
|
|
289
|
+
for n, p in zip(softmax_neurons, probabilities):
|
|
290
|
+
n.output = float(p)
|
|
291
|
+
else:
|
|
292
|
+
# Fallback: single softmax neuron acts like identity
|
|
293
|
+
n = softmax_neurons[0]
|
|
294
|
+
n.output = n.input + n.bias
|
|
295
|
+
|
|
277
296
|
# Activate all neurons in this layer
|
|
278
297
|
for n in layer.neurons:
|
|
279
|
-
|
|
280
|
-
|
|
298
|
+
if n.activation_name != "softmax":
|
|
299
|
+
total = n.input + n.bias
|
|
300
|
+
n.output = n.activation(total)
|
|
281
301
|
|
|
282
302
|
# Propagate to targets (exclude recurrent edges)
|
|
283
303
|
for n in layer.neurons:
|
|
@@ -310,7 +330,7 @@ class Nnet:
|
|
|
310
330
|
f"{total_connections} connections "
|
|
311
331
|
)
|
|
312
332
|
|
|
313
|
-
def
|
|
333
|
+
def plot(
|
|
314
334
|
self,
|
|
315
335
|
name: str,
|
|
316
336
|
engine: str = "dot",
|
|
@@ -344,7 +364,7 @@ class Nnet:
|
|
|
344
364
|
ratio="fill",
|
|
345
365
|
splines="spline",
|
|
346
366
|
size="6.68,5!",
|
|
347
|
-
dpi="
|
|
367
|
+
dpi="600",
|
|
348
368
|
)
|
|
349
369
|
dot.node_attr.update(
|
|
350
370
|
shape="circle", style="filled", fixedsize="shape", width="1.8"
|
|
@@ -530,3 +550,55 @@ class Nnet:
|
|
|
530
550
|
|
|
531
551
|
for b, n in zip(flat, targets):
|
|
532
552
|
n.bias = float(b)
|
|
553
|
+
|
|
554
|
+
def save(self, path: str) -> None:
|
|
555
|
+
"""
|
|
556
|
+
Save this network to a file.
|
|
557
|
+
|
|
558
|
+
The file format is chosen automatically based on the extension:
|
|
559
|
+
- .yaml / .yml --> YAML (human-readable, recommended)
|
|
560
|
+
- .json --> JSON (machine-friendly)
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
path (str): Output file path.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
from . import serialization # local import to avoid circular import
|
|
567
|
+
|
|
568
|
+
suffix = Path(path).suffix.lower()
|
|
569
|
+
if suffix in (".yaml", ".yml"):
|
|
570
|
+
serialization.save_yaml(self, path)
|
|
571
|
+
elif suffix == ".json":
|
|
572
|
+
serialization.save_json(self, path)
|
|
573
|
+
else:
|
|
574
|
+
raise ValueError(
|
|
575
|
+
f"Unsupported file extension '{suffix}'. Use .yaml, .yml or .json"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
@classmethod
|
|
579
|
+
def load(cls, path: str) -> "Nnet":
|
|
580
|
+
"""
|
|
581
|
+
Load a network from a file.
|
|
582
|
+
|
|
583
|
+
The file format is chosen automatically based on the extension:
|
|
584
|
+
- .yaml / .yml --> YAML
|
|
585
|
+
- .json --> JSON
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
path (str): Path to the serialized network file.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Nnet: The reconstructed network.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
from . import serialization # local import to avoid circular import
|
|
595
|
+
|
|
596
|
+
suffix = Path(path).suffix.lower()
|
|
597
|
+
if suffix in (".yaml", ".yml"):
|
|
598
|
+
return serialization.load_yaml(path)
|
|
599
|
+
elif suffix == ".json":
|
|
600
|
+
return serialization.load_json(path)
|
|
601
|
+
else:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"Unsupported file extension '{suffix}'. Use .yaml, .yml or .json"
|
|
604
|
+
)
|
|
@@ -0,0 +1,645 @@
|
|
|
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
|
+
from typing import Literal, Optional
|
|
13
|
+
|
|
14
|
+
import graphviz
|
|
15
|
+
import numpy as np
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
from evonet.activation import softmax as softmax_vec
|
|
19
|
+
from evonet.connection import Connection
|
|
20
|
+
from evonet.enums import ConnectionType, NeuronRole, RecurrentKind
|
|
21
|
+
from evonet.layer import Layer
|
|
22
|
+
from evonet.neuron import Neuron
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Nnet:
|
|
26
|
+
"""
|
|
27
|
+
Evolvable neural network with explicit layered topology.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
layers (list[Layer]): Ordered list of network layers.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self.layers: list[Layer] = []
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def num_weights(self) -> int:
|
|
38
|
+
"""Number of connections in the network (no allocation)."""
|
|
39
|
+
return len(self.get_all_connections())
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def num_biases(self) -> int:
|
|
43
|
+
"""
|
|
44
|
+
Return the number of trainable biases (excludes input neurons).
|
|
45
|
+
|
|
46
|
+
Inputs are feature holders and have no trainable bias in this design.
|
|
47
|
+
"""
|
|
48
|
+
count = 0
|
|
49
|
+
for layer in self.layers:
|
|
50
|
+
for neuron in layer.neurons:
|
|
51
|
+
if neuron.role is not NeuronRole.INPUT:
|
|
52
|
+
count += 1
|
|
53
|
+
return count
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def num_params(self) -> int:
|
|
57
|
+
"""Total parameter count = weights + biases."""
|
|
58
|
+
return self.num_weights + self.num_biases
|
|
59
|
+
|
|
60
|
+
def add_layer(self, count: int = 1) -> int:
|
|
61
|
+
"""
|
|
62
|
+
Append one or more empty layers to the network.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
count (int): Number of layers to add (must be > 0).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
int: Index of the last added layer.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If count is not positive.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if count <= 0:
|
|
75
|
+
raise ValueError("Number of layers must be greater then zero")
|
|
76
|
+
|
|
77
|
+
for _ in range(count):
|
|
78
|
+
self.layers.append(Layer())
|
|
79
|
+
|
|
80
|
+
return len(self.layers) - 1
|
|
81
|
+
|
|
82
|
+
def insert_layer(self, index: int) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Insert an empty layer at a given index.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
index (int): Position to insert the new layer (0 = before input).
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If index is out of bounds.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
if not (0 <= index <= len(self.layers)):
|
|
94
|
+
raise ValueError(f"insert_layer: index {index} out of bounds.")
|
|
95
|
+
self.layers.insert(index, Layer())
|
|
96
|
+
|
|
97
|
+
def add_neuron(
|
|
98
|
+
self,
|
|
99
|
+
layer_idx: int | None = None,
|
|
100
|
+
activation: str = "tanh",
|
|
101
|
+
bias: float = 0.0,
|
|
102
|
+
label: str = "",
|
|
103
|
+
role: NeuronRole = NeuronRole.HIDDEN,
|
|
104
|
+
count: int = 1,
|
|
105
|
+
connection_init: Literal["random", "zero", "none"] = "random",
|
|
106
|
+
recurrent: Optional[set[RecurrentKind]] = None,
|
|
107
|
+
) -> list[Neuron]:
|
|
108
|
+
"""
|
|
109
|
+
Add one or more neurons to the network.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
layer_idx: Target layer index. Defaults to last layer.
|
|
113
|
+
activation: Activation function name.
|
|
114
|
+
bias: Initial bias value.
|
|
115
|
+
label: Optional label.
|
|
116
|
+
role: Role of the neuron (INPUT, HIDDEN, OUTPUT).
|
|
117
|
+
count: Number of neurons to add (default: 1).
|
|
118
|
+
connection_init:
|
|
119
|
+
"random" – connect with random weights (feedforward + recurrent)
|
|
120
|
+
"zero" – connect with weight 0.0 (feedforward + recurrent)
|
|
121
|
+
"none" – do not create connections (feedforward + recurrent)
|
|
122
|
+
recurrent: Optional recurrent connection types.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
list[Neuron]: List of added neurons.
|
|
126
|
+
"""
|
|
127
|
+
if layer_idx is None:
|
|
128
|
+
layer_idx = len(self.layers) - 1 # Add neuron to last layer
|
|
129
|
+
|
|
130
|
+
if layer_idx < 0:
|
|
131
|
+
raise ValueError(f"Layer index must be >= 0 (got {layer_idx})")
|
|
132
|
+
if layer_idx >= len(self.layers):
|
|
133
|
+
raise ValueError(f"Layer index out of bounds: {layer_idx}")
|
|
134
|
+
|
|
135
|
+
target_layer = self.layers[layer_idx]
|
|
136
|
+
new_neurons: list[Neuron] = []
|
|
137
|
+
|
|
138
|
+
# Create neurons without connections
|
|
139
|
+
for _ in range(count):
|
|
140
|
+
neuron = Neuron(activation=activation, bias=bias)
|
|
141
|
+
neuron.role = role
|
|
142
|
+
neuron.label = label
|
|
143
|
+
target_layer.neurons.append(neuron)
|
|
144
|
+
new_neurons.append(neuron)
|
|
145
|
+
|
|
146
|
+
# Weights based on init mode
|
|
147
|
+
weight_map = {"random": None, "zero": 0.0, "none": None}
|
|
148
|
+
if connection_init not in weight_map:
|
|
149
|
+
raise ValueError(f"Invalid connection_init: {connection_init}")
|
|
150
|
+
weight = weight_map[connection_init]
|
|
151
|
+
|
|
152
|
+
skip_connections = connection_init == "none"
|
|
153
|
+
|
|
154
|
+
# Connect to previous layer
|
|
155
|
+
if not skip_connections and layer_idx > 0:
|
|
156
|
+
for prev_neuron in self.layers[layer_idx - 1].neurons:
|
|
157
|
+
for n in new_neurons:
|
|
158
|
+
self.add_connection(prev_neuron, n, weight=weight)
|
|
159
|
+
|
|
160
|
+
# Connect to next layer (hidden only)
|
|
161
|
+
if (
|
|
162
|
+
not skip_connections
|
|
163
|
+
and role == NeuronRole.HIDDEN
|
|
164
|
+
and layer_idx < len(self.layers) - 1
|
|
165
|
+
):
|
|
166
|
+
for next_neuron in self.layers[layer_idx + 1].neurons:
|
|
167
|
+
for n in new_neurons:
|
|
168
|
+
self.add_connection(n, next_neuron, weight=weight)
|
|
169
|
+
|
|
170
|
+
# Recurrent connections
|
|
171
|
+
if recurrent and not skip_connections:
|
|
172
|
+
if RecurrentKind.DIRECT in recurrent:
|
|
173
|
+
for n in new_neurons:
|
|
174
|
+
if n.role == NeuronRole.HIDDEN:
|
|
175
|
+
self.add_connection(
|
|
176
|
+
n, n, weight=weight, conn_type=ConnectionType.RECURRENT
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if RecurrentKind.LATERAL in recurrent:
|
|
180
|
+
full_layer = list(self.layers[layer_idx].neurons)
|
|
181
|
+
for src in full_layer:
|
|
182
|
+
for dst in new_neurons:
|
|
183
|
+
if src is not dst:
|
|
184
|
+
self.add_connection(
|
|
185
|
+
src,
|
|
186
|
+
dst,
|
|
187
|
+
weight=weight,
|
|
188
|
+
conn_type=ConnectionType.RECURRENT,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if RecurrentKind.INDIRECT in recurrent:
|
|
192
|
+
for src in new_neurons:
|
|
193
|
+
for lower_layer in self.layers[1:layer_idx]:
|
|
194
|
+
for dst in lower_layer.neurons:
|
|
195
|
+
self.add_connection(
|
|
196
|
+
src,
|
|
197
|
+
dst,
|
|
198
|
+
weight=weight,
|
|
199
|
+
conn_type=ConnectionType.RECURRENT,
|
|
200
|
+
)
|
|
201
|
+
for higher_layer in self.layers[layer_idx + 1 :]:
|
|
202
|
+
for src in higher_layer.neurons:
|
|
203
|
+
for dst in new_neurons:
|
|
204
|
+
self.add_connection(
|
|
205
|
+
src,
|
|
206
|
+
dst,
|
|
207
|
+
weight=weight,
|
|
208
|
+
conn_type=ConnectionType.RECURRENT,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return new_neurons
|
|
212
|
+
|
|
213
|
+
def add_connection(
|
|
214
|
+
self,
|
|
215
|
+
source: Neuron,
|
|
216
|
+
target: Neuron,
|
|
217
|
+
weight: float | None = None,
|
|
218
|
+
conn_type: ConnectionType = ConnectionType.STANDARD,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Create a directed connection between two neurons.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
source (Neuron): Source neuron.
|
|
225
|
+
target (Neuron): Target neuron.
|
|
226
|
+
weight (float | None): Initial weight. If None, random value is used.
|
|
227
|
+
conn_type (ConnectionType): Type of connection (e.g. standard, recurrent).
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if weight is None:
|
|
231
|
+
weight = np.random.randn() * 0.5
|
|
232
|
+
|
|
233
|
+
conn = Connection(source, target, weight=weight, conn_type=conn_type)
|
|
234
|
+
source.outgoing.append(conn)
|
|
235
|
+
target.incoming.append(conn)
|
|
236
|
+
|
|
237
|
+
def reset(self, full: bool = False) -> None:
|
|
238
|
+
"""Reset all neurons (clears input, output, and caches)."""
|
|
239
|
+
for layer in self.layers:
|
|
240
|
+
for neuron in layer.neurons:
|
|
241
|
+
neuron.reset(full=full)
|
|
242
|
+
|
|
243
|
+
def calc(self, input_values: list[float]) -> list[float]:
|
|
244
|
+
"""
|
|
245
|
+
Perform a forward pass through the network.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
input_values (list[float]): Input vector (must match input layer size).
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
list[float]: Output values from the last layer.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
AssertionError: If input size does not match input layer.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
# Save LAST OUTPUT
|
|
258
|
+
for layer in self.layers:
|
|
259
|
+
for neuron in layer.neurons:
|
|
260
|
+
neuron.last_output = neuron.output
|
|
261
|
+
|
|
262
|
+
self.reset()
|
|
263
|
+
|
|
264
|
+
# Set inputs
|
|
265
|
+
input_layer = self.layers[0]
|
|
266
|
+
assert len(input_layer.neurons) == len(input_values)
|
|
267
|
+
for i, n in enumerate(input_layer.neurons):
|
|
268
|
+
n.input = float(input_values[i])
|
|
269
|
+
|
|
270
|
+
# Preload recurrent contributions from previous time step (last_output)
|
|
271
|
+
for layer in self.layers:
|
|
272
|
+
for n in layer.neurons:
|
|
273
|
+
for c in n.incoming:
|
|
274
|
+
if c.type is ConnectionType.RECURRENT:
|
|
275
|
+
c.target.input += c.weight * c.source.last_output
|
|
276
|
+
|
|
277
|
+
# Feed-forward by layers: activate first, then propagate non-recurrent edges
|
|
278
|
+
for layer in self.layers:
|
|
279
|
+
|
|
280
|
+
# Apply softmax to all neurons with activation_name == "softmax"
|
|
281
|
+
softmax_neurons = [
|
|
282
|
+
n for n in layer.neurons if n.activation_name == "softmax"
|
|
283
|
+
]
|
|
284
|
+
if softmax_neurons:
|
|
285
|
+
if len(softmax_neurons) >= 2:
|
|
286
|
+
# Normal softmax behaviour
|
|
287
|
+
totals = [n.input + n.bias for n in softmax_neurons]
|
|
288
|
+
probabilities = softmax_vec(totals)
|
|
289
|
+
for n, p in zip(softmax_neurons, probabilities):
|
|
290
|
+
n.output = float(p)
|
|
291
|
+
else:
|
|
292
|
+
# Fallback: single softmax neuron acts like identity
|
|
293
|
+
n = softmax_neurons[0]
|
|
294
|
+
n.output = n.input + n.bias
|
|
295
|
+
|
|
296
|
+
# Activate all neurons in this layer
|
|
297
|
+
for n in layer.neurons:
|
|
298
|
+
if n.activation_name != "softmax":
|
|
299
|
+
total = n.input + n.bias
|
|
300
|
+
n.output = n.activation(total)
|
|
301
|
+
|
|
302
|
+
# Propagate to targets (exclude recurrent edges)
|
|
303
|
+
for n in layer.neurons:
|
|
304
|
+
for c in n.outgoing:
|
|
305
|
+
if c.type is not ConnectionType.RECURRENT:
|
|
306
|
+
c.target.input += c.weight * n.output
|
|
307
|
+
|
|
308
|
+
return [n.output for n in self.layers[-1].neurons]
|
|
309
|
+
|
|
310
|
+
def get_all_neurons(self) -> list[Neuron]:
|
|
311
|
+
"""Return all neurons in all layers (flattened)."""
|
|
312
|
+
return [n for layer in self.layers for n in layer.neurons]
|
|
313
|
+
|
|
314
|
+
def get_all_connections(self) -> list[Connection]:
|
|
315
|
+
"""Return all outgoing connections in the network."""
|
|
316
|
+
return [c for n in self.get_all_neurons() for c in n.outgoing]
|
|
317
|
+
|
|
318
|
+
def __repr__(self) -> str:
|
|
319
|
+
total_neurons = sum(len(layer.neurons) for layer in self.layers)
|
|
320
|
+
input_neurons = len(self.layers[0].neurons) if self.layers else 0
|
|
321
|
+
output_neurons = len(self.layers[-1].neurons) if len(self.layers) > 1 else 0
|
|
322
|
+
hidden_neurons = total_neurons - input_neurons - output_neurons
|
|
323
|
+
|
|
324
|
+
total_connections = len(self.get_all_connections())
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
f"<Nnet | {len(self.layers)} layers, "
|
|
328
|
+
f"{total_neurons} neurons (I:{input_neurons} H:{hidden_neurons} "
|
|
329
|
+
f"O:{output_neurons}), "
|
|
330
|
+
f"{total_connections} connections "
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def print_graph(
|
|
334
|
+
self,
|
|
335
|
+
name: str,
|
|
336
|
+
engine: str = "dot",
|
|
337
|
+
labels_on: bool = True,
|
|
338
|
+
colors_on: bool = True,
|
|
339
|
+
thickness_on: bool = False,
|
|
340
|
+
fillcolors_on: bool = False,
|
|
341
|
+
) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Render a visual representation of the network using Graphviz.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
name (str): Output file name (without extension).
|
|
347
|
+
engine (str): Graphviz layout engine (e.g., 'dot', 'neato').
|
|
348
|
+
labels_on (bool): Whether to show edge weights as labels.
|
|
349
|
+
colors_on (bool): Whether to color edges by sign.
|
|
350
|
+
thickness_on (bool): Whether to scale edge thickness by weight.
|
|
351
|
+
fillcolors_on (bool): Whether to color neurons by role.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
if not self.layers:
|
|
355
|
+
print("No layers to visualize.")
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
dot = graphviz.Digraph(name=name, format="png", engine=engine)
|
|
359
|
+
dot.graph_attr.update(
|
|
360
|
+
bgcolor="white",
|
|
361
|
+
rankdir="LR",
|
|
362
|
+
overlap="prism",
|
|
363
|
+
sep="15",
|
|
364
|
+
ratio="fill",
|
|
365
|
+
splines="spline",
|
|
366
|
+
size="6.68,5!",
|
|
367
|
+
dpi="600",
|
|
368
|
+
)
|
|
369
|
+
dot.node_attr.update(
|
|
370
|
+
shape="circle", style="filled", fixedsize="shape", width="1.8"
|
|
371
|
+
)
|
|
372
|
+
dot.edge_attr.update(arrowsize="0.8")
|
|
373
|
+
|
|
374
|
+
# Add neurons with coordinates (x = layer, y = index)
|
|
375
|
+
for layer_idx, layer in enumerate(self.layers):
|
|
376
|
+
for neuron_idx, neuron in enumerate(layer.neurons):
|
|
377
|
+
if neuron.role.name == "INPUT":
|
|
378
|
+
fillcolor = "lightblue" if fillcolors_on else "white"
|
|
379
|
+
elif neuron.role.name == "OUTPUT":
|
|
380
|
+
fillcolor = "orange" if fillcolors_on else "white"
|
|
381
|
+
else:
|
|
382
|
+
fillcolor = "lightgreen" if fillcolors_on else "white"
|
|
383
|
+
|
|
384
|
+
label = (
|
|
385
|
+
f"{neuron.label or neuron.role.name}({layer_idx})\n"
|
|
386
|
+
f"In: {neuron.input:.3f}\n"
|
|
387
|
+
f"Out: {neuron.output:.3f}\n"
|
|
388
|
+
f"LastOut: {neuron.last_output:.3f}\n"
|
|
389
|
+
f"Bias: {neuron.bias:.3f}\n"
|
|
390
|
+
f"{neuron.activation_name}"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
pos = f"{layer_idx},{-neuron_idx}!"
|
|
394
|
+
dot.node(
|
|
395
|
+
name=neuron.id,
|
|
396
|
+
label=label,
|
|
397
|
+
fillcolor=fillcolor,
|
|
398
|
+
pos=pos,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Add edges
|
|
402
|
+
for conn in self.get_all_connections():
|
|
403
|
+
label = f"{conn.weight:.2f}" if labels_on else ""
|
|
404
|
+
color = (
|
|
405
|
+
"green"
|
|
406
|
+
if colors_on and conn.weight >= 0
|
|
407
|
+
else "red" if colors_on else "black"
|
|
408
|
+
)
|
|
409
|
+
penwidth = (
|
|
410
|
+
str(max(1, min(5, abs(conn.weight * 5)))) if thickness_on else "1"
|
|
411
|
+
)
|
|
412
|
+
style = "dashed" if conn.type.name == "RECURRENT" else "solid"
|
|
413
|
+
|
|
414
|
+
dot.edge(
|
|
415
|
+
conn.source.id,
|
|
416
|
+
conn.target.id,
|
|
417
|
+
label=label,
|
|
418
|
+
color=color,
|
|
419
|
+
penwidth=penwidth,
|
|
420
|
+
style=style,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
dot.render(name, cleanup=True)
|
|
424
|
+
|
|
425
|
+
def plot_simple(
|
|
426
|
+
net,
|
|
427
|
+
show_weights: bool = False,
|
|
428
|
+
show_values: bool = True,
|
|
429
|
+
path: str | None = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""
|
|
432
|
+
Visualize the network in a simple layered layout using Matplotlib.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
net: The Nnet object to visualize.
|
|
436
|
+
show_weights (bool): If True, draw weight values on edges.
|
|
437
|
+
show_values (bool): If True, neuron output values are shown,
|
|
438
|
+
else neuron indices.
|
|
439
|
+
path (str | None): If given, save the plot to this path instead
|
|
440
|
+
of displaying.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
444
|
+
ax.set_aspect("equal")
|
|
445
|
+
ax.axis("off")
|
|
446
|
+
|
|
447
|
+
neuron_positions: dict = {}
|
|
448
|
+
|
|
449
|
+
# Place neurons
|
|
450
|
+
for layer_idx, layer in enumerate(net.layers):
|
|
451
|
+
y_positions = list(range(len(layer.neurons)))
|
|
452
|
+
offset = (
|
|
453
|
+
max(len(layer.neurons) for layer in net.layers) - len(y_positions)
|
|
454
|
+
) / 2.0
|
|
455
|
+
|
|
456
|
+
for neuron_idx, neuron in enumerate(layer.neurons):
|
|
457
|
+
x, y = layer_idx, neuron_idx + offset
|
|
458
|
+
neuron_positions[neuron] = (x, y)
|
|
459
|
+
|
|
460
|
+
ax.scatter(
|
|
461
|
+
x,
|
|
462
|
+
y,
|
|
463
|
+
c="royalblue",
|
|
464
|
+
s=400,
|
|
465
|
+
zorder=3,
|
|
466
|
+
edgecolors="white",
|
|
467
|
+
linewidths=1.5,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
label = f"{neuron.output:.2f}" if show_values else str(neuron_idx)
|
|
471
|
+
ax.text(
|
|
472
|
+
x,
|
|
473
|
+
y,
|
|
474
|
+
label,
|
|
475
|
+
ha="center",
|
|
476
|
+
va="center",
|
|
477
|
+
color="white",
|
|
478
|
+
fontsize=9,
|
|
479
|
+
zorder=4,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Draw connections
|
|
483
|
+
for layer in net.layers[1:]:
|
|
484
|
+
for neuron in layer.neurons:
|
|
485
|
+
x2, y2 = neuron_positions[neuron]
|
|
486
|
+
for conn in neuron.incoming:
|
|
487
|
+
x1, y1 = neuron_positions[conn.source]
|
|
488
|
+
style = "--" if getattr(conn, "recurrent", False) else "-"
|
|
489
|
+
color = "crimson" if getattr(conn, "recurrent", False) else "gray"
|
|
490
|
+
ax.plot(
|
|
491
|
+
[x1, x2],
|
|
492
|
+
[y1, y2],
|
|
493
|
+
color=color,
|
|
494
|
+
linestyle=style,
|
|
495
|
+
alpha=0.7,
|
|
496
|
+
zorder=1,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if show_weights:
|
|
500
|
+
xm, ym = (x1 + x2) / 2, (y1 + y2) / 2
|
|
501
|
+
ax.text(
|
|
502
|
+
xm,
|
|
503
|
+
ym,
|
|
504
|
+
f"{conn.weight:.2f}",
|
|
505
|
+
fontsize=7,
|
|
506
|
+
color="black",
|
|
507
|
+
alpha=0.6,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
plt.tight_layout()
|
|
511
|
+
|
|
512
|
+
if path:
|
|
513
|
+
plt.savefig(path, dpi=150)
|
|
514
|
+
plt.close(fig)
|
|
515
|
+
else:
|
|
516
|
+
plt.show()
|
|
517
|
+
|
|
518
|
+
def _build_index_map(self) -> dict[Neuron, tuple[int, int]]:
|
|
519
|
+
"""Build a mapping from neuron -> (layer_idx, neuron_idx) for O(1) lookups."""
|
|
520
|
+
index_map: dict[Neuron, tuple[int, int]] = {}
|
|
521
|
+
for layer_idx, layer in enumerate(self.layers):
|
|
522
|
+
for neuron_idx, neuron in enumerate(layer.neurons):
|
|
523
|
+
index_map[neuron] = (layer_idx, neuron_idx)
|
|
524
|
+
return index_map
|
|
525
|
+
|
|
526
|
+
def get_weights(self) -> np.ndarray:
|
|
527
|
+
"""
|
|
528
|
+
Return all connection weights as a flat vector in a deterministic order.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
np.ndarray: 1D array of connection weights.
|
|
532
|
+
|
|
533
|
+
Order key:
|
|
534
|
+
(src_layer_idx, src_neuron_idx, dst_layer_idx,
|
|
535
|
+
dst_neuron_idx, connection_type)
|
|
536
|
+
"""
|
|
537
|
+
conns = self.get_all_connections()
|
|
538
|
+
if not conns:
|
|
539
|
+
return np.empty(0, dtype=float)
|
|
540
|
+
|
|
541
|
+
index_map = self._build_index_map()
|
|
542
|
+
|
|
543
|
+
def sort_key(c: Connection) -> tuple[int, int, int, int, int]:
|
|
544
|
+
src_layer_idx, src_neuron_idx = index_map[c.source]
|
|
545
|
+
dst_layer_idx, dst_neuron_idx = index_map[c.target]
|
|
546
|
+
return (
|
|
547
|
+
src_layer_idx,
|
|
548
|
+
src_neuron_idx,
|
|
549
|
+
dst_layer_idx,
|
|
550
|
+
dst_neuron_idx,
|
|
551
|
+
int(c.type.value),
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
conns_sorted = sorted(conns, key=sort_key)
|
|
555
|
+
return np.array([c.weight for c in conns_sorted], dtype=float)
|
|
556
|
+
|
|
557
|
+
def set_weights(self, flat: np.ndarray) -> None:
|
|
558
|
+
"""
|
|
559
|
+
Set all connection weights from a flat vector using the same deterministic order
|
|
560
|
+
as `get_weights()`.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
flat (np.ndarray): Flat array of weights (must match number of connections).
|
|
564
|
+
|
|
565
|
+
Raises:
|
|
566
|
+
ValueError: If the length of the array does not match.
|
|
567
|
+
"""
|
|
568
|
+
flat = np.asarray(flat, dtype=float).ravel()
|
|
569
|
+
|
|
570
|
+
conns = self.get_all_connections()
|
|
571
|
+
if not conns and flat.size == 0:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
index_map = self._build_index_map()
|
|
575
|
+
|
|
576
|
+
def sort_key(c: Connection) -> tuple[int, int, int, int, int]:
|
|
577
|
+
src_layer_idx, src_neuron_idx = index_map[c.source]
|
|
578
|
+
dst_layer_idx, dst_neuron_idx = index_map[c.target]
|
|
579
|
+
return (
|
|
580
|
+
src_layer_idx,
|
|
581
|
+
src_neuron_idx,
|
|
582
|
+
dst_layer_idx,
|
|
583
|
+
dst_neuron_idx,
|
|
584
|
+
int(c.type.value),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
conns_sorted = sorted(conns, key=sort_key)
|
|
588
|
+
|
|
589
|
+
if flat.size != len(conns_sorted):
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"Length mismatch for weights: expected {len(conns_sorted)}, "
|
|
592
|
+
f"got {flat.size}."
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
for weight_value, conn in zip(flat, conns_sorted):
|
|
596
|
+
conn.weight = float(weight_value)
|
|
597
|
+
|
|
598
|
+
def get_biases(self) -> np.ndarray:
|
|
599
|
+
"""
|
|
600
|
+
Return all trainable biases as a flat vector (excluding input neurons).
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
np.ndarray: Bias vector.
|
|
604
|
+
|
|
605
|
+
Order:
|
|
606
|
+
(layer_index, neuron_index) over all non-input neurons.
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
if not self.layers:
|
|
610
|
+
return np.empty(0, dtype=float)
|
|
611
|
+
|
|
612
|
+
biases: list[float] = []
|
|
613
|
+
for _, layer in enumerate(self.layers):
|
|
614
|
+
for _, neuron in enumerate(layer.neurons):
|
|
615
|
+
if neuron.role is not NeuronRole.INPUT:
|
|
616
|
+
biases.append(neuron.bias)
|
|
617
|
+
return np.asarray(biases, dtype=float)
|
|
618
|
+
|
|
619
|
+
def set_biases(self, flat: np.ndarray) -> None:
|
|
620
|
+
"""
|
|
621
|
+
Set all neuron biases (excluding input neurons) from a flat vector using the
|
|
622
|
+
same ordering as `get_biases()`.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
flat (np.ndarray): Bias values in deterministic order.
|
|
626
|
+
|
|
627
|
+
Raises:
|
|
628
|
+
ValueError: If length does not match the number of biases.
|
|
629
|
+
"""
|
|
630
|
+
flat = np.asarray(flat, dtype=float).ravel()
|
|
631
|
+
|
|
632
|
+
# Collect non-input neurons in deterministic order
|
|
633
|
+
targets: list[Neuron] = []
|
|
634
|
+
for _, layer in enumerate(self.layers):
|
|
635
|
+
for _, neuron in enumerate(layer.neurons):
|
|
636
|
+
if neuron.role is not NeuronRole.INPUT:
|
|
637
|
+
targets.append(neuron)
|
|
638
|
+
|
|
639
|
+
if flat.size != len(targets):
|
|
640
|
+
raise ValueError(
|
|
641
|
+
f"Length mismatch for biases: expected {len(targets)}, got {flat.size}."
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
for b, n in zip(flat, targets):
|
|
645
|
+
n.bias = float(b)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""
|
|
3
|
+
Serialization utilities for EvoNet networks.
|
|
4
|
+
|
|
5
|
+
This module provides functions to save and load EvoNet networks
|
|
6
|
+
to and from human-readable YAML files (default) or JSON files.
|
|
7
|
+
The serialization preserves the full network topology:
|
|
8
|
+
- Layers (index, role, label)
|
|
9
|
+
- Neurons (id, activation, bias, role, label)
|
|
10
|
+
- Connections (src, dst, weight, recurrent)
|
|
11
|
+
|
|
12
|
+
YAML is recommended for readability and manual editing.
|
|
13
|
+
JSON is provided as a secondary option for interoperability.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
from evonet.core import Neuron, Nnet
|
|
24
|
+
from evonet.enums import ConnectionType, NeuronRole
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helper
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_dict(net: Nnet) -> dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Convert an EvoNet network into a serializable dictionary.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
net (Nnet): The network to convert.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
dict[str, Any]: A nested dictionary representation.
|
|
40
|
+
"""
|
|
41
|
+
return {
|
|
42
|
+
"layers": [
|
|
43
|
+
{
|
|
44
|
+
"index": i,
|
|
45
|
+
"neurons": [
|
|
46
|
+
{
|
|
47
|
+
"id": n.id,
|
|
48
|
+
"activation": n.activation_name,
|
|
49
|
+
"bias": n.bias,
|
|
50
|
+
"role": n.role.name,
|
|
51
|
+
"label": n.label,
|
|
52
|
+
"incoming": [
|
|
53
|
+
{
|
|
54
|
+
"source": c.source.id,
|
|
55
|
+
"target": c.target.id,
|
|
56
|
+
"weight": c.weight,
|
|
57
|
+
"type": c.type.name, # store enum as string
|
|
58
|
+
}
|
|
59
|
+
for c in n.incoming
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
for n in layer.neurons
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
for i, layer in enumerate(net.layers)
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def from_dict(data: dict[str, Any]) -> Nnet:
|
|
71
|
+
"""Reconstruct a network from a dictionary created by `to_dict`."""
|
|
72
|
+
net = Nnet()
|
|
73
|
+
neuron_map: dict[str, Neuron] = {}
|
|
74
|
+
|
|
75
|
+
# Rebuild layers and neurons
|
|
76
|
+
for layer_info in data["layers"]:
|
|
77
|
+
net.add_layer()
|
|
78
|
+
for n_info in layer_info["neurons"]:
|
|
79
|
+
n = net.add_neuron(
|
|
80
|
+
activation=n_info["activation"],
|
|
81
|
+
bias=n_info["bias"],
|
|
82
|
+
role=NeuronRole[n_info["role"]],
|
|
83
|
+
label=n_info.get("label", ""),
|
|
84
|
+
connection_init="none",
|
|
85
|
+
)[0]
|
|
86
|
+
n.id = n_info["id"]
|
|
87
|
+
neuron_map[n.id] = n
|
|
88
|
+
|
|
89
|
+
# Rebuild connections
|
|
90
|
+
for layer_info in data["layers"]:
|
|
91
|
+
for n_info in layer_info["neurons"]:
|
|
92
|
+
for c_info in n_info["incoming"]:
|
|
93
|
+
src = neuron_map[c_info["source"]]
|
|
94
|
+
dst = neuron_map[c_info["target"]]
|
|
95
|
+
net.add_connection(
|
|
96
|
+
src,
|
|
97
|
+
dst,
|
|
98
|
+
weight=c_info["weight"],
|
|
99
|
+
conn_type=ConnectionType[c_info["type"]],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return net
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# YAML interface
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def save_yaml(net: Nnet, path: str) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Save a network to a YAML file (human-readable).
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
net (Nnet): The network to save.
|
|
116
|
+
path (str): Output file path.
|
|
117
|
+
"""
|
|
118
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
119
|
+
yaml.safe_dump(
|
|
120
|
+
to_dict(net),
|
|
121
|
+
f,
|
|
122
|
+
sort_keys=False, # preserve order for readability
|
|
123
|
+
default_flow_style=False, # block style (YAML best practice)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_yaml(path: str) -> Nnet:
|
|
128
|
+
"""
|
|
129
|
+
Load a network from a YAML file.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
path (str): Path to the YAML file.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Nnet: Reconstructed network.
|
|
136
|
+
"""
|
|
137
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
138
|
+
data = yaml.safe_load(f)
|
|
139
|
+
return from_dict(data)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# JSON interface
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def save_json(net: Nnet, path: str) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Save a network to a JSON file.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
net (Nnet): The network to save.
|
|
153
|
+
path (str): Output file path.
|
|
154
|
+
"""
|
|
155
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
156
|
+
json.dump(to_dict(net), f, indent=2)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def load_json(path: str) -> Nnet:
|
|
160
|
+
"""
|
|
161
|
+
Load a network from a JSON file.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
path (str): Path to the JSON file.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Nnet: Reconstructed network.
|
|
168
|
+
"""
|
|
169
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
170
|
+
data = json.load(f)
|
|
171
|
+
return from_dict(data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evonet
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev10
|
|
4
4
|
Summary: Evolvable neural network core for integration with EvoLib
|
|
5
5
|
Author-email: EvoLib <evolib@dismail.de>
|
|
6
6
|
License: MIT License
|
|
@@ -39,6 +39,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
39
39
|
Requires-Dist: pandas>=2.3.0
|
|
40
40
|
Requires-Dist: pydantic<3.0,>=2.7
|
|
41
41
|
Requires-Dist: graphviz>=0.20.1
|
|
42
|
+
Requires-Dist: matplotlib
|
|
42
43
|
Provides-Extra: dev
|
|
43
44
|
Requires-Dist: mypy; extra == "dev"
|
|
44
45
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
@@ -6,11 +6,13 @@ evonet/__init__.py
|
|
|
6
6
|
evonet/activation.py
|
|
7
7
|
evonet/connection.py
|
|
8
8
|
evonet/core.py
|
|
9
|
+
evonet/core_mit_plot_simple.py
|
|
9
10
|
evonet/enums.py
|
|
10
11
|
evonet/io.py
|
|
11
12
|
evonet/layer.py
|
|
12
13
|
evonet/mutation.py
|
|
13
14
|
evonet/neuron.py
|
|
15
|
+
evonet/serialization.py
|
|
14
16
|
evonet/utils.py
|
|
15
17
|
evonet/visualize.py
|
|
16
18
|
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.0.
|
|
7
|
+
version = "0.1.0.dev10"
|
|
8
8
|
description = "Evolvable neural network core for integration with EvoLib"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "EvoLib", email = "evolib@dismail.de" }
|
|
@@ -17,7 +17,8 @@ dependencies = [
|
|
|
17
17
|
"pyyaml >=6.0",
|
|
18
18
|
"pandas >=2.3.0",
|
|
19
19
|
"pydantic >= 2.7,<3.0",
|
|
20
|
-
"graphviz>=0.20.1"
|
|
20
|
+
"graphviz>=0.20.1",
|
|
21
|
+
"matplotlib"
|
|
21
22
|
]
|
|
22
23
|
|
|
23
24
|
classifiers = [
|
|
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
|
|
File without changes
|