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.
Files changed (27) hide show
  1. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/PKG-INFO +1 -1
  2. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/core.py +144 -89
  3. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/mutation.py +6 -0
  4. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/PKG-INFO +1 -1
  5. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/pyproject.toml +1 -1
  6. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/LICENSE +0 -0
  7. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/README.md +0 -0
  8. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/__init__.py +0 -0
  9. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/activation.py +0 -0
  10. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/connection.py +0 -0
  11. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/core_mit_plot_simple.py +0 -0
  12. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/enums.py +0 -0
  13. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/io.py +0 -0
  14. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/layer.py +0 -0
  15. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/neuron.py +0 -0
  16. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/serialization.py +0 -0
  17. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/utils.py +0 -0
  18. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet/visualize.py +0 -0
  19. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/SOURCES.txt +0 -0
  20. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/dependency_links.txt +0 -0
  21. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/requires.txt +0 -0
  22. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/evonet.egg-info/top_level.txt +0 -0
  23. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/setup.cfg +0 -0
  24. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_activation.py +0 -0
  25. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_core.py +0 -0
  26. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/tests/test_nnet_io_and_forward.py +0 -0
  27. {evonet-0.1.0.dev22 → evonet-0.1.0.dev24}/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.dev22
3
+ Version: 0.1.0.dev24
4
4
  Summary: Evolvable neural network core for integration with EvoLib
5
5
  Author-email: EvoLib <evolib@dismail.de>
6
6
  License: MIT License
@@ -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 (default: 1).
136
+ count: Number of neurons to add.
135
137
  connection_init:
136
- "random" - connect with random weights (feedforward + recurrent)
137
- "zero" - connect with weight 0.0 (feedforward + recurrent)
138
- "near_zero" - connect with small random weights (-0.05, 0.05)
139
- "none" - do not create connections (feedforward + recurrent)
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 connect to directly neighboring layers (default)
143
- - "crosslayer": connect to all earlier and later
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 connections that should actually be created.
147
- Must be in (0, 1]. A value <1.0 randomly samples a subset.
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]: List of added neurons.
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 layer_idx < 0 or layer_idx >= len(self.layers):
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
- # Skip connection creation if requested
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 each new neuron
188
+ # Build connections for every new neuron
179
189
  for n in new_neurons:
180
190
 
181
- possible_in: list[Neuron] = []
182
- possible_out: list[Neuron] = []
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
- possible_in.extend(self.layers[layer_idx - 1].neurons)
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
- possible_out.extend(self.layers[layer_idx + 1].neurons)
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
- # All earlier layers ---> new neuron
208
+ # all earlier layers -> new neuron
194
209
  for i in range(0, layer_idx):
195
- possible_in.extend(self.layers[i].neurons)
196
- # New neuron ---> all later layers
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
- possible_out.extend(self.layers[j].neurons)
199
-
200
- # Density control per neuron
201
- if connection_density < 1.0:
202
- if possible_in:
203
- k_in = max(1, int(len(possible_in) * connection_density))
204
- possible_in = random.sample(possible_in, k_in)
205
- if possible_out:
206
- k_out = max(1, int(len(possible_out) * connection_density))
207
- possible_out = random.sample(possible_out, k_out)
208
-
209
- # Create the actual connections
210
- for src in possible_in:
211
- self.add_connection(src, n, weight=weight)
212
- for dst in possible_out:
213
- self.add_connection(n, dst, weight=weight)
214
-
215
- # Recurrent connections
216
- if recurrent:
217
- if RecurrentKind.DIRECT in recurrent:
218
- for n in new_neurons:
219
- if n.role != NeuronRole.HIDDEN:
220
- continue # No recurrence on INPUT or OUTPUT
221
- self.add_connection(
222
- n, n, weight=weight, conn_type=ConnectionType.RECURRENT
223
- )
224
-
225
- if RecurrentKind.LATERAL in recurrent:
226
- full_layer = list(self.layers[layer_idx].neurons)
227
- for src in full_layer:
228
- if src.role != NeuronRole.HIDDEN:
229
- continue # No recurrence on INPUT or OUTPUT
230
- for dst in new_neurons:
231
- if src is not dst:
232
- self.add_connection(
233
- src,
234
- dst,
235
- weight=weight,
236
- conn_type=ConnectionType.RECURRENT,
237
- )
238
-
239
- if RecurrentKind.INDIRECT in recurrent:
240
- for src in new_neurons:
241
- if src.role != NeuronRole.HIDDEN:
242
- continue # No recurrence on INPUT or OUTPUT
243
- for lower_layer in self.layers[1:layer_idx]:
244
- for dst in lower_layer.neurons:
245
- self.add_connection(
246
- src,
247
- dst,
248
- weight=weight,
249
- conn_type=ConnectionType.RECURRENT,
250
- )
251
- for higher_layer in self.layers[layer_idx + 1 :]:
252
- for src in higher_layer.neurons:
253
- if src != NeuronRole.HIDDEN:
254
- continue # No recurrence on INPUT or OUTPUT
255
- for dst in new_neurons:
256
- self.add_connection(
257
- src,
258
- dst,
259
- weight=weight,
260
- conn_type=ConnectionType.RECURRENT,
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evonet
3
- Version: 0.1.0.dev22
3
+ Version: 0.1.0.dev24
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.dev22"
7
+ version = "0.1.0.dev24"
8
8
  description = "Evolvable neural network core for integration with EvoLib"
9
9
  authors = [
10
10
  { name = "EvoLib", email = "evolib@dismail.de" }
File without changes
File without changes
File without changes
File without changes