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,556 @@
1
+ """Differential Evolution (DE) for neural architecture search.
2
+
3
+ Differential Evolution is a powerful evolutionary algorithm that uses vector
4
+ differences for mutation. It's particularly effective for continuous optimization
5
+ and has few parameters to tune.
6
+
7
+ Key Features:
8
+ - Vector difference-based mutation
9
+ - Multiple mutation strategies (rand/1, best/1, rand/2)
10
+ - Binomial/exponential crossover
11
+ - Greedy selection
12
+ - Self-adaptive variants
13
+
14
+ Reference:
15
+ Storn, R., and Price, K. "Differential Evolution - A Simple and Efficient
16
+ Heuristic for Global Optimization over Continuous Spaces." Journal of Global
17
+ Optimization, 1997.
18
+
19
+ Author: Eshan Roy <eshanized@proton.me>
20
+ Organization: TONMOY INFRASTRUCTURE & VISION
21
+ """
22
+
23
+ import random
24
+ from typing import Any, Callable, Dict, List, Optional
25
+
26
+ import numpy as np
27
+
28
+ from morphml.core.dsl import SearchSpace
29
+ from morphml.core.search import Individual
30
+ from morphml.logging_config import get_logger
31
+ from morphml.optimizers.evolutionary.encoding import ArchitectureEncoder
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ class DifferentialEvolution:
37
+ """
38
+ Differential Evolution optimizer for architecture search.
39
+
40
+ DE uses vector differences for mutation, creating trial vectors that
41
+ are compared against target vectors. The algorithm is simple yet
42
+ powerful for continuous optimization.
43
+
44
+ Mutation Strategies:
45
+ 1. **DE/rand/1:** mutant = x_r1 + F * (x_r2 - x_r3)
46
+ - Random base vector with one difference
47
+ - Good exploration
48
+
49
+ 2. **DE/best/1:** mutant = x_best + F * (x_r1 - x_r2)
50
+ - Best vector as base
51
+ - Faster convergence, risk of premature convergence
52
+
53
+ 3. **DE/rand/2:** mutant = x_r1 + F * (x_r2 - x_r3) + F * (x_r4 - x_r5)
54
+ - Two difference vectors
55
+ - More exploration
56
+
57
+ 4. **DE/current-to-best/1:** mutant = x_i + F * (x_best - x_i) + F * (x_r1 - x_r2)
58
+ - Moves toward best
59
+ - Balanced exploration/exploitation
60
+
61
+ Crossover:
62
+ - **Binomial:** Each parameter inherited from mutant with probability CR
63
+ - **Exponential:** Consecutive parameters inherited from mutant
64
+
65
+ Configuration:
66
+ population_size: Population size (default: 50)
67
+ max_generations: Maximum generations (default: 100)
68
+ F: Mutation scaling factor (default: 0.8)
69
+ CR: Crossover probability (default: 0.9)
70
+ strategy: Mutation strategy (default: 'rand/1')
71
+
72
+ Example:
73
+ >>> from morphml.optimizers.evolutionary import DifferentialEvolution
74
+ >>> optimizer = DifferentialEvolution(
75
+ ... search_space=space,
76
+ ... config={
77
+ ... 'population_size': 50,
78
+ ... 'F': 0.8,
79
+ ... 'CR': 0.9,
80
+ ... 'strategy': 'rand/1'
81
+ ... }
82
+ ... )
83
+ >>> best = optimizer.optimize(evaluator)
84
+ """
85
+
86
+ def __init__(self, search_space: SearchSpace, config: Optional[Dict[str, Any]] = None):
87
+ """
88
+ Initialize Differential Evolution optimizer.
89
+
90
+ Args:
91
+ search_space: SearchSpace for architecture sampling
92
+ config: Configuration dictionary
93
+ """
94
+ self.search_space = search_space
95
+ self.config = config or {}
96
+
97
+ # DE parameters
98
+ self.population_size = self.config.get("population_size", 50)
99
+ self.max_generations = self.config.get("max_generations", 100)
100
+
101
+ # F: Differential weight (scaling factor)
102
+ self.F = self.config.get("F", 0.8)
103
+ self.F_min = self.config.get("F_min", 0.5)
104
+ self.F_max = self.config.get("F_max", 1.0)
105
+
106
+ # CR: Crossover probability
107
+ self.CR = self.config.get("CR", 0.9)
108
+
109
+ # Strategy
110
+ self.strategy = self.config.get("strategy", "rand/1")
111
+ self.crossover_type = self.config.get("crossover_type", "binomial")
112
+
113
+ # Adaptive parameters
114
+ self.adaptive = self.config.get("adaptive", False)
115
+
116
+ # Architecture encoding
117
+ self.max_nodes = self.config.get("max_nodes", 20)
118
+ self.encoder = ArchitectureEncoder(search_space, self.max_nodes)
119
+ self.dim = self.encoder.get_dimension()
120
+
121
+ # Population state
122
+ self.population: List[Individual] = []
123
+ self.population_vectors: List[np.ndarray] = []
124
+ self.fitnesses: List[float] = []
125
+
126
+ self.best_individual: Optional[Individual] = None
127
+ self.best_vector: Optional[np.ndarray] = None
128
+ self.best_fitness: float = -np.inf
129
+
130
+ # History
131
+ self.generation = 0
132
+ self.history: List[Dict[str, Any]] = []
133
+
134
+ logger.info(
135
+ f"Initialized DE: strategy={self.strategy}, "
136
+ f"pop_size={self.population_size}, F={self.F}, CR={self.CR}"
137
+ )
138
+
139
+ def initialize_population(self, evaluator: Callable) -> None:
140
+ """
141
+ Initialize random population and evaluate.
142
+
143
+ Args:
144
+ evaluator: Fitness evaluation function
145
+ """
146
+ self.population = []
147
+ self.population_vectors = []
148
+ self.fitnesses = []
149
+
150
+ for _i in range(self.population_size):
151
+ # Random vector in [0, 1]^dim
152
+ vector = np.random.rand(self.dim)
153
+
154
+ # Decode to architecture
155
+ graph = self.encoder.decode(vector)
156
+
157
+ # Evaluate
158
+ fitness = evaluator(graph)
159
+
160
+ # Store
161
+ individual = Individual(graph)
162
+ individual.fitness = fitness
163
+
164
+ self.population.append(individual)
165
+ self.population_vectors.append(vector)
166
+ self.fitnesses.append(fitness)
167
+
168
+ # Update best
169
+ if fitness > self.best_fitness:
170
+ self.best_fitness = fitness
171
+ self.best_vector = vector.copy()
172
+ self.best_individual = individual
173
+
174
+ logger.debug(f"Initialized population: best_fitness={self.best_fitness:.4f}")
175
+
176
+ def mutate(self, target_idx: int) -> np.ndarray:
177
+ """
178
+ Create mutant vector using selected strategy.
179
+
180
+ Args:
181
+ target_idx: Index of target individual
182
+
183
+ Returns:
184
+ Mutant vector
185
+ """
186
+ # Get indices excluding target
187
+ indices = [i for i in range(self.population_size) if i != target_idx]
188
+
189
+ if self.strategy == "rand/1":
190
+ return self._mutate_rand_1(indices)
191
+ elif self.strategy == "best/1":
192
+ return self._mutate_best_1(indices)
193
+ elif self.strategy == "rand/2":
194
+ return self._mutate_rand_2(indices)
195
+ elif self.strategy == "current-to-best/1":
196
+ return self._mutate_current_to_best_1(target_idx, indices)
197
+ else:
198
+ raise ValueError(f"Unknown strategy: {self.strategy}")
199
+
200
+ def _mutate_rand_1(self, indices: List[int]) -> np.ndarray:
201
+ """
202
+ DE/rand/1 mutation.
203
+
204
+ mutant = x_r1 + F * (x_r2 - x_r3)
205
+ """
206
+ r1, r2, r3 = random.sample(indices, 3)
207
+
208
+ mutant = self.population_vectors[r1] + self.F * (
209
+ self.population_vectors[r2] - self.population_vectors[r3]
210
+ )
211
+
212
+ return mutant
213
+
214
+ def _mutate_best_1(self, indices: List[int]) -> np.ndarray:
215
+ """
216
+ DE/best/1 mutation.
217
+
218
+ mutant = x_best + F * (x_r1 - x_r2)
219
+ """
220
+ r1, r2 = random.sample(indices, 2)
221
+
222
+ mutant = self.best_vector + self.F * (
223
+ self.population_vectors[r1] - self.population_vectors[r2]
224
+ )
225
+
226
+ return mutant
227
+
228
+ def _mutate_rand_2(self, indices: List[int]) -> np.ndarray:
229
+ """
230
+ DE/rand/2 mutation.
231
+
232
+ mutant = x_r1 + F * (x_r2 - x_r3) + F * (x_r4 - x_r5)
233
+ """
234
+ r1, r2, r3, r4, r5 = random.sample(indices, 5)
235
+
236
+ mutant = (
237
+ self.population_vectors[r1]
238
+ + self.F * (self.population_vectors[r2] - self.population_vectors[r3])
239
+ + self.F * (self.population_vectors[r4] - self.population_vectors[r5])
240
+ )
241
+
242
+ return mutant
243
+
244
+ def _mutate_current_to_best_1(self, target_idx: int, indices: List[int]) -> np.ndarray:
245
+ """
246
+ DE/current-to-best/1 mutation.
247
+
248
+ mutant = x_i + F * (x_best - x_i) + F * (x_r1 - x_r2)
249
+ """
250
+ r1, r2 = random.sample(indices, 2)
251
+
252
+ mutant = (
253
+ self.population_vectors[target_idx]
254
+ + self.F * (self.best_vector - self.population_vectors[target_idx])
255
+ + self.F * (self.population_vectors[r1] - self.population_vectors[r2])
256
+ )
257
+
258
+ return mutant
259
+
260
+ def crossover(self, target: np.ndarray, mutant: np.ndarray) -> np.ndarray:
261
+ """
262
+ Crossover target and mutant to create trial vector.
263
+
264
+ Args:
265
+ target: Target vector
266
+ mutant: Mutant vector
267
+
268
+ Returns:
269
+ Trial vector
270
+ """
271
+ if self.crossover_type == "binomial":
272
+ return self._crossover_binomial(target, mutant)
273
+ elif self.crossover_type == "exponential":
274
+ return self._crossover_exponential(target, mutant)
275
+ else:
276
+ raise ValueError(f"Unknown crossover type: {self.crossover_type}")
277
+
278
+ def _crossover_binomial(self, target: np.ndarray, mutant: np.ndarray) -> np.ndarray:
279
+ """
280
+ Binomial crossover.
281
+
282
+ Each dimension inherited from mutant with probability CR.
283
+ """
284
+ trial = target.copy()
285
+
286
+ # Ensure at least one dimension from mutant
287
+ j_rand = random.randint(0, self.dim - 1)
288
+
289
+ for j in range(self.dim):
290
+ if random.random() < self.CR or j == j_rand:
291
+ trial[j] = mutant[j]
292
+
293
+ return trial
294
+
295
+ def _crossover_exponential(self, target: np.ndarray, mutant: np.ndarray) -> np.ndarray:
296
+ """
297
+ Exponential crossover.
298
+
299
+ Copy consecutive dimensions from mutant.
300
+ """
301
+ trial = target.copy()
302
+
303
+ # Start position
304
+ n = random.randint(0, self.dim - 1)
305
+ L = 0
306
+
307
+ # Copy consecutive dimensions
308
+ while True:
309
+ trial[n] = mutant[n]
310
+ n = (n + 1) % self.dim
311
+ L += 1
312
+
313
+ if random.random() >= self.CR or L >= self.dim:
314
+ break
315
+
316
+ return trial
317
+
318
+ def select(
319
+ self,
320
+ target_fitness: float,
321
+ trial_fitness: float,
322
+ target_vector: np.ndarray,
323
+ trial_vector: np.ndarray,
324
+ ) -> tuple:
325
+ """
326
+ Greedy selection between target and trial.
327
+
328
+ Args:
329
+ target_fitness: Target fitness
330
+ trial_fitness: Trial fitness
331
+ target_vector: Target vector
332
+ trial_vector: Trial vector
333
+
334
+ Returns:
335
+ (selected_fitness, selected_vector) tuple
336
+ """
337
+ if trial_fitness >= target_fitness:
338
+ return trial_fitness, trial_vector
339
+ else:
340
+ return target_fitness, target_vector
341
+
342
+ def optimize(self, evaluator: Callable) -> Individual:
343
+ """
344
+ Run Differential Evolution optimization.
345
+
346
+ Args:
347
+ evaluator: Function that evaluates ModelGraph -> fitness
348
+
349
+ Returns:
350
+ Best Individual found
351
+
352
+ Example:
353
+ >>> def my_evaluator(graph):
354
+ ... return train_and_evaluate(graph)
355
+ >>> best = optimizer.optimize(my_evaluator)
356
+ >>> print(f"Best fitness: {best.fitness:.4f}")
357
+ """
358
+ logger.info(f"Starting DE optimization for {self.max_generations} generations")
359
+
360
+ # Initialize population
361
+ self.initialize_population(evaluator)
362
+
363
+ # Evolution loop
364
+ for generation in range(self.max_generations):
365
+ self.generation = generation
366
+
367
+ new_population = []
368
+ new_vectors = []
369
+ new_fitnesses = []
370
+
371
+ # For each individual
372
+ for i in range(self.population_size):
373
+ target_vector = self.population_vectors[i]
374
+ target_fitness = self.fitnesses[i]
375
+
376
+ # Mutation
377
+ mutant_vector = self.mutate(i)
378
+
379
+ # Boundary handling (clamp to [0, 1])
380
+ mutant_vector = np.clip(mutant_vector, 0.0, 1.0)
381
+
382
+ # Crossover
383
+ trial_vector = self.crossover(target_vector, mutant_vector)
384
+
385
+ # Decode and evaluate trial
386
+ trial_graph = self.encoder.decode(trial_vector)
387
+ trial_fitness = evaluator(trial_graph)
388
+
389
+ # Selection
390
+ selected_fitness, selected_vector = self.select(
391
+ target_fitness, trial_fitness, target_vector, trial_vector
392
+ )
393
+
394
+ # Update population
395
+ if selected_fitness == trial_fitness:
396
+ # Trial won
397
+ individual = Individual(trial_graph)
398
+ individual.fitness = trial_fitness
399
+ else:
400
+ # Target won
401
+ individual = self.population[i]
402
+
403
+ new_population.append(individual)
404
+ new_vectors.append(selected_vector)
405
+ new_fitnesses.append(selected_fitness)
406
+
407
+ # Update global best
408
+ if selected_fitness > self.best_fitness:
409
+ self.best_fitness = selected_fitness
410
+ self.best_vector = selected_vector.copy()
411
+ self.best_individual = individual
412
+
413
+ # Update population
414
+ self.population = new_population
415
+ self.population_vectors = new_vectors
416
+ self.fitnesses = new_fitnesses
417
+
418
+ # Record history
419
+ avg_fitness = np.mean(self.fitnesses)
420
+
421
+ self.history.append(
422
+ {
423
+ "generation": generation,
424
+ "best_fitness": self.best_fitness,
425
+ "avg_fitness": avg_fitness,
426
+ "std_fitness": np.std(self.fitnesses),
427
+ }
428
+ )
429
+
430
+ # Logging
431
+ if generation % 10 == 0 or generation == self.max_generations - 1:
432
+ logger.info(
433
+ f"Generation {generation}/{self.max_generations}: "
434
+ f"best={self.best_fitness:.4f}, "
435
+ f"avg={avg_fitness:.4f}"
436
+ )
437
+
438
+ logger.info(f"DE complete. Best fitness: {self.best_fitness:.4f}")
439
+
440
+ return self.best_individual
441
+
442
+ def get_history(self) -> List[Dict[str, Any]]:
443
+ """Get optimization history."""
444
+ return self.history
445
+
446
+ def plot_convergence(self, save_path: Optional[str] = None) -> None:
447
+ """
448
+ Plot DE convergence.
449
+
450
+ Args:
451
+ save_path: Optional path to save plot
452
+ """
453
+ try:
454
+ import matplotlib.pyplot as plt
455
+ except ImportError:
456
+ logger.warning("matplotlib not available, cannot plot")
457
+ return
458
+
459
+ if not self.history:
460
+ logger.warning("No history to plot")
461
+ return
462
+
463
+ generations = [h["generation"] for h in self.history]
464
+ best_fitness = [h["best_fitness"] for h in self.history]
465
+ avg_fitness = [h["avg_fitness"] for h in self.history]
466
+
467
+ plt.figure(figsize=(10, 6))
468
+ plt.plot(generations, best_fitness, "b-", linewidth=2, label="Best Fitness")
469
+ plt.plot(generations, avg_fitness, "r--", linewidth=2, label="Average Fitness")
470
+
471
+ plt.xlabel("Generation", fontsize=12)
472
+ plt.ylabel("Fitness", fontsize=12)
473
+ plt.title("Differential Evolution Convergence", fontsize=14, fontweight="bold")
474
+ plt.legend()
475
+ plt.grid(True, alpha=0.3)
476
+ plt.tight_layout()
477
+
478
+ if save_path:
479
+ plt.savefig(save_path, dpi=300, bbox_inches="tight")
480
+ logger.info(f"Convergence plot saved to {save_path}")
481
+ else:
482
+ plt.show()
483
+
484
+ plt.close()
485
+
486
+ def __repr__(self) -> str:
487
+ """String representation."""
488
+ return (
489
+ f"DifferentialEvolution("
490
+ f"strategy={self.strategy}, "
491
+ f"pop_size={self.population_size}, "
492
+ f"F={self.F:.2f}, "
493
+ f"CR={self.CR:.2f})"
494
+ )
495
+
496
+
497
+ # Convenience function
498
+ def optimize_with_de(
499
+ search_space: SearchSpace,
500
+ evaluator: Callable,
501
+ population_size: int = 50,
502
+ max_generations: int = 100,
503
+ F: float = 0.8,
504
+ CR: float = 0.9,
505
+ strategy: str = "rand/1",
506
+ verbose: bool = True,
507
+ ) -> Individual:
508
+ """
509
+ Quick Differential Evolution optimization with sensible defaults.
510
+
511
+ Args:
512
+ search_space: SearchSpace to optimize over
513
+ evaluator: Fitness evaluation function
514
+ population_size: Population size
515
+ max_generations: Maximum generations
516
+ F: Mutation scaling factor
517
+ CR: Crossover probability
518
+ strategy: Mutation strategy ('rand/1', 'best/1', 'rand/2')
519
+ verbose: Print progress
520
+
521
+ Returns:
522
+ Best Individual found
523
+
524
+ Example:
525
+ >>> from morphml.optimizers.evolutionary import optimize_with_de
526
+ >>> best = optimize_with_de(
527
+ ... search_space=space,
528
+ ... evaluator=my_evaluator,
529
+ ... population_size=50,
530
+ ... strategy='rand/1'
531
+ ... )
532
+ """
533
+ optimizer = DifferentialEvolution(
534
+ search_space=search_space,
535
+ config={
536
+ "population_size": population_size,
537
+ "max_generations": max_generations,
538
+ "F": F,
539
+ "CR": CR,
540
+ "strategy": strategy,
541
+ },
542
+ )
543
+
544
+ best = optimizer.optimize(evaluator)
545
+
546
+ if verbose:
547
+ print(f"\n{'='*60}")
548
+ print("Differential Evolution Complete")
549
+ print(f"{'='*60}")
550
+ print(f"Strategy: {strategy}")
551
+ print(f"Best Fitness: {best.fitness:.4f}")
552
+ print(f"Generations: {max_generations}")
553
+ print(f"Population Size: {population_size}")
554
+ print(f"{'='*60}\n")
555
+
556
+ return best