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.
- evograd/__init__.py +67 -0
- evograd/algorithms/__init__.py +138 -0
- evograd/algorithms/cmaes.py +1365 -0
- evograd/algorithms/de.py +895 -0
- evograd/algorithms/ga.py +532 -0
- evograd/algorithms/pso.py +648 -0
- evograd/algorithms/shade.py +1165 -0
- evograd/benchmarks/functions/__init__.py +229 -0
- evograd/benchmarks/functions/base.py +217 -0
- evograd/benchmarks/functions/cec2017/__init__.py +250 -0
- evograd/benchmarks/functions/cec2017/basic.py +413 -0
- evograd/benchmarks/functions/cec2017/composition.py +580 -0
- evograd/benchmarks/functions/cec2017/data.pkl +0 -0
- evograd/benchmarks/functions/cec2017/data.py +350 -0
- evograd/benchmarks/functions/cec2017/hybrid.py +406 -0
- evograd/benchmarks/functions/cec2017/simple.py +326 -0
- evograd/benchmarks/functions/classical.py +649 -0
- evograd/benchmarks/functions/smoothed_funnel.py +476 -0
- evograd/benchmarks/functions/transforms.py +463 -0
- evograd/benchmarks/run_benchmark_functions.py +1208 -0
- evograd/core/__init__.py +73 -0
- evograd/core/algorithm.py +778 -0
- evograd/core/maximize.py +269 -0
- evograd/core/minimize.py +740 -0
- evograd/core/problem.py +444 -0
- evograd/core/result.py +571 -0
- evograd/core/termination.py +602 -0
- evograd/operators/__init__.py +178 -0
- evograd/operators/crossover.py +1117 -0
- evograd/operators/mutation.py +1098 -0
- evograd/operators/relaxations.py +175 -0
- evograd/operators/repair.py +601 -0
- evograd/operators/sampling.py +577 -0
- evograd/operators/selection.py +981 -0
- evograd/operators/survival.py +1000 -0
- evograd/tests/__init__.py +11 -0
- evograd/tests/run_all.py +78 -0
- evograd/tests/test_core.py +528 -0
- evograd/tests/test_ga.py +572 -0
- evograd/tests/test_operators.py +662 -0
- evograd/tests/test_per_individual.py +326 -0
- evograd/tests/test_utils.py +328 -0
- evograd/utils/__init__.py +97 -0
- evograd/utils/callbacks.py +926 -0
- evograd/utils/device.py +502 -0
- evograd/utils/duplicates.py +421 -0
- evograd_diff-0.1.0.dist-info/METADATA +439 -0
- evograd_diff-0.1.0.dist-info/RECORD +50 -0
- evograd_diff-0.1.0.dist-info/WHEEL +4 -0
- 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()
|