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,601 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repair operators for bounds handling.
|
|
3
|
+
|
|
4
|
+
This module provides repair operators that handle constraint
|
|
5
|
+
violations, primarily keeping solutions within variable bounds.
|
|
6
|
+
All methods are differentiable-friendly.
|
|
7
|
+
|
|
8
|
+
Available repair methods:
|
|
9
|
+
- ClipRepair: Clamp values to bounds (simplest)
|
|
10
|
+
- ReflectRepair: Bounce off boundaries (preserves momentum)
|
|
11
|
+
- WrapRepair: Periodic/toroidal wrapping
|
|
12
|
+
- RandomRepair: Reset violating genes randomly
|
|
13
|
+
- BoundsRepair: Configurable repair with method selection
|
|
14
|
+
|
|
15
|
+
Differentiable Considerations:
|
|
16
|
+
- ClipRepair: Gradient is zero at boundaries (can cause issues)
|
|
17
|
+
- ReflectRepair: Gradient flows through reflection
|
|
18
|
+
- WrapRepair: Gradient flows through modulo (discontinuous)
|
|
19
|
+
- For differentiable mode, ReflectRepair is generally recommended
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> from evograd.operators import BoundsRepair
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Using method string
|
|
25
|
+
>>> repair = BoundsRepair(method='reflect')
|
|
26
|
+
>>> repaired = repair(population, xl, xu)
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Or use specific class
|
|
29
|
+
>>> from evograd.operators import ReflectRepair
|
|
30
|
+
>>> repair = ReflectRepair()
|
|
31
|
+
>>> repaired = repair(population, xl, xu)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from abc import ABC, abstractmethod
|
|
37
|
+
from enum import Enum
|
|
38
|
+
from typing import Optional, Union
|
|
39
|
+
|
|
40
|
+
import torch
|
|
41
|
+
import torch.nn as nn
|
|
42
|
+
from torch import Tensor
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Repair",
|
|
46
|
+
"ClipRepair",
|
|
47
|
+
"ReflectRepair",
|
|
48
|
+
"WrapRepair",
|
|
49
|
+
"RandomRepair",
|
|
50
|
+
"BoundsRepair",
|
|
51
|
+
"RepairMethod",
|
|
52
|
+
"NoRepair",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RepairMethod(Enum):
|
|
57
|
+
"""Available repair methods."""
|
|
58
|
+
CLIP = "clip"
|
|
59
|
+
REFLECT = "reflect"
|
|
60
|
+
WRAP = "wrap"
|
|
61
|
+
RANDOM = "random"
|
|
62
|
+
NONE = "none"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# Base Repair Class
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
class Repair(nn.Module, ABC):
|
|
70
|
+
"""
|
|
71
|
+
Abstract base class for repair operators.
|
|
72
|
+
|
|
73
|
+
Subclasses must implement:
|
|
74
|
+
- _repair(): Apply repair to bring solutions within bounds
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
super().__init__()
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def _repair(
|
|
82
|
+
self,
|
|
83
|
+
x: Tensor,
|
|
84
|
+
xl: Tensor,
|
|
85
|
+
xu: Tensor,
|
|
86
|
+
) -> Tensor:
|
|
87
|
+
"""
|
|
88
|
+
Repair solutions to satisfy bounds.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
x: Solutions to repair [n_pop, n_var].
|
|
92
|
+
xl: Lower bounds [n_var] or scalar.
|
|
93
|
+
xu: Upper bounds [n_var] or scalar.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Repaired solutions [n_pop, n_var].
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def forward(
|
|
101
|
+
self,
|
|
102
|
+
x: Tensor,
|
|
103
|
+
xl: Optional[Tensor] = None,
|
|
104
|
+
xu: Optional[Tensor] = None,
|
|
105
|
+
problem: Optional["Problem"] = None,
|
|
106
|
+
) -> Tensor:
|
|
107
|
+
"""
|
|
108
|
+
Apply repair.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
x: Solutions to repair [n_pop, n_var].
|
|
112
|
+
xl: Lower bounds (or provide problem).
|
|
113
|
+
xu: Upper bounds (or provide problem).
|
|
114
|
+
problem: Problem instance with bounds.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Repaired solutions [n_pop, n_var].
|
|
118
|
+
"""
|
|
119
|
+
# Get bounds from problem if provided
|
|
120
|
+
if problem is not None:
|
|
121
|
+
xl = problem.xl
|
|
122
|
+
xu = problem.xu
|
|
123
|
+
|
|
124
|
+
# Ensure bounds are provided
|
|
125
|
+
if xl is None or xu is None:
|
|
126
|
+
raise ValueError("Bounds must be provided via xl/xu or problem")
|
|
127
|
+
|
|
128
|
+
# Ensure bounds have correct shape
|
|
129
|
+
n_var = x.shape[-1]
|
|
130
|
+
if xl.dim() == 0:
|
|
131
|
+
xl = xl.expand(n_var)
|
|
132
|
+
if xu.dim() == 0:
|
|
133
|
+
xu = xu.expand(n_var)
|
|
134
|
+
|
|
135
|
+
return self._repair(x, xl, xu)
|
|
136
|
+
|
|
137
|
+
# Note: Do NOT override __call__. nn.Module.__call__ dispatches to
|
|
138
|
+
# forward() and fires registered hooks (forward_pre_hooks, forward_hooks,
|
|
139
|
+
# and the autograd profiler). Overriding __call__ would bypass all of these.
|
|
140
|
+
|
|
141
|
+
def is_within_bounds(
|
|
142
|
+
self,
|
|
143
|
+
x: Tensor,
|
|
144
|
+
xl: Tensor,
|
|
145
|
+
xu: Tensor,
|
|
146
|
+
tol: float = 1e-8,
|
|
147
|
+
) -> Tensor:
|
|
148
|
+
"""
|
|
149
|
+
Check if solutions are within bounds.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
x: Solutions to check [n_pop, n_var].
|
|
153
|
+
xl: Lower bounds.
|
|
154
|
+
xu: Upper bounds.
|
|
155
|
+
tol: Tolerance for boundary check.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Boolean tensor [n_pop] indicating feasibility.
|
|
159
|
+
"""
|
|
160
|
+
within_lower = (x >= xl - tol).all(dim=-1)
|
|
161
|
+
within_upper = (x <= xu + tol).all(dim=-1)
|
|
162
|
+
return within_lower & within_upper
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# =============================================================================
|
|
166
|
+
# Clip (Clamp) Repair
|
|
167
|
+
# =============================================================================
|
|
168
|
+
|
|
169
|
+
class ClipRepair(Repair):
|
|
170
|
+
"""
|
|
171
|
+
Clip repair (clamping to bounds).
|
|
172
|
+
|
|
173
|
+
Simply clamps values to [xl, xu]. This is the simplest and
|
|
174
|
+
most common repair method.
|
|
175
|
+
|
|
176
|
+
Note:
|
|
177
|
+
Gradient is zero when values are clipped, which can cause
|
|
178
|
+
issues in differentiable mode. Consider ReflectRepair for
|
|
179
|
+
better gradient flow.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
>>> repair = ClipRepair()
|
|
183
|
+
>>> repaired = repair(population, xl, xu)
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def _repair(
|
|
187
|
+
self,
|
|
188
|
+
x: Tensor,
|
|
189
|
+
xl: Tensor,
|
|
190
|
+
xu: Tensor,
|
|
191
|
+
) -> Tensor:
|
|
192
|
+
return torch.clamp(x, min=xl, max=xu)
|
|
193
|
+
|
|
194
|
+
def __repr__(self) -> str:
|
|
195
|
+
return "ClipRepair()"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# =============================================================================
|
|
199
|
+
# Reflect Repair
|
|
200
|
+
# =============================================================================
|
|
201
|
+
|
|
202
|
+
class ReflectRepair(Repair):
|
|
203
|
+
"""
|
|
204
|
+
Reflection repair (bounce off boundaries).
|
|
205
|
+
|
|
206
|
+
When a value exceeds a bound, it bounces back into the
|
|
207
|
+
feasible region. This preserves the "momentum" of the
|
|
208
|
+
search and provides better gradient flow than clipping.
|
|
209
|
+
|
|
210
|
+
The reflection is computed as:
|
|
211
|
+
x' = xl + |x - xl| mod (2 * range)
|
|
212
|
+
if x' > xu: x' = 2*xu - x'
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
max_iterations: Maximum reflection iterations (prevents
|
|
216
|
+
infinite loops for extreme violations).
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> repair = ReflectRepair()
|
|
220
|
+
>>> repaired = repair(population, xl, xu)
|
|
221
|
+
|
|
222
|
+
Note:
|
|
223
|
+
This is the recommended repair method for differentiable
|
|
224
|
+
mode as gradients flow through the reflection operation.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(self, max_iterations: int = 100) -> None:
|
|
228
|
+
super().__init__()
|
|
229
|
+
self.max_iterations = max_iterations
|
|
230
|
+
|
|
231
|
+
def _repair(
|
|
232
|
+
self,
|
|
233
|
+
x: Tensor,
|
|
234
|
+
xl: Tensor,
|
|
235
|
+
xu: Tensor,
|
|
236
|
+
) -> Tensor:
|
|
237
|
+
# Compute range
|
|
238
|
+
span = xu - xl
|
|
239
|
+
|
|
240
|
+
# Handle zero span (fixed variables)
|
|
241
|
+
span = torch.where(span > 0, span, torch.ones_like(span))
|
|
242
|
+
|
|
243
|
+
# Normalise to [0, 2*span] then fold
|
|
244
|
+
x_shifted = x - xl
|
|
245
|
+
x_mod = torch.remainder(x_shifted, 2 * span)
|
|
246
|
+
|
|
247
|
+
# Fold back: if > span, reflect from upper bound
|
|
248
|
+
x_folded = torch.where(x_mod > span, 2 * span - x_mod, x_mod)
|
|
249
|
+
|
|
250
|
+
# Shift back to original space
|
|
251
|
+
x_repaired = xl + x_folded
|
|
252
|
+
|
|
253
|
+
return x_repaired
|
|
254
|
+
|
|
255
|
+
def __repr__(self) -> str:
|
|
256
|
+
return f"ReflectRepair(max_iterations={self.max_iterations})"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# =============================================================================
|
|
260
|
+
# Wrap (Periodic) Repair
|
|
261
|
+
# =============================================================================
|
|
262
|
+
|
|
263
|
+
class WrapRepair(Repair):
|
|
264
|
+
"""
|
|
265
|
+
Wrap repair (periodic/toroidal boundaries).
|
|
266
|
+
|
|
267
|
+
Values that exceed bounds wrap around to the other side,
|
|
268
|
+
treating the search space as a torus. Useful for periodic
|
|
269
|
+
domains like angles.
|
|
270
|
+
|
|
271
|
+
The wrapping is computed as:
|
|
272
|
+
x' = xl + (x - xl) mod (xu - xl)
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> # For angular variables [0, 2*pi]
|
|
276
|
+
>>> repair = WrapRepair()
|
|
277
|
+
>>> repaired = repair(angles, 0, 2*np.pi)
|
|
278
|
+
|
|
279
|
+
Note:
|
|
280
|
+
Gradient is discontinuous at boundaries but flows
|
|
281
|
+
through the modulo operation.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def _repair(
|
|
285
|
+
self,
|
|
286
|
+
x: Tensor,
|
|
287
|
+
xl: Tensor,
|
|
288
|
+
xu: Tensor,
|
|
289
|
+
) -> Tensor:
|
|
290
|
+
span = xu - xl
|
|
291
|
+
|
|
292
|
+
# Handle zero span
|
|
293
|
+
span = torch.where(span > 0, span, torch.ones_like(span))
|
|
294
|
+
|
|
295
|
+
# Periodic wrapping
|
|
296
|
+
x_wrapped = xl + torch.remainder(x - xl, span)
|
|
297
|
+
|
|
298
|
+
return x_wrapped
|
|
299
|
+
|
|
300
|
+
def __repr__(self) -> str:
|
|
301
|
+
return "WrapRepair()"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# =============================================================================
|
|
305
|
+
# Random Repair
|
|
306
|
+
# =============================================================================
|
|
307
|
+
|
|
308
|
+
class RandomRepair(Repair):
|
|
309
|
+
"""
|
|
310
|
+
Random repair (reset violating genes).
|
|
311
|
+
|
|
312
|
+
Genes that violate bounds are reset to random values within
|
|
313
|
+
the feasible region. This is more disruptive than other
|
|
314
|
+
methods but can help escape from boundary regions.
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
>>> repair = RandomRepair()
|
|
318
|
+
>>> repaired = repair(population, xl, xu)
|
|
319
|
+
|
|
320
|
+
Note:
|
|
321
|
+
Not differentiable through the random reset operation.
|
|
322
|
+
Gradient is zero for repaired genes.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def _repair(
|
|
326
|
+
self,
|
|
327
|
+
x: Tensor,
|
|
328
|
+
xl: Tensor,
|
|
329
|
+
xu: Tensor,
|
|
330
|
+
) -> Tensor:
|
|
331
|
+
# Find violations
|
|
332
|
+
below = x < xl
|
|
333
|
+
above = x > xu
|
|
334
|
+
violates = below | above
|
|
335
|
+
|
|
336
|
+
# Generate random replacements
|
|
337
|
+
random_vals = xl + (xu - xl) * torch.rand_like(x)
|
|
338
|
+
|
|
339
|
+
# Replace only violating genes
|
|
340
|
+
x_repaired = torch.where(violates, random_vals, x)
|
|
341
|
+
|
|
342
|
+
return x_repaired
|
|
343
|
+
|
|
344
|
+
def __repr__(self) -> str:
|
|
345
|
+
return "RandomRepair()"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# =============================================================================
|
|
349
|
+
# No Repair (Identity)
|
|
350
|
+
# =============================================================================
|
|
351
|
+
|
|
352
|
+
class NoRepair(Repair):
|
|
353
|
+
"""
|
|
354
|
+
No repair (identity operator).
|
|
355
|
+
|
|
356
|
+
Returns input unchanged. Useful as a placeholder when
|
|
357
|
+
repair should be disabled or handled elsewhere.
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
>>> repair = NoRepair()
|
|
361
|
+
>>> repaired = repair(population, xl, xu) # Returns unchanged
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def _repair(
|
|
365
|
+
self,
|
|
366
|
+
x: Tensor,
|
|
367
|
+
xl: Tensor,
|
|
368
|
+
xu: Tensor,
|
|
369
|
+
) -> Tensor:
|
|
370
|
+
return x
|
|
371
|
+
|
|
372
|
+
def __repr__(self) -> str:
|
|
373
|
+
return "NoRepair()"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# =============================================================================
|
|
377
|
+
# Configurable Bounds Repair
|
|
378
|
+
# =============================================================================
|
|
379
|
+
|
|
380
|
+
class BoundsRepair(Repair):
|
|
381
|
+
"""
|
|
382
|
+
Configurable bounds repair with method selection.
|
|
383
|
+
|
|
384
|
+
Convenience class that allows selecting the repair method
|
|
385
|
+
via a string or enum parameter.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
method: Repair method to use. Options:
|
|
389
|
+
- 'clip': Clamp to bounds (default)
|
|
390
|
+
- 'reflect': Bounce off boundaries
|
|
391
|
+
- 'wrap': Periodic wrapping
|
|
392
|
+
- 'random': Reset violating genes
|
|
393
|
+
- 'none': No repair
|
|
394
|
+
|
|
395
|
+
Example:
|
|
396
|
+
>>> repair = BoundsRepair(method='reflect')
|
|
397
|
+
>>> repaired = repair(population, xl, xu)
|
|
398
|
+
>>>
|
|
399
|
+
>>> # Or use enum
|
|
400
|
+
>>> from evograd.operators import RepairMethod
|
|
401
|
+
>>> repair = BoundsRepair(method=RepairMethod.WRAP)
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
_METHOD_MAP = {
|
|
405
|
+
'clip': ClipRepair,
|
|
406
|
+
'clamp': ClipRepair,
|
|
407
|
+
'reflect': ReflectRepair,
|
|
408
|
+
'bounce': ReflectRepair,
|
|
409
|
+
'wrap': WrapRepair,
|
|
410
|
+
'periodic': WrapRepair,
|
|
411
|
+
'toroidal': WrapRepair,
|
|
412
|
+
'random': RandomRepair,
|
|
413
|
+
'none': NoRepair,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def __init__(
|
|
417
|
+
self,
|
|
418
|
+
method: Union[str, RepairMethod] = "clip",
|
|
419
|
+
) -> None:
|
|
420
|
+
super().__init__()
|
|
421
|
+
|
|
422
|
+
# Convert enum to string
|
|
423
|
+
if isinstance(method, RepairMethod):
|
|
424
|
+
method = method.value
|
|
425
|
+
|
|
426
|
+
method = method.lower()
|
|
427
|
+
|
|
428
|
+
if method not in self._METHOD_MAP:
|
|
429
|
+
valid = list(self._METHOD_MAP.keys())
|
|
430
|
+
raise ValueError(
|
|
431
|
+
f"Unknown repair method '{method}'. Valid options: {valid}"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
self.method = method
|
|
435
|
+
self._repair_impl = self._METHOD_MAP[method]()
|
|
436
|
+
|
|
437
|
+
def _repair(
|
|
438
|
+
self,
|
|
439
|
+
x: Tensor,
|
|
440
|
+
xl: Tensor,
|
|
441
|
+
xu: Tensor,
|
|
442
|
+
) -> Tensor:
|
|
443
|
+
return self._repair_impl._repair(x, xl, xu)
|
|
444
|
+
|
|
445
|
+
def __repr__(self) -> str:
|
|
446
|
+
return f"BoundsRepair(method='{self.method}')"
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# =============================================================================
|
|
450
|
+
# Soft Clip Repair (Differentiable-friendly)
|
|
451
|
+
# =============================================================================
|
|
452
|
+
|
|
453
|
+
class SoftClipRepair(Repair):
|
|
454
|
+
"""
|
|
455
|
+
Soft clip repair using smooth approximation.
|
|
456
|
+
|
|
457
|
+
Uses a smooth approximation of the clip function that
|
|
458
|
+
provides non-zero gradients near boundaries. This is
|
|
459
|
+
useful when gradients are important but values should
|
|
460
|
+
still be approximately within bounds.
|
|
461
|
+
|
|
462
|
+
The soft clip is computed using:
|
|
463
|
+
softplus(x - xl) - softplus(x - xu) + xl
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
beta: Smoothness parameter (higher = sharper, closer to hard clip).
|
|
467
|
+
margin: How far outside bounds the soft clip extends.
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
>>> repair = SoftClipRepair(beta=10.0)
|
|
471
|
+
>>> repaired = repair(population, xl, xu)
|
|
472
|
+
|
|
473
|
+
Note:
|
|
474
|
+
Values may slightly exceed bounds. Use hard clip after
|
|
475
|
+
if strict feasibility is required.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
def __init__(
|
|
479
|
+
self,
|
|
480
|
+
beta: float = 10.0,
|
|
481
|
+
margin: float = 0.1,
|
|
482
|
+
) -> None:
|
|
483
|
+
super().__init__()
|
|
484
|
+
self.beta = beta
|
|
485
|
+
self.margin = margin
|
|
486
|
+
|
|
487
|
+
def _soft_clip(
|
|
488
|
+
self,
|
|
489
|
+
x: Tensor,
|
|
490
|
+
lower: Tensor,
|
|
491
|
+
upper: Tensor,
|
|
492
|
+
) -> Tensor:
|
|
493
|
+
"""Smooth clip using softplus."""
|
|
494
|
+
# Soft lower bound: max(x, lower) ≈ lower + softplus(x - lower)
|
|
495
|
+
x_lower = lower + torch.nn.functional.softplus(
|
|
496
|
+
(x - lower) * self.beta
|
|
497
|
+
) / self.beta
|
|
498
|
+
|
|
499
|
+
# Soft upper bound: min(x, upper) ≈ upper - softplus(upper - x)
|
|
500
|
+
x_clipped = upper - torch.nn.functional.softplus(
|
|
501
|
+
(upper - x_lower) * self.beta
|
|
502
|
+
) / self.beta
|
|
503
|
+
|
|
504
|
+
return x_clipped
|
|
505
|
+
|
|
506
|
+
def _repair(
|
|
507
|
+
self,
|
|
508
|
+
x: Tensor,
|
|
509
|
+
xl: Tensor,
|
|
510
|
+
xu: Tensor,
|
|
511
|
+
) -> Tensor:
|
|
512
|
+
return self._soft_clip(x, xl, xu)
|
|
513
|
+
|
|
514
|
+
def __repr__(self) -> str:
|
|
515
|
+
return f"SoftClipRepair(beta={self.beta}, margin={self.margin})"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# =============================================================================
|
|
519
|
+
# Penalty-based Repair (returns penalty instead of repairing)
|
|
520
|
+
# =============================================================================
|
|
521
|
+
|
|
522
|
+
class PenaltyRepair(Repair):
|
|
523
|
+
"""
|
|
524
|
+
Penalty-based "repair" that computes constraint violation.
|
|
525
|
+
|
|
526
|
+
Instead of modifying solutions, this computes a penalty
|
|
527
|
+
term that can be added to the fitness. Useful for
|
|
528
|
+
constrained optimisation with penalty methods.
|
|
529
|
+
|
|
530
|
+
The penalty is computed as:
|
|
531
|
+
penalty = sum(max(0, xl - x)^2 + max(0, x - xu)^2)
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
penalty_weight: Multiplier for the penalty term.
|
|
535
|
+
power: Exponent for violation (1=linear, 2=quadratic).
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
>>> repair = PenaltyRepair(penalty_weight=1000)
|
|
539
|
+
>>> penalty = repair.compute_penalty(population, xl, xu)
|
|
540
|
+
>>> fitness_penalised = fitness + penalty
|
|
541
|
+
|
|
542
|
+
Note:
|
|
543
|
+
forward() still returns the input unchanged. Use
|
|
544
|
+
compute_penalty() to get the penalty values.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
def __init__(
|
|
548
|
+
self,
|
|
549
|
+
penalty_weight: float = 1.0,
|
|
550
|
+
power: float = 2.0,
|
|
551
|
+
) -> None:
|
|
552
|
+
super().__init__()
|
|
553
|
+
self.penalty_weight = penalty_weight
|
|
554
|
+
self.power = power
|
|
555
|
+
|
|
556
|
+
def compute_penalty(
|
|
557
|
+
self,
|
|
558
|
+
x: Tensor,
|
|
559
|
+
xl: Tensor,
|
|
560
|
+
xu: Tensor,
|
|
561
|
+
) -> Tensor:
|
|
562
|
+
"""
|
|
563
|
+
Compute penalty for constraint violations.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
x: Solutions [n_pop, n_var].
|
|
567
|
+
xl: Lower bounds.
|
|
568
|
+
xu: Upper bounds.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Penalty values [n_pop].
|
|
572
|
+
"""
|
|
573
|
+
# Lower bound violations
|
|
574
|
+
lower_violation = torch.clamp(xl - x, min=0.0)
|
|
575
|
+
|
|
576
|
+
# Upper bound violations
|
|
577
|
+
upper_violation = torch.clamp(x - xu, min=0.0)
|
|
578
|
+
|
|
579
|
+
# Total penalty per individual
|
|
580
|
+
penalty = (
|
|
581
|
+
lower_violation.pow(self.power).sum(dim=-1) +
|
|
582
|
+
upper_violation.pow(self.power).sum(dim=-1)
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return self.penalty_weight * penalty
|
|
586
|
+
|
|
587
|
+
def _repair(
|
|
588
|
+
self,
|
|
589
|
+
x: Tensor,
|
|
590
|
+
xl: Tensor,
|
|
591
|
+
xu: Tensor,
|
|
592
|
+
) -> Tensor:
|
|
593
|
+
# No modification - penalty is computed separately
|
|
594
|
+
return x
|
|
595
|
+
|
|
596
|
+
def __repr__(self) -> str:
|
|
597
|
+
return (
|
|
598
|
+
f"PenaltyRepair("
|
|
599
|
+
f"penalty_weight={self.penalty_weight}, "
|
|
600
|
+
f"power={self.power})"
|
|
601
|
+
)
|