morphml 1.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of morphml might be problematic. Click here for more details.

Files changed (158) hide show
  1. morphml/__init__.py +14 -0
  2. morphml/api/__init__.py +26 -0
  3. morphml/api/app.py +326 -0
  4. morphml/api/auth.py +193 -0
  5. morphml/api/client.py +338 -0
  6. morphml/api/models.py +132 -0
  7. morphml/api/rate_limit.py +192 -0
  8. morphml/benchmarking/__init__.py +36 -0
  9. morphml/benchmarking/comparison.py +430 -0
  10. morphml/benchmarks/__init__.py +56 -0
  11. morphml/benchmarks/comparator.py +409 -0
  12. morphml/benchmarks/datasets.py +280 -0
  13. morphml/benchmarks/metrics.py +199 -0
  14. morphml/benchmarks/openml_suite.py +201 -0
  15. morphml/benchmarks/problems.py +289 -0
  16. morphml/benchmarks/suite.py +318 -0
  17. morphml/cli/__init__.py +5 -0
  18. morphml/cli/commands/experiment.py +329 -0
  19. morphml/cli/main.py +457 -0
  20. morphml/cli/quickstart.py +312 -0
  21. morphml/config.py +278 -0
  22. morphml/constraints/__init__.py +19 -0
  23. morphml/constraints/handler.py +205 -0
  24. morphml/constraints/predicates.py +285 -0
  25. morphml/core/__init__.py +3 -0
  26. morphml/core/crossover.py +449 -0
  27. morphml/core/dsl/README.md +359 -0
  28. morphml/core/dsl/__init__.py +72 -0
  29. morphml/core/dsl/ast_nodes.py +364 -0
  30. morphml/core/dsl/compiler.py +318 -0
  31. morphml/core/dsl/layers.py +368 -0
  32. morphml/core/dsl/lexer.py +336 -0
  33. morphml/core/dsl/parser.py +455 -0
  34. morphml/core/dsl/search_space.py +386 -0
  35. morphml/core/dsl/syntax.py +199 -0
  36. morphml/core/dsl/type_system.py +361 -0
  37. morphml/core/dsl/validator.py +386 -0
  38. morphml/core/graph/__init__.py +40 -0
  39. morphml/core/graph/edge.py +124 -0
  40. morphml/core/graph/graph.py +507 -0
  41. morphml/core/graph/mutations.py +409 -0
  42. morphml/core/graph/node.py +196 -0
  43. morphml/core/graph/serialization.py +361 -0
  44. morphml/core/graph/visualization.py +431 -0
  45. morphml/core/objectives/__init__.py +20 -0
  46. morphml/core/search/__init__.py +33 -0
  47. morphml/core/search/individual.py +252 -0
  48. morphml/core/search/parameters.py +453 -0
  49. morphml/core/search/population.py +375 -0
  50. morphml/core/search/search_engine.py +340 -0
  51. morphml/distributed/__init__.py +76 -0
  52. morphml/distributed/fault_tolerance.py +497 -0
  53. morphml/distributed/health_monitor.py +348 -0
  54. morphml/distributed/master.py +709 -0
  55. morphml/distributed/proto/README.md +224 -0
  56. morphml/distributed/proto/__init__.py +74 -0
  57. morphml/distributed/proto/worker.proto +170 -0
  58. morphml/distributed/proto/worker_pb2.py +79 -0
  59. morphml/distributed/proto/worker_pb2_grpc.py +423 -0
  60. morphml/distributed/resource_manager.py +416 -0
  61. morphml/distributed/scheduler.py +567 -0
  62. morphml/distributed/storage/__init__.py +33 -0
  63. morphml/distributed/storage/artifacts.py +381 -0
  64. morphml/distributed/storage/cache.py +366 -0
  65. morphml/distributed/storage/checkpointing.py +329 -0
  66. morphml/distributed/storage/database.py +459 -0
  67. morphml/distributed/worker.py +549 -0
  68. morphml/evaluation/__init__.py +5 -0
  69. morphml/evaluation/heuristic.py +237 -0
  70. morphml/exceptions.py +55 -0
  71. morphml/execution/__init__.py +5 -0
  72. morphml/execution/local_executor.py +350 -0
  73. morphml/integrations/__init__.py +28 -0
  74. morphml/integrations/jax_adapter.py +206 -0
  75. morphml/integrations/pytorch_adapter.py +530 -0
  76. morphml/integrations/sklearn_adapter.py +206 -0
  77. morphml/integrations/tensorflow_adapter.py +230 -0
  78. morphml/logging_config.py +93 -0
  79. morphml/meta_learning/__init__.py +66 -0
  80. morphml/meta_learning/architecture_similarity.py +277 -0
  81. morphml/meta_learning/experiment_database.py +240 -0
  82. morphml/meta_learning/knowledge_base/__init__.py +19 -0
  83. morphml/meta_learning/knowledge_base/embedder.py +179 -0
  84. morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
  85. morphml/meta_learning/knowledge_base/meta_features.py +265 -0
  86. morphml/meta_learning/knowledge_base/vector_store.py +271 -0
  87. morphml/meta_learning/predictors/__init__.py +27 -0
  88. morphml/meta_learning/predictors/ensemble.py +221 -0
  89. morphml/meta_learning/predictors/gnn_predictor.py +552 -0
  90. morphml/meta_learning/predictors/learning_curve.py +231 -0
  91. morphml/meta_learning/predictors/proxy_metrics.py +261 -0
  92. morphml/meta_learning/strategy_evolution/__init__.py +27 -0
  93. morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
  94. morphml/meta_learning/strategy_evolution/bandit.py +276 -0
  95. morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
  96. morphml/meta_learning/transfer.py +581 -0
  97. morphml/meta_learning/warm_start.py +286 -0
  98. morphml/optimizers/__init__.py +74 -0
  99. morphml/optimizers/adaptive_operators.py +399 -0
  100. morphml/optimizers/bayesian/__init__.py +52 -0
  101. morphml/optimizers/bayesian/acquisition.py +387 -0
  102. morphml/optimizers/bayesian/base.py +319 -0
  103. morphml/optimizers/bayesian/gaussian_process.py +635 -0
  104. morphml/optimizers/bayesian/smac.py +534 -0
  105. morphml/optimizers/bayesian/tpe.py +411 -0
  106. morphml/optimizers/differential_evolution.py +220 -0
  107. morphml/optimizers/evolutionary/__init__.py +61 -0
  108. morphml/optimizers/evolutionary/cma_es.py +416 -0
  109. morphml/optimizers/evolutionary/differential_evolution.py +556 -0
  110. morphml/optimizers/evolutionary/encoding.py +426 -0
  111. morphml/optimizers/evolutionary/particle_swarm.py +449 -0
  112. morphml/optimizers/genetic_algorithm.py +486 -0
  113. morphml/optimizers/gradient_based/__init__.py +22 -0
  114. morphml/optimizers/gradient_based/darts.py +550 -0
  115. morphml/optimizers/gradient_based/enas.py +585 -0
  116. morphml/optimizers/gradient_based/operations.py +474 -0
  117. morphml/optimizers/gradient_based/utils.py +601 -0
  118. morphml/optimizers/hill_climbing.py +169 -0
  119. morphml/optimizers/multi_objective/__init__.py +56 -0
  120. morphml/optimizers/multi_objective/indicators.py +504 -0
  121. morphml/optimizers/multi_objective/nsga2.py +647 -0
  122. morphml/optimizers/multi_objective/visualization.py +427 -0
  123. morphml/optimizers/nsga2.py +308 -0
  124. morphml/optimizers/random_search.py +172 -0
  125. morphml/optimizers/simulated_annealing.py +181 -0
  126. morphml/plugins/__init__.py +35 -0
  127. morphml/plugins/custom_evaluator_example.py +81 -0
  128. morphml/plugins/custom_optimizer_example.py +63 -0
  129. morphml/plugins/plugin_system.py +454 -0
  130. morphml/reports/__init__.py +30 -0
  131. morphml/reports/generator.py +362 -0
  132. morphml/tracking/__init__.py +7 -0
  133. morphml/tracking/experiment.py +309 -0
  134. morphml/tracking/logger.py +301 -0
  135. morphml/tracking/reporter.py +357 -0
  136. morphml/utils/__init__.py +6 -0
  137. morphml/utils/checkpoint.py +189 -0
  138. morphml/utils/comparison.py +390 -0
  139. morphml/utils/export.py +407 -0
  140. morphml/utils/progress.py +392 -0
  141. morphml/utils/validation.py +392 -0
  142. morphml/version.py +7 -0
  143. morphml/visualization/__init__.py +50 -0
  144. morphml/visualization/analytics.py +423 -0
  145. morphml/visualization/architecture_diagrams.py +353 -0
  146. morphml/visualization/architecture_plot.py +223 -0
  147. morphml/visualization/convergence_plot.py +174 -0
  148. morphml/visualization/crossover_viz.py +386 -0
  149. morphml/visualization/graph_viz.py +338 -0
  150. morphml/visualization/pareto_plot.py +149 -0
  151. morphml/visualization/plotly_dashboards.py +422 -0
  152. morphml/visualization/population.py +309 -0
  153. morphml/visualization/progress.py +260 -0
  154. morphml-1.0.0.dist-info/METADATA +434 -0
  155. morphml-1.0.0.dist-info/RECORD +158 -0
  156. morphml-1.0.0.dist-info/WHEEL +4 -0
  157. morphml-1.0.0.dist-info/entry_points.txt +3 -0
  158. morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,449 @@
1
+ """Advanced crossover operators for genetic algorithms."""
2
+
3
+ import random
4
+ from typing import List, Tuple
5
+
6
+ from morphml.core.graph import GraphEdge, ModelGraph
7
+ from morphml.logging_config import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class CrossoverOperator:
13
+ """Base class for crossover operators."""
14
+
15
+ def __init__(self, name: str):
16
+ """Initialize crossover operator."""
17
+ self.name = name
18
+
19
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
20
+ """
21
+ Perform crossover between two parents.
22
+
23
+ Args:
24
+ parent1: First parent graph
25
+ parent2: Second parent graph
26
+
27
+ Returns:
28
+ Tuple of two offspring graphs
29
+ """
30
+ raise NotImplementedError
31
+
32
+
33
+ class SinglePointCrossover(CrossoverOperator):
34
+ """Single-point crossover for graphs."""
35
+
36
+ def __init__(self):
37
+ """Initialize single-point crossover."""
38
+ super().__init__("SinglePointCrossover")
39
+
40
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
41
+ """Perform single-point crossover."""
42
+ # Get sorted nodes
43
+ try:
44
+ nodes1 = parent1.topological_sort()
45
+ nodes2 = parent2.topological_sort()
46
+ except Exception:
47
+ # Fallback to random ordering
48
+ nodes1 = list(parent1.nodes.values())
49
+ nodes2 = list(parent2.nodes.values())
50
+
51
+ if len(nodes1) < 2 or len(nodes2) < 2:
52
+ # Can't crossover, return clones
53
+ return parent1.clone(), parent2.clone()
54
+
55
+ # Choose crossover point
56
+ point = random.randint(1, min(len(nodes1), len(nodes2)) - 1)
57
+
58
+ # Create offspring
59
+ child1 = ModelGraph()
60
+ child2 = ModelGraph()
61
+
62
+ # Child 1: nodes1[:point] + nodes2[point:]
63
+ for node in nodes1[:point]:
64
+ child1.add_node(node.clone())
65
+ for node in nodes2[point:]:
66
+ child1.add_node(node.clone())
67
+
68
+ # Child 2: nodes2[:point] + nodes1[point:]
69
+ for node in nodes2[:point]:
70
+ child2.add_node(node.clone())
71
+ for node in nodes1[point:]:
72
+ child2.add_node(node.clone())
73
+
74
+ # Reconnect edges
75
+ self._reconnect_edges(child1)
76
+ self._reconnect_edges(child2)
77
+
78
+ return child1, child2
79
+
80
+ def _reconnect_edges(self, graph: ModelGraph) -> None:
81
+ """Reconnect edges in a sensible way."""
82
+ nodes = list(graph.nodes.values())
83
+
84
+ for i in range(len(nodes) - 1):
85
+ try:
86
+ graph.add_edge(GraphEdge(nodes[i], nodes[i + 1]))
87
+ except Exception:
88
+ pass
89
+
90
+
91
+ class TwoPointCrossover(CrossoverOperator):
92
+ """Two-point crossover for graphs."""
93
+
94
+ def __init__(self):
95
+ """Initialize two-point crossover."""
96
+ super().__init__("TwoPointCrossover")
97
+
98
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
99
+ """Perform two-point crossover."""
100
+ try:
101
+ nodes1 = parent1.topological_sort()
102
+ nodes2 = parent2.topological_sort()
103
+ except Exception:
104
+ nodes1 = list(parent1.nodes.values())
105
+ nodes2 = list(parent2.nodes.values())
106
+
107
+ if len(nodes1) < 3 or len(nodes2) < 3:
108
+ return parent1.clone(), parent2.clone()
109
+
110
+ # Choose two crossover points
111
+ max_len = min(len(nodes1), len(nodes2))
112
+ point1 = random.randint(1, max_len - 2)
113
+ point2 = random.randint(point1 + 1, max_len - 1)
114
+
115
+ # Create offspring
116
+ child1 = ModelGraph()
117
+ child2 = ModelGraph()
118
+
119
+ # Child 1: nodes1[:point1] + nodes2[point1:point2] + nodes1[point2:]
120
+ for node in nodes1[:point1]:
121
+ child1.add_node(node.clone())
122
+ for node in nodes2[point1:point2]:
123
+ child1.add_node(node.clone())
124
+ for node in nodes1[point2:]:
125
+ child1.add_node(node.clone())
126
+
127
+ # Child 2: nodes2[:point1] + nodes1[point1:point2] + nodes2[point2:]
128
+ for node in nodes2[:point1]:
129
+ child2.add_node(node.clone())
130
+ for node in nodes1[point1:point2]:
131
+ child2.add_node(node.clone())
132
+ for node in nodes2[point2:]:
133
+ child2.add_node(node.clone())
134
+
135
+ # Reconnect edges
136
+ self._reconnect_edges(child1)
137
+ self._reconnect_edges(child2)
138
+
139
+ return child1, child2
140
+
141
+ def _reconnect_edges(self, graph: ModelGraph) -> None:
142
+ """Reconnect edges."""
143
+ nodes = list(graph.nodes.values())
144
+
145
+ for i in range(len(nodes) - 1):
146
+ try:
147
+ graph.add_edge(GraphEdge(nodes[i], nodes[i + 1]))
148
+ except Exception:
149
+ pass
150
+
151
+
152
+ class UniformCrossover(CrossoverOperator):
153
+ """Uniform crossover for graphs."""
154
+
155
+ def __init__(self, swap_probability: float = 0.5):
156
+ """
157
+ Initialize uniform crossover.
158
+
159
+ Args:
160
+ swap_probability: Probability of swapping each node
161
+ """
162
+ super().__init__("UniformCrossover")
163
+ self.swap_probability = swap_probability
164
+
165
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
166
+ """Perform uniform crossover."""
167
+ try:
168
+ nodes1 = parent1.topological_sort()
169
+ nodes2 = parent2.topological_sort()
170
+ except Exception:
171
+ nodes1 = list(parent1.nodes.values())
172
+ nodes2 = list(parent2.nodes.values())
173
+
174
+ # Make lists same length by padding
175
+ max_len = max(len(nodes1), len(nodes2))
176
+
177
+ child1 = ModelGraph()
178
+ child2 = ModelGraph()
179
+
180
+ for i in range(max_len):
181
+ if random.random() < self.swap_probability:
182
+ # Swap
183
+ if i < len(nodes2):
184
+ child1.add_node(nodes2[i].clone())
185
+ if i < len(nodes1):
186
+ child2.add_node(nodes1[i].clone())
187
+ else:
188
+ # Don't swap
189
+ if i < len(nodes1):
190
+ child1.add_node(nodes1[i].clone())
191
+ if i < len(nodes2):
192
+ child2.add_node(nodes2[i].clone())
193
+
194
+ # Reconnect edges
195
+ self._reconnect_edges(child1)
196
+ self._reconnect_edges(child2)
197
+
198
+ return child1, child2
199
+
200
+ def _reconnect_edges(self, graph: ModelGraph) -> None:
201
+ """Reconnect edges."""
202
+ nodes = list(graph.nodes.values())
203
+
204
+ for i in range(len(nodes) - 1):
205
+ try:
206
+ graph.add_edge(GraphEdge(nodes[i], nodes[i + 1]))
207
+ except Exception:
208
+ pass
209
+
210
+
211
+ class SubgraphCrossover(CrossoverOperator):
212
+ """Crossover by exchanging subgraphs."""
213
+
214
+ def __init__(self):
215
+ """Initialize subgraph crossover."""
216
+ super().__init__("SubgraphCrossover")
217
+
218
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
219
+ """Perform subgraph crossover."""
220
+ # Find compatible subgraphs
221
+ nodes1 = list(parent1.nodes.values())
222
+ nodes2 = list(parent2.nodes.values())
223
+
224
+ if len(nodes1) < 2 or len(nodes2) < 2:
225
+ return parent1.clone(), parent2.clone()
226
+
227
+ # Select random subgraph from each parent
228
+ subgraph_size = min(3, len(nodes1) // 2, len(nodes2) // 2)
229
+
230
+ start1 = random.randint(0, len(nodes1) - subgraph_size)
231
+ subgraph1 = nodes1[start1 : start1 + subgraph_size]
232
+
233
+ start2 = random.randint(0, len(nodes2) - subgraph_size)
234
+ subgraph2 = nodes2[start2 : start2 + subgraph_size]
235
+
236
+ # Create offspring
237
+ child1 = parent1.clone()
238
+ child2 = parent2.clone()
239
+
240
+ # Replace subgraphs
241
+ for i, node in enumerate(subgraph2):
242
+ if start1 + i < len(nodes1):
243
+ old_id = nodes1[start1 + i].id
244
+ if old_id in child1.nodes:
245
+ child1.nodes[old_id] = node.clone()
246
+
247
+ for i, node in enumerate(subgraph1):
248
+ if start2 + i < len(nodes2):
249
+ old_id = nodes2[start2 + i].id
250
+ if old_id in child2.nodes:
251
+ child2.nodes[old_id] = node.clone()
252
+
253
+ return child1, child2
254
+
255
+
256
+ class LayerWiseCrossover(CrossoverOperator):
257
+ """Crossover by exchanging layers of same type."""
258
+
259
+ def __init__(self):
260
+ """Initialize layer-wise crossover."""
261
+ super().__init__("LayerWiseCrossover")
262
+
263
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
264
+ """Perform layer-wise crossover."""
265
+ child1 = parent1.clone()
266
+ child2 = parent2.clone()
267
+
268
+ # Group nodes by operation type
269
+ ops1 = {}
270
+ for node in child1.nodes.values():
271
+ if node.operation not in ops1:
272
+ ops1[node.operation] = []
273
+ ops1[node.operation].append(node)
274
+
275
+ ops2 = {}
276
+ for node in child2.nodes.values():
277
+ if node.operation not in ops2:
278
+ ops2[node.operation] = []
279
+ ops2[node.operation].append(node)
280
+
281
+ # Exchange layers of same type
282
+ common_ops = set(ops1.keys()) & set(ops2.keys())
283
+
284
+ for op in common_ops:
285
+ if random.random() < 0.5:
286
+ # Swap parameters of first node of this type
287
+ if ops1[op] and ops2[op]:
288
+ node1 = ops1[op][0]
289
+ node2 = ops2[op][0]
290
+
291
+ # Swap parameters
292
+ node1.params, node2.params = node2.params.copy(), node1.params.copy()
293
+
294
+ return child1, child2
295
+
296
+
297
+ class AdaptiveCrossover(CrossoverOperator):
298
+ """Adaptive crossover that chooses operator based on parents."""
299
+
300
+ def __init__(self):
301
+ """Initialize adaptive crossover."""
302
+ super().__init__("AdaptiveCrossover")
303
+ self.operators = [
304
+ SinglePointCrossover(),
305
+ TwoPointCrossover(),
306
+ UniformCrossover(),
307
+ SubgraphCrossover(),
308
+ LayerWiseCrossover(),
309
+ ]
310
+
311
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
312
+ """Perform adaptive crossover."""
313
+ # Choose operator based on parent similarity
314
+ similarity = self._calculate_similarity(parent1, parent2)
315
+
316
+ if similarity > 0.7:
317
+ # High similarity - use more explorative crossover
318
+ operator = random.choice(
319
+ [
320
+ self.operators[2], # Uniform
321
+ self.operators[3], # Subgraph
322
+ ]
323
+ )
324
+ elif similarity < 0.3:
325
+ # Low similarity - use more conservative crossover
326
+ operator = random.choice(
327
+ [
328
+ self.operators[0], # Single-point
329
+ self.operators[4], # Layer-wise
330
+ ]
331
+ )
332
+ else:
333
+ # Medium similarity - use balanced crossover
334
+ operator = self.operators[1] # Two-point
335
+
336
+ return operator.crossover(parent1, parent2)
337
+
338
+ def _calculate_similarity(self, graph1: ModelGraph, graph2: ModelGraph) -> float:
339
+ """Calculate structural similarity between graphs."""
340
+ # Simple similarity based on node count and types
341
+ nodes1 = graph1.nodes.values()
342
+ nodes2 = graph2.nodes.values()
343
+
344
+ if len(nodes1) == 0 or len(nodes2) == 0:
345
+ return 0.0
346
+
347
+ # Size similarity
348
+ size_sim = 1.0 - abs(len(nodes1) - len(nodes2)) / max(len(nodes1), len(nodes2))
349
+
350
+ # Operation type similarity
351
+ ops1 = {n.operation for n in nodes1}
352
+ ops2 = {n.operation for n in nodes2}
353
+
354
+ if not ops1 or not ops2:
355
+ return size_sim
356
+
357
+ intersection = len(ops1 & ops2)
358
+ union = len(ops1 | ops2)
359
+ op_sim = intersection / union if union > 0 else 0.0
360
+
361
+ return 0.5 * size_sim + 0.5 * op_sim
362
+
363
+
364
+ class MultiParentCrossover(CrossoverOperator):
365
+ """Crossover using multiple parents."""
366
+
367
+ def __init__(self, num_parents: int = 3):
368
+ """
369
+ Initialize multi-parent crossover.
370
+
371
+ Args:
372
+ num_parents: Number of parents to use
373
+ """
374
+ super().__init__("MultiParentCrossover")
375
+ self.num_parents = num_parents
376
+
377
+ def crossover_multi(self, parents: List[ModelGraph]) -> ModelGraph:
378
+ """
379
+ Perform multi-parent crossover.
380
+
381
+ Args:
382
+ parents: List of parent graphs
383
+
384
+ Returns:
385
+ Single offspring graph
386
+ """
387
+ if not parents:
388
+ raise ValueError("No parents provided")
389
+
390
+ if len(parents) < 2:
391
+ return parents[0].clone()
392
+
393
+ # Collect all nodes from parents
394
+ all_nodes = []
395
+ for parent in parents:
396
+ try:
397
+ nodes = parent.topological_sort()
398
+ except Exception:
399
+ nodes = list(parent.nodes.values())
400
+ all_nodes.append(nodes)
401
+
402
+ # Build offspring by selecting from parents
403
+ child = ModelGraph()
404
+ max_len = max(len(nodes) for nodes in all_nodes)
405
+
406
+ for i in range(max_len):
407
+ # Select node from random parent
408
+ available_parents = [nodes for nodes in all_nodes if i < len(nodes)]
409
+ if available_parents:
410
+ selected_nodes = random.choice(available_parents)
411
+ child.add_node(selected_nodes[i].clone())
412
+
413
+ # Reconnect edges
414
+ nodes = list(child.nodes.values())
415
+ for i in range(len(nodes) - 1):
416
+ try:
417
+ child.add_edge(GraphEdge(nodes[i], nodes[i + 1]))
418
+ except Exception:
419
+ pass
420
+
421
+ return child
422
+
423
+
424
+ def get_crossover_operator(name: str, **kwargs) -> CrossoverOperator:
425
+ """
426
+ Get crossover operator by name.
427
+
428
+ Args:
429
+ name: Operator name
430
+ **kwargs: Operator parameters
431
+
432
+ Returns:
433
+ CrossoverOperator instance
434
+ """
435
+ operators = {
436
+ "single_point": SinglePointCrossover,
437
+ "two_point": TwoPointCrossover,
438
+ "uniform": UniformCrossover,
439
+ "subgraph": SubgraphCrossover,
440
+ "layerwise": LayerWiseCrossover,
441
+ "adaptive": AdaptiveCrossover,
442
+ }
443
+
444
+ operator_class = operators.get(name.lower())
445
+
446
+ if operator_class is None:
447
+ raise ValueError(f"Unknown crossover operator: {name}")
448
+
449
+ return operator_class(**kwargs)