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,649 @@
1
+ """
2
+ Classical benchmark functions for optimization.
3
+
4
+ This module provides standard unimodal and multimodal test functions
5
+ commonly used in evolutionary computation literature.
6
+
7
+ Categories:
8
+ - Unimodal: Sphere, Ellipsoid, Schwefel222, Cigar, Discus, Rosenbrock
9
+ - Multimodal: Rastrigin, Ackley, Griewank, Schwefel, Levy, Michalewicz
10
+ - Other: Zakharov, Dixon-Price, Powell, Trid
11
+ """
12
+
13
+ from typing import Tuple
14
+
15
+ import torch
16
+ from torch import Tensor
17
+
18
+ from .base import BenchmarkFunction
19
+
20
+
21
+ # =============================================================================
22
+ # UNIMODAL FUNCTIONS
23
+ # =============================================================================
24
+
25
+ class Sphere(BenchmarkFunction):
26
+ """
27
+ Sphere function (De Jong's function 1).
28
+
29
+ f(x) = sum(x_i^2)
30
+
31
+ Properties:
32
+ - Unimodal, separable, convex
33
+ - Continuous, differentiable
34
+ - Global minimum: f(0,...,0) = 0
35
+ """
36
+ name = "sphere"
37
+ optimal_value = 0.0
38
+
39
+ def default_bounds(self) -> Tuple[float, float]:
40
+ return (-100.0, 100.0)
41
+
42
+ def __call__(self, x: Tensor) -> Tensor:
43
+ return (x ** 2).sum(dim=-1)
44
+
45
+
46
+ class Ellipsoid(BenchmarkFunction):
47
+ """
48
+ Ellipsoid function (Schwefel's problem 1.2 variant).
49
+
50
+ f(x) = sum(10^(6*(i-1)/(n-1)) * x_i^2)
51
+
52
+ Properties:
53
+ - Unimodal, separable
54
+ - Ill-conditioned (condition number ~10^6)
55
+ - Global minimum: f(0,...,0) = 0
56
+ """
57
+ name = "ellipsoid"
58
+ optimal_value = 0.0
59
+
60
+ def default_bounds(self) -> Tuple[float, float]:
61
+ return (-100.0, 100.0)
62
+
63
+ def __call__(self, x: Tensor) -> Tensor:
64
+ n = x.shape[-1]
65
+ i = torch.arange(n, device=x.device, dtype=x.dtype)
66
+ weights = 10 ** (6 * i / max(n - 1, 1))
67
+ return (weights * x ** 2).sum(dim=-1)
68
+
69
+
70
+ class SumOfDifferentPowers(BenchmarkFunction):
71
+ """
72
+ Sum of Different Powers function.
73
+
74
+ f(x) = sum(|x_i|^(i+1))
75
+
76
+ Properties:
77
+ - Unimodal, separable
78
+ - Different sensitivity per dimension
79
+ - Global minimum: f(0,...,0) = 0
80
+ """
81
+ name = "sum_of_different_powers"
82
+ optimal_value = 0.0
83
+
84
+ def default_bounds(self) -> Tuple[float, float]:
85
+ return (-1.0, 1.0)
86
+
87
+ def __call__(self, x: Tensor) -> Tensor:
88
+ n = x.shape[-1]
89
+ i = torch.arange(1, n + 1, device=x.device, dtype=x.dtype)
90
+ return (torch.abs(x) ** i).sum(dim=-1)
91
+
92
+
93
+ class Schwefel222(BenchmarkFunction):
94
+ """
95
+ Schwefel's Problem 2.22.
96
+
97
+ f(x) = sum(|x_i|) + prod(|x_i|)
98
+
99
+ Properties:
100
+ - Unimodal, non-separable
101
+ - Global minimum: f(0,...,0) = 0
102
+ """
103
+ name = "schwefel222"
104
+ optimal_value = 0.0
105
+
106
+ def default_bounds(self) -> Tuple[float, float]:
107
+ return (-10.0, 10.0)
108
+
109
+ def __call__(self, x: Tensor) -> Tensor:
110
+ abs_x = torch.abs(x)
111
+ return abs_x.sum(dim=-1) + abs_x.prod(dim=-1)
112
+
113
+
114
+ class Cigar(BenchmarkFunction):
115
+ """
116
+ Cigar function.
117
+
118
+ f(x) = x_1^2 + 10^6 * sum(x_i^2) for i>1
119
+
120
+ Properties:
121
+ - Unimodal, separable
122
+ - Ill-conditioned (one narrow direction)
123
+ - Global minimum: f(0,...,0) = 0
124
+ """
125
+ name = "cigar"
126
+ optimal_value = 0.0
127
+
128
+ def default_bounds(self) -> Tuple[float, float]:
129
+ return (-100.0, 100.0)
130
+
131
+ def __call__(self, x: Tensor) -> Tensor:
132
+ return x[..., 0] ** 2 + 1e6 * (x[..., 1:] ** 2).sum(dim=-1)
133
+
134
+
135
+ class Discus(BenchmarkFunction):
136
+ """
137
+ Discus (Tablet) function.
138
+
139
+ f(x) = 10^6 * x_1^2 + sum(x_i^2) for i>1
140
+
141
+ Properties:
142
+ - Unimodal, separable
143
+ - Ill-conditioned (one dominant direction)
144
+ - Global minimum: f(0,...,0) = 0
145
+ """
146
+ name = "discus"
147
+ optimal_value = 0.0
148
+
149
+ def default_bounds(self) -> Tuple[float, float]:
150
+ return (-100.0, 100.0)
151
+
152
+ def __call__(self, x: Tensor) -> Tensor:
153
+ return 1e6 * x[..., 0] ** 2 + (x[..., 1:] ** 2).sum(dim=-1)
154
+
155
+
156
+ class BentCigar(BenchmarkFunction):
157
+ """
158
+ Bent Cigar function.
159
+
160
+ f(x) = x_1^2 + 10^6 * sum(x_i^2) for i>1
161
+
162
+ Properties:
163
+ - Unimodal
164
+ - Non-separable when rotated
165
+ - Global minimum: f(0,...,0) = 0
166
+ """
167
+ name = "bent_cigar"
168
+ optimal_value = 0.0
169
+
170
+ def default_bounds(self) -> Tuple[float, float]:
171
+ return (-100.0, 100.0)
172
+
173
+ def __call__(self, x: Tensor) -> Tensor:
174
+ return x[..., 0] ** 2 + 1e6 * (x[..., 1:] ** 2).sum(dim=-1)
175
+
176
+
177
+ class Rosenbrock(BenchmarkFunction):
178
+ """
179
+ Rosenbrock function (Banana function).
180
+
181
+ f(x) = sum(100*(x_{i+1} - x_i^2)^2 + (1 - x_i)^2)
182
+
183
+ Properties:
184
+ - Unimodal (for n<=3), multimodal (for n>3)
185
+ - Non-separable
186
+ - Global minimum: f(1,...,1) = 0
187
+ """
188
+ name = "rosenbrock"
189
+ optimal_value = 0.0
190
+
191
+ def default_bounds(self) -> Tuple[float, float]:
192
+ return (-5.0, 10.0)
193
+
194
+ def _compute_optimal_x(self) -> Tensor:
195
+ return torch.ones(self.n_var)
196
+
197
+ def __call__(self, x: Tensor) -> Tensor:
198
+ x_i = x[..., :-1]
199
+ x_ip1 = x[..., 1:]
200
+ return (100 * (x_ip1 - x_i ** 2) ** 2 + (1 - x_i) ** 2).sum(dim=-1)
201
+
202
+
203
+ class DixonPrice(BenchmarkFunction):
204
+ """
205
+ Dixon-Price function.
206
+
207
+ f(x) = (x_1 - 1)^2 + sum(i * (2*x_i^2 - x_{i-1})^2)
208
+
209
+ Properties:
210
+ - Unimodal
211
+ - Non-separable
212
+ """
213
+ name = "dixon_price"
214
+ optimal_value = 0.0
215
+
216
+ def default_bounds(self) -> Tuple[float, float]:
217
+ return (-10.0, 10.0)
218
+
219
+ def _compute_optimal_x(self) -> Tensor:
220
+ # x_i = 2^(-(2^i - 2)/2^i) for i = 1, ..., n
221
+ i = torch.arange(1, self.n_var + 1, dtype=torch.float32)
222
+ return 2.0 ** (-(2.0 ** i - 2.0) / (2.0 ** i))
223
+
224
+ def __call__(self, x: Tensor) -> Tensor:
225
+ n = x.shape[-1]
226
+ term1 = (x[..., 0] - 1) ** 2
227
+ i = torch.arange(2, n + 1, device=x.device, dtype=x.dtype)
228
+ term2 = (i * (2 * x[..., 1:] ** 2 - x[..., :-1]) ** 2).sum(dim=-1)
229
+ return term1 + term2
230
+
231
+
232
+ class Powell(BenchmarkFunction):
233
+ """
234
+ Powell function.
235
+
236
+ f(x) = sum of grouped terms (requires n divisible by 4)
237
+
238
+ Properties:
239
+ - Unimodal
240
+ - Non-separable
241
+ - Global minimum: f(0,...,0) = 0
242
+ """
243
+ name = "powell"
244
+ optimal_value = 0.0
245
+
246
+ def __init__(self, n_var: int = 24, **kwargs):
247
+ # Ensure n_var is divisible by 4
248
+ n_var = max(4, (n_var // 4) * 4)
249
+ super().__init__(n_var=n_var, **kwargs)
250
+
251
+ def default_bounds(self) -> Tuple[float, float]:
252
+ return (-4.0, 5.0)
253
+
254
+ def __call__(self, x: Tensor) -> Tensor:
255
+ n = x.shape[-1]
256
+ n_groups = n // 4
257
+
258
+ result = torch.zeros(x.shape[:-1], device=x.device, dtype=x.dtype)
259
+
260
+ for j in range(n_groups):
261
+ i = 4 * j
262
+ term1 = (x[..., i] + 10 * x[..., i + 1]) ** 2
263
+ term2 = 5 * (x[..., i + 2] - x[..., i + 3]) ** 2
264
+ term3 = (x[..., i + 1] - 2 * x[..., i + 2]) ** 4
265
+ term4 = 10 * (x[..., i] - x[..., i + 3]) ** 4
266
+ result = result + term1 + term2 + term3 + term4
267
+
268
+ return result
269
+
270
+
271
+ class Trid(BenchmarkFunction):
272
+ """
273
+ Trid function.
274
+
275
+ f(x) = sum((x_i - 1)^2) - sum(x_i * x_{i-1})
276
+
277
+ Properties:
278
+ - Unimodal
279
+ - Non-separable
280
+ """
281
+ name = "trid"
282
+
283
+ def __init__(self, n_var: int = 30, **kwargs):
284
+ super().__init__(n_var=n_var, **kwargs)
285
+ # Optimal value is -n(n+4)(n-1)/6
286
+ self.optimal_value = -n_var * (n_var + 4) * (n_var - 1) / 6
287
+
288
+ def default_bounds(self) -> Tuple[float, float]:
289
+ return (-self.n_var ** 2, self.n_var ** 2)
290
+
291
+ def _compute_optimal_x(self) -> Tensor:
292
+ # x_i = i(n + 1 - i)
293
+ i = torch.arange(1, self.n_var + 1, dtype=torch.float32)
294
+ return i * (self.n_var + 1 - i)
295
+
296
+ def __call__(self, x: Tensor) -> Tensor:
297
+ term1 = ((x - 1) ** 2).sum(dim=-1)
298
+ term2 = (x[..., 1:] * x[..., :-1]).sum(dim=-1)
299
+ return term1 - term2
300
+
301
+
302
+ # =============================================================================
303
+ # MULTIMODAL FUNCTIONS
304
+ # =============================================================================
305
+
306
+ class Rastrigin(BenchmarkFunction):
307
+ """
308
+ Rastrigin function.
309
+
310
+ f(x) = 10*n + sum(x_i^2 - 10*cos(2*pi*x_i))
311
+
312
+ Properties:
313
+ - Highly multimodal (10^n local minima)
314
+ - Separable
315
+ - Regular spacing of local minima
316
+ - Global minimum: f(0,...,0) = 0
317
+ """
318
+ name = "rastrigin"
319
+ optimal_value = 0.0
320
+
321
+ def default_bounds(self) -> Tuple[float, float]:
322
+ return (-5.12, 5.12)
323
+
324
+ def __call__(self, x: Tensor) -> Tensor:
325
+ n = x.shape[-1]
326
+ return 10 * n + (x ** 2 - 10 * torch.cos(2 * torch.pi * x)).sum(dim=-1)
327
+
328
+
329
+ class Ackley(BenchmarkFunction):
330
+ """
331
+ Ackley function.
332
+
333
+ f(x) = -20*exp(-0.2*sqrt(mean(x^2))) - exp(mean(cos(2*pi*x))) + 20 + e
334
+
335
+ Properties:
336
+ - Multimodal with large nearly flat outer region
337
+ - Non-separable
338
+ - Global minimum: f(0,...,0) = 0
339
+ """
340
+ name = "ackley"
341
+ optimal_value = 0.0
342
+
343
+ def default_bounds(self) -> Tuple[float, float]:
344
+ return (-32.768, 32.768)
345
+
346
+ def __call__(self, x: Tensor) -> Tensor:
347
+ n = x.shape[-1]
348
+ sum1 = (x ** 2).sum(dim=-1)
349
+ sum2 = torch.cos(2 * torch.pi * x).sum(dim=-1)
350
+ return (
351
+ -20 * torch.exp(-0.2 * torch.sqrt(sum1 / n))
352
+ - torch.exp(sum2 / n)
353
+ + 20
354
+ + torch.e
355
+ )
356
+
357
+
358
+ class Griewank(BenchmarkFunction):
359
+ """
360
+ Griewank function.
361
+
362
+ f(x) = sum(x_i^2)/4000 - prod(cos(x_i/sqrt(i))) + 1
363
+
364
+ Properties:
365
+ - Multimodal with regular local minima
366
+ - Non-separable
367
+ - Global minimum: f(0,...,0) = 0
368
+ """
369
+ name = "griewank"
370
+ optimal_value = 0.0
371
+
372
+ def default_bounds(self) -> Tuple[float, float]:
373
+ return (-600.0, 600.0)
374
+
375
+ def __call__(self, x: Tensor) -> Tensor:
376
+ n = x.shape[-1]
377
+ i = torch.arange(1, n + 1, device=x.device, dtype=x.dtype)
378
+ sum_term = (x ** 2).sum(dim=-1) / 4000
379
+ prod_term = torch.cos(x / torch.sqrt(i)).prod(dim=-1)
380
+ return sum_term - prod_term + 1
381
+
382
+
383
+ class Schwefel(BenchmarkFunction):
384
+ """
385
+ Schwefel function.
386
+
387
+ f(x) = 418.9829*n - sum(x_i * sin(sqrt(|x_i|)))
388
+
389
+ Properties:
390
+ - Multimodal
391
+ - Separable
392
+ - Global optimum far from local optima
393
+ - Global minimum: f(420.9687,...,420.9687) ≈ 0
394
+ """
395
+ name = "schwefel"
396
+
397
+ def __init__(self, n_var: int = 30, **kwargs):
398
+ super().__init__(n_var=n_var, **kwargs)
399
+ self.optimal_value = 0.0 # After normalization
400
+
401
+ def default_bounds(self) -> Tuple[float, float]:
402
+ return (-500.0, 500.0)
403
+
404
+ def _compute_optimal_x(self) -> Tensor:
405
+ return torch.full((self.n_var,), 420.9687)
406
+
407
+ def __call__(self, x: Tensor) -> Tensor:
408
+ n = x.shape[-1]
409
+ return 418.9829 * n - (x * torch.sin(torch.sqrt(torch.abs(x)))).sum(dim=-1)
410
+
411
+
412
+ class Levy(BenchmarkFunction):
413
+ """
414
+ Levy function.
415
+
416
+ f(x) = sin^2(pi*w_1) + sum((w_i-1)^2 * (1 + 10*sin^2(pi*w_i+1)))
417
+ + (w_n-1)^2 * (1 + sin^2(2*pi*w_n))
418
+ where w_i = 1 + (x_i - 1)/4
419
+
420
+ Properties:
421
+ - Multimodal
422
+ - Non-separable
423
+ - Global minimum: f(1,...,1) = 0
424
+ """
425
+ name = "levy"
426
+ optimal_value = 0.0
427
+
428
+ def default_bounds(self) -> Tuple[float, float]:
429
+ return (-10.0, 10.0)
430
+
431
+ def _compute_optimal_x(self) -> Tensor:
432
+ return torch.ones(self.n_var)
433
+
434
+ def __call__(self, x: Tensor) -> Tensor:
435
+ w = 1 + (x - 1) / 4
436
+ term1 = torch.sin(torch.pi * w[..., 0]) ** 2
437
+ term2 = (
438
+ (w[..., :-1] - 1) ** 2
439
+ * (1 + 10 * torch.sin(torch.pi * w[..., :-1] + 1) ** 2)
440
+ ).sum(dim=-1)
441
+ term3 = (w[..., -1] - 1) ** 2 * (
442
+ 1 + torch.sin(2 * torch.pi * w[..., -1]) ** 2
443
+ )
444
+ return term1 + term2 + term3
445
+
446
+
447
+ class Michalewicz(BenchmarkFunction):
448
+ """
449
+ Michalewicz function.
450
+
451
+ f(x) = -sum(sin(x_i) * sin(i*x_i^2 / pi)^(2*m))
452
+
453
+ Properties:
454
+ - Multimodal with n! local minima
455
+ - Separable
456
+ - Steepness controlled by parameter m
457
+ """
458
+ name = "michalewicz"
459
+
460
+ def __init__(self, n_var: int = 30, m: float = 10.0, **kwargs):
461
+ super().__init__(n_var=n_var, **kwargs)
462
+ self.m = m
463
+ # Optimal value depends on n and m
464
+ self.optimal_value = -4.687 if n_var >= 5 else -1.8013 # Approximate
465
+
466
+ def default_bounds(self) -> Tuple[float, float]:
467
+ return (0.0, torch.pi)
468
+
469
+ def __call__(self, x: Tensor) -> Tensor:
470
+ n = x.shape[-1]
471
+ i = torch.arange(1, n + 1, device=x.device, dtype=x.dtype)
472
+ return -(
473
+ torch.sin(x) * torch.sin(i * x ** 2 / torch.pi) ** (2 * self.m)
474
+ ).sum(dim=-1)
475
+
476
+
477
+ class Zakharov(BenchmarkFunction):
478
+ """
479
+ Zakharov function.
480
+
481
+ f(x) = sum(x_i^2) + (sum(0.5*i*x_i))^2 + (sum(0.5*i*x_i))^4
482
+
483
+ Properties:
484
+ - Multimodal
485
+ - Non-separable
486
+ - Global minimum: f(0,...,0) = 0
487
+ """
488
+ name = "zakharov"
489
+ optimal_value = 0.0
490
+
491
+ def default_bounds(self) -> Tuple[float, float]:
492
+ return (-5.0, 10.0)
493
+
494
+ def __call__(self, x: Tensor) -> Tensor:
495
+ n = x.shape[-1]
496
+ i = torch.arange(1, n + 1, device=x.device, dtype=x.dtype)
497
+ sum1 = (x ** 2).sum(dim=-1)
498
+ sum2 = (0.5 * i * x).sum(dim=-1)
499
+ return sum1 + sum2 ** 2 + sum2 ** 4
500
+
501
+
502
+ class Weierstrass(BenchmarkFunction):
503
+ """
504
+ Weierstrass function.
505
+
506
+ f(x) = sum_i(sum_k(a^k * cos(2*pi*b^k*(x_i + 0.5))))
507
+ - n * sum_k(a^k * cos(pi*b^k))
508
+
509
+ Properties:
510
+ - Multimodal
511
+ - Continuous but not differentiable
512
+ - Non-separable
513
+ - Global minimum: f(0,...,0) = 0
514
+ """
515
+ name = "weierstrass"
516
+ optimal_value = 0.0
517
+
518
+ def __init__(self, n_var: int = 30, a: float = 0.5, b: float = 3.0, k_max: int = 20, **kwargs):
519
+ super().__init__(n_var=n_var, **kwargs)
520
+ self.a = a
521
+ self.b = b
522
+ self.k_max = k_max
523
+
524
+ def default_bounds(self) -> Tuple[float, float]:
525
+ return (-0.5, 0.5)
526
+
527
+ def __call__(self, x: Tensor) -> Tensor:
528
+ n = x.shape[-1]
529
+ k = torch.arange(self.k_max + 1, device=x.device, dtype=x.dtype)
530
+
531
+ a_k = self.a ** k # Shape: [k_max+1]
532
+ b_k = self.b ** k # Shape: [k_max+1]
533
+
534
+ # Constant term: sum_k(a^k * cos(pi * b^k))
535
+ const = (a_k * torch.cos(torch.pi * b_k)).sum()
536
+
537
+ # Sum over dimensions
538
+ # x shape: [..., n], need to compute for each dimension
539
+ result = torch.zeros(x.shape[:-1], device=x.device, dtype=x.dtype)
540
+ for d in range(n):
541
+ x_d = x[..., d] # Shape: [...]
542
+ # sum_k(a^k * cos(2*pi*b^k*(x_d + 0.5)))
543
+ # Broadcast: [..., 1] * [k_max+1] -> [..., k_max+1]
544
+ inner = 2 * torch.pi * b_k * (x_d.unsqueeze(-1) + 0.5)
545
+ result = result + (a_k * torch.cos(inner)).sum(dim=-1)
546
+
547
+ return result - n * const
548
+
549
+
550
+ class Alpine(BenchmarkFunction):
551
+ """
552
+ Alpine function (No. 1).
553
+
554
+ f(x) = sum(|x_i * sin(x_i) + 0.1 * x_i|)
555
+
556
+ Properties:
557
+ - Multimodal
558
+ - Separable
559
+ - Global minimum: f(0,...,0) = 0
560
+ """
561
+ name = "alpine"
562
+ optimal_value = 0.0
563
+
564
+ def default_bounds(self) -> Tuple[float, float]:
565
+ return (-10.0, 10.0)
566
+
567
+ def __call__(self, x: Tensor) -> Tensor:
568
+ return torch.abs(x * torch.sin(x) + 0.1 * x).sum(dim=-1)
569
+
570
+
571
+ class Salomon(BenchmarkFunction):
572
+ """
573
+ Salomon function.
574
+
575
+ f(x) = 1 - cos(2*pi*||x||) + 0.1*||x||
576
+
577
+ Properties:
578
+ - Multimodal
579
+ - Non-separable (radially symmetric)
580
+ - Global minimum: f(0,...,0) = 0
581
+ """
582
+ name = "salomon"
583
+ optimal_value = 0.0
584
+
585
+ def default_bounds(self) -> Tuple[float, float]:
586
+ return (-100.0, 100.0)
587
+
588
+ def __call__(self, x: Tensor) -> Tensor:
589
+ norm = torch.sqrt((x ** 2).sum(dim=-1))
590
+ return 1 - torch.cos(2 * torch.pi * norm) + 0.1 * norm
591
+
592
+
593
+ class StyblinskiTang(BenchmarkFunction):
594
+ """
595
+ Styblinski-Tang function.
596
+
597
+ f(x) = 0.5 * sum(x_i^4 - 16*x_i^2 + 5*x_i)
598
+
599
+ Properties:
600
+ - Multimodal
601
+ - Separable
602
+ - Global minimum: f(-2.9035,...,-2.9035) ≈ -39.16599*n
603
+ """
604
+ name = "styblinski_tang"
605
+
606
+ def __init__(self, n_var: int = 30, **kwargs):
607
+ super().__init__(n_var=n_var, **kwargs)
608
+ self.optimal_value = -39.16599 * n_var
609
+
610
+ def default_bounds(self) -> Tuple[float, float]:
611
+ return (-5.0, 5.0)
612
+
613
+ def _compute_optimal_x(self) -> Tensor:
614
+ return torch.full((self.n_var,), -2.903534)
615
+
616
+ def __call__(self, x: Tensor) -> Tensor:
617
+ return 0.5 * (x ** 4 - 16 * x ** 2 + 5 * x).sum(dim=-1)
618
+
619
+
620
+ # =============================================================================
621
+ # FUNCTION REGISTRY
622
+ # =============================================================================
623
+
624
+ CLASSICAL_FUNCTIONS = {
625
+ # Unimodal
626
+ "sphere": Sphere,
627
+ "ellipsoid": Ellipsoid,
628
+ "sum_of_different_powers": SumOfDifferentPowers,
629
+ "schwefel222": Schwefel222,
630
+ "cigar": Cigar,
631
+ "discus": Discus,
632
+ "bent_cigar": BentCigar,
633
+ "rosenbrock": Rosenbrock,
634
+ "dixon_price": DixonPrice,
635
+ "powell": Powell,
636
+ "trid": Trid,
637
+ # Multimodal
638
+ "rastrigin": Rastrigin,
639
+ "ackley": Ackley,
640
+ "griewank": Griewank,
641
+ "schwefel": Schwefel,
642
+ "levy": Levy,
643
+ "michalewicz": Michalewicz,
644
+ "zakharov": Zakharov,
645
+ "weierstrass": Weierstrass,
646
+ "alpine": Alpine,
647
+ "salomon": Salomon,
648
+ "styblinski_tang": StyblinskiTang,
649
+ }