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,648 @@
1
+ """
2
+ Particle Swarm Optimisation (PSO) implementation for EvoGrad.
3
+
4
+ This module provides a fully differentiable Particle Swarm Optimisation
5
+ algorithm that supports both classical and gradient-enabled optimisation modes.
6
+
7
+ PSO evolves a swarm of particles through:
8
+ 1. Velocity update: Combine inertia, cognitive, and social components
9
+ 2. Position update: Move particles according to velocity
10
+ 3. Personal best update: Track each particle's best position
11
+ 4. Global best update: Track the swarm's best position
12
+
13
+ Modes:
14
+ - adaptive=False, differentiable=False: Classical PSO
15
+ - adaptive=True, differentiable=False: Hyperparameters (inertia, c1, c2)
16
+ are learnable via backpropagation
17
+ - adaptive=False, differentiable=True: Particle positions are learnable
18
+ via backpropagation
19
+ - adaptive=True, differentiable=True: Both hyperparameters and positions
20
+ are learnable
21
+
22
+ Example:
23
+ >>> from evograd.algorithms import PSO
24
+ >>> from evograd.core import Problem, minimize
25
+ >>>
26
+ >>> problem = Problem(
27
+ ... objective=lambda x: (x**2).sum(dim=-1),
28
+ ... n_var=30,
29
+ ... xl=-100.0,
30
+ ... xu=100.0,
31
+ ... )
32
+ >>>
33
+ >>> # Classical PSO
34
+ >>> pso = PSO(pop_size=100, inertia=0.7, c1=1.5, c2=1.5)
35
+ >>> result = minimize(problem, pso, max_evals=10000)
36
+ >>>
37
+ >>> # Adaptive PSO with learnable hyperparameters
38
+ >>> pso = PSO(pop_size=100, adaptive=True)
39
+ >>> result = minimize(problem, pso, max_evals=10000)
40
+
41
+ Reference:
42
+ Kennedy, J. & Eberhart, R. (1995). Particle Swarm Optimization.
43
+ Proceedings of ICNN'95.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
49
+
50
+ import torch
51
+ import torch.nn as nn
52
+ from torch import Tensor
53
+
54
+ from evograd.core.algorithm import Algorithm
55
+
56
+ if TYPE_CHECKING:
57
+ from evograd.core.problem import Problem
58
+
59
+ __all__ = ["PSO", "pso_default", "pso_constriction"]
60
+
61
+
62
+ class PSO(Algorithm):
63
+ """
64
+ Particle Swarm Optimisation (PSO) for continuous optimisation.
65
+
66
+ PSO simulates a swarm of particles moving through the search space,
67
+ influenced by their own best known position and the swarm's best
68
+ known position.
69
+
70
+ The velocity update equation is:
71
+ v = w*v + c1*r1*(p_best - x) + c2*r2*(g_best - x)
72
+
73
+ where:
74
+ - w: inertia weight (controls momentum)
75
+ - c1: cognitive coefficient (attraction to personal best)
76
+ - c2: social coefficient (attraction to global best)
77
+ - r1, r2: random vectors in [0, 1]
78
+
79
+ Args:
80
+ pop_size: Swarm size (number of particles).
81
+ inertia: Inertia weight w. Default: 0.7.
82
+ c1: Cognitive coefficient. Default: 1.5.
83
+ c2: Social coefficient. Default: 1.5.
84
+ v_max_ratio: Maximum velocity as ratio of search space range.
85
+ Default: 0.2 (20% of range).
86
+ sampling: Operator for initial population generation.
87
+ repair: Repair operator for constraint handling.
88
+ adaptive: If True, hyperparameters (inertia, c1, c2) are
89
+ learnable via backpropagation.
90
+ differentiable: If True, particle positions are learnable
91
+ via backpropagation.
92
+ per_particle_coeffs: If True and adaptive=True, each particle
93
+ has its own inertia, c1, c2 values. Default: False.
94
+ dtype: Tensor dtype.
95
+
96
+ Attributes:
97
+ inertia: Current inertia weight.
98
+ c1: Current cognitive coefficient.
99
+ c2: Current social coefficient.
100
+ velocity: Current velocity vectors [pop_size, n_var].
101
+ p_best: Personal best positions [pop_size, n_var].
102
+ p_best_fitness: Personal best fitness values [pop_size].
103
+
104
+ Example:
105
+ >>> # Classical PSO
106
+ >>> pso = PSO(pop_size=50, inertia=0.7, c1=1.5, c2=1.5)
107
+ >>>
108
+ >>> # Adaptive PSO with learnable coefficients
109
+ >>> pso = PSO(pop_size=50, adaptive=True)
110
+ >>>
111
+ >>> # Differentiable particle positions
112
+ >>> pso = PSO(pop_size=50, differentiable=True)
113
+ >>>
114
+ >>> # Fully differentiable
115
+ >>> pso = PSO(pop_size=50, adaptive=True, differentiable=True)
116
+ >>>
117
+ >>> # Per-particle adaptive coefficients
118
+ >>> pso = PSO(pop_size=50, adaptive=True, per_particle_coeffs=True)
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ pop_size: int = 100,
124
+ w: float = 0.7,
125
+ c1: float = 1.5,
126
+ c2: float = 1.5,
127
+ v_max_ratio: float = 0.2,
128
+ sampling: Optional[nn.Module] = None,
129
+ repair: Optional[nn.Module] = None,
130
+ adaptive: bool = False,
131
+ differentiable: bool = False,
132
+ per_particle_coeffs: bool = False,
133
+ dtype: torch.dtype = torch.float32,
134
+ ) -> None:
135
+ self.adaptive = adaptive
136
+ self.per_particle_coeffs = per_particle_coeffs
137
+ self._init_inertia = w
138
+ self._init_c1 = c1
139
+ self._init_c2 = c2
140
+ self._v_max_ratio = v_max_ratio
141
+
142
+ # PSO doesn't use standard EA operators (selection, crossover, mutation)
143
+ super().__init__(
144
+ pop_size=pop_size,
145
+ sampling=sampling,
146
+ selection=None,
147
+ crossover=None,
148
+ mutation=None,
149
+ survival=None,
150
+ repair=repair,
151
+ eliminate_duplicates=False,
152
+ n_offsprings=pop_size,
153
+ differentiable=differentiable,
154
+ adaptive=adaptive,
155
+ dtype=dtype,
156
+ )
157
+
158
+ # =========================================================================
159
+ # Setup
160
+ # =========================================================================
161
+
162
+ def _setup(self) -> None:
163
+ """PSO-specific setup after initialization."""
164
+ n_var = self.problem.n_var
165
+ N = self.pop_size
166
+
167
+ # Compute velocity bounds
168
+ search_range = self.xu - self.xl
169
+ v_max = self._v_max_ratio * search_range
170
+ v_min = -v_max
171
+
172
+ # Register velocity bounds
173
+ self.register_buffer("_v_max", v_max)
174
+ self.register_buffer("_v_min", v_min)
175
+
176
+ # Initialize velocities to zero
177
+ self.register_buffer(
178
+ "_velocity",
179
+ torch.zeros(N, n_var, device=self.device, dtype=self.dtype)
180
+ )
181
+
182
+ # Initialize personal bests
183
+ self.register_buffer(
184
+ "_p_best",
185
+ self._population.clone().detach()
186
+ )
187
+ self.register_buffer(
188
+ "_p_best_fitness",
189
+ self.state.fitness.clone().detach()
190
+ )
191
+
192
+ # Setup hyperparameters
193
+ self._setup_hyperparameters(N, n_var)
194
+
195
+ def _setup_hyperparameters(self, N: int, n_var: int) -> None:
196
+ """Setup inertia, c1, c2 as learnable or fixed parameters."""
197
+ if self.adaptive:
198
+ if self.per_particle_coeffs:
199
+ # Per-particle coefficients [N, 1] for broadcasting
200
+ self._inertia = nn.Parameter(
201
+ torch.full((N, 1), self._init_inertia,
202
+ device=self.device, dtype=self.dtype)
203
+ )
204
+ self._c1 = nn.Parameter(
205
+ torch.full((N, 1), self._init_c1,
206
+ device=self.device, dtype=self.dtype)
207
+ )
208
+ self._c2 = nn.Parameter(
209
+ torch.full((N, 1), self._init_c2,
210
+ device=self.device, dtype=self.dtype)
211
+ )
212
+ else:
213
+ # Scalar coefficients (shared by all particles)
214
+ self._inertia = nn.Parameter(
215
+ torch.tensor(self._init_inertia,
216
+ device=self.device, dtype=self.dtype)
217
+ )
218
+ self._c1 = nn.Parameter(
219
+ torch.tensor(self._init_c1,
220
+ device=self.device, dtype=self.dtype)
221
+ )
222
+ self._c2 = nn.Parameter(
223
+ torch.tensor(self._init_c2,
224
+ device=self.device, dtype=self.dtype)
225
+ )
226
+ else:
227
+ # Fixed coefficients (buffers)
228
+ self.register_buffer(
229
+ "_inertia",
230
+ torch.tensor(self._init_inertia, device=self.device, dtype=self.dtype)
231
+ )
232
+ self.register_buffer(
233
+ "_c1",
234
+ torch.tensor(self._init_c1, device=self.device, dtype=self.dtype)
235
+ )
236
+ self.register_buffer(
237
+ "_c2",
238
+ torch.tensor(self._init_c2, device=self.device, dtype=self.dtype)
239
+ )
240
+
241
+ # =========================================================================
242
+ # Properties
243
+ # =========================================================================
244
+
245
+ @property
246
+ def population(self) -> Tensor:
247
+ """Current particle positions."""
248
+ return self._population
249
+
250
+ @property
251
+ def fitness(self) -> Tensor:
252
+ """Current fitness values."""
253
+ return self.state.fitness
254
+
255
+ @property
256
+ def velocity(self) -> Tensor:
257
+ """Current velocity vectors."""
258
+ return self._velocity
259
+
260
+ @property
261
+ def p_best(self) -> Tensor:
262
+ """Personal best positions."""
263
+ return self._p_best
264
+
265
+ @property
266
+ def p_best_fitness(self) -> Tensor:
267
+ """Personal best fitness values."""
268
+ return self._p_best_fitness
269
+
270
+ @property
271
+ def inertia(self) -> Tensor:
272
+ """Current inertia weight."""
273
+ return self._inertia
274
+
275
+ @property
276
+ def c1(self) -> Tensor:
277
+ """Current cognitive coefficient."""
278
+ return self._c1
279
+
280
+ @property
281
+ def c2(self) -> Tensor:
282
+ """Current social coefficient."""
283
+ return self._c2
284
+
285
+ # =========================================================================
286
+ # Core PSO Methods
287
+ # =========================================================================
288
+
289
+ def _update_velocity(self) -> Tensor:
290
+ """
291
+ Compute new velocities using the PSO velocity update equation.
292
+
293
+ v_new = w*v + c1*r1*(p_best - x) + c2*r2*(g_best - x)
294
+
295
+ Returns:
296
+ New velocity vectors [pop_size, n_var].
297
+ """
298
+ N, D = self.pop_size, self.n_var
299
+
300
+ # Random vectors
301
+ r1 = torch.rand(N, D, device=self.device, dtype=self.dtype)
302
+ r2 = torch.rand(N, D, device=self.device, dtype=self.dtype)
303
+
304
+ # Get global best
305
+ g_best = self.state.best_solution
306
+ if g_best is None:
307
+ best_idx = torch.argmin(self._p_best_fitness)
308
+ g_best = self._p_best[best_idx]
309
+
310
+ # Velocity components
311
+ inertia_term = self.inertia * self._velocity
312
+ cognitive_term = self.c1 * r1 * (self._p_best - self.population)
313
+ social_term = self.c2 * r2 * (g_best.unsqueeze(0) - self.population)
314
+
315
+ # New velocity
316
+ v_new = inertia_term + cognitive_term + social_term
317
+
318
+ # Clamp velocity
319
+ v_new = torch.clamp(v_new, self._v_min, self._v_max)
320
+
321
+ return v_new
322
+
323
+ def _update_position(self, velocity: Tensor) -> Tensor:
324
+ """
325
+ Update particle positions based on velocity.
326
+
327
+ Args:
328
+ velocity: Velocity vectors [pop_size, n_var].
329
+
330
+ Returns:
331
+ New positions [pop_size, n_var].
332
+ """
333
+ new_pos = self.population + velocity
334
+ return new_pos
335
+
336
+ def _reflect_bounds(self, position: Tensor, velocity: Tensor) -> tuple:
337
+ """
338
+ Handle boundary violations by reflection.
339
+
340
+ When a particle hits a boundary, it bounces back and its
341
+ velocity component is reversed.
342
+
343
+ Args:
344
+ position: Particle positions [pop_size, n_var].
345
+ velocity: Particle velocities [pop_size, n_var].
346
+
347
+ Returns:
348
+ Tuple of (repaired_position, repaired_velocity).
349
+ """
350
+ # Detect violations
351
+ below = position < self.xl
352
+ above = position > self.xu
353
+
354
+ # Reflect positions
355
+ pos_repaired = position.clone()
356
+ pos_repaired = torch.where(below, 2 * self.xl - position, pos_repaired)
357
+ pos_repaired = torch.where(above, 2 * self.xu - position, pos_repaired)
358
+
359
+ # Clamp to ensure within bounds (in case of large violations)
360
+ pos_repaired = torch.clamp(pos_repaired, self.xl, self.xu)
361
+
362
+ # Reverse velocity at boundaries
363
+ vel_repaired = velocity.clone()
364
+ vel_repaired = torch.where(below | above, -velocity, vel_repaired)
365
+
366
+ return pos_repaired, vel_repaired
367
+
368
+ def _update_personal_best(
369
+ self,
370
+ new_pos: Tensor,
371
+ new_fitness: Tensor,
372
+ ) -> tuple:
373
+ """
374
+ Update personal best positions where improved.
375
+
376
+ Args:
377
+ new_pos: New particle positions [pop_size, n_var].
378
+ new_fitness: Fitness at new positions [pop_size].
379
+
380
+ Returns:
381
+ Tuple of (new_p_best, new_p_best_fitness).
382
+ """
383
+ improved = new_fitness < self._p_best_fitness
384
+
385
+ new_p_best = torch.where(
386
+ improved.unsqueeze(-1),
387
+ new_pos,
388
+ self._p_best
389
+ )
390
+ new_p_best_fitness = torch.where(
391
+ improved,
392
+ new_fitness,
393
+ self._p_best_fitness
394
+ )
395
+
396
+ return new_p_best, new_p_best_fitness
397
+
398
+ def _infill(self) -> Tensor:
399
+ """
400
+ Generate new particle positions through velocity update.
401
+
402
+ Returns:
403
+ New positions [pop_size, n_var].
404
+ """
405
+ # 1. Update velocities
406
+ new_velocity = self._update_velocity()
407
+
408
+ # 2. Update positions
409
+ new_pos = self._update_position(new_velocity)
410
+
411
+ # 3. Handle boundary violations
412
+ if self.repair is not None:
413
+ new_pos = self.repair(new_pos, self.xl, self.xu)
414
+ # Recompute velocity to match repaired position
415
+ new_velocity = new_pos - self.population
416
+ else:
417
+ new_pos, new_velocity = self._reflect_bounds(new_pos, new_velocity)
418
+
419
+ # Store velocity for state update
420
+ self._pending_velocity = new_velocity
421
+
422
+ return new_pos
423
+
424
+ def _advance(self, offspring: Tensor, offspring_fitness: Tensor) -> None:
425
+ """
426
+ Update swarm state with new positions.
427
+
428
+ Args:
429
+ offspring: New particle positions [pop_size, n_var].
430
+ offspring_fitness: Fitness at new positions [pop_size].
431
+ """
432
+ # Update personal bests
433
+ new_p_best, new_p_best_fitness = self._update_personal_best(
434
+ offspring, offspring_fitness
435
+ )
436
+
437
+ # Update state
438
+ self._update_state(
439
+ new_pos=offspring,
440
+ new_fitness=offspring_fitness,
441
+ new_velocity=self._pending_velocity,
442
+ new_p_best=new_p_best,
443
+ new_p_best_fitness=new_p_best_fitness,
444
+ )
445
+
446
+ # Update global best
447
+ self.state.update_best(self.population, self.state.fitness)
448
+
449
+ # Cleanup
450
+ if hasattr(self, '_pending_velocity'):
451
+ del self._pending_velocity
452
+
453
+ def _update_state(
454
+ self,
455
+ new_pos: Tensor,
456
+ new_fitness: Tensor,
457
+ new_velocity: Tensor,
458
+ new_p_best: Tensor,
459
+ new_p_best_fitness: Tensor,
460
+ ) -> None:
461
+ """Update all PSO state tensors.
462
+
463
+ In differentiable mode the SGD optimiser has already nudged
464
+ ``self._population`` by a gradient step (P₀ → P₀ − lr·∇).
465
+ The PSO velocity update produced ``new_pos = P₀ + v``.
466
+ We combine both forces so that the final position is
467
+ ``P₀ + v − lr·∇``, preserving the gradient correction that
468
+ would otherwise be overwritten by ``copy_(new_pos)``.
469
+ """
470
+ with torch.no_grad():
471
+ if self.differentiable and isinstance(self._population, nn.Parameter):
472
+ # Recover pre-velocity positions and extract the gradient delta
473
+ old_pos = new_pos - new_velocity # P₀
474
+ grad_delta = self._population.data - old_pos # −lr·∇
475
+ combined = new_pos + grad_delta # P₀ + v − lr·∇
476
+ combined = torch.clamp(combined, self.xl, self.xu)
477
+ self._population.copy_(combined)
478
+ else:
479
+ self._population.copy_(new_pos)
480
+ self._velocity.copy_(new_velocity)
481
+ self._p_best.copy_(new_p_best)
482
+ self._p_best_fitness.copy_(new_p_best_fitness)
483
+
484
+ self.state.fitness = new_fitness
485
+ self.state.population = self._population
486
+
487
+ # =========================================================================
488
+ # Hyperparameter Management
489
+ # =========================================================================
490
+
491
+ @torch.no_grad()
492
+ def _clamp_hyperparams(self) -> None:
493
+ """Clamp learnable hyperparameters to valid ranges."""
494
+ if self.adaptive:
495
+ # Inertia in [0, 1.5]
496
+ self._inertia.clamp_(min=0.0, max=1.5)
497
+ # c1, c2 in [0, 4]
498
+ self._c1.clamp_(min=0.0, max=4.0)
499
+ self._c2.clamp_(min=0.0, max=4.0)
500
+
501
+ def update_state(self) -> None:
502
+ """Commit pending changes and clamp hyperparameters."""
503
+ super().update_state()
504
+ self._clamp_hyperparams()
505
+
506
+ def _get_hyperparams(self) -> Dict[str, Any]:
507
+ """Return current hyperparameter values."""
508
+ def _to_float(x: Tensor) -> float:
509
+ if x.numel() == 1:
510
+ return float(x.item())
511
+ return float(x.mean().item())
512
+
513
+ return {
514
+ 'pop_size': self.pop_size,
515
+ 'inertia': _to_float(self.inertia),
516
+ 'c1': _to_float(self.c1),
517
+ 'c2': _to_float(self.c2),
518
+ 'v_max_ratio': self._v_max_ratio,
519
+ 'adaptive': self.adaptive,
520
+ 'differentiable': self.differentiable,
521
+ 'per_particle_coeffs': self.per_particle_coeffs,
522
+ }
523
+
524
+ # =========================================================================
525
+ # String Representation
526
+ # =========================================================================
527
+
528
+ def __repr__(self) -> str:
529
+ def _fmt(x: Tensor) -> str:
530
+ if x.numel() == 1:
531
+ return f"{float(x.item()):.3f}"
532
+ return f"{float(x.mean().item()):.3f}"
533
+
534
+ return (
535
+ f"PSO(pop_size={self.pop_size}, "
536
+ f"w={_fmt(self.inertia)}, "
537
+ f"c1={_fmt(self.c1)}, "
538
+ f"c2={_fmt(self.c2)}, "
539
+ f"adaptive={self.adaptive}, "
540
+ f"differentiable={self.differentiable})"
541
+ )
542
+
543
+
544
+ # =============================================================================
545
+ # Convenience Factory Functions
546
+ # =============================================================================
547
+
548
+ def pso_default(
549
+ pop_size: int = 100,
550
+ adaptive: bool = False,
551
+ differentiable: bool = False,
552
+ **kwargs,
553
+ ) -> PSO:
554
+ """
555
+ Create PSO with default settings.
556
+
557
+ Uses standard coefficients:
558
+ - inertia = 0.7
559
+ - c1 = c2 = 1.5
560
+
561
+ Args:
562
+ pop_size: Swarm size.
563
+ adaptive: If True, hyperparameters are learnable.
564
+ differentiable: If True, positions are learnable.
565
+ **kwargs: Additional arguments passed to PSO.
566
+
567
+ Returns:
568
+ Configured PSO instance.
569
+ """
570
+ return PSO(
571
+ pop_size=pop_size,
572
+ w=0.7,
573
+ c1=1.5,
574
+ c2=1.5,
575
+ adaptive=adaptive,
576
+ differentiable=differentiable,
577
+ **kwargs,
578
+ )
579
+
580
+
581
+ def pso_constriction(
582
+ pop_size: int = 100,
583
+ adaptive: bool = False,
584
+ differentiable: bool = False,
585
+ **kwargs,
586
+ ) -> PSO:
587
+ """
588
+ Create PSO with constriction coefficients.
589
+
590
+ Uses Clerc's constriction factor approach:
591
+ - inertia = 0.7298
592
+ - c1 = c2 = 1.4962
593
+
594
+ This configuration provides guaranteed convergence.
595
+
596
+ Args:
597
+ pop_size: Swarm size.
598
+ adaptive: If True, hyperparameters are learnable.
599
+ differentiable: If True, positions are learnable.
600
+ **kwargs: Additional arguments passed to PSO.
601
+
602
+ Returns:
603
+ Configured PSO instance.
604
+
605
+ Reference:
606
+ Clerc, M. & Kennedy, J. (2002). The particle swarm - explosion,
607
+ stability, and convergence in a multidimensional complex space.
608
+ """
609
+ # Constriction coefficients
610
+ phi = 4.1
611
+ chi = 2.0 / abs(2.0 - phi - (phi**2 - 4*phi)**0.5)
612
+
613
+ return PSO(
614
+ pop_size=pop_size,
615
+ w=chi, # ~0.7298
616
+ c1=chi * 2.05, # ~1.4962
617
+ c2=chi * 2.05, # ~1.4962
618
+ adaptive=adaptive,
619
+ differentiable=differentiable,
620
+ **kwargs,
621
+ )
622
+
623
+
624
+ def pso_adaptive(
625
+ pop_size: int = 100,
626
+ per_particle: bool = False,
627
+ differentiable: bool = False,
628
+ **kwargs,
629
+ ) -> PSO:
630
+ """
631
+ Create PSO with adaptive (learnable) hyperparameters.
632
+
633
+ Args:
634
+ pop_size: Swarm size.
635
+ per_particle: If True, each particle has its own coefficients.
636
+ differentiable: If True, positions are also learnable.
637
+ **kwargs: Additional arguments passed to PSO.
638
+
639
+ Returns:
640
+ Configured PSO instance with adaptive=True.
641
+ """
642
+ return PSO(
643
+ pop_size=pop_size,
644
+ adaptive=True,
645
+ differentiable=differentiable,
646
+ per_particle_coeffs=per_particle,
647
+ **kwargs,
648
+ )