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,308 @@
1
+ """NSGA-II - Non-dominated Sorting Genetic Algorithm II.
2
+
3
+ Multi-objective optimization using Pareto dominance and crowding distance.
4
+ """
5
+
6
+ import random
7
+ from typing import Callable, Dict, List, Optional
8
+
9
+ from morphml.core.dsl.search_space import SearchSpace
10
+ from morphml.core.graph import GraphMutator, ModelGraph
11
+ from morphml.core.search import Individual, Population
12
+ from morphml.exceptions import OptimizerError
13
+ from morphml.logging_config import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class NSGA2:
19
+ """
20
+ NSGA-II for multi-objective NAS.
21
+
22
+ Optimizes multiple objectives simultaneously using:
23
+ - Non-dominated sorting
24
+ - Crowding distance
25
+ - Pareto front preservation
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ search_space: SearchSpace,
31
+ objectives: List[str],
32
+ population_size: int = 100,
33
+ num_generations: int = 100,
34
+ mutation_rate: float = 0.2,
35
+ crossover_rate: float = 0.8,
36
+ **kwargs,
37
+ ):
38
+ """Initialize NSGA-II."""
39
+ self.search_space = search_space
40
+ self.objectives = objectives
41
+ self.population_size = population_size
42
+ self.num_generations = num_generations
43
+ self.mutation_rate = mutation_rate
44
+ self.crossover_rate = crossover_rate
45
+
46
+ self.population = Population(max_size=population_size * 2, elitism=0)
47
+ self.mutator = GraphMutator()
48
+ self.pareto_front: List[Individual] = []
49
+ self.history: List[dict] = []
50
+
51
+ logger.info(f"Created NSGA2: objectives={objectives}, pop={population_size}")
52
+
53
+ def dominates(self, ind1: Individual, ind2: Individual) -> bool:
54
+ """Check if ind1 dominates ind2."""
55
+ better_in_any = False
56
+
57
+ for obj in self.objectives:
58
+ val1 = ind1.get_metric(obj, 0.0)
59
+ val2 = ind2.get_metric(obj, 0.0)
60
+
61
+ if val1 < val2:
62
+ return False # Worse in this objective
63
+ if val1 > val2:
64
+ better_in_any = True
65
+
66
+ return better_in_any
67
+
68
+ def fast_non_dominated_sort(self, individuals: List[Individual]) -> List[List[Individual]]:
69
+ """Perform fast non-dominated sorting."""
70
+ fronts: List[List[Individual]] = [[]]
71
+ domination_count: Dict[str, int] = {}
72
+ dominated_solutions: Dict[str, List[Individual]] = {}
73
+
74
+ # Initialize
75
+ for ind in individuals:
76
+ ind_id = ind.id
77
+ domination_count[ind_id] = 0
78
+ dominated_solutions[ind_id] = []
79
+
80
+ # Find domination relationships
81
+ for i, ind1 in enumerate(individuals):
82
+ for ind2 in individuals[i + 1 :]:
83
+ if self.dominates(ind1, ind2):
84
+ dominated_solutions[ind1.id].append(ind2)
85
+ domination_count[ind2.id] += 1
86
+ elif self.dominates(ind2, ind1):
87
+ dominated_solutions[ind2.id].append(ind1)
88
+ domination_count[ind1.id] += 1
89
+
90
+ # If not dominated by anyone, belongs to first front
91
+ if domination_count[ind1.id] == 0:
92
+ fronts[0].append(ind1)
93
+
94
+ # Build subsequent fronts
95
+ current_front = 0
96
+ while fronts[current_front]:
97
+ next_front = []
98
+ for ind in fronts[current_front]:
99
+ for dominated in dominated_solutions[ind.id]:
100
+ domination_count[dominated.id] -= 1
101
+ if domination_count[dominated.id] == 0:
102
+ next_front.append(dominated)
103
+
104
+ current_front += 1
105
+ if next_front:
106
+ fronts.append(next_front)
107
+ else:
108
+ break
109
+
110
+ return [f for f in fronts if f] # Remove empty fronts
111
+
112
+ def calculate_crowding_distance(self, front: List[Individual]) -> Dict[str, float]:
113
+ """Calculate crowding distance for individuals in front."""
114
+ distances: Dict[str, float] = {ind.id: 0.0 for ind in front}
115
+
116
+ if len(front) <= 2:
117
+ for ind in front:
118
+ distances[ind.id] = float("inf")
119
+ return distances
120
+
121
+ # For each objective
122
+ for obj in self.objectives:
123
+ # Sort by objective value
124
+ sorted_front = sorted(front, key=lambda x: x.get_metric(obj, 0.0))
125
+
126
+ # Boundary points get infinite distance
127
+ distances[sorted_front[0].id] = float("inf")
128
+ distances[sorted_front[-1].id] = float("inf")
129
+
130
+ # Get objective range
131
+ obj_min = sorted_front[0].get_metric(obj, 0.0)
132
+ obj_max = sorted_front[-1].get_metric(obj, 0.0)
133
+ obj_range = obj_max - obj_min
134
+
135
+ if obj_range == 0:
136
+ continue
137
+
138
+ # Calculate crowding distance
139
+ for i in range(1, len(sorted_front) - 1):
140
+ prev_val = sorted_front[i - 1].get_metric(obj, 0.0)
141
+ next_val = sorted_front[i + 1].get_metric(obj, 0.0)
142
+ distances[sorted_front[i].id] += (next_val - prev_val) / obj_range
143
+
144
+ return distances
145
+
146
+ def environmental_selection(self, individuals: List[Individual]) -> List[Individual]:
147
+ """Select next generation using NSGA-II selection."""
148
+ # Non-dominated sorting
149
+ fronts = self.fast_non_dominated_sort(individuals)
150
+
151
+ # Select individuals
152
+ selected = []
153
+ for front in fronts:
154
+ if len(selected) + len(front) <= self.population_size:
155
+ selected.extend(front)
156
+ else:
157
+ # Calculate crowding distance for this front
158
+ distances = self.calculate_crowding_distance(front)
159
+
160
+ # Sort by crowding distance (descending)
161
+ front_sorted = sorted(front, key=lambda x: distances[x.id], reverse=True)
162
+
163
+ # Add individuals until population is full
164
+ remaining = self.population_size - len(selected)
165
+ selected.extend(front_sorted[:remaining])
166
+ break
167
+
168
+ return selected
169
+
170
+ def initialize_population(self) -> None:
171
+ """Initialize population."""
172
+ logger.info(f"Initializing population of size {self.population_size}")
173
+
174
+ for i in range(self.population_size):
175
+ try:
176
+ graph = self.search_space.sample()
177
+ individual = Individual(graph)
178
+ self.population.add(individual)
179
+ except Exception as e:
180
+ logger.warning(f"Failed to sample individual {i}: {e}")
181
+ continue
182
+
183
+ def evaluate_population(self, evaluator: Callable[[ModelGraph], Dict[str, float]]) -> None:
184
+ """Evaluate population on multiple objectives."""
185
+ unevaluated = self.population.get_unevaluated()
186
+
187
+ if not unevaluated:
188
+ return
189
+
190
+ logger.info(f"Evaluating {len(unevaluated)} individuals")
191
+
192
+ for ind in unevaluated:
193
+ try:
194
+ # Evaluator returns dict of objective values
195
+ metrics = evaluator(ind.graph)
196
+
197
+ # Set fitness as primary objective
198
+ if self.objectives:
199
+ primary_fitness = metrics.get(self.objectives[0], 0.0)
200
+ ind.set_fitness(primary_fitness, **metrics)
201
+ else:
202
+ ind.set_fitness(sum(metrics.values()) / len(metrics), **metrics)
203
+
204
+ except Exception as e:
205
+ logger.error(f"Evaluation failed: {e}")
206
+ ind.set_fitness(0.0)
207
+
208
+ def generate_offspring(self, parents: List[Individual]) -> List[Individual]:
209
+ """Generate offspring through crossover and mutation."""
210
+ offspring = []
211
+
212
+ while len(offspring) < len(parents):
213
+ # Select parents
214
+ parent1, parent2 = random.sample(parents, 2)
215
+
216
+ # Crossover
217
+ if random.random() < self.crossover_rate:
218
+ child = parent1.clone(keep_fitness=False)
219
+ child.parent_ids = [parent1.id, parent2.id]
220
+ else:
221
+ child = random.choice([parent1, parent2]).clone(keep_fitness=False)
222
+
223
+ # Mutation
224
+ if random.random() < self.mutation_rate:
225
+ mutated_graph = self.mutator.mutate(child.graph)
226
+ child = Individual(mutated_graph, parent_ids=child.parent_ids)
227
+
228
+ offspring.append(child)
229
+
230
+ return offspring
231
+
232
+ def optimize(
233
+ self,
234
+ evaluator: Callable[[ModelGraph], Dict[str, float]],
235
+ callback: Optional[Callable[[int, List[Individual]], None]] = None,
236
+ ) -> List[Individual]:
237
+ """Run NSGA-II optimization."""
238
+ try:
239
+ # Initialize
240
+ self.initialize_population()
241
+ self.evaluate_population(evaluator)
242
+
243
+ # Evolution loop
244
+ for gen in range(self.num_generations):
245
+ logger.info(f"Generation {gen + 1}/{self.num_generations}")
246
+
247
+ # Generate offspring
248
+ parents = list(self.population.individuals)
249
+ offspring = self.generate_offspring(parents)
250
+
251
+ # Evaluate offspring
252
+ self.population.add_many(offspring)
253
+ self.evaluate_population(evaluator)
254
+
255
+ # Environmental selection
256
+ combined = list(self.population.individuals)
257
+ selected = self.environmental_selection(combined)
258
+
259
+ # Update population
260
+ self.population.clear()
261
+ self.population.add_many(selected)
262
+
263
+ # Update Pareto front
264
+ fronts = self.fast_non_dominated_sort(selected)
265
+ self.pareto_front = fronts[0] if fronts else []
266
+
267
+ # Record stats
268
+ stats = {
269
+ "generation": gen + 1,
270
+ "pareto_size": len(self.pareto_front),
271
+ "population_size": len(selected),
272
+ }
273
+ self.history.append(stats)
274
+
275
+ logger.info(f"Gen {gen + 1}: Pareto front size = {len(self.pareto_front)}")
276
+
277
+ # Callback
278
+ if callback:
279
+ callback(gen + 1, self.pareto_front)
280
+
281
+ logger.info(f"NSGA-II complete: Pareto front size = {len(self.pareto_front)}")
282
+ return self.pareto_front
283
+
284
+ except Exception as e:
285
+ logger.error(f"NSGA-II optimization failed: {e}")
286
+ raise OptimizerError(f"NSGA-II optimization failed: {e}") from e
287
+
288
+ def get_pareto_front(self) -> List[Individual]:
289
+ """Get current Pareto front."""
290
+ return self.pareto_front
291
+
292
+ def get_history(self) -> List[dict]:
293
+ """Get optimization history."""
294
+ return self.history
295
+
296
+ def reset(self) -> None:
297
+ """Reset optimizer."""
298
+ self.population.clear()
299
+ self.pareto_front.clear()
300
+ self.history.clear()
301
+ logger.info("NSGA-II reset")
302
+
303
+ def __repr__(self) -> str:
304
+ return (
305
+ f"NSGA2(objectives={self.objectives}, "
306
+ f"pop={self.population_size}, "
307
+ f"pareto_size={len(self.pareto_front)})"
308
+ )
@@ -0,0 +1,172 @@
1
+ """Random Search optimizer - baseline for NAS.
2
+
3
+ Simplest possible search strategy: randomly sample architectures
4
+ and return the best one. Serves as a baseline for comparison.
5
+
6
+ Example:
7
+ >>> from morphml.optimizers import RandomSearch
8
+ >>> from morphml.core.dsl import create_cnn_space
9
+ >>>
10
+ >>> space = create_cnn_space(num_classes=10)
11
+ >>> rs = RandomSearch(search_space=space, num_samples=100)
12
+ >>> best = rs.optimize(evaluator=my_evaluator)
13
+ """
14
+
15
+ from typing import Callable, List, Optional
16
+
17
+ from morphml.core.dsl.search_space import SearchSpace
18
+ from morphml.core.graph import ModelGraph
19
+ from morphml.core.search import Individual
20
+ from morphml.exceptions import OptimizerError
21
+ from morphml.logging_config import get_logger
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class RandomSearch:
27
+ """
28
+ Random Search optimizer - baseline for Neural Architecture Search.
29
+
30
+ Randomly samples N architectures from the search space, evaluates
31
+ them all, and returns the best one. Despite its simplicity, often
32
+ surprisingly effective and serves as an important baseline.
33
+
34
+ Attributes:
35
+ search_space: SearchSpace to sample from
36
+ num_samples: Number of architectures to evaluate
37
+ evaluated: List of evaluated individuals
38
+ best_individual: Best architecture found
39
+
40
+ Example:
41
+ >>> rs = RandomSearch(
42
+ ... search_space=space,
43
+ ... num_samples=100,
44
+ ... allow_duplicates=False
45
+ ... )
46
+ >>> best = rs.optimize(evaluator=evaluate_func)
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ search_space: SearchSpace,
52
+ num_samples: int = 100,
53
+ allow_duplicates: bool = False,
54
+ **kwargs,
55
+ ):
56
+ """
57
+ Initialize Random Search optimizer.
58
+
59
+ Args:
60
+ search_space: SearchSpace to sample architectures from
61
+ num_samples: Number of architectures to sample and evaluate
62
+ allow_duplicates: Whether to allow duplicate architectures
63
+ **kwargs: Additional configuration (for compatibility)
64
+ """
65
+ self.search_space = search_space
66
+ self.num_samples = num_samples
67
+ self.allow_duplicates = allow_duplicates
68
+
69
+ self.evaluated: List[Individual] = []
70
+ self.best_individual: Optional[Individual] = None
71
+
72
+ logger.info(f"Created RandomSearch: num_samples={num_samples}")
73
+
74
+ def optimize(self, evaluator: Callable[[ModelGraph], float]) -> Individual:
75
+ """
76
+ Run random search optimization.
77
+
78
+ Args:
79
+ evaluator: Function to evaluate fitness of architectures
80
+
81
+ Returns:
82
+ Best individual found
83
+
84
+ Raises:
85
+ OptimizerError: If optimization fails
86
+
87
+ Example:
88
+ >>> def evaluate(graph):
89
+ ... return accuracy_score
90
+ >>> best = rs.optimize(evaluate)
91
+ """
92
+ try:
93
+ logger.info(f"Starting random search with {self.num_samples} samples")
94
+
95
+ seen_hashes = set()
96
+ sampled = 0
97
+
98
+ while sampled < self.num_samples:
99
+ try:
100
+ # Sample architecture
101
+ graph = self.search_space.sample()
102
+
103
+ # Check for duplicates
104
+ if not self.allow_duplicates:
105
+ graph_hash = graph.hash()
106
+ if graph_hash in seen_hashes:
107
+ logger.debug("Skipping duplicate architecture")
108
+ continue
109
+ seen_hashes.add(graph_hash)
110
+
111
+ # Create individual and evaluate
112
+ individual = Individual(graph)
113
+ fitness = evaluator(graph)
114
+ individual.set_fitness(fitness)
115
+
116
+ self.evaluated.append(individual)
117
+ sampled += 1
118
+
119
+ # Track best
120
+ if self.best_individual is None or fitness > self.best_individual.fitness:
121
+ self.best_individual = individual
122
+ logger.info(f"New best: {fitness:.4f} (sample {sampled})")
123
+
124
+ if sampled % 10 == 0:
125
+ logger.debug(f"Evaluated {sampled}/{self.num_samples} architectures")
126
+
127
+ except Exception as e:
128
+ logger.warning(f"Sample failed: {e}")
129
+ continue
130
+
131
+ # Final results
132
+ logger.info(
133
+ f"Random search complete: " f"Best fitness = {self.best_individual.fitness:.4f}"
134
+ )
135
+
136
+ return self.best_individual
137
+
138
+ except Exception as e:
139
+ logger.error(f"Random search failed: {e}")
140
+ raise OptimizerError(f"Random search optimization failed: {e}") from e
141
+
142
+ def get_all_evaluated(self) -> List[Individual]:
143
+ """
144
+ Get all evaluated individuals.
145
+
146
+ Returns:
147
+ List of all evaluated individuals
148
+ """
149
+ return self.evaluated
150
+
151
+ def get_best_n(self, n: int = 10) -> List[Individual]:
152
+ """
153
+ Get top N individuals.
154
+
155
+ Args:
156
+ n: Number of individuals to return
157
+
158
+ Returns:
159
+ List of best individuals sorted by fitness
160
+ """
161
+ sorted_inds = sorted(self.evaluated, key=lambda x: x.fitness or 0, reverse=True)
162
+ return sorted_inds[:n]
163
+
164
+ def reset(self) -> None:
165
+ """Reset optimizer state."""
166
+ self.evaluated.clear()
167
+ self.best_individual = None
168
+ logger.info("Random search reset")
169
+
170
+ def __repr__(self) -> str:
171
+ """String representation."""
172
+ return f"RandomSearch(num_samples={self.num_samples}, " f"evaluated={len(self.evaluated)})"
@@ -0,0 +1,181 @@
1
+ """Simulated Annealing optimizer for NAS.
2
+
3
+ Probabilistic hill climbing that accepts worse solutions with
4
+ decreasing probability to escape local optima.
5
+ """
6
+
7
+ import math
8
+ import random
9
+ from typing import Callable, List, Optional
10
+
11
+ from morphml.core.dsl.search_space import SearchSpace
12
+ from morphml.core.graph import GraphMutator, ModelGraph
13
+ from morphml.core.search import Individual
14
+ from morphml.exceptions import OptimizerError
15
+ from morphml.logging_config import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class SimulatedAnnealing:
21
+ """
22
+ Simulated Annealing optimizer.
23
+
24
+ Accepts worse solutions with probability based on temperature
25
+ schedule, enabling escape from local optima.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ search_space: SearchSpace,
31
+ max_iterations: int = 1000,
32
+ initial_temp: float = 100.0,
33
+ final_temp: float = 0.1,
34
+ cooling_rate: float = 0.95,
35
+ cooling_schedule: str = "exponential",
36
+ num_mutations: int = 3,
37
+ mutation_rate: float = 0.3,
38
+ **kwargs,
39
+ ):
40
+ """Initialize SA optimizer."""
41
+ self.search_space = search_space
42
+ self.max_iterations = max_iterations
43
+ self.initial_temp = initial_temp
44
+ self.final_temp = final_temp
45
+ self.cooling_rate = cooling_rate
46
+ self.cooling_schedule = cooling_schedule
47
+ self.num_mutations = num_mutations
48
+ self.mutation_rate = mutation_rate
49
+
50
+ self.mutator = GraphMutator()
51
+ self.current: Optional[Individual] = None
52
+ self.best: Optional[Individual] = None
53
+ self.history: List[dict] = []
54
+
55
+ logger.info(f"Created SimulatedAnnealing: T0={initial_temp}, Tf={final_temp}")
56
+
57
+ def get_temperature(self, iteration: int) -> float:
58
+ """Calculate temperature at iteration."""
59
+ progress = iteration / self.max_iterations
60
+
61
+ if self.cooling_schedule == "exponential":
62
+ return self.initial_temp * (self.cooling_rate**iteration)
63
+ elif self.cooling_schedule == "linear":
64
+ return self.initial_temp - (self.initial_temp - self.final_temp) * progress
65
+ elif self.cooling_schedule == "logarithmic":
66
+ return self.initial_temp / (1 + math.log(1 + iteration))
67
+ else:
68
+ return self.initial_temp * (self.cooling_rate**iteration)
69
+
70
+ def acceptance_probability(self, old_fitness: float, new_fitness: float, temp: float) -> float:
71
+ """Calculate acceptance probability."""
72
+ if new_fitness > old_fitness:
73
+ return 1.0
74
+
75
+ if temp <= 0:
76
+ return 0.0
77
+
78
+ delta = new_fitness - old_fitness
79
+ return math.exp(delta / temp)
80
+
81
+ def optimize(self, evaluator: Callable[[ModelGraph], float]) -> Individual:
82
+ """Run simulated annealing."""
83
+ try:
84
+ # Initialize
85
+ logger.info("Initializing simulated annealing")
86
+ init_graph = self.search_space.sample()
87
+ self.current = Individual(init_graph)
88
+ fitness = evaluator(self.current.graph)
89
+ self.current.set_fitness(fitness)
90
+ self.best = self.current
91
+
92
+ self.history.append(
93
+ {
94
+ "iteration": 0,
95
+ "fitness": fitness,
96
+ "temperature": self.initial_temp,
97
+ "accepted": True,
98
+ }
99
+ )
100
+
101
+ logger.info(f"Initial fitness: {fitness:.4f}")
102
+
103
+ for iteration in range(1, self.max_iterations + 1):
104
+ # Get temperature
105
+ temp = self.get_temperature(iteration)
106
+
107
+ # Generate neighbor
108
+ mutated_graph = self.mutator.mutate(
109
+ self.current.graph,
110
+ mutation_rate=self.mutation_rate,
111
+ max_mutations=self.num_mutations,
112
+ )
113
+
114
+ neighbor = Individual(mutated_graph)
115
+ neighbor_fitness = evaluator(neighbor.graph)
116
+ neighbor.set_fitness(neighbor_fitness)
117
+
118
+ # Accept or reject
119
+ prob = self.acceptance_probability(self.current.fitness, neighbor_fitness, temp)
120
+
121
+ accepted = random.random() < prob
122
+
123
+ if accepted:
124
+ self.current = neighbor
125
+
126
+ # Update best
127
+ if neighbor_fitness > self.best.fitness:
128
+ self.best = neighbor
129
+ logger.info(
130
+ f"Iteration {iteration}: New best {neighbor_fitness:.4f} "
131
+ f"(T={temp:.3f})"
132
+ )
133
+
134
+ self.history.append(
135
+ {
136
+ "iteration": iteration,
137
+ "fitness": neighbor_fitness,
138
+ "temperature": temp,
139
+ "accepted": accepted,
140
+ }
141
+ )
142
+
143
+ if iteration % 100 == 0:
144
+ logger.debug(
145
+ f"Iteration {iteration}/{self.max_iterations}: "
146
+ f"Current={self.current.fitness:.4f}, "
147
+ f"Best={self.best.fitness:.4f}, "
148
+ f"T={temp:.3f}"
149
+ )
150
+
151
+ logger.info(f"SA complete: Best fitness = {self.best.fitness:.4f}")
152
+ return self.best
153
+
154
+ except Exception as e:
155
+ logger.error(f"Simulated annealing failed: {e}")
156
+ raise OptimizerError(f"SA optimization failed: {e}") from e
157
+
158
+ def get_history(self) -> List[dict]:
159
+ """Get optimization history."""
160
+ return self.history
161
+
162
+ def get_acceptance_rate(self) -> float:
163
+ """Calculate overall acceptance rate."""
164
+ if not self.history:
165
+ return 0.0
166
+ accepted = sum(1 for h in self.history if h["accepted"])
167
+ return accepted / len(self.history)
168
+
169
+ def reset(self) -> None:
170
+ """Reset optimizer."""
171
+ self.current = None
172
+ self.best = None
173
+ self.history.clear()
174
+ logger.info("SA reset")
175
+
176
+ def __repr__(self) -> str:
177
+ return (
178
+ f"SimulatedAnnealing(T0={self.initial_temp}, "
179
+ f"Tf={self.final_temp}, "
180
+ f"iterations={self.max_iterations})"
181
+ )
@@ -0,0 +1,35 @@
1
+ """Plugin system for MorphML.
2
+
3
+ Provides extensibility through plugins for:
4
+ - Custom optimizers
5
+ - Custom evaluators
6
+ - Custom mutation operators
7
+ - Custom objectives
8
+ - Custom visualizations
9
+
10
+ Example:
11
+ >>> from morphml.plugins import PluginManager
12
+ >>> manager = PluginManager()
13
+ >>> manager.load_plugin('custom_optimizer')
14
+ >>> plugin = manager.get_plugin('custom_optimizer')
15
+ """
16
+
17
+ from morphml.plugins.plugin_system import (
18
+ EvaluatorPlugin,
19
+ MutationPlugin,
20
+ ObjectivePlugin,
21
+ OptimizerPlugin,
22
+ Plugin,
23
+ PluginManager,
24
+ VisualizationPlugin,
25
+ )
26
+
27
+ __all__ = [
28
+ "Plugin",
29
+ "OptimizerPlugin",
30
+ "EvaluatorPlugin",
31
+ "MutationPlugin",
32
+ "ObjectivePlugin",
33
+ "VisualizationPlugin",
34
+ "PluginManager",
35
+ ]