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.
- morphml/__init__.py +14 -0
- morphml/api/__init__.py +26 -0
- morphml/api/app.py +326 -0
- morphml/api/auth.py +193 -0
- morphml/api/client.py +338 -0
- morphml/api/models.py +132 -0
- morphml/api/rate_limit.py +192 -0
- morphml/benchmarking/__init__.py +36 -0
- morphml/benchmarking/comparison.py +430 -0
- morphml/benchmarks/__init__.py +56 -0
- morphml/benchmarks/comparator.py +409 -0
- morphml/benchmarks/datasets.py +280 -0
- morphml/benchmarks/metrics.py +199 -0
- morphml/benchmarks/openml_suite.py +201 -0
- morphml/benchmarks/problems.py +289 -0
- morphml/benchmarks/suite.py +318 -0
- morphml/cli/__init__.py +5 -0
- morphml/cli/commands/experiment.py +329 -0
- morphml/cli/main.py +457 -0
- morphml/cli/quickstart.py +312 -0
- morphml/config.py +278 -0
- morphml/constraints/__init__.py +19 -0
- morphml/constraints/handler.py +205 -0
- morphml/constraints/predicates.py +285 -0
- morphml/core/__init__.py +3 -0
- morphml/core/crossover.py +449 -0
- morphml/core/dsl/README.md +359 -0
- morphml/core/dsl/__init__.py +72 -0
- morphml/core/dsl/ast_nodes.py +364 -0
- morphml/core/dsl/compiler.py +318 -0
- morphml/core/dsl/layers.py +368 -0
- morphml/core/dsl/lexer.py +336 -0
- morphml/core/dsl/parser.py +455 -0
- morphml/core/dsl/search_space.py +386 -0
- morphml/core/dsl/syntax.py +199 -0
- morphml/core/dsl/type_system.py +361 -0
- morphml/core/dsl/validator.py +386 -0
- morphml/core/graph/__init__.py +40 -0
- morphml/core/graph/edge.py +124 -0
- morphml/core/graph/graph.py +507 -0
- morphml/core/graph/mutations.py +409 -0
- morphml/core/graph/node.py +196 -0
- morphml/core/graph/serialization.py +361 -0
- morphml/core/graph/visualization.py +431 -0
- morphml/core/objectives/__init__.py +20 -0
- morphml/core/search/__init__.py +33 -0
- morphml/core/search/individual.py +252 -0
- morphml/core/search/parameters.py +453 -0
- morphml/core/search/population.py +375 -0
- morphml/core/search/search_engine.py +340 -0
- morphml/distributed/__init__.py +76 -0
- morphml/distributed/fault_tolerance.py +497 -0
- morphml/distributed/health_monitor.py +348 -0
- morphml/distributed/master.py +709 -0
- morphml/distributed/proto/README.md +224 -0
- morphml/distributed/proto/__init__.py +74 -0
- morphml/distributed/proto/worker.proto +170 -0
- morphml/distributed/proto/worker_pb2.py +79 -0
- morphml/distributed/proto/worker_pb2_grpc.py +423 -0
- morphml/distributed/resource_manager.py +416 -0
- morphml/distributed/scheduler.py +567 -0
- morphml/distributed/storage/__init__.py +33 -0
- morphml/distributed/storage/artifacts.py +381 -0
- morphml/distributed/storage/cache.py +366 -0
- morphml/distributed/storage/checkpointing.py +329 -0
- morphml/distributed/storage/database.py +459 -0
- morphml/distributed/worker.py +549 -0
- morphml/evaluation/__init__.py +5 -0
- morphml/evaluation/heuristic.py +237 -0
- morphml/exceptions.py +55 -0
- morphml/execution/__init__.py +5 -0
- morphml/execution/local_executor.py +350 -0
- morphml/integrations/__init__.py +28 -0
- morphml/integrations/jax_adapter.py +206 -0
- morphml/integrations/pytorch_adapter.py +530 -0
- morphml/integrations/sklearn_adapter.py +206 -0
- morphml/integrations/tensorflow_adapter.py +230 -0
- morphml/logging_config.py +93 -0
- morphml/meta_learning/__init__.py +66 -0
- morphml/meta_learning/architecture_similarity.py +277 -0
- morphml/meta_learning/experiment_database.py +240 -0
- morphml/meta_learning/knowledge_base/__init__.py +19 -0
- morphml/meta_learning/knowledge_base/embedder.py +179 -0
- morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
- morphml/meta_learning/knowledge_base/meta_features.py +265 -0
- morphml/meta_learning/knowledge_base/vector_store.py +271 -0
- morphml/meta_learning/predictors/__init__.py +27 -0
- morphml/meta_learning/predictors/ensemble.py +221 -0
- morphml/meta_learning/predictors/gnn_predictor.py +552 -0
- morphml/meta_learning/predictors/learning_curve.py +231 -0
- morphml/meta_learning/predictors/proxy_metrics.py +261 -0
- morphml/meta_learning/strategy_evolution/__init__.py +27 -0
- morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
- morphml/meta_learning/strategy_evolution/bandit.py +276 -0
- morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
- morphml/meta_learning/transfer.py +581 -0
- morphml/meta_learning/warm_start.py +286 -0
- morphml/optimizers/__init__.py +74 -0
- morphml/optimizers/adaptive_operators.py +399 -0
- morphml/optimizers/bayesian/__init__.py +52 -0
- morphml/optimizers/bayesian/acquisition.py +387 -0
- morphml/optimizers/bayesian/base.py +319 -0
- morphml/optimizers/bayesian/gaussian_process.py +635 -0
- morphml/optimizers/bayesian/smac.py +534 -0
- morphml/optimizers/bayesian/tpe.py +411 -0
- morphml/optimizers/differential_evolution.py +220 -0
- morphml/optimizers/evolutionary/__init__.py +61 -0
- morphml/optimizers/evolutionary/cma_es.py +416 -0
- morphml/optimizers/evolutionary/differential_evolution.py +556 -0
- morphml/optimizers/evolutionary/encoding.py +426 -0
- morphml/optimizers/evolutionary/particle_swarm.py +449 -0
- morphml/optimizers/genetic_algorithm.py +486 -0
- morphml/optimizers/gradient_based/__init__.py +22 -0
- morphml/optimizers/gradient_based/darts.py +550 -0
- morphml/optimizers/gradient_based/enas.py +585 -0
- morphml/optimizers/gradient_based/operations.py +474 -0
- morphml/optimizers/gradient_based/utils.py +601 -0
- morphml/optimizers/hill_climbing.py +169 -0
- morphml/optimizers/multi_objective/__init__.py +56 -0
- morphml/optimizers/multi_objective/indicators.py +504 -0
- morphml/optimizers/multi_objective/nsga2.py +647 -0
- morphml/optimizers/multi_objective/visualization.py +427 -0
- morphml/optimizers/nsga2.py +308 -0
- morphml/optimizers/random_search.py +172 -0
- morphml/optimizers/simulated_annealing.py +181 -0
- morphml/plugins/__init__.py +35 -0
- morphml/plugins/custom_evaluator_example.py +81 -0
- morphml/plugins/custom_optimizer_example.py +63 -0
- morphml/plugins/plugin_system.py +454 -0
- morphml/reports/__init__.py +30 -0
- morphml/reports/generator.py +362 -0
- morphml/tracking/__init__.py +7 -0
- morphml/tracking/experiment.py +309 -0
- morphml/tracking/logger.py +301 -0
- morphml/tracking/reporter.py +357 -0
- morphml/utils/__init__.py +6 -0
- morphml/utils/checkpoint.py +189 -0
- morphml/utils/comparison.py +390 -0
- morphml/utils/export.py +407 -0
- morphml/utils/progress.py +392 -0
- morphml/utils/validation.py +392 -0
- morphml/version.py +7 -0
- morphml/visualization/__init__.py +50 -0
- morphml/visualization/analytics.py +423 -0
- morphml/visualization/architecture_diagrams.py +353 -0
- morphml/visualization/architecture_plot.py +223 -0
- morphml/visualization/convergence_plot.py +174 -0
- morphml/visualization/crossover_viz.py +386 -0
- morphml/visualization/graph_viz.py +338 -0
- morphml/visualization/pareto_plot.py +149 -0
- morphml/visualization/plotly_dashboards.py +422 -0
- morphml/visualization/population.py +309 -0
- morphml/visualization/progress.py +260 -0
- morphml-1.0.0.dist-info/METADATA +434 -0
- morphml-1.0.0.dist-info/RECORD +158 -0
- morphml-1.0.0.dist-info/WHEEL +4 -0
- morphml-1.0.0.dist-info/entry_points.txt +3 -0
- 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)
|