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,647 @@
1
+ """NSGA-II (Non-dominated Sorting Genetic Algorithm II) implementation.
2
+
3
+ NSGA-II is a state-of-the-art multi-objective evolutionary algorithm that finds
4
+ a set of Pareto-optimal solutions balancing multiple competing objectives.
5
+
6
+ Key Features:
7
+ - Fast non-dominated sorting (O(MN²))
8
+ - Crowding distance for diversity maintenance
9
+ - Elitism (parent + offspring selection)
10
+ - Multiple objective optimization
11
+
12
+ Reference:
13
+ Deb, K., et al. "A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II."
14
+ IEEE Transactions on Evolutionary Computation, 2002.
15
+
16
+ Author: Eshan Roy <eshanized@proton.me>
17
+ Organization: TONMOY INFRASTRUCTURE & VISION
18
+ """
19
+
20
+ import random
21
+ from dataclasses import dataclass, field
22
+ from typing import Any, Callable, Dict, List, Optional, Tuple
23
+
24
+ import numpy as np
25
+
26
+ from morphml.core.dsl import SearchSpace
27
+ from morphml.core.graph import GraphMutator, ModelGraph
28
+ from morphml.logging_config import get_logger
29
+
30
+ logger = get_logger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class MultiObjectiveIndividual:
35
+ """
36
+ Individual with multiple fitness objectives.
37
+
38
+ Attributes:
39
+ genome: ModelGraph architecture
40
+ objectives: Dictionary of objective values {'accuracy': 0.95, 'latency': 12.5}
41
+ rank: Pareto rank (0 = non-dominated front)
42
+ crowding_distance: Density measure in objective space
43
+ domination_count: Number of solutions dominating this one
44
+ dominated_solutions: List of solutions this one dominates
45
+
46
+ Example:
47
+ >>> ind = MultiObjectiveIndividual(
48
+ ... genome=graph,
49
+ ... objectives={'accuracy': 0.95, 'latency': 12.5, 'params': 2.1}
50
+ ... )
51
+ >>> ind.rank = 0 # Non-dominated
52
+ >>> ind.crowding_distance = 1.5
53
+ """
54
+
55
+ genome: ModelGraph
56
+ objectives: Dict[str, float] = field(default_factory=dict)
57
+ rank: int = 0
58
+ crowding_distance: float = 0.0
59
+ domination_count: int = 0
60
+ dominated_solutions: List["MultiObjectiveIndividual"] = field(default_factory=list)
61
+
62
+ def dominates(self, other: "MultiObjectiveIndividual", objective_specs: List[Dict]) -> bool:
63
+ """
64
+ Check if this individual Pareto-dominates another.
65
+
66
+ Individual A dominates B if:
67
+ 1. A is better than or equal to B in all objectives
68
+ 2. A is strictly better than B in at least one objective
69
+
70
+ Args:
71
+ other: Other individual to compare
72
+ objective_specs: List of objective specifications with 'maximize' flags
73
+
74
+ Returns:
75
+ True if this individual dominates the other
76
+
77
+ Example:
78
+ >>> ind1.objectives = {'acc': 0.95, 'latency': -10}
79
+ >>> ind2.objectives = {'acc': 0.90, 'latency': -15}
80
+ >>> ind1.dominates(ind2, objectives) # True (better in both)
81
+ """
82
+ better_in_any = False
83
+
84
+ for obj_spec in objective_specs:
85
+ obj_name = obj_spec["name"]
86
+ maximize = obj_spec["maximize"]
87
+
88
+ my_value = self.objectives.get(obj_name, 0.0)
89
+ other_value = other.objectives.get(obj_name, 0.0)
90
+
91
+ # Compare based on optimization direction
92
+ if maximize:
93
+ if my_value < other_value:
94
+ return False # Worse in this objective
95
+ elif my_value > other_value:
96
+ better_in_any = True
97
+ else: # Minimize
98
+ if my_value > other_value:
99
+ return False # Worse in this objective
100
+ elif my_value < other_value:
101
+ better_in_any = True
102
+
103
+ return better_in_any
104
+
105
+ def __repr__(self) -> str:
106
+ """String representation."""
107
+ obj_str = ", ".join(f"{k}={v:.4f}" for k, v in self.objectives.items())
108
+ return f"Individual(rank={self.rank}, crowding={self.crowding_distance:.4f}, {obj_str})"
109
+
110
+
111
+ class NSGA2Optimizer:
112
+ """
113
+ NSGA-II: Non-dominated Sorting Genetic Algorithm II.
114
+
115
+ Multi-objective evolutionary algorithm that finds a diverse set of
116
+ Pareto-optimal solutions balancing multiple competing objectives.
117
+
118
+ Algorithm:
119
+ 1. Initialize random population
120
+ 2. Evaluate all objectives for each individual
121
+ 3. Fast non-dominated sorting (rank assignment)
122
+ 4. Crowding distance calculation (diversity measure)
123
+ 5. Tournament selection based on rank and crowding
124
+ 6. Crossover and mutation to generate offspring
125
+ 7. Elitist selection from parent + offspring
126
+ 8. Repeat steps 3-7 for N generations
127
+
128
+ Configuration:
129
+ population_size: Population size (default: 100)
130
+ num_generations: Number of generations (default: 100)
131
+ crossover_rate: Crossover probability (default: 0.9)
132
+ mutation_rate: Mutation probability (default: 0.1)
133
+ tournament_size: Tournament selection size (default: 2)
134
+ objectives: List of objective specifications
135
+ Format: [{'name': 'accuracy', 'maximize': True}, ...]
136
+
137
+ Example:
138
+ >>> from morphml.optimizers.multi_objective import NSGA2Optimizer
139
+ >>> optimizer = NSGA2Optimizer(
140
+ ... search_space=space,
141
+ ... config={
142
+ ... 'population_size': 100,
143
+ ... 'num_generations': 100,
144
+ ... 'objectives': [
145
+ ... {'name': 'accuracy', 'maximize': True},
146
+ ... {'name': 'latency', 'maximize': False},
147
+ ... {'name': 'params', 'maximize': False}
148
+ ... ]
149
+ ... }
150
+ ... )
151
+ >>> pareto_front = optimizer.optimize(evaluator)
152
+ >>> print(f"Found {len(pareto_front)} Pareto-optimal solutions")
153
+ """
154
+
155
+ def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
156
+ """
157
+ Initialize NSGA-II optimizer.
158
+
159
+ Args:
160
+ search_space: SearchSpace defining architecture options
161
+ config: Configuration dictionary
162
+ """
163
+ self.search_space = search_space
164
+ self.config = config or {}
165
+
166
+ # Algorithm parameters
167
+ self.pop_size = self.config.get("population_size", 100)
168
+ self.num_generations = self.config.get("num_generations", 100)
169
+ self.crossover_rate = self.config.get("crossover_rate", 0.9)
170
+ self.mutation_rate = self.config.get("mutation_rate", 0.1)
171
+ self.tournament_size = self.config.get("tournament_size", 2)
172
+
173
+ # Objective specifications
174
+ self.objectives = self.config.get(
175
+ "objectives",
176
+ [{"name": "fitness", "maximize": True}, {"name": "complexity", "maximize": False}],
177
+ )
178
+
179
+ # State
180
+ self.population: List[MultiObjectiveIndividual] = []
181
+ self.generation = 0
182
+ self.history: List[Dict[str, Any]] = []
183
+
184
+ # Mutation operator
185
+ self.mutator = GraphMutator()
186
+
187
+ logger.info(
188
+ f"Initialized NSGA-II with pop_size={self.pop_size}, "
189
+ f"generations={self.num_generations}, {len(self.objectives)} objectives"
190
+ )
191
+
192
+ def initialize_population(self) -> None:
193
+ """Initialize random population from search space."""
194
+ self.population = []
195
+ for _i in range(self.pop_size):
196
+ genome = self.search_space.sample()
197
+ individual = MultiObjectiveIndividual(genome=genome)
198
+ self.population.append(individual)
199
+
200
+ logger.debug(f"Initialized population of {self.pop_size} individuals")
201
+
202
+ def evaluate_individual(
203
+ self, individual: MultiObjectiveIndividual, evaluator: Callable
204
+ ) -> None:
205
+ """
206
+ Evaluate all objectives for an individual.
207
+
208
+ Args:
209
+ individual: Individual to evaluate
210
+ evaluator: Function that returns dict of objective values
211
+ evaluator(graph) -> {'accuracy': 0.95, 'latency': 12.5, ...}
212
+ """
213
+ # Call evaluator
214
+ results = evaluator(individual.genome)
215
+
216
+ # Store objective values
217
+ for obj_spec in self.objectives:
218
+ obj_name = obj_spec["name"]
219
+ value = results.get(obj_name, 0.0)
220
+ individual.objectives[obj_name] = value
221
+
222
+ def fast_non_dominated_sort(
223
+ self, population: List[MultiObjectiveIndividual]
224
+ ) -> List[List[MultiObjectiveIndividual]]:
225
+ """
226
+ Fast non-dominated sorting algorithm.
227
+
228
+ Assigns Pareto ranks to individuals and returns fronts.
229
+
230
+ Complexity: O(MN²) where M = objectives, N = population size
231
+
232
+ Args:
233
+ population: Population to sort
234
+
235
+ Returns:
236
+ List of Pareto fronts [F0, F1, F2, ...]
237
+ where F0 is the non-dominated front
238
+
239
+ Example:
240
+ >>> fronts = optimizer.fast_non_dominated_sort(population)
241
+ >>> pareto_front = fronts[0] # Non-dominated solutions
242
+ """
243
+ # Initialize
244
+ for ind in population:
245
+ ind.domination_count = 0
246
+ ind.dominated_solutions = []
247
+
248
+ fronts: List[List[MultiObjectiveIndividual]] = [[]]
249
+
250
+ # Find dominated solutions
251
+ for p in population:
252
+ for q in population:
253
+ if p is q:
254
+ continue
255
+
256
+ if p.dominates(q, self.objectives):
257
+ p.dominated_solutions.append(q)
258
+ elif q.dominates(p, self.objectives):
259
+ p.domination_count += 1
260
+
261
+ # Non-dominated solutions (rank 0)
262
+ if p.domination_count == 0:
263
+ p.rank = 0
264
+ fronts[0].append(p)
265
+
266
+ # Build subsequent fronts
267
+ i = 0
268
+ while fronts[i]:
269
+ next_front = []
270
+
271
+ for p in fronts[i]:
272
+ for q in p.dominated_solutions:
273
+ q.domination_count -= 1
274
+ if q.domination_count == 0:
275
+ q.rank = i + 1
276
+ next_front.append(q)
277
+
278
+ i += 1
279
+ if next_front:
280
+ fronts.append(next_front)
281
+
282
+ return fronts
283
+
284
+ def calculate_crowding_distance(self, front: List[MultiObjectiveIndividual]) -> None:
285
+ """
286
+ Calculate crowding distance for individuals in a front.
287
+
288
+ Crowding distance measures the density of solutions around an
289
+ individual in objective space. Higher distance = more isolated.
290
+
291
+ Boundary points get infinite distance to preserve diversity.
292
+
293
+ Args:
294
+ front: List of individuals in the same front
295
+ """
296
+ n = len(front)
297
+
298
+ if n == 0:
299
+ return
300
+
301
+ # Initialize distances to 0
302
+ for ind in front:
303
+ ind.crowding_distance = 0.0
304
+
305
+ # For each objective
306
+ for obj_spec in self.objectives:
307
+ obj_name = obj_spec["name"]
308
+
309
+ # Sort by objective value
310
+ front_sorted = sorted(front, key=lambda x: x.objectives[obj_name])
311
+
312
+ # Boundary points get infinite distance
313
+ front_sorted[0].crowding_distance = float("inf")
314
+ front_sorted[-1].crowding_distance = float("inf")
315
+
316
+ # Objective range
317
+ obj_min = front_sorted[0].objectives[obj_name]
318
+ obj_max = front_sorted[-1].objectives[obj_name]
319
+ obj_range = obj_max - obj_min
320
+
321
+ if obj_range == 0:
322
+ continue # All values same, no distance contribution
323
+
324
+ # Calculate crowding distance
325
+ for i in range(1, n - 1):
326
+ distance = (
327
+ front_sorted[i + 1].objectives[obj_name]
328
+ - front_sorted[i - 1].objectives[obj_name]
329
+ ) / obj_range
330
+
331
+ front_sorted[i].crowding_distance += distance
332
+
333
+ def tournament_selection(self) -> MultiObjectiveIndividual:
334
+ """
335
+ Binary tournament selection.
336
+
337
+ Select k random individuals and return the best based on:
338
+ 1. Lower rank (closer to Pareto front)
339
+ 2. If same rank, higher crowding distance (more isolated)
340
+
341
+ Returns:
342
+ Selected individual
343
+ """
344
+ contestants = random.sample(self.population, self.tournament_size)
345
+
346
+ # Sort by rank (ascending), then crowding distance (descending)
347
+ contestants.sort(key=lambda x: (x.rank, -x.crowding_distance))
348
+
349
+ return contestants[0]
350
+
351
+ def crossover(self, parent1: ModelGraph, parent2: ModelGraph) -> Tuple[ModelGraph, ModelGraph]:
352
+ """
353
+ Graph crossover operation.
354
+
355
+ Exchanges subgraphs between two parent architectures.
356
+
357
+ Args:
358
+ parent1: First parent graph
359
+ parent2: Second parent graph
360
+
361
+ Returns:
362
+ Two offspring graphs
363
+ """
364
+ # Clone parents
365
+ child1 = parent1.clone()
366
+ child2 = parent2.clone()
367
+
368
+ # Simple crossover: swap random nodes
369
+ if len(child1.nodes) > 2 and len(child2.nodes) > 2:
370
+ # Get non-terminal nodes
371
+ nodes1 = [n for n in child1.nodes.values() if n.operation not in ["input", "output"]]
372
+ nodes2 = [n for n in child2.nodes.values() if n.operation not in ["input", "output"]]
373
+
374
+ if nodes1 and nodes2:
375
+ # Select random nodes to swap
376
+ idx1 = random.randint(0, len(nodes1) - 1)
377
+ idx2 = random.randint(0, len(nodes2) - 1)
378
+
379
+ # Swap node operations (simplified crossover)
380
+ nodes1[idx1].operation, nodes2[idx2].operation = (
381
+ nodes2[idx2].operation,
382
+ nodes1[idx1].operation,
383
+ )
384
+
385
+ return child1, child2
386
+
387
+ def mutate(self, graph: ModelGraph) -> ModelGraph:
388
+ """
389
+ Mutation operation on architecture.
390
+
391
+ Args:
392
+ graph: Graph to mutate
393
+
394
+ Returns:
395
+ Mutated graph
396
+ """
397
+ mutated = graph.clone()
398
+
399
+ # Random mutation type
400
+ mutation_type = random.choice(["add_node", "remove_node", "modify_node"])
401
+
402
+ try:
403
+ if mutation_type == "add_node":
404
+ self.mutator.add_node(mutated)
405
+ elif mutation_type == "remove_node" and len(mutated.nodes) > 3:
406
+ self.mutator.remove_node(mutated)
407
+ elif mutation_type == "modify_node":
408
+ self.mutator.mutate_node_params(mutated)
409
+ except Exception as e:
410
+ logger.warning(f"Mutation failed: {e}. Returning original.")
411
+ return graph
412
+
413
+ return mutated if mutated.is_valid_dag() else graph
414
+
415
+ def generate_offspring(self) -> List[MultiObjectiveIndividual]:
416
+ """
417
+ Generate offspring population via selection, crossover, and mutation.
418
+
419
+ Returns:
420
+ Offspring population
421
+ """
422
+ offspring = []
423
+
424
+ while len(offspring) < self.pop_size:
425
+ # Selection
426
+ parent1 = self.tournament_selection()
427
+ parent2 = self.tournament_selection()
428
+
429
+ # Crossover
430
+ if random.random() < self.crossover_rate:
431
+ child1_genome, child2_genome = self.crossover(parent1.genome, parent2.genome)
432
+ else:
433
+ child1_genome = parent1.genome.clone()
434
+ child2_genome = parent2.genome.clone()
435
+
436
+ # Mutation
437
+ if random.random() < self.mutation_rate:
438
+ child1_genome = self.mutate(child1_genome)
439
+ if random.random() < self.mutation_rate:
440
+ child2_genome = self.mutate(child2_genome)
441
+
442
+ # Create offspring individuals
443
+ child1 = MultiObjectiveIndividual(genome=child1_genome)
444
+ child2 = MultiObjectiveIndividual(genome=child2_genome)
445
+
446
+ offspring.extend([child1, child2])
447
+
448
+ return offspring[: self.pop_size]
449
+
450
+ def environmental_selection(
451
+ self, combined_population: List[MultiObjectiveIndividual]
452
+ ) -> List[MultiObjectiveIndividual]:
453
+ """
454
+ Select next generation from combined parent + offspring population.
455
+
456
+ Elitist selection preserving best solutions based on:
457
+ 1. Pareto rank (lower is better)
458
+ 2. Crowding distance (higher is better for diversity)
459
+
460
+ Args:
461
+ combined_population: Combined parent + offspring population
462
+
463
+ Returns:
464
+ Selected population of size pop_size
465
+ """
466
+ # Fast non-dominated sorting
467
+ fronts = self.fast_non_dominated_sort(combined_population)
468
+
469
+ # Calculate crowding distances
470
+ for front in fronts:
471
+ self.calculate_crowding_distance(front)
472
+
473
+ # Select individuals for next generation
474
+ next_population = []
475
+
476
+ for front in fronts:
477
+ if len(next_population) + len(front) <= self.pop_size:
478
+ # Add entire front
479
+ next_population.extend(front)
480
+ else:
481
+ # Fill remaining slots with most isolated individuals
482
+ remaining = self.pop_size - len(next_population)
483
+ front_sorted = sorted(front, key=lambda x: -x.crowding_distance)
484
+ next_population.extend(front_sorted[:remaining])
485
+ break
486
+
487
+ return next_population
488
+
489
+ def optimize(self, evaluator: Callable) -> List[MultiObjectiveIndividual]:
490
+ """
491
+ Run NSGA-II optimization.
492
+
493
+ Args:
494
+ evaluator: Function that evaluates objectives
495
+ evaluator(graph) -> {'accuracy': 0.95, 'latency': 12.5, ...}
496
+
497
+ Returns:
498
+ Pareto front (list of non-dominated solutions)
499
+
500
+ Example:
501
+ >>> def my_evaluator(graph):
502
+ ... return {
503
+ ... 'accuracy': evaluate_accuracy(graph),
504
+ ... 'latency': measure_latency(graph),
505
+ ... 'params': count_parameters(graph)
506
+ ... }
507
+ >>> pareto_front = optimizer.optimize(my_evaluator)
508
+ """
509
+ logger.info(f"Starting NSGA-II optimization with {self.pop_size} individuals")
510
+
511
+ # Initialize population
512
+ self.initialize_population()
513
+
514
+ # Evaluate initial population
515
+ for individual in self.population:
516
+ self.evaluate_individual(individual, evaluator)
517
+
518
+ # Evolution loop
519
+ for generation in range(self.num_generations):
520
+ self.generation = generation
521
+
522
+ # Generate offspring
523
+ offspring = self.generate_offspring()
524
+
525
+ # Evaluate offspring
526
+ for individual in offspring:
527
+ self.evaluate_individual(individual, evaluator)
528
+
529
+ # Combine parent + offspring
530
+ combined = self.population + offspring
531
+
532
+ # Environmental selection
533
+ self.population = self.environmental_selection(combined)
534
+
535
+ # Logging
536
+ if generation % 10 == 0 or generation == self.num_generations - 1:
537
+ pareto_front = [ind for ind in self.population if ind.rank == 0]
538
+ logger.info(
539
+ f"Generation {generation}/{self.num_generations}: "
540
+ f"Pareto front size = {len(pareto_front)}"
541
+ )
542
+
543
+ # Log objective statistics
544
+ self._log_pareto_statistics(pareto_front)
545
+
546
+ # Save to history
547
+ self.history.append(
548
+ {
549
+ "generation": generation,
550
+ "pareto_size": len(pareto_front),
551
+ "pareto_front": pareto_front,
552
+ }
553
+ )
554
+
555
+ # Extract final Pareto front
556
+ pareto_front = [ind for ind in self.population if ind.rank == 0]
557
+
558
+ logger.info(f"NSGA-II complete. Final Pareto front: {len(pareto_front)} solutions")
559
+
560
+ return pareto_front
561
+
562
+ def _log_pareto_statistics(self, pareto_front: List[MultiObjectiveIndividual]) -> None:
563
+ """Log statistics of Pareto front objectives."""
564
+ if not pareto_front:
565
+ return
566
+
567
+ for obj_spec in self.objectives:
568
+ obj_name = obj_spec["name"]
569
+ values = [ind.objectives[obj_name] for ind in pareto_front]
570
+
571
+ if values:
572
+ logger.info(
573
+ f" {obj_name}: "
574
+ f"min={min(values):.4f}, "
575
+ f"max={max(values):.4f}, "
576
+ f"mean={np.mean(values):.4f}"
577
+ )
578
+
579
+ def get_pareto_front(self) -> List[MultiObjectiveIndividual]:
580
+ """
581
+ Get current Pareto front (non-dominated solutions).
582
+
583
+ Returns:
584
+ List of non-dominated individuals
585
+ """
586
+ return [ind for ind in self.population if ind.rank == 0]
587
+
588
+ def get_history(self) -> List[Dict[str, Any]]:
589
+ """Get optimization history."""
590
+ return self.history
591
+
592
+
593
+ # Convenience function
594
+ def optimize_with_nsga2(
595
+ search_space: SearchSpace,
596
+ evaluator: Callable,
597
+ objectives: List[Dict[str, Any]],
598
+ population_size: int = 100,
599
+ num_generations: int = 100,
600
+ verbose: bool = True,
601
+ ) -> List[MultiObjectiveIndividual]:
602
+ """
603
+ Quick NSGA-II optimization with sensible defaults.
604
+
605
+ Args:
606
+ search_space: SearchSpace to optimize over
607
+ evaluator: Multi-objective evaluator function
608
+ objectives: List of objective specifications
609
+ population_size: Population size
610
+ num_generations: Number of generations
611
+ verbose: Print progress
612
+
613
+ Returns:
614
+ Pareto front
615
+
616
+ Example:
617
+ >>> pareto_front = optimize_with_nsga2(
618
+ ... search_space=space,
619
+ ... evaluator=my_evaluator,
620
+ ... objectives=[
621
+ ... {'name': 'accuracy', 'maximize': True},
622
+ ... {'name': 'latency', 'maximize': False}
623
+ ... ],
624
+ ... population_size=100,
625
+ ... num_generations=50
626
+ ... )
627
+ """
628
+ optimizer = NSGA2Optimizer(
629
+ search_space=search_space,
630
+ config={
631
+ "population_size": population_size,
632
+ "num_generations": num_generations,
633
+ "objectives": objectives,
634
+ },
635
+ )
636
+
637
+ pareto_front = optimizer.optimize(evaluator)
638
+
639
+ if verbose:
640
+ print(f"\n{'='*60}")
641
+ print("NSGA-II Optimization Complete")
642
+ print(f"{'='*60}")
643
+ print(f"Pareto Front Size: {len(pareto_front)}")
644
+ print(f"Objectives: {[obj['name'] for obj in objectives]}")
645
+ print(f"{'='*60}\n")
646
+
647
+ return pareto_front