evograd-diff 0.1.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.
Files changed (50) hide show
  1. evograd/__init__.py +67 -0
  2. evograd/algorithms/__init__.py +138 -0
  3. evograd/algorithms/cmaes.py +1365 -0
  4. evograd/algorithms/de.py +895 -0
  5. evograd/algorithms/ga.py +532 -0
  6. evograd/algorithms/pso.py +648 -0
  7. evograd/algorithms/shade.py +1165 -0
  8. evograd/benchmarks/functions/__init__.py +229 -0
  9. evograd/benchmarks/functions/base.py +217 -0
  10. evograd/benchmarks/functions/cec2017/__init__.py +250 -0
  11. evograd/benchmarks/functions/cec2017/basic.py +413 -0
  12. evograd/benchmarks/functions/cec2017/composition.py +580 -0
  13. evograd/benchmarks/functions/cec2017/data.pkl +0 -0
  14. evograd/benchmarks/functions/cec2017/data.py +350 -0
  15. evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
  16. evograd/benchmarks/functions/cec2017/simple.py +326 -0
  17. evograd/benchmarks/functions/classical.py +649 -0
  18. evograd/benchmarks/functions/smoothed_funnel.py +476 -0
  19. evograd/benchmarks/functions/transforms.py +463 -0
  20. evograd/benchmarks/run_benchmark_functions.py +1208 -0
  21. evograd/core/__init__.py +73 -0
  22. evograd/core/algorithm.py +778 -0
  23. evograd/core/maximize.py +269 -0
  24. evograd/core/minimize.py +740 -0
  25. evograd/core/problem.py +444 -0
  26. evograd/core/result.py +571 -0
  27. evograd/core/termination.py +602 -0
  28. evograd/operators/__init__.py +178 -0
  29. evograd/operators/crossover.py +1117 -0
  30. evograd/operators/mutation.py +1098 -0
  31. evograd/operators/relaxations.py +175 -0
  32. evograd/operators/repair.py +601 -0
  33. evograd/operators/sampling.py +577 -0
  34. evograd/operators/selection.py +981 -0
  35. evograd/operators/survival.py +1000 -0
  36. evograd/tests/__init__.py +11 -0
  37. evograd/tests/run_all.py +78 -0
  38. evograd/tests/test_core.py +528 -0
  39. evograd/tests/test_ga.py +572 -0
  40. evograd/tests/test_operators.py +662 -0
  41. evograd/tests/test_per_individual.py +326 -0
  42. evograd/tests/test_utils.py +328 -0
  43. evograd/utils/__init__.py +97 -0
  44. evograd/utils/callbacks.py +926 -0
  45. evograd/utils/device.py +502 -0
  46. evograd/utils/duplicates.py +421 -0
  47. evograd_diff-0.1.0.dist-info/METADATA +439 -0
  48. evograd_diff-0.1.0.dist-info/RECORD +50 -0
  49. evograd_diff-0.1.0.dist-info/WHEEL +4 -0
  50. evograd_diff-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1208 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EvoGrad Parallel Benchmark Suite
4
+
5
+ Runs EvoGrad algorithms in four configurations:
6
+ 1. Classical (adaptive=False, differentiable=False)
7
+ 2. Differentiable (adaptive=False, differentiable=True)
8
+ 3. Adaptive (adaptive=True, differentiable=False)
9
+ 4. Full (adaptive=True, differentiable=True)
10
+
11
+ Also supports baselines (included by default when running evolutionary algorithms):
12
+ - pymoo: Reference implementation baseline (--no_pymoo to disable)
13
+ - Adam: Gradient-based optimizer baseline (--no_adam to disable)
14
+
15
+ Function categories:
16
+ - Classical: Standard test functions (Sphere, Rosenbrock, Rastrigin, etc.)
17
+ - CEC 2017: Competition benchmark suite (F1-F30)
18
+ - Smoothed Funnel: Multi-basin problems designed for differentiable EAs
19
+
20
+ Usage:
21
+ python run_benchmark.py --algorithm DE --n_runs 30
22
+ python run_benchmark.py --algorithm SHADE --suite cec2017 --n_var 10
23
+ python run_benchmark.py --algorithm GA -D 10 --xl -100 --xu 100 --max_evals 50000
24
+
25
+ # Run only Adam optimizer
26
+ python run_benchmark.py -a ADAM -s quick -D 10 -r 30
27
+ python run_benchmark.py -a ADAM -s standard -D 10 --adam_lr 0.01
28
+
29
+ # Run DE with all baselines (pymoo + Adam)
30
+ python run_benchmark.py -a DE -s quick -D 10 -r 5
31
+
32
+ # Run DE without Adam baseline
33
+ python run_benchmark.py -a DE -s quick -D 10 -r 5 --no_adam
34
+
35
+ # Run DE without pymoo baseline
36
+ python run_benchmark.py -a DE -s quick -D 10 -r 5 --no_pymoo
37
+
38
+ # CEC 2017 examples
39
+ python run_benchmark.py -a DE -s cec2017_simple -D 10 -r 5 # F1-F10
40
+ python run_benchmark.py -a DE -s cec2017_hybrid -D 10 -r 5 # F11-F20
41
+ python run_benchmark.py -a DE -s cec2017 -D 10 -r 5 # All F1-F30
42
+
43
+ # Smoothed funnel functions (designed for differentiable EAs)
44
+ python run_benchmark.py -a DE -s funnel -D 10 -r 30 # All funnel functions
45
+ python run_benchmark.py -a DE -f multibasinrastrigin -D 10 # Single funnel function
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ # =============================================================================
51
+ # CRITICAL: Set thread limits BEFORE importing torch/numpy
52
+ # This prevents thread oversubscription on Linux with multiprocessing
53
+ # =============================================================================
54
+ import os
55
+ os.environ.setdefault('OMP_NUM_THREADS', '1')
56
+ os.environ.setdefault('MKL_NUM_THREADS', '1')
57
+ os.environ.setdefault('OPENBLAS_NUM_THREADS', '1')
58
+ os.environ.setdefault('VECLIB_MAXIMUM_THREADS', '1')
59
+ os.environ.setdefault('NUMEXPR_NUM_THREADS', '1')
60
+
61
+ import argparse
62
+ import json
63
+ import sys
64
+ import time
65
+ import warnings
66
+ import numpy as np
67
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
68
+ from dataclasses import dataclass, field, asdict
69
+ from datetime import datetime
70
+ from multiprocessing import cpu_count
71
+ from pathlib import Path
72
+ from typing import Any, Dict, List, Optional, Tuple
73
+
74
+ import torch
75
+ from torch import Tensor
76
+
77
+ warnings.filterwarnings('ignore')
78
+
79
+ # =============================================================================
80
+ # Path Setup - Make sure we can import both 'functions' and 'evograd'
81
+ # =============================================================================
82
+
83
+ # Directory containing this script (benchmarks/)
84
+ SCRIPT_DIR = Path(__file__).resolve().parent
85
+
86
+ # Parent of benchmarks/ is evograd/, parent of evograd/ contains evograd package
87
+ EVOGRAD_PARENT = SCRIPT_DIR.parent.parent # Go up two levels to find evograd package
88
+
89
+ # Add paths for imports
90
+ if str(SCRIPT_DIR) not in sys.path:
91
+ sys.path.insert(0, str(SCRIPT_DIR)) # For 'functions' subpackage
92
+ if str(EVOGRAD_PARENT) not in sys.path:
93
+ sys.path.insert(0, str(EVOGRAD_PARENT)) # For 'evograd' package
94
+
95
+ torch.set_num_threads(1) # Limit PyTorch threads per process
96
+
97
+ # Import benchmark functions from the functions subpackage
98
+ from functions import (
99
+ CLASSICAL_FUNCTIONS,
100
+ ALL_FUNCTIONS,
101
+ )
102
+
103
+ # Import smoothed funnel functions
104
+ SMOOTHED_FUNNEL_AVAILABLE = False
105
+ SMOOTHED_FUNNEL_FUNCTIONS = {}
106
+ try:
107
+ from functions.smoothed_funnel import (
108
+ MultiBasinRastrigin,
109
+ MultiBasinRosenbrock,
110
+ DeceptiveLandscape,
111
+ SMOOTHED_FUNNEL_FUNCTIONS as _FUNNEL_FUNCS,
112
+ )
113
+ SMOOTHED_FUNNEL_FUNCTIONS = _FUNNEL_FUNCS
114
+ SMOOTHED_FUNNEL_AVAILABLE = True
115
+ except ImportError as e:
116
+ print(f"Warning: Smoothed funnel functions not available: {e}")
117
+
118
+ # Import CEC 2017 functions
119
+ CEC2017_AVAILABLE = False
120
+ CEC2017_FUNCTIONS = {}
121
+ try:
122
+ from functions.cec2017 import (
123
+ CEC2017_FUNCTIONS as _CEC2017_FUNCS,
124
+ ALL_CEC2017_CLASSES,
125
+ get_function as get_cec2017_function,
126
+ )
127
+ CEC2017_FUNCTIONS = _CEC2017_FUNCS
128
+ CEC2017_AVAILABLE = True
129
+ except ImportError as e:
130
+ print(f"Warning: CEC 2017 functions not available: {e}")
131
+
132
+ # Import pymoo (optional)
133
+ try:
134
+ from pymoo.core.problem import Problem as PymooProblem
135
+ from pymoo.optimize import minimize as pymoo_minimize
136
+ from pymoo.termination import get_termination
137
+ from pymoo.operators.sampling.rnd import FloatRandomSampling
138
+ PYMOO_AVAILABLE = True
139
+ except ImportError:
140
+ PYMOO_AVAILABLE = False
141
+
142
+ # Import EvoGrad components
143
+ EVOGRAD_AVAILABLE = False
144
+ EVOGRAD_IMPORT_ERROR = ""
145
+
146
+ try:
147
+ from evograd.core.problem import Problem
148
+ from evograd.core.termination import MaxEvaluations
149
+ from evograd.core.minimize import minimize
150
+
151
+ from evograd.algorithms.de import DE
152
+ from evograd.algorithms.shade import SHADE
153
+ from evograd.algorithms.pso import PSO
154
+ from evograd.algorithms.ga import GA
155
+ from evograd.algorithms.cmaes import CMAES
156
+
157
+ from evograd.operators.selection import TournamentSelection
158
+ from evograd.operators.crossover import SBXCrossover
159
+ from evograd.operators.mutation import PolynomialMutation
160
+ from evograd.operators.survival import FitnessSurvival
161
+ from evograd.operators.repair import ReflectRepair
162
+
163
+ EVOGRAD_AVAILABLE = True
164
+ except ImportError as e:
165
+ EVOGRAD_IMPORT_ERROR = str(e)
166
+
167
+
168
+ # =============================================================================
169
+ # Function Suites
170
+ # =============================================================================
171
+
172
+ # Build CEC 2017 function lists
173
+ CEC2017_SIMPLE = [f"cec2017_f{i}" for i in range(1, 11)] # F1-F10
174
+ CEC2017_HYBRID = [f"cec2017_f{i}" for i in range(11, 21)] # F11-F20
175
+ CEC2017_COMPOSITION = [f"cec2017_f{i}" for i in range(21, 31)] # F21-F30
176
+ CEC2017_ALL = [f"cec2017_f{i}" for i in range(1, 31) if i != 2] # F1-F30
177
+
178
+ SUITES = {
179
+ # Classical functions
180
+ "classical": list(CLASSICAL_FUNCTIONS.keys()),
181
+ "standard": ["sphere", "ellipsoid", "rosenbrock", "rastrigin", "ackley", "griewank", "schwefel", "levy"],
182
+ "quick": ["sphere", "rastrigin", "ackley"],
183
+
184
+ # CEC 2017 suites
185
+ "cec2017": CEC2017_ALL,
186
+ "cec2017_simple": CEC2017_SIMPLE,
187
+ "cec2017_hybrid": CEC2017_HYBRID,
188
+ "cec2017_composition": CEC2017_COMPOSITION,
189
+ "cec2017_quick": ["cec2017_f1", "cec2017_f11", "cec2017_f21"], # One from each category
190
+
191
+ # Smoothed funnel functions (designed for differentiable EAs)
192
+ "funnel": ["multibasinrastrigin", "multibasinrosenbrock", "deceptivelandscape"],
193
+ "funnel_quick": ["multibasinrastrigin"],
194
+
195
+ # Mixed suites
196
+ "all": list(CLASSICAL_FUNCTIONS.keys()) + CEC2017_ALL,
197
+ "all_with_funnel": list(CLASSICAL_FUNCTIONS.keys()) + CEC2017_ALL + ["multibasinrastrigin", "multibasinrosenbrock", "deceptivelandscape"],
198
+ }
199
+
200
+
201
+ # =============================================================================
202
+ # Result Data Structures
203
+ # =============================================================================
204
+
205
+ @dataclass
206
+ class RunResult:
207
+ """Result of a single optimization run."""
208
+ algorithm: str
209
+ config: str
210
+ function: str
211
+ n_var: int
212
+ seed: int
213
+ best_fitness_history: List[float]
214
+ best_fitness: float
215
+ n_evals: int
216
+ n_gen: int
217
+ wall_time: float
218
+ success: bool
219
+ error_message: Optional[str] = None
220
+
221
+ def to_dict(self) -> Dict[str, Any]:
222
+ return asdict(self)
223
+
224
+
225
+ @dataclass
226
+ class BenchmarkResults:
227
+ """Collection of benchmark results."""
228
+ algorithm: str
229
+ timestamp: str
230
+ device: str
231
+ n_var: int
232
+ xl: float
233
+ xu: float
234
+ max_evals: int
235
+ n_runs: int
236
+ results: List[RunResult] = field(default_factory=list)
237
+
238
+ def add_result(self, result: RunResult):
239
+ self.results.append(result)
240
+
241
+ def get_summary(self) -> Dict[str, Dict[str, Dict[str, float]]]:
242
+ """Get summary statistics grouped by function and config."""
243
+ summary = {}
244
+
245
+ for result in self.results:
246
+ func_name = result.function
247
+ config = result.config
248
+
249
+ if func_name not in summary:
250
+ summary[func_name] = {}
251
+ if config not in summary[func_name]:
252
+ summary[func_name][config] = {"fitness_values": [], "times": [], "success_count": 0}
253
+
254
+ summary[func_name][config]["fitness_values"].append(result.best_fitness)
255
+ summary[func_name][config]["times"].append(result.wall_time)
256
+ if result.success:
257
+ summary[func_name][config]["success_count"] += 1
258
+
259
+ for func_name in summary:
260
+ for config in summary[func_name]:
261
+ data = summary[func_name][config]
262
+ fitness = np.array(data["fitness_values"])
263
+ times = np.array(data["times"])
264
+ n = len(fitness)
265
+ valid = fitness[np.isfinite(fitness)]
266
+
267
+ summary[func_name][config] = {
268
+ "best": float(np.min(valid)) if len(valid) > 0 else float('inf'),
269
+ "mean": float(np.mean(valid)) if len(valid) > 0 else float('inf'),
270
+ "std": float(np.std(valid)) if len(valid) > 0 else 0,
271
+ "median": float(np.median(valid)) if len(valid) > 0 else float('inf'),
272
+ "worst": float(np.max(valid)) if len(valid) > 0 else float('inf'),
273
+ "mean_time": float(np.mean(times)) if n > 0 else 0,
274
+ "success_rate": data["success_count"] / n if n > 0 else 0,
275
+ "n_runs": n,
276
+ }
277
+
278
+ return summary
279
+
280
+ def save(self, output_dir: Path):
281
+ """Save results to JSON and CSV."""
282
+ output_dir = Path(output_dir)
283
+ output_dir.mkdir(parents=True, exist_ok=True)
284
+
285
+ base_name = f"{self.algorithm}_{self.timestamp}"
286
+
287
+ # JSON
288
+ json_path = output_dir / f"{base_name}.json"
289
+ with open(json_path, 'w') as f:
290
+ json.dump({
291
+ "algorithm": self.algorithm,
292
+ "timestamp": self.timestamp,
293
+ "device": self.device,
294
+ "n_var": self.n_var,
295
+ "xl": self.xl,
296
+ "xu": self.xu,
297
+ "max_evals": self.max_evals,
298
+ "n_runs": self.n_runs,
299
+ "results": [r.to_dict() for r in self.results],
300
+ }, f, indent=2)
301
+
302
+ # CSV
303
+ csv_path = output_dir / f"{base_name}_summary.csv"
304
+ summary = self.get_summary()
305
+
306
+ with open(csv_path, 'w') as f:
307
+ f.write("function,config,best,mean,std,median,worst,mean_time,success_rate,n_runs\n")
308
+ for func_name in sorted(summary.keys()):
309
+ for config in sorted(summary[func_name].keys()):
310
+ s = summary[func_name][config]
311
+ f.write(f"{func_name},{config},{s['best']:.6e},{s['mean']:.6e},{s['std']:.6e},"
312
+ f"{s['median']:.6e},{s['worst']:.6e},{s['mean_time']:.2f},"
313
+ f"{s['success_rate']:.2f},{s['n_runs']}\n")
314
+
315
+ print(f"\nResults saved to:\n - {json_path}\n - {csv_path}")
316
+
317
+
318
+ # =============================================================================
319
+ # Algorithm Factory
320
+ # =============================================================================
321
+
322
+ def create_evograd_algorithm(algorithm_name: str, config: str, pop_size: int, device: str, seed: int):
323
+ """Create EvoGrad algorithm with specified configuration."""
324
+ differentiable = config in ("differentiable", "full")
325
+ adaptive = config in ("adaptive", "full")
326
+
327
+ name = algorithm_name.upper()
328
+
329
+ if name == "DE":
330
+ return DE(pop_size=pop_size,
331
+ F=0.5,
332
+ CR=0.2,
333
+ variant='DE/best/1/bin',
334
+ repair=ReflectRepair(),
335
+ adaptive=adaptive,
336
+ differentiable=differentiable)
337
+
338
+ elif name == "SHADE":
339
+ return SHADE(pop_size=pop_size,
340
+ repair=ReflectRepair(),
341
+ adaptive=adaptive,
342
+ differentiable=differentiable)
343
+
344
+ elif name == "PSO":
345
+ return PSO(pop_size=pop_size,
346
+ w=0.7,
347
+ c1=1.5,
348
+ c2=1.5,
349
+ repair=ReflectRepair(),
350
+ adaptive=adaptive,
351
+ differentiable=differentiable)
352
+
353
+ elif name == "GA":
354
+ return GA(
355
+ pop_size=pop_size,
356
+ selection=TournamentSelection(tournament_size=2, adaptive=adaptive),
357
+ crossover=SBXCrossover(eta=15, prob=0.5, adaptive=adaptive),
358
+ mutation=PolynomialMutation(eta=20, prob=0.9, adaptive=adaptive),
359
+ survival=FitnessSurvival(adaptive=adaptive),
360
+ repair=ReflectRepair(),
361
+ differentiable=differentiable
362
+ )
363
+
364
+ elif name == "CMAES":
365
+ return CMAES(pop_size=pop_size,
366
+ sigma=0.1,
367
+ repair=ReflectRepair(),
368
+ adaptive=adaptive,
369
+ differentiable=differentiable)
370
+
371
+ else:
372
+ raise ValueError(f"Unknown algorithm: {algorithm_name}")
373
+
374
+
375
+ def create_pymoo_algorithm(algorithm_name: str, pop_size: int):
376
+ """Create pymoo algorithm for baseline comparison."""
377
+ name = algorithm_name.upper()
378
+
379
+ if name == "DE":
380
+ from pymoo.algorithms.soo.nonconvex.de import DE as PymooDE
381
+ return PymooDE(pop_size=pop_size,
382
+ sampling=FloatRandomSampling(),
383
+ )
384
+ elif name == "SHADE":
385
+ # Pymoo doesn't have SHADE, use DE as baseline
386
+ from pymoo.algorithms.soo.nonconvex.de import DE as PymooDE
387
+ return PymooDE(pop_size=pop_size,
388
+ sampling=FloatRandomSampling(),)
389
+ elif name == "PSO":
390
+ from pymoo.algorithms.soo.nonconvex.pso import PSO as PymooPSO
391
+ return PymooPSO(pop_size=pop_size,
392
+ sampling=FloatRandomSampling(),
393
+ w=0.7,
394
+ c1=1.5,
395
+ c2=1.5,
396
+ adaptive=False,)
397
+ elif name == "GA":
398
+ from pymoo.algorithms.soo.nonconvex.ga import GA as PymooGA
399
+ return PymooGA(pop_size=pop_size,
400
+ sampling=FloatRandomSampling(),)
401
+ elif name == "CMAES":
402
+ from pymoo.algorithms.soo.nonconvex.cmaes import CMAES as PymooCMAES
403
+ return PymooCMAES(sigma=0.1)
404
+ else:
405
+ raise ValueError(f"Unknown pymoo algorithm: {algorithm_name}")
406
+
407
+
408
+ # =============================================================================
409
+ # Function Instance Creation
410
+ # =============================================================================
411
+
412
+ def get_function_instance(func_name: str, n_var: int, xl: float, xu: float):
413
+ """
414
+ Create a function instance from name.
415
+
416
+ Handles classical functions, CEC 2017 functions, and smoothed funnel functions.
417
+ CEC 2017 functions use fixed bounds [-100, 100].
418
+ Smoothed funnel functions use default bounds [-5, 5].
419
+ """
420
+ # Check if it's a CEC 2017 function
421
+ if func_name.startswith("cec2017_f"):
422
+ if not CEC2017_AVAILABLE:
423
+ raise ImportError("CEC 2017 functions not available")
424
+
425
+ # Extract function number
426
+ func_num = int(func_name.replace("cec2017_f", ""))
427
+
428
+ # CEC 2017 uses fixed bounds [-100, 100]
429
+ func_instance = get_cec2017_function(func_num, n_var=n_var)
430
+ return func_instance
431
+
432
+ # Check if it's a smoothed funnel function
433
+ elif func_name in SMOOTHED_FUNNEL_FUNCTIONS:
434
+ if not SMOOTHED_FUNNEL_AVAILABLE:
435
+ raise ImportError("Smoothed funnel functions not available")
436
+
437
+ func_class = SMOOTHED_FUNNEL_FUNCTIONS[func_name]
438
+ # Smoothed funnels use [-5, 5] by default, but respect user bounds
439
+ return func_class(n_var=n_var, xl=xl, xu=xu)
440
+
441
+ # Classical function
442
+ elif func_name in CLASSICAL_FUNCTIONS:
443
+ func_class = CLASSICAL_FUNCTIONS[func_name]
444
+ return func_class(n_var=n_var, xl=xl, xu=xu)
445
+
446
+ # Check ALL_FUNCTIONS as fallback
447
+ elif func_name in ALL_FUNCTIONS:
448
+ func_class = ALL_FUNCTIONS[func_name]
449
+ return func_class(n_var=n_var, xl=xl, xu=xu)
450
+
451
+ else:
452
+ raise ValueError(f"Unknown function: {func_name}")
453
+
454
+
455
+ # =============================================================================
456
+ # Adam Optimizer Baseline
457
+ # =============================================================================
458
+
459
+ def run_adam_population(
460
+ func_instance,
461
+ n_var: int,
462
+ xl: float,
463
+ xu: float,
464
+ pop_size: int,
465
+ max_evals: int,
466
+ seed: int,
467
+ device: str,
468
+ lr: float = 0.05,
469
+ b1: float = 0.9,
470
+ b2: float = 0.999,
471
+ wd: float = 0.0,
472
+ ) -> Tuple[float, List[float], int, int, Optional[Tensor]]:
473
+ """
474
+ Run Adam optimizer on benchmark function.
475
+
476
+ Optimizes directly in [xl, xu] space using projected gradient
477
+ descent. After each Adam update, values are clamped to maintain
478
+ feasibility. Uses multiple parallel "individuals" for fair
479
+ comparison with population-based methods.
480
+
481
+ Args:
482
+ func_instance: Benchmark function instance (callable).
483
+ n_var: Number of variables.
484
+ xl: Lower bound.
485
+ xu: Upper bound.
486
+ pop_size: Number of parallel solutions (for fair eval count comparison).
487
+ max_evals: Maximum fitness evaluations.
488
+ seed: Random seed.
489
+ device: Torch device string.
490
+ lr: Learning rate.
491
+ b1: Adam beta1 parameter.
492
+ b2: Adam beta2 parameter.
493
+ wd: Weight decay (L2 regularisation).
494
+
495
+ Returns:
496
+ Tuple of (best_fitness, fitness_history, n_evaluations, n_generations, best_solution).
497
+ """
498
+ torch.manual_seed(seed)
499
+ dev = torch.device(device)
500
+
501
+ # Initialize population uniformly in [xl, xu] (same as evolutionary algorithms)
502
+ x_init = torch.rand(pop_size, n_var, device=dev, dtype=torch.float32) * (xu - xl) + xl
503
+ x = x_init.clone().requires_grad_(True)
504
+
505
+ opt = torch.optim.Adam([x], lr=lr, betas=(b1, b2), weight_decay=wd)
506
+
507
+ best = float("inf")
508
+ best_solution = None
509
+ hist = []
510
+ evals = 0
511
+ n_gen = 0
512
+
513
+ while evals < max_evals:
514
+ opt.zero_grad()
515
+
516
+ # Evaluate fitness
517
+ f = func_instance(x)
518
+
519
+ # Handle different output shapes
520
+ if f.dim() > 1:
521
+ f = f.squeeze()
522
+
523
+ # Backpropagate mean loss across population
524
+ loss = f.mean()
525
+ loss.backward()
526
+ opt.step()
527
+
528
+ # Project back to feasible region [xl, xu]
529
+ with torch.no_grad():
530
+ x.clamp_(xl, xu)
531
+
532
+ # Track best solution
533
+ min_idx = f.argmin()
534
+ min_val = float(f[min_idx])
535
+ if min_val < best:
536
+ best = min_val
537
+ best_solution = x[min_idx].detach().clone()
538
+
539
+ hist.append(best)
540
+ evals += pop_size
541
+ n_gen += 1
542
+
543
+ return best, hist, evals, n_gen, best_solution
544
+
545
+
546
+ # =============================================================================
547
+ # Single Run Functions
548
+ # =============================================================================
549
+
550
+ def _worker_init():
551
+ """Initialize worker process with thread limits."""
552
+ import os
553
+ os.environ['OMP_NUM_THREADS'] = '1'
554
+ os.environ['MKL_NUM_THREADS'] = '1'
555
+ os.environ['OPENBLAS_NUM_THREADS'] = '1'
556
+ os.environ['VECLIB_MAXIMUM_THREADS'] = '1'
557
+ os.environ['NUMEXPR_NUM_THREADS'] = '1'
558
+
559
+ import torch
560
+ torch.set_num_threads(1)
561
+
562
+
563
+ def run_evograd_single(
564
+ algorithm_name: str,
565
+ config: str,
566
+ func_name: str,
567
+ n_var: int,
568
+ xl: float,
569
+ xu: float,
570
+ seed: int,
571
+ max_evals: int,
572
+ pop_size: int,
573
+ lr_pop: float,
574
+ lr_hyper: float,
575
+ grad_clip_pop: float,
576
+ grad_clip_hyper: float,
577
+ device: str,
578
+ ) -> RunResult:
579
+ """Run a single EvoGrad optimization."""
580
+ _worker_init() # Ensure thread limits in worker
581
+
582
+ if not EVOGRAD_AVAILABLE:
583
+ return RunResult(
584
+ algorithm=algorithm_name, config=config, function=func_name,
585
+ n_var=n_var, seed=seed, best_fitness_history=[], best_fitness=float('inf'),
586
+ n_evals=0, n_gen=0, wall_time=0.0, success=False,
587
+ error_message=f"EvoGrad not available: {EVOGRAD_IMPORT_ERROR}",
588
+ )
589
+
590
+ # Comprehensive seeding for reproducibility
591
+ import random
592
+ random.seed(seed)
593
+ np.random.seed(seed)
594
+ torch.manual_seed(seed)
595
+ if torch.cuda.is_available():
596
+ torch.cuda.manual_seed_all(seed)
597
+
598
+ start_time = time.time()
599
+
600
+ try:
601
+ # Get function instance
602
+ func_instance = get_function_instance(func_name, n_var, xl, xu)
603
+
604
+ # Get actual bounds from the instance
605
+ actual_xl = func_instance.xl[0].item()
606
+ actual_xu = func_instance.xu[0].item()
607
+
608
+ # Create Problem with the function's __call__ as objective
609
+ problem = Problem(
610
+ objective=func_instance,
611
+ n_var=n_var,
612
+ xl=actual_xl,
613
+ xu=actual_xu,
614
+ device=device,
615
+ )
616
+
617
+ # Create Algorithm
618
+ algorithm = create_evograd_algorithm(algorithm_name, config, pop_size, device, seed)
619
+
620
+ # Run
621
+ result = minimize(
622
+ problem=problem,
623
+ algorithm=algorithm,
624
+ termination=MaxEvaluations(max_evals),
625
+ seed=seed,
626
+ verbose=False,
627
+ save_history=True,
628
+ lr_pop=lr_pop,
629
+ lr_hyper=lr_hyper,
630
+ grad_clip_pop=grad_clip_pop,
631
+ grad_clip_hyper=grad_clip_hyper,
632
+ )
633
+
634
+ return RunResult(
635
+ algorithm=algorithm_name, config=config, function=func_name,
636
+ n_var=n_var, seed=seed,
637
+ best_fitness_history=result.history.get("best_fitness", []),
638
+ best_fitness=result.best_fitness,
639
+ n_evals=result.n_evals,
640
+ n_gen=result.n_gen,
641
+ wall_time=time.time() - start_time,
642
+ success=True,
643
+ )
644
+
645
+ except Exception as e:
646
+ import traceback
647
+ return RunResult(
648
+ algorithm=algorithm_name, config=config, function=func_name,
649
+ n_var=n_var, seed=seed, best_fitness_history=[], best_fitness=float('inf'),
650
+ n_evals=0, n_gen=0, wall_time=time.time() - start_time,
651
+ success=False, error_message=f"{str(e)}\n{traceback.format_exc()}",
652
+ )
653
+
654
+
655
+ def run_pymoo_single(
656
+ algorithm_name: str,
657
+ func_name: str,
658
+ n_var: int,
659
+ xl: float,
660
+ xu: float,
661
+ seed: int,
662
+ max_evals: int,
663
+ pop_size: int,
664
+ ) -> RunResult:
665
+ """Run a single pymoo optimization."""
666
+ _worker_init() # Ensure thread limits in worker
667
+
668
+ if not PYMOO_AVAILABLE:
669
+ return RunResult(
670
+ algorithm=algorithm_name, config="pymoo", function=func_name,
671
+ n_var=n_var, seed=seed, best_fitness_history=[], best_fitness=float('inf'),
672
+ n_evals=0, n_gen=0, wall_time=0.0, success=False,
673
+ error_message="pymoo not available",
674
+ )
675
+
676
+ np.random.seed(seed)
677
+ start_time = time.time()
678
+
679
+ try:
680
+ # Get function instance
681
+ func_instance = get_function_instance(func_name, n_var, xl, xu)
682
+
683
+ # Get actual bounds from the instance
684
+ actual_xl = func_instance.xl[0].item()
685
+ actual_xu = func_instance.xu[0].item()
686
+
687
+ # Pymoo problem wrapper
688
+ class Wrapper(PymooProblem):
689
+ def __init__(self):
690
+ super().__init__(n_var=n_var, n_obj=1, n_constr=0,
691
+ xl=np.full(n_var, actual_xl), xu=np.full(n_var, actual_xu))
692
+
693
+ def _evaluate(self, x, out, *args, **kwargs):
694
+ x_t = torch.tensor(x, dtype=torch.float32)
695
+ out["F"] = func_instance(x_t).numpy().reshape(-1, 1)
696
+
697
+ problem = Wrapper()
698
+ algorithm = create_pymoo_algorithm(algorithm_name, pop_size)
699
+
700
+ history = []
701
+ def callback(algo):
702
+ if algo.opt is not None:
703
+ history.append(float(algo.opt.get("F").min()))
704
+
705
+ result = pymoo_minimize(
706
+ problem, algorithm, get_termination("n_eval", max_evals),
707
+ seed=seed, verbose=False, callback=callback,
708
+ )
709
+
710
+ return RunResult(
711
+ algorithm=algorithm_name, config="pymoo", function=func_name,
712
+ n_var=n_var, seed=seed, best_fitness_history=history,
713
+ best_fitness=float(result.F[0]) if result.F is not None else float('inf'),
714
+ n_evals=result.algorithm.evaluator.n_eval,
715
+ n_gen=len(history),
716
+ wall_time=time.time() - start_time, success=True,
717
+ )
718
+
719
+ except Exception as e:
720
+ import traceback
721
+ return RunResult(
722
+ algorithm=algorithm_name, config="pymoo", function=func_name,
723
+ n_var=n_var, seed=seed, best_fitness_history=[], best_fitness=float('inf'),
724
+ n_evals=0, n_gen=0, wall_time=time.time() - start_time,
725
+ success=False, error_message=f"{str(e)}\n{traceback.format_exc()}",
726
+ )
727
+
728
+
729
+ def run_adam_single(
730
+ func_name: str,
731
+ n_var: int,
732
+ xl: float,
733
+ xu: float,
734
+ seed: int,
735
+ max_evals: int,
736
+ pop_size: int,
737
+ device: str,
738
+ lr: float = 0.05,
739
+ ) -> RunResult:
740
+ """Run a single Adam optimization."""
741
+ _worker_init() # Ensure thread limits in worker
742
+
743
+ # Comprehensive seeding for reproducibility
744
+ import random
745
+ random.seed(seed)
746
+ np.random.seed(seed)
747
+ torch.manual_seed(seed)
748
+ if torch.cuda.is_available():
749
+ torch.cuda.manual_seed_all(seed)
750
+
751
+ start_time = time.time()
752
+
753
+ try:
754
+ # Get function instance
755
+ func_instance = get_function_instance(func_name, n_var, xl, xu)
756
+
757
+ # Get actual bounds from the instance
758
+ actual_xl = func_instance.xl[0].item()
759
+ actual_xu = func_instance.xu[0].item()
760
+
761
+ # Run Adam
762
+ best_fitness, history, n_evals, n_gen, best_solution = run_adam_population(
763
+ func_instance=func_instance,
764
+ n_var=n_var,
765
+ xl=actual_xl,
766
+ xu=actual_xu,
767
+ pop_size=pop_size,
768
+ max_evals=max_evals,
769
+ seed=seed,
770
+ device=device,
771
+ lr=lr,
772
+ )
773
+
774
+ return RunResult(
775
+ algorithm="ADAM", config="Adam", function=func_name,
776
+ n_var=n_var, seed=seed,
777
+ best_fitness_history=history,
778
+ best_fitness=best_fitness,
779
+ n_evals=n_evals,
780
+ n_gen=n_gen,
781
+ wall_time=time.time() - start_time,
782
+ success=True,
783
+ )
784
+
785
+ except Exception as e:
786
+ import traceback
787
+ return RunResult(
788
+ algorithm="ADAM", config="Adam", function=func_name,
789
+ n_var=n_var, seed=seed, best_fitness_history=[], best_fitness=float('inf'),
790
+ n_evals=0, n_gen=0, wall_time=time.time() - start_time,
791
+ success=False, error_message=f"{str(e)}\n{traceback.format_exc()}",
792
+ )
793
+
794
+
795
+ # =============================================================================
796
+ # Parallel Execution
797
+ # =============================================================================
798
+
799
+ def run_single_job(job: Dict[str, Any]) -> RunResult:
800
+ """Worker function for parallel execution."""
801
+ if job["config"] == "pymoo":
802
+ return run_pymoo_single(
803
+ job["algorithm"], job["function"], job["n_var"],
804
+ job["xl"], job["xu"], job["seed"], job["max_evals"], job["pop_size"],
805
+ )
806
+ elif job["config"] == "Adam":
807
+ return run_adam_single(
808
+ job["function"], job["n_var"],
809
+ job["xl"], job["xu"], job["seed"], job["max_evals"], job["pop_size"],
810
+ job["device"], job.get("adam_lr", 0.05),
811
+ )
812
+ else:
813
+ return run_evograd_single(
814
+ job["algorithm"], job["config"], job["function"], job["n_var"],
815
+ job["xl"], job["xu"], job["seed"], job["max_evals"], job["pop_size"],
816
+ job["lr_pop"], job["lr_hyper"], job["grad_clip_pop"],
817
+ job["grad_clip_hyper"], job["device"],
818
+ )
819
+
820
+
821
+ def run_benchmark_parallel(
822
+ algorithm_name: str,
823
+ functions: List[str],
824
+ n_var: int,
825
+ xl: float,
826
+ xu: float,
827
+ max_evals: int,
828
+ lr_pop: float,
829
+ lr_hyper: float,
830
+ grad_clip_pop: float,
831
+ grad_clip_hyper: float,
832
+ n_runs: int = 30,
833
+ pop_size: int = 100,
834
+ device: str = "cpu",
835
+ include_pymoo: bool = True,
836
+ include_adam: bool = True,
837
+ adam_lr: float = 0.05,
838
+ n_workers: int = -1,
839
+ base_seed: int = 0,
840
+ ) -> BenchmarkResults:
841
+ """Run full benchmark suite in parallel."""
842
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
843
+
844
+ results = BenchmarkResults(
845
+ algorithm=algorithm_name, timestamp=timestamp, device=device,
846
+ n_var=n_var, xl=xl, xu=xu, max_evals=max_evals, n_runs=n_runs,
847
+ )
848
+
849
+ if n_workers == -1:
850
+ n_workers = cpu_count()
851
+ n_workers = max(1, min(n_workers, cpu_count()))
852
+
853
+ # Determine configs based on algorithm
854
+ is_adam = algorithm_name.upper() == "ADAM"
855
+
856
+ if is_adam:
857
+ configs = ["Adam"] # Adam only has one config
858
+ else:
859
+ configs = ["classical", "differentiable", "adaptive", "full"]
860
+
861
+ # Build jobs
862
+ jobs = []
863
+ for func_name in functions:
864
+ for config in configs:
865
+ for run_idx in range(n_runs):
866
+ seed = base_seed + run_idx
867
+ job = {
868
+ "algorithm": algorithm_name, "config": config, "function": func_name,
869
+ "n_var": n_var, "xl": xl, "xu": xu, "seed": seed,
870
+ "max_evals": max_evals, "pop_size": pop_size,
871
+ "lr_pop": lr_pop, "lr_hyper": lr_hyper, "grad_clip_pop": grad_clip_pop,
872
+ "grad_clip_hyper": grad_clip_hyper, "device": device,
873
+ }
874
+ if is_adam:
875
+ job["adam_lr"] = adam_lr
876
+ jobs.append(job)
877
+
878
+ # Add pymoo baseline (not for Adam algorithm)
879
+ if include_pymoo and PYMOO_AVAILABLE and not is_adam:
880
+ for run_idx in range(n_runs):
881
+ seed = base_seed + run_idx
882
+ jobs.append({
883
+ "algorithm": algorithm_name, "config": "pymoo", "function": func_name,
884
+ "n_var": n_var, "xl": xl, "xu": xu, "seed": seed,
885
+ "max_evals": max_evals, "pop_size": pop_size, "device": "cpu",
886
+ })
887
+
888
+ # Add Adam baseline (not for Adam algorithm)
889
+ if include_adam and not is_adam:
890
+ for run_idx in range(n_runs):
891
+ seed = base_seed + run_idx
892
+ jobs.append({
893
+ "algorithm": algorithm_name, "config": "Adam", "function": func_name,
894
+ "n_var": n_var, "xl": xl, "xu": xu, "seed": seed,
895
+ "max_evals": max_evals, "pop_size": pop_size, "device": device,
896
+ "adam_lr": adam_lr,
897
+ })
898
+
899
+ total = len(jobs)
900
+
901
+ # Count CEC 2017 functions
902
+ n_cec2017 = sum(1 for f in functions if f.startswith("cec2017_"))
903
+ n_classical = len(functions) - n_cec2017
904
+
905
+ # Count configs
906
+ n_configs = len(configs)
907
+ if include_pymoo and PYMOO_AVAILABLE and not is_adam:
908
+ n_configs += 1
909
+ if include_adam and not is_adam:
910
+ n_configs += 1
911
+
912
+ print(f"\n{'='*70}")
913
+ print(f"EvoGrad Parallel Benchmark Suite")
914
+ print(f"{'='*70}")
915
+ print(f"Algorithm: {algorithm_name}")
916
+ print(f"Functions: {len(functions)} ({n_classical} classical, {n_cec2017} CEC2017)")
917
+ if is_adam:
918
+ print(f"Configs: {n_configs} (Adam gradient-based)")
919
+ print(f"Adam LR: {adam_lr}")
920
+ else:
921
+ baselines = []
922
+ if include_pymoo and PYMOO_AVAILABLE:
923
+ baselines.append("pymoo")
924
+ if include_adam:
925
+ baselines.append("Adam")
926
+ baseline_str = f", baselines: {', '.join(baselines)}" if baselines else ""
927
+ print(f"Configs: {n_configs} (EvoGrad: 4{baseline_str})")
928
+ if include_adam:
929
+ print(f"Adam LR: {adam_lr}")
930
+ print(f"D (n_var): {n_var}")
931
+ print(f"Bounds: [{xl}, {xu}] (CEC2017 uses [-100, 100])")
932
+ print(f"Max evals: {max_evals}")
933
+ print(f"Runs/config: {n_runs}")
934
+ print(f"Total jobs: {total}")
935
+ print(f"Workers: {n_workers}")
936
+ print(f"Device: {device}")
937
+ print(f"{'='*70}\n")
938
+
939
+ if not EVOGRAD_AVAILABLE and not is_adam:
940
+ print(f"WARNING: EvoGrad not available - {EVOGRAD_IMPORT_ERROR}\n")
941
+
942
+ if n_cec2017 > 0 and not CEC2017_AVAILABLE:
943
+ print(f"WARNING: CEC 2017 functions requested but not available\n")
944
+
945
+ completed = 0
946
+ start = time.time()
947
+
948
+ executor_cls = ProcessPoolExecutor if device == "cpu" else ThreadPoolExecutor
949
+
950
+ with executor_cls(max_workers=n_workers) as executor:
951
+ futures = {executor.submit(run_single_job, job): job for job in jobs}
952
+
953
+ for future in as_completed(futures):
954
+ try:
955
+ results.add_result(future.result())
956
+ except Exception as e:
957
+ job = futures[future]
958
+ results.add_result(RunResult(
959
+ algorithm=job["algorithm"], config=job["config"], function=job["function"],
960
+ n_var=job["n_var"], seed=job["seed"], best_fitness_history=[],
961
+ best_fitness=float('inf'), n_evals=0, n_gen=0,
962
+ wall_time=0.0, success=False, error_message=str(e),
963
+ ))
964
+
965
+ completed += 1
966
+ if completed % max(1, total // 20) == 0 or completed == total:
967
+ elapsed = time.time() - start
968
+ eta = (total - completed) / (completed / elapsed) if completed > 0 else 0
969
+ print(f"Progress: {completed:3d}/{total:3d} ({100*completed/total:3.0f}%) | "
970
+ f"Elapsed: {elapsed:5.1f}s | ETA: {eta:5.1f}s")
971
+
972
+ print(f"\nCompleted {len(results.results)}/{total} jobs in {time.time()-start:.1f}s")
973
+ return results
974
+
975
+
976
+ def print_summary(results: BenchmarkResults):
977
+ """Print formatted summary."""
978
+ summary = results.get_summary()
979
+
980
+ print(f"\n{'='*80}")
981
+ print(f"RESULTS SUMMARY (D={results.n_var}, bounds=[{results.xl}, {results.xu}])")
982
+ print(f"{'='*80}")
983
+
984
+ # Group functions by type
985
+ classical_funcs = sorted([f for f in summary.keys() if not f.startswith("cec2017_")])
986
+ cec2017_funcs = sorted([f for f in summary.keys() if f.startswith("cec2017_")],
987
+ key=lambda x: int(x.replace("cec2017_f", "")))
988
+
989
+ def print_func_results(func_list, title):
990
+ if not func_list:
991
+ return
992
+
993
+ print(f"\n{title}")
994
+ print("=" * 80)
995
+
996
+ for func in func_list:
997
+ print(f"\n{func.upper()}")
998
+ print("-" * 70)
999
+ print(f"{'Config':<15} {'Best':>12} {'Mean':>12} {'Std':>12} {'Time':>8}")
1000
+ print("-" * 70)
1001
+
1002
+ # Order: classical, differentiable, adaptive, full, Adam, pymoo
1003
+ config_order = ["classical", "differentiable", "adaptive", "full", "Adam", "pymoo"]
1004
+ for config in config_order:
1005
+ if config in summary[func]:
1006
+ s = summary[func][config]
1007
+ print(f"{config:<15} {s['best']:>12.4e} {s['mean']:>12.4e} "
1008
+ f"{s['std']:>12.4e} {s['mean_time']:>7.2f}s")
1009
+
1010
+ print_func_results(classical_funcs, "CLASSICAL FUNCTIONS")
1011
+ print_func_results(cec2017_funcs, "CEC 2017 FUNCTIONS")
1012
+
1013
+ print(f"\n{'='*80}")
1014
+
1015
+
1016
+ # =============================================================================
1017
+ # Main
1018
+ # =============================================================================
1019
+
1020
+ def main():
1021
+ parser = argparse.ArgumentParser(
1022
+ description="EvoGrad Parallel Benchmark Suite",
1023
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1024
+ epilog="""
1025
+ Examples:
1026
+ # Quick test with 3 classical functions (includes pymoo + Adam baselines)
1027
+ python run_benchmark.py -a DE -s quick -D 10 -r 5
1028
+
1029
+ # Run without Adam baseline
1030
+ python run_benchmark.py -a DE -s quick -D 10 -r 5 --no_adam
1031
+
1032
+ # Run without pymoo baseline
1033
+ python run_benchmark.py -a DE -s quick -D 10 -r 5 --no_pymoo
1034
+
1035
+ # Run only Adam optimizer
1036
+ python run_benchmark.py -a ADAM -s quick -D 10 -r 30
1037
+
1038
+ # Adam with custom learning rate
1039
+ python run_benchmark.py -a ADAM -s standard -D 10 --adam_lr 0.01
1040
+
1041
+ # CEC 2017 simple functions (F1-F10)
1042
+ python run_benchmark.py -a DE -s cec2017_simple -D 10 -r 30
1043
+
1044
+ # CEC 2017 all functions (F1-F30)
1045
+ python run_benchmark.py -a SHADE -s cec2017 -D 10 -r 30
1046
+
1047
+ # Smoothed funnel functions (designed for differentiable EAs)
1048
+ python run_benchmark.py -a DE -s funnel -D 10 -r 30
1049
+
1050
+ # Single funnel function (best for demonstrating EvoGrad advantages)
1051
+ python run_benchmark.py -a DE -f multibasinrastrigin -D 10 -r 30
1052
+
1053
+ # Specific functions
1054
+ python run_benchmark.py -a DE -f sphere rastrigin cec2017_f1 multibasinrastrigin -D 10 -r 10
1055
+ """,
1056
+ )
1057
+
1058
+ parser.add_argument("-a", "--algorithm", type=str, default="DE",
1059
+ choices=["DE", "SHADE", "PSO", "GA", "CMAES", "ADAM"],
1060
+ help="Algorithm to benchmark (ADAM for gradient-based only)")
1061
+ parser.add_argument("-s", "--suite", type=str, default="standard",
1062
+ choices=list(SUITES.keys()),
1063
+ help="Function suite to benchmark")
1064
+ parser.add_argument("-f", "--functions", type=str, nargs="+", default=None,
1065
+ help="Specific functions (overrides --suite)")
1066
+ parser.add_argument("-D", "--n_var", type=int, default=10,
1067
+ help="Number of variables (default: 10)")
1068
+ parser.add_argument("--xl", type=float, default=-100.0,
1069
+ help="Lower bound (default: -100)")
1070
+ parser.add_argument("--xu", type=float, default=100.0,
1071
+ help="Upper bound (default: 100)")
1072
+ parser.add_argument("-e", "--max_evals", type=int, default=None,
1073
+ help="Max evaluations (default: 10000*D for CEC2017, 5000*D otherwise)")
1074
+ parser.add_argument("-r", "--n_runs", type=int, default=30,
1075
+ help="Runs per configuration (default: 30)")
1076
+ parser.add_argument("-p", "--pop_size", type=int, default=100,
1077
+ help="Population size (default: 100)")
1078
+ parser.add_argument("--device", type=str, default="cpu",
1079
+ choices=["cpu", "cuda", "mps"])
1080
+ parser.add_argument("-w", "--workers", type=int, default=-1,
1081
+ help="Parallel workers (-1 for all CPUs)")
1082
+ parser.add_argument("--no_pymoo", action="store_true",
1083
+ help="Disable pymoo baseline comparison")
1084
+ parser.add_argument("--no_adam", action="store_true",
1085
+ help="Disable Adam baseline comparison")
1086
+ parser.add_argument("--adam_lr", type=float, default=0.05,
1087
+ help="Adam learning rate (default: 0.05)")
1088
+ parser.add_argument("-o", "--output_dir", type=str, default="results",
1089
+ help="Output directory for results")
1090
+ parser.add_argument("--list_functions", action="store_true",
1091
+ help="List all available functions and exit")
1092
+
1093
+ parser.add_argument("--lr_pop", type=float, default=-1)
1094
+ parser.add_argument("--lr_hyper", type=float, default=-1)
1095
+ parser.add_argument("--grad_clip_pop", type=float, default=-1)
1096
+ parser.add_argument("--grad_clip_hyper", type=float, default=-1)
1097
+ parser.add_argument("--deterministic", action="store_true",
1098
+ help="Enable deterministic mode for reproducibility (may be slower)")
1099
+ parser.add_argument("--base_seed", type=int, default=0,
1100
+ help="Base seed for random number generation (default: 0)")
1101
+
1102
+ args = parser.parse_args()
1103
+
1104
+ # Enable deterministic mode if requested
1105
+ if args.deterministic:
1106
+ import random
1107
+ import numpy as np
1108
+ torch.backends.cudnn.deterministic = True
1109
+ torch.backends.cudnn.benchmark = False
1110
+ if hasattr(torch, 'use_deterministic_algorithms'):
1111
+ try:
1112
+ torch.use_deterministic_algorithms(True)
1113
+ except Exception:
1114
+ pass # Some operations may not support deterministic mode
1115
+ print("Deterministic mode enabled")
1116
+
1117
+ # List functions mode
1118
+ if args.list_functions:
1119
+ print("\nAvailable functions:")
1120
+ print("\nClassical:")
1121
+ for name in sorted(CLASSICAL_FUNCTIONS.keys()):
1122
+ print(f" {name}")
1123
+
1124
+ if CEC2017_AVAILABLE:
1125
+ print("\nCEC 2017 (F1-F30):")
1126
+ for i in range(1, 31):
1127
+ category = "simple" if i <= 10 else ("hybrid" if i <= 20 else "composition")
1128
+ print(f" cec2017_f{i} ({category})")
1129
+
1130
+ if SMOOTHED_FUNNEL_AVAILABLE:
1131
+ print("\nSmoothed Funnel (designed for differentiable EAs):")
1132
+ print(" multibasinrastrigin - Multiple smoothed Rastrigin basins")
1133
+ print(" multibasinrosenbrock - Multiple smoothed Rosenbrock basins")
1134
+ print(" deceptivelandscape - Configurable deceptive multi-basin")
1135
+
1136
+ print("\nSuites:")
1137
+ for suite_name, funcs in SUITES.items():
1138
+ print(f" {suite_name}: {len(funcs)} functions")
1139
+
1140
+ print("\nAlgorithms:")
1141
+ print(" DE, SHADE, PSO, GA, CMAES - Evolutionary algorithms (4 configs + baselines)")
1142
+ print(" ADAM - Gradient-based optimizer (standalone)")
1143
+
1144
+ print("\nBaselines (for evolutionary algorithms):")
1145
+ print(" pymoo - Reference implementation (--no_pymoo to disable)")
1146
+ print(" Adam - Gradient-based baseline (--no_adam to disable)")
1147
+
1148
+ sys.exit(0)
1149
+
1150
+ # Get functions
1151
+ functions = args.functions if args.functions else SUITES.get(args.suite, [])
1152
+
1153
+ # Filter to available functions
1154
+ available = set(CLASSICAL_FUNCTIONS.keys())
1155
+ if CEC2017_AVAILABLE:
1156
+ available.update(f"cec2017_f{i}" for i in range(1, 31))
1157
+ if SMOOTHED_FUNNEL_AVAILABLE:
1158
+ available.update(SMOOTHED_FUNNEL_FUNCTIONS.keys())
1159
+
1160
+ functions = [f for f in functions if f in available]
1161
+
1162
+ if not functions:
1163
+ print(f"No valid functions. Use --list_functions to see available options.")
1164
+ sys.exit(1)
1165
+
1166
+ # Check if any CEC 2017 functions
1167
+ has_cec2017 = any(f.startswith("cec2017_") for f in functions)
1168
+
1169
+ # Default max_evals: 10000*D for CEC2017, 5000*D otherwise
1170
+ if args.max_evals is None:
1171
+ args.max_evals = 10000 * args.n_var if has_cec2017 else 5000 * args.n_var
1172
+
1173
+ # Check device
1174
+ if args.device == "cuda" and not torch.cuda.is_available():
1175
+ print("CUDA not available, using CPU")
1176
+ args.device = "cpu"
1177
+ elif args.device == "mps" and not torch.backends.mps.is_available():
1178
+ print("MPS not available, using CPU")
1179
+ args.device = "cpu"
1180
+
1181
+ # Run
1182
+ results = run_benchmark_parallel(
1183
+ algorithm_name=args.algorithm,
1184
+ functions=functions,
1185
+ n_var=args.n_var,
1186
+ xl=args.xl,
1187
+ xu=args.xu,
1188
+ max_evals=args.max_evals,
1189
+ n_runs=args.n_runs,
1190
+ lr_pop=args.lr_pop,
1191
+ lr_hyper=args.lr_hyper,
1192
+ grad_clip_pop=args.grad_clip_pop,
1193
+ grad_clip_hyper=args.grad_clip_hyper,
1194
+ pop_size=args.pop_size,
1195
+ device=args.device,
1196
+ include_pymoo=not args.no_pymoo,
1197
+ include_adam=not args.no_adam,
1198
+ adam_lr=args.adam_lr,
1199
+ n_workers=args.workers,
1200
+ base_seed=args.base_seed,
1201
+ )
1202
+
1203
+ print_summary(results)
1204
+ results.save(Path(args.output_dir))
1205
+
1206
+
1207
+ if __name__ == "__main__":
1208
+ main()