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,375 @@
1
+ """Population management for evolutionary algorithms.
2
+
3
+ Manages a collection of individuals with selection, sorting, and diversity tracking.
4
+
5
+ Example:
6
+ >>> from morphml.core.search import Population, Individual
7
+ >>>
8
+ >>> population = Population(max_size=50)
9
+ >>> population.add(individual1)
10
+ >>> population.add(individual2)
11
+ >>>
12
+ >>> # Get best individuals
13
+ >>> best = population.get_best(n=10)
14
+ >>>
15
+ >>> # Select for breeding
16
+ >>> parents = population.select(n=20, method='tournament')
17
+ """
18
+
19
+ import random
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from morphml.core.search.individual import Individual
23
+ from morphml.exceptions import SearchSpaceError
24
+ from morphml.logging_config import get_logger
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ class Population:
30
+ """
31
+ Manages a collection of individuals in evolutionary search.
32
+
33
+ Provides methods for:
34
+ - Adding/removing individuals
35
+ - Selection strategies
36
+ - Sorting and filtering
37
+ - Diversity metrics
38
+ - Statistics tracking
39
+
40
+ Attributes:
41
+ max_size: Maximum population size
42
+ individuals: List of Individual instances
43
+ generation: Current generation number
44
+ history: Historical statistics
45
+
46
+ Example:
47
+ >>> pop = Population(max_size=100)
48
+ >>> pop.add_many(initial_individuals)
49
+ >>> best = pop.get_best(n=10)
50
+ >>> parents = pop.select(n=20, method='tournament', k=3)
51
+ """
52
+
53
+ def __init__(self, max_size: int = 100, elitism: int = 5):
54
+ """
55
+ Initialize population.
56
+
57
+ Args:
58
+ max_size: Maximum population size
59
+ elitism: Number of best individuals to always keep
60
+ """
61
+ self.max_size = max_size
62
+ self.elitism = elitism
63
+ self.individuals: List[Individual] = []
64
+ self.generation = 0
65
+ self.history: List[Dict[str, Any]] = []
66
+
67
+ logger.debug(f"Created Population: max_size={max_size}, elitism={elitism}")
68
+
69
+ def add(self, individual: Individual) -> None:
70
+ """
71
+ Add an individual to the population.
72
+
73
+ Args:
74
+ individual: Individual to add
75
+ """
76
+ self.individuals.append(individual)
77
+ individual.birth_generation = self.generation
78
+
79
+ def add_many(self, individuals: List[Individual]) -> None:
80
+ """Add multiple individuals."""
81
+ for ind in individuals:
82
+ self.add(ind)
83
+
84
+ def remove(self, individual: Individual) -> None:
85
+ """Remove an individual from the population."""
86
+ if individual in self.individuals:
87
+ self.individuals.remove(individual)
88
+
89
+ def clear(self) -> None:
90
+ """Remove all individuals."""
91
+ self.individuals.clear()
92
+
93
+ def size(self) -> int:
94
+ """Get current population size."""
95
+ return len(self.individuals)
96
+
97
+ def is_full(self) -> bool:
98
+ """Check if population is at maximum size."""
99
+ return self.size() >= self.max_size
100
+
101
+ def get_best(self, n: int = 1) -> List[Individual]:
102
+ """
103
+ Get the n best individuals.
104
+
105
+ Args:
106
+ n: Number of individuals to return
107
+
108
+ Returns:
109
+ List of top n individuals sorted by fitness
110
+ """
111
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
112
+ sorted_inds = sorted(evaluated, key=lambda x: x.fitness or 0, reverse=True)
113
+ return sorted_inds[:n]
114
+
115
+ def get_worst(self, n: int = 1) -> List[Individual]:
116
+ """Get the n worst individuals."""
117
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
118
+ sorted_inds = sorted(evaluated, key=lambda x: x.fitness or 0)
119
+ return sorted_inds[:n]
120
+
121
+ def get_unevaluated(self) -> List[Individual]:
122
+ """Get all unevaluated individuals."""
123
+ return [ind for ind in self.individuals if not ind.is_evaluated()]
124
+
125
+ def select(
126
+ self,
127
+ n: int,
128
+ method: str = "tournament",
129
+ **kwargs: Any,
130
+ ) -> List[Individual]:
131
+ """
132
+ Select individuals for breeding.
133
+
134
+ Args:
135
+ n: Number of individuals to select
136
+ method: Selection method ('tournament', 'roulette', 'rank', 'random')
137
+ **kwargs: Method-specific parameters
138
+
139
+ Returns:
140
+ List of selected individuals
141
+
142
+ Example:
143
+ >>> parents = pop.select(20, method='tournament', k=3)
144
+ """
145
+ if method == "tournament":
146
+ return self._tournament_selection(n, k=kwargs.get("k", 3))
147
+ elif method == "roulette":
148
+ return self._roulette_selection(n)
149
+ elif method == "rank":
150
+ return self._rank_selection(n)
151
+ elif method == "random":
152
+ return self._random_selection(n)
153
+ else:
154
+ raise SearchSpaceError(f"Unknown selection method: {method}")
155
+
156
+ def _tournament_selection(self, n: int, k: int = 3) -> List[Individual]:
157
+ """
158
+ Tournament selection.
159
+
160
+ Args:
161
+ n: Number of individuals to select
162
+ k: Tournament size
163
+
164
+ Returns:
165
+ Selected individuals
166
+ """
167
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
168
+
169
+ if len(evaluated) < k:
170
+ logger.warning(f"Not enough evaluated individuals for tournament size {k}")
171
+ k = max(1, len(evaluated))
172
+
173
+ selected = []
174
+ for _ in range(n):
175
+ # Run tournament
176
+ tournament = random.sample(evaluated, k)
177
+ winner = max(tournament, key=lambda x: x.fitness or 0)
178
+ selected.append(winner)
179
+
180
+ return selected
181
+
182
+ def _roulette_selection(self, n: int) -> List[Individual]:
183
+ """
184
+ Roulette wheel selection (fitness-proportionate).
185
+
186
+ Args:
187
+ n: Number of individuals to select
188
+
189
+ Returns:
190
+ Selected individuals
191
+ """
192
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
193
+
194
+ if not evaluated:
195
+ return []
196
+
197
+ # Shift fitnesses to be positive
198
+ min_fitness = min(ind.fitness or 0 for ind in evaluated)
199
+ if min_fitness < 0:
200
+ fitnesses = [(ind.fitness or 0) - min_fitness + 1e-6 for ind in evaluated]
201
+ else:
202
+ fitnesses = [ind.fitness or 1e-6 for ind in evaluated]
203
+
204
+ # Normalize to probabilities
205
+ total_fitness = sum(fitnesses)
206
+ probabilities = [f / total_fitness for f in fitnesses]
207
+
208
+ # Select with replacement
209
+ selected = random.choices(evaluated, weights=probabilities, k=n)
210
+
211
+ return selected
212
+
213
+ def _rank_selection(self, n: int) -> List[Individual]:
214
+ """
215
+ Rank-based selection.
216
+
217
+ Args:
218
+ n: Number of individuals to select
219
+
220
+ Returns:
221
+ Selected individuals
222
+ """
223
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
224
+
225
+ if not evaluated:
226
+ return []
227
+
228
+ # Sort by fitness
229
+ sorted_inds = sorted(evaluated, key=lambda x: x.fitness or 0)
230
+
231
+ # Assign ranks (linear ranking)
232
+ ranks = list(range(1, len(sorted_inds) + 1))
233
+
234
+ # Select based on ranks
235
+ selected = random.choices(sorted_inds, weights=ranks, k=n)
236
+
237
+ return selected
238
+
239
+ def _random_selection(self, n: int) -> List[Individual]:
240
+ """Random selection."""
241
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
242
+ return random.sample(evaluated, min(n, len(evaluated)))
243
+
244
+ def trim(self, target_size: Optional[int] = None) -> None:
245
+ """
246
+ Trim population to target size, keeping best individuals.
247
+
248
+ Args:
249
+ target_size: Target size (defaults to max_size)
250
+ """
251
+ target_size = target_size or self.max_size
252
+
253
+ if self.size() <= target_size:
254
+ return
255
+
256
+ # Always keep elite
257
+ elite = self.get_best(self.elitism)
258
+ elite_ids = {ind.id for ind in elite}
259
+
260
+ # Get remaining individuals
261
+ others = [ind for ind in self.individuals if ind.id not in elite_ids]
262
+
263
+ # Sort others by fitness
264
+ others_evaluated = [ind for ind in others if ind.is_evaluated()]
265
+ [ind for ind in others if not ind.is_evaluated()]
266
+
267
+ others_evaluated.sort(key=lambda x: x.fitness or 0, reverse=True)
268
+
269
+ # Keep best from others
270
+ remaining_slots = target_size - len(elite)
271
+ keep_others = others_evaluated[:remaining_slots]
272
+
273
+ # Update population
274
+ self.individuals = elite + keep_others
275
+
276
+ logger.debug(f"Trimmed population to {len(self.individuals)} individuals")
277
+
278
+ def increment_ages(self) -> None:
279
+ """Increment age of all individuals."""
280
+ for ind in self.individuals:
281
+ ind.increment_age()
282
+
283
+ def next_generation(self) -> None:
284
+ """Advance to next generation."""
285
+ self.generation += 1
286
+ self.increment_ages()
287
+
288
+ # Record statistics
289
+ stats = self.get_statistics()
290
+ self.history.append(stats)
291
+
292
+ logger.info(
293
+ f"Generation {self.generation}: "
294
+ f"size={self.size()}, "
295
+ f"best={stats.get('best_fitness', 0):.4f}, "
296
+ f"mean={stats.get('mean_fitness', 0):.4f}"
297
+ )
298
+
299
+ def get_statistics(self) -> Dict[str, Any]:
300
+ """
301
+ Get population statistics.
302
+
303
+ Returns:
304
+ Dictionary of statistics
305
+ """
306
+ evaluated = [ind for ind in self.individuals if ind.is_evaluated()]
307
+
308
+ if not evaluated:
309
+ return {
310
+ "generation": self.generation,
311
+ "size": self.size(),
312
+ "evaluated": 0,
313
+ }
314
+
315
+ fitnesses = [ind.fitness for ind in evaluated if ind.fitness is not None]
316
+
317
+ stats = {
318
+ "generation": self.generation,
319
+ "size": self.size(),
320
+ "evaluated": len(evaluated),
321
+ "best_fitness": max(fitnesses) if fitnesses else 0,
322
+ "worst_fitness": min(fitnesses) if fitnesses else 0,
323
+ "mean_fitness": sum(fitnesses) / len(fitnesses) if fitnesses else 0,
324
+ "median_fitness": sorted(fitnesses)[len(fitnesses) // 2] if fitnesses else 0,
325
+ }
326
+
327
+ return stats
328
+
329
+ def get_diversity(self, method: str = "hash") -> float:
330
+ """
331
+ Calculate population diversity.
332
+
333
+ Args:
334
+ method: Diversity metric ('hash', 'hamming', 'depth')
335
+
336
+ Returns:
337
+ Diversity score (0-1, higher = more diverse)
338
+ """
339
+ if self.size() <= 1:
340
+ return 0.0
341
+
342
+ if method == "hash":
343
+ # Count unique graph hashes
344
+ hashes = {ind.graph.hash() for ind in self.individuals}
345
+ return len(hashes) / self.size()
346
+
347
+ elif method == "depth":
348
+ # Variance in graph depths
349
+ depths = [ind.graph.get_depth() for ind in self.individuals]
350
+ if len(set(depths)) == 1:
351
+ return 0.0
352
+ mean_depth = sum(depths) / len(depths)
353
+ variance = sum((d - mean_depth) ** 2 for d in depths) / len(depths)
354
+ # Normalize
355
+ return min(1.0, variance / (mean_depth + 1))
356
+
357
+ else:
358
+ return 0.0
359
+
360
+ def __len__(self) -> int:
361
+ """Return population size."""
362
+ return self.size()
363
+
364
+ def __iter__(self) -> Any:
365
+ """Iterate over individuals."""
366
+ return iter(self.individuals)
367
+
368
+ def __repr__(self) -> str:
369
+ """String representation."""
370
+ stats = self.get_statistics()
371
+ return (
372
+ f"Population(generation={self.generation}, "
373
+ f"size={self.size()}/{self.max_size}, "
374
+ f"best_fitness={stats.get('best_fitness', 0):.4f})"
375
+ )
@@ -0,0 +1,340 @@
1
+ """Base search engine for optimization algorithms.
2
+
3
+ Provides a unified interface for all search/optimization algorithms
4
+ with common functionality for initialization, sampling, and termination.
5
+
6
+ Author: Eshan Roy <eshanized@proton.me>
7
+ Organization: TONMOY INFRASTRUCTURE & VISION
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+ from morphml.core.dsl.search_space import SearchSpace
14
+ from morphml.core.graph import ModelGraph
15
+ from morphml.core.search.individual import Individual
16
+ from morphml.core.search.population import Population
17
+ from morphml.logging_config import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class SearchEngine(ABC):
23
+ """
24
+ Base class for all search/optimization algorithms.
25
+
26
+ Provides common interface and functionality for:
27
+ - Population initialization
28
+ - Architecture sampling
29
+ - Evolution/search steps
30
+ - History tracking
31
+ - Termination criteria
32
+
33
+ Subclass this to implement specific search algorithms
34
+ (genetic algorithms, Bayesian optimization, random search, etc.)
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ search_space: SearchSpace,
40
+ config: Optional[Dict[str, Any]] = None,
41
+ ):
42
+ """
43
+ Initialize search engine.
44
+
45
+ Args:
46
+ search_space: Search space definition
47
+ config: Configuration dictionary
48
+ """
49
+ self.search_space = search_space
50
+ self.config = config or {}
51
+ self.generation = 0
52
+ self.history: List[Dict[str, Any]] = []
53
+ self.best_individual: Optional[Individual] = None
54
+ self.num_evaluations = 0
55
+
56
+ logger.info(f"Initialized {self.__class__.__name__}")
57
+
58
+ @abstractmethod
59
+ def initialize_population(self, size: int) -> Population:
60
+ """
61
+ Create initial population.
62
+
63
+ Args:
64
+ size: Population size
65
+
66
+ Returns:
67
+ Initialized population
68
+ """
69
+ pass
70
+
71
+ @abstractmethod
72
+ def step(self, population: Population, evaluator: Callable) -> Population:
73
+ """
74
+ Execute one search iteration.
75
+
76
+ Args:
77
+ population: Current population
78
+ evaluator: Function to evaluate fitness
79
+
80
+ Returns:
81
+ Updated population after one step
82
+ """
83
+ pass
84
+
85
+ def should_stop(self) -> bool:
86
+ """
87
+ Check if search should terminate.
88
+
89
+ Returns:
90
+ True if termination criteria met
91
+ """
92
+ max_generations = self.config.get("max_generations", float("inf"))
93
+ max_evaluations = self.config.get("max_evaluations", float("inf"))
94
+
95
+ if self.generation >= max_generations:
96
+ logger.info(f"Stopping: reached max generations ({max_generations})")
97
+ return True
98
+
99
+ if self.num_evaluations >= max_evaluations:
100
+ logger.info(f"Stopping: reached max evaluations ({max_evaluations})")
101
+ return True
102
+
103
+ # Early stopping based on improvement
104
+ patience = self.config.get("early_stopping_patience", 0)
105
+ if patience > 0 and len(self.history) >= patience:
106
+ recent_fitness = [h["best_fitness"] for h in self.history[-patience:]]
107
+ if len(set(recent_fitness)) == 1: # No improvement
108
+ logger.info(f"Stopping: no improvement for {patience} generations")
109
+ return True
110
+
111
+ return False
112
+
113
+ def search(
114
+ self,
115
+ evaluator: Callable,
116
+ population_size: int,
117
+ max_generations: int,
118
+ callbacks: Optional[List[Callable]] = None,
119
+ ) -> Individual:
120
+ """
121
+ Main search loop.
122
+
123
+ Args:
124
+ evaluator: Function to evaluate fitness
125
+ population_size: Size of population
126
+ max_generations: Maximum number of generations
127
+ callbacks: Optional callbacks to call each generation
128
+
129
+ Returns:
130
+ Best individual found
131
+
132
+ Example:
133
+ >>> engine = MySearchEngine(search_space)
134
+ >>> best = engine.search(evaluator, population_size=50, max_generations=100)
135
+ """
136
+ self.config["max_generations"] = max_generations
137
+ callbacks = callbacks or []
138
+
139
+ logger.info(
140
+ f"Starting search with population_size={population_size}, max_generations={max_generations}"
141
+ )
142
+
143
+ # Initialize population
144
+ population = self.initialize_population(population_size)
145
+
146
+ # Evaluate initial population
147
+ self._evaluate_population(population, evaluator)
148
+
149
+ # Track best
150
+ self._update_best(population)
151
+
152
+ # Main search loop
153
+ while not self.should_stop():
154
+ # Evolution/search step
155
+ population = self.step(population, evaluator)
156
+
157
+ # Evaluate new individuals
158
+ self._evaluate_population(population, evaluator)
159
+
160
+ # Update tracking
161
+ self._update_best(population)
162
+ self._record_history(population)
163
+
164
+ # Call callbacks
165
+ for callback in callbacks:
166
+ callback(self, population)
167
+
168
+ # Log progress
169
+ if self.generation % 10 == 0:
170
+ stats = self.get_statistics()
171
+ logger.info(
172
+ f"Generation {self.generation}: "
173
+ f"best={stats['best_fitness']:.4f}, "
174
+ f"mean={stats['mean_fitness']:.4f}, "
175
+ f"evaluations={self.num_evaluations}"
176
+ )
177
+
178
+ self.generation += 1
179
+
180
+ logger.info(
181
+ f"Search complete after {self.generation} generations, {self.num_evaluations} evaluations"
182
+ )
183
+ logger.info(f"Best fitness: {self.best_individual.fitness:.4f}")
184
+
185
+ return self.best_individual
186
+
187
+ def _evaluate_population(self, population: Population, evaluator: Callable) -> None:
188
+ """Evaluate all unevaluated individuals."""
189
+ for individual in population.individuals:
190
+ if individual.fitness is None:
191
+ individual.set_fitness(evaluator(individual.graph))
192
+ self.num_evaluations += 1
193
+
194
+ def _update_best(self, population: Population) -> None:
195
+ """Update best individual if improvement found."""
196
+ current_best = population.get_best(1)[0]
197
+
198
+ if self.best_individual is None or current_best.fitness > self.best_individual.fitness:
199
+ self.best_individual = current_best.clone()
200
+ logger.debug(f"New best: {self.best_individual.fitness:.4f}")
201
+
202
+ def _record_history(self, population: Population) -> None:
203
+ """Record generation statistics."""
204
+ entry = {
205
+ "generation": self.generation,
206
+ "best_fitness": population.best_fitness(),
207
+ "mean_fitness": population.average_fitness(),
208
+ "worst_fitness": min(
209
+ ind.fitness for ind in population.individuals if ind.fitness is not None
210
+ ),
211
+ "diversity": population.diversity_metric(),
212
+ "num_evaluations": self.num_evaluations,
213
+ }
214
+ self.history.append(entry)
215
+
216
+ def get_statistics(self) -> Dict[str, Any]:
217
+ """
218
+ Get current statistics.
219
+
220
+ Returns:
221
+ Dictionary with statistics
222
+ """
223
+ if not self.history:
224
+ return {
225
+ "generation": 0,
226
+ "num_evaluations": 0,
227
+ "best_fitness": 0.0,
228
+ "mean_fitness": 0.0,
229
+ }
230
+
231
+ latest = self.history[-1]
232
+ return {
233
+ "generation": self.generation,
234
+ "num_evaluations": self.num_evaluations,
235
+ "best_fitness": latest["best_fitness"],
236
+ "mean_fitness": latest["mean_fitness"],
237
+ "diversity": latest["diversity"],
238
+ }
239
+
240
+ def get_best(self) -> ModelGraph:
241
+ """
242
+ Get best architecture found.
243
+
244
+ Returns:
245
+ Best ModelGraph
246
+ """
247
+ if self.best_individual is None:
248
+ raise ValueError("No evaluations performed yet")
249
+
250
+ return self.best_individual.graph
251
+
252
+ def get_best_fitness(self) -> float:
253
+ """
254
+ Get best fitness found.
255
+
256
+ Returns:
257
+ Best fitness value
258
+ """
259
+ if self.best_individual is None:
260
+ raise ValueError("No evaluations performed yet")
261
+
262
+ return self.best_individual.fitness
263
+
264
+ def get_history(self) -> List[Dict[str, Any]]:
265
+ """
266
+ Get complete search history.
267
+
268
+ Returns:
269
+ List of generation statistics
270
+ """
271
+ return self.history
272
+
273
+ def reset(self) -> None:
274
+ """Reset search engine state."""
275
+ self.generation = 0
276
+ self.history = []
277
+ self.best_individual = None
278
+ self.num_evaluations = 0
279
+ logger.info("Search engine reset")
280
+
281
+
282
+ class RandomSearchEngine(SearchEngine):
283
+ """
284
+ Random search engine (baseline).
285
+
286
+ Simply samples random architectures from search space
287
+ without any guided search.
288
+
289
+ Example:
290
+ >>> engine = RandomSearchEngine(search_space)
291
+ >>> best = engine.search(evaluator, population_size=1, max_generations=100)
292
+ """
293
+
294
+ def initialize_population(self, size: int) -> Population:
295
+ """Create initial random population."""
296
+ individuals = []
297
+ for _ in range(size):
298
+ graph = self.search_space.sample()
299
+ individual = Individual(graph=graph)
300
+ individuals.append(individual)
301
+
302
+ return Population(individuals)
303
+
304
+ def step(self, population: Population, evaluator: Callable) -> Population:
305
+ """Sample new random individuals."""
306
+ # Replace entire population with new random samples
307
+ return self.initialize_population(len(population))
308
+
309
+
310
+ class GridSearchEngine(SearchEngine):
311
+ """
312
+ Grid search engine.
313
+
314
+ Systematically explores all combinations of parameter values.
315
+ Only practical for small discrete search spaces.
316
+
317
+ Note: This is a placeholder - full grid search requires
318
+ combinatorial enumeration of the search space.
319
+ """
320
+
321
+ def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
322
+ """Initialize grid search engine."""
323
+ super().__init__(search_space, config)
324
+ self.grid_iterator = None
325
+
326
+ def initialize_population(self, size: int) -> Population:
327
+ """Initialize with first batch from grid."""
328
+ individuals = []
329
+ for _ in range(size):
330
+ graph = self.search_space.sample()
331
+ individual = Individual(graph=graph)
332
+ individuals.append(individual)
333
+
334
+ return Population(individuals)
335
+
336
+ def step(self, population: Population, evaluator: Callable) -> Population:
337
+ """Get next batch from grid."""
338
+ # Simplified: just sample randomly
339
+ # Full implementation would enumerate combinations
340
+ return self.initialize_population(len(population))