evonet 0.1.0.dev22__tar.gz → 0.1.0.dev24__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.dev22 → evonet-0.1.0.dev24}/PKG-INFO +1 -1
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/core.py +144 -89
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/mutation.py +6 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/PKG-INFO +1 -1
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/pyproject.toml +1 -1
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/LICENSE +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/README.md +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/__init__.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/activation.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/connection.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/core_mit_plot_simple.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/enums.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/io.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/layer.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/neuron.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/serialization.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/utils.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/visualize.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/SOURCES.txt +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/dependency_links.txt +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/requires.txt +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/top_level.txt +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/setup.cfg +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_activation.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_core.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_nnet_io_and_forward.py +0 -0
- {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_recurrent_dynamics.py +0 -0
|
@@ -121,6 +121,8 @@ class Nnet:
|
|
|
121
121
|
recurrent: Optional[set[RecurrentKind]] = None,
|
|
122
122
|
connection_scope: Literal["adjacent", "crosslayer"] = "adjacent",
|
|
123
123
|
connection_density: float = 1.0,
|
|
124
|
+
max_connections: int = 2**63 - 1,
|
|
125
|
+
require_io_connections: bool = True,
|
|
124
126
|
) -> list[Neuron]:
|
|
125
127
|
"""
|
|
126
128
|
Add one or more neurons to the network.
|
|
@@ -129,39 +131,47 @@ class Nnet:
|
|
|
129
131
|
layer_idx: Target layer index. Defaults to last layer.
|
|
130
132
|
activation: Activation function name.
|
|
131
133
|
bias: Initial bias value.
|
|
132
|
-
label: Optional label.
|
|
134
|
+
label: Optional textual label.
|
|
133
135
|
role: Role of the neuron (INPUT, HIDDEN, OUTPUT).
|
|
134
|
-
count: Number of neurons to add
|
|
136
|
+
count: Number of neurons to add.
|
|
135
137
|
connection_init:
|
|
136
|
-
"random" - connect with random weights
|
|
137
|
-
"zero" -
|
|
138
|
-
"near_zero" -
|
|
139
|
-
"none" - do not create connections
|
|
138
|
+
"random" - connect with random weights
|
|
139
|
+
"zero" - weight = 0.0
|
|
140
|
+
"near_zero" - small uniform weights in (-0.05, 0.05)
|
|
141
|
+
"none" - do not create any connections
|
|
140
142
|
recurrent: Optional recurrent connection types.
|
|
141
143
|
connection_scope:
|
|
142
|
-
- "adjacent": only
|
|
143
|
-
- "crosslayer": connect to all earlier
|
|
144
|
-
layers (feedforward only)
|
|
144
|
+
- "adjacent": only neighbor layers
|
|
145
|
+
- "crosslayer": connect to all earlier/later layers
|
|
145
146
|
connection_density:
|
|
146
|
-
Fraction of possible
|
|
147
|
-
Must be in
|
|
147
|
+
Fraction of all possible feed-forward + recurrent connections
|
|
148
|
+
that should actually be created. Must be in [0, 1].
|
|
149
|
+
max_connections:
|
|
150
|
+
Hard limit for the number of created connections per neuron.
|
|
151
|
+
require_io_connections:
|
|
152
|
+
If True, ensure that each newly added neuron has at least
|
|
153
|
+
one incoming and one outgoing connection (if structurally possible).
|
|
148
154
|
|
|
149
155
|
Returns:
|
|
150
|
-
list[Neuron]:
|
|
156
|
+
list[Neuron]: The created neuron objects.
|
|
151
157
|
"""
|
|
158
|
+
|
|
152
159
|
if layer_idx is None:
|
|
153
160
|
layer_idx = len(self.layers) - 1 # Add neuron to last layer
|
|
154
161
|
|
|
155
|
-
if
|
|
162
|
+
if not (0 <= layer_idx < len(self.layers)):
|
|
156
163
|
raise ValueError(f"Layer index out of bounds: {layer_idx}")
|
|
157
164
|
|
|
158
165
|
if not (0.0 < connection_density <= 1.0):
|
|
159
166
|
raise ValueError("connection_density must be in (0, 1].")
|
|
160
167
|
|
|
168
|
+
if not (0 <= max_connections <= 2**63 - 1):
|
|
169
|
+
raise ValueError("max_connections must be >= 0 ")
|
|
170
|
+
|
|
161
171
|
target_layer = self.layers[layer_idx]
|
|
162
172
|
new_neurons: list[Neuron] = []
|
|
163
173
|
|
|
164
|
-
# Create neurons without connections
|
|
174
|
+
# Create neurons without any connections
|
|
165
175
|
for _ in range(count):
|
|
166
176
|
neuron = Neuron(activation=activation, bias=bias)
|
|
167
177
|
neuron.role = role
|
|
@@ -169,96 +179,141 @@ class Nnet:
|
|
|
169
179
|
target_layer.neurons.append(neuron)
|
|
170
180
|
new_neurons.append(neuron)
|
|
171
181
|
|
|
172
|
-
#
|
|
182
|
+
# No connections
|
|
173
183
|
if connection_init == "none":
|
|
174
184
|
return new_neurons
|
|
175
185
|
|
|
176
186
|
weight = connection_init_value(connection_init)
|
|
177
187
|
|
|
178
|
-
# Build connections for
|
|
188
|
+
# Build connections for every new neuron
|
|
179
189
|
for n in new_neurons:
|
|
180
190
|
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
# --------------------------------------------------------------
|
|
192
|
+
# FEED-FORWARD candidates
|
|
193
|
+
# --------------------------------------------------------------
|
|
194
|
+
ff_candidates: list[tuple[Neuron, Neuron, ConnectionType]] = []
|
|
183
195
|
|
|
184
|
-
# ADJACENT connections
|
|
185
196
|
if connection_scope == "adjacent":
|
|
197
|
+
# previous layer -> new neuron
|
|
186
198
|
if layer_idx > 0:
|
|
187
|
-
|
|
199
|
+
for src in self.layers[layer_idx - 1].neurons:
|
|
200
|
+
ff_candidates.append((src, n, ConnectionType.STANDARD))
|
|
201
|
+
|
|
202
|
+
# new neuron -> next layer (only hidden neurons produce FF forward)
|
|
188
203
|
if role == NeuronRole.HIDDEN and layer_idx < len(self.layers) - 1:
|
|
189
|
-
|
|
204
|
+
for dst in self.layers[layer_idx + 1].neurons:
|
|
205
|
+
ff_candidates.append((n, dst, ConnectionType.STANDARD))
|
|
190
206
|
|
|
191
|
-
# CROSSLAYER connections
|
|
192
207
|
elif connection_scope == "crosslayer":
|
|
193
|
-
#
|
|
208
|
+
# all earlier layers -> new neuron
|
|
194
209
|
for i in range(0, layer_idx):
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
for src in self.layers[i].neurons:
|
|
211
|
+
ff_candidates.append((src, n, ConnectionType.STANDARD))
|
|
212
|
+
|
|
213
|
+
# new neuron -> all later layers
|
|
197
214
|
for j in range(layer_idx + 1, len(self.layers)):
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
215
|
+
for dst in self.layers[j].neurons:
|
|
216
|
+
ff_candidates.append((n, dst, ConnectionType.STANDARD))
|
|
217
|
+
|
|
218
|
+
# --------------------------------------------------------------
|
|
219
|
+
# RECURRENT candidates
|
|
220
|
+
# --------------------------------------------------------------
|
|
221
|
+
rec_candidates: list[tuple[Neuron, Neuron, ConnectionType]] = []
|
|
222
|
+
|
|
223
|
+
if recurrent:
|
|
224
|
+
|
|
225
|
+
# DIRECT: self-loop
|
|
226
|
+
if RecurrentKind.DIRECT in recurrent:
|
|
227
|
+
if n.role == NeuronRole.HIDDEN:
|
|
228
|
+
rec_candidates.append((n, n, ConnectionType.RECURRENT))
|
|
229
|
+
|
|
230
|
+
# LATERAL: same-layer recurrent edges
|
|
231
|
+
if RecurrentKind.LATERAL in recurrent:
|
|
232
|
+
full_layer = list(self.layers[layer_idx].neurons)
|
|
233
|
+
for src in full_layer:
|
|
234
|
+
if src is n:
|
|
235
|
+
continue
|
|
236
|
+
if src.role != NeuronRole.HIDDEN:
|
|
237
|
+
continue
|
|
238
|
+
rec_candidates.append((src, n, ConnectionType.RECURRENT))
|
|
239
|
+
|
|
240
|
+
# INDIRECT: across earlier and later layers
|
|
241
|
+
if RecurrentKind.INDIRECT in recurrent:
|
|
242
|
+
|
|
243
|
+
# From new neuron -> lower layers
|
|
244
|
+
for lower in self.layers[1:layer_idx]:
|
|
245
|
+
for dst in lower.neurons:
|
|
246
|
+
if n.role == NeuronRole.HIDDEN:
|
|
247
|
+
rec_candidates.append(
|
|
248
|
+
(n, dst, ConnectionType.RECURRENT)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# From higher layers -> new neuron
|
|
252
|
+
for higher in self.layers[layer_idx + 1 :]:
|
|
253
|
+
for src in higher.neurons:
|
|
254
|
+
if src.role == NeuronRole.HIDDEN:
|
|
255
|
+
rec_candidates.append(
|
|
256
|
+
(src, n, ConnectionType.RECURRENT)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# --------------------------------------------------------------
|
|
260
|
+
# GLOBAL CANDIDATE LIST (FF + RECURRENT)
|
|
261
|
+
# --------------------------------------------------------------
|
|
262
|
+
all_candidates = ff_candidates + rec_candidates
|
|
263
|
+
|
|
264
|
+
if not all_candidates:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
forced: list[tuple[Neuron, Neuron, ConnectionType]] = []
|
|
268
|
+
|
|
269
|
+
# --------------------------------------------------------------
|
|
270
|
+
# IO enforcement
|
|
271
|
+
# --------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
if require_io_connections and max_connections > 0:
|
|
274
|
+
incoming_candidates = [
|
|
275
|
+
(src, dst, ctype) for src, dst, ctype in all_candidates if dst is n
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
outgoing_candidates = [
|
|
279
|
+
(src, dst, ctype) for src, dst, ctype in all_candidates if src is n
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
# Enforce incoming if possible
|
|
283
|
+
if incoming_candidates:
|
|
284
|
+
forced.append(random.choice(incoming_candidates))
|
|
285
|
+
|
|
286
|
+
# Enforce outgoing if possible and budget allows
|
|
287
|
+
if outgoing_candidates and len(forced) < max_connections:
|
|
288
|
+
forced.append(random.choice(outgoing_candidates))
|
|
289
|
+
|
|
290
|
+
# --------------------------------------------------------------
|
|
291
|
+
# Remaining
|
|
292
|
+
# --------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
total = len(all_candidates)
|
|
295
|
+
k = int(np.ceil(total * connection_density))
|
|
296
|
+
k = min(k, max_connections - len(forced))
|
|
297
|
+
k = max(k, 0)
|
|
298
|
+
|
|
299
|
+
remaining_candidates = [c for c in all_candidates if c not in forced]
|
|
300
|
+
|
|
301
|
+
# Create forced connections
|
|
302
|
+
for src, dst, ctype in forced:
|
|
303
|
+
self.add_connection(src, dst, weight=weight, conn_type=ctype)
|
|
304
|
+
|
|
305
|
+
k = min(k, len(remaining_candidates))
|
|
306
|
+
|
|
307
|
+
if k == 0:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
sampled = random.sample(remaining_candidates, k)
|
|
311
|
+
|
|
312
|
+
# --------------------------------------------------------------
|
|
313
|
+
# Create connections
|
|
314
|
+
# --------------------------------------------------------------
|
|
315
|
+
for src, dst, ctype in forced + sampled:
|
|
316
|
+
self.add_connection(src, dst, weight=weight, conn_type=ctype)
|
|
262
317
|
|
|
263
318
|
return new_neurons
|
|
264
319
|
|
|
@@ -272,6 +272,7 @@ def add_random_neuron(
|
|
|
272
272
|
connection_init: Literal["zero", "random", "near_zero", "none"] = "zero",
|
|
273
273
|
connection_scope: Literal["adjacent", "crosslayer"] = "adjacent",
|
|
274
274
|
connection_density: float = 1.0,
|
|
275
|
+
max_connections: int = 2**63 - 1,
|
|
275
276
|
) -> Neuron | None:
|
|
276
277
|
"""
|
|
277
278
|
Insert a new hidden neuron into a random layer.
|
|
@@ -282,6 +283,10 @@ def add_random_neuron(
|
|
|
282
283
|
net (Nnet): The target network.
|
|
283
284
|
activations (list[str] | None): Optional list of allowed activation functions.
|
|
284
285
|
If None, all registered activations are used.
|
|
286
|
+
connection_density:
|
|
287
|
+
Fraction of possible connections that should actually be created.
|
|
288
|
+
Must be in (0, 1]. A value <1.0 randomly samples a subset.
|
|
289
|
+
max_connections: maximal number allowed connections
|
|
285
290
|
|
|
286
291
|
Returns:
|
|
287
292
|
Neuron: Added neurons, or None
|
|
@@ -307,6 +312,7 @@ def add_random_neuron(
|
|
|
307
312
|
connection_init=connection_init,
|
|
308
313
|
connection_scope=connection_scope,
|
|
309
314
|
connection_density=connection_density,
|
|
315
|
+
max_connections=max_connections,
|
|
310
316
|
)[0]
|
|
311
317
|
|
|
312
318
|
return new_neuron
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|