27
27
)
28
28
29
29
30
- class khan_function :
31
- r"""Function to smothly enforce optimisation parameter bounds as Michal Khan used to do:
30
+ class base_khan_function :
31
+ r"""Base class for a function to smothly enforce optimisation parameter bounds as Michal Khan
32
+ used to do:
32
33
33
34
.. math::
34
35
35
- x = \frac{x_{max} + x_{min}}{2} + \frac{x_{max} - x_{min}}{2} \cdot \sin (x_{khan})
36
+ x = \frac{x_{max} + x_{min}}{2} + \frac{x_{max} - x_{min}}{2} \cdot \f (x_{khan})
36
37
37
38
Where :math:`x` is the pagmo decision vector and :math:`x_{khan}` is the decision vector
38
39
passed to OPTGRA. In this way parameter bounds are guaranteed to be satisfied, but the gradients
39
40
near the bounds approach zero.
41
+
42
+ The child class needs to implement the methods `_eval`, `_eval_inv`, `_eval_grad` and
43
+ `_eval_inv_grad`
40
44
""" # noqa: W605
41
45
42
- def __init__ (self , lb : List [float ], ub : List [float ], unity_gradient : bool = True ):
46
+ def __init__ (self , lb : List [float ], ub : List [float ]):
43
47
"""Constructor
44
48
45
49
Parameters
@@ -48,11 +52,6 @@ def __init__(self, lb: List[float], ub: List[float], unity_gradient: bool = True
48
52
Lower pagmo parameter bounds
49
53
ub : List[float]
50
54
Upper pagmo parameter bounds
51
- unity_gradient : bool, optional
52
- Uses an internal scaling that ensures that the derivative of pagmo parameters w.r.t.
53
- khan parameters are unity at (lb + ub)/2. By default True.
54
- Otherwise, the original Khan method is used that can result in strongly modified
55
- gradients
56
55
"""
57
56
self ._lb = np .asarray (lb )
58
57
self ._ub = np .asarray (ub )
@@ -86,54 +85,6 @@ def _isfinite(a: np.ndarray):
86
85
self ._lb_masked = self ._lb [self .mask ]
87
86
self ._ub_masked = self ._ub [self .mask ]
88
87
89
- # determine coefficients inside the sin function
90
- self ._a = 2 / (self ._ub_masked - self ._lb_masked ) if unity_gradient else 1.0
91
- self ._b = (
92
- - (self ._ub_masked + self ._lb_masked ) / (self ._ub_masked - self ._lb_masked )
93
- if unity_gradient
94
- else 0.0
95
- )
96
-
97
- def _eval (self , x_khan_masked : np .ndarray ) -> np .ndarray :
98
- return (self ._ub_masked + self ._lb_masked ) / 2 + (
99
- self ._ub_masked - self ._lb_masked
100
- ) / 2 * np .sin (x_khan_masked * self ._a + self ._b )
101
-
102
- def _eval_inv (self , x_masked : np .ndarray ) -> np .ndarray :
103
- arg = (2 * x_masked - self ._ub_masked - self ._lb_masked ) / (
104
- self ._ub_masked - self ._lb_masked
105
- )
106
-
107
- clip_value = 1.0 - 1e-8 # avoid boundaries
108
- if np .any ((arg < - clip_value ) | (arg > clip_value )):
109
- print (
110
- "WARNING: Numerical inaccuracies encountered during khan_function inverse." ,
111
- "Clipping parameters to valid range." ,
112
- )
113
- arg = np .clip (arg , - clip_value , clip_value )
114
- return (np .arcsin (arg ) - self ._b ) / self ._a
115
-
116
- def _eval_grad (self , x_khan_masked : np .ndarray ) -> np .ndarray :
117
- return (
118
- (self ._ub_masked - self ._lb_masked )
119
- / 2
120
- * np .cos (self ._a * x_khan_masked + self ._b )
121
- * self ._a
122
- )
123
-
124
- def _eval_inv_grad (self , x_masked : np .ndarray ) -> np .ndarray :
125
- return (
126
- - 1
127
- / self ._a
128
- / (
129
- (self ._lb_masked - self ._ub_masked )
130
- * np .sqrt (
131
- ((self ._lb_masked - x_masked ) * (x_masked - self ._ub_masked ))
132
- / (self ._ub_masked - self ._lb_masked ) ** 2
133
- )
134
- )
135
- )
136
-
137
88
def _apply_to_subset (
138
89
self , x : np .ndarray , func : Callable , default_result : Optional [np .ndarray ] = None
139
90
) -> np .ndarray :
@@ -144,6 +95,18 @@ def _apply_to_subset(
144
95
result [self .mask ] = func (x [self .mask ])
145
96
return result
146
97
98
+ def _eval (self , x_khan_masked : np .ndarray ) -> np .ndarray :
99
+ raise NotImplementedError
100
+
101
+ def _eval_inv (self , x_masked : np .ndarray ) -> np .ndarray :
102
+ raise NotImplementedError
103
+
104
+ def _eval_grad (self , x_khan_masked : np .ndarray ) -> np .ndarray :
105
+ raise NotImplementedError
106
+
107
+ def _eval_inv_grad (self , x_masked : np .ndarray ) -> np .ndarray :
108
+ raise NotImplementedError
109
+
147
110
def eval (self , x_khan : np .ndarray ) -> np .ndarray :
148
111
"""Convert :math:`x_{optgra}` to :math:`x`.
149
112
@@ -208,6 +171,153 @@ def eval_inv_grad(self, x: np.ndarray) -> np.ndarray:
208
171
return np .diag (self ._apply_to_subset (np .asarray (x ), self ._eval_inv_grad , np .ones (self ._nx )))
209
172
210
173
174
+ class khan_function_sin (base_khan_function ):
175
+ r"""Function to smothly enforce optimisation parameter bounds as Michal Khan used to do:
176
+
177
+ .. math::
178
+
179
+ x = \frac{x_{max} + x_{min}}{2} + \frac{x_{max} - x_{min}}{2} \cdot \sin(x_{khan})
180
+
181
+ Where :math:`x` is the pagmo decision vector and :math:`x_{khan}` is the decision vector
182
+ passed to OPTGRA. In this way parameter bounds are guaranteed to be satisfied, but the gradients
183
+ near the bounds approach zero.
184
+ """ # noqa: W605
185
+
186
+ def __init__ (self , lb : List [float ], ub : List [float ], unity_gradient : bool = True ):
187
+ """Constructor
188
+
189
+ Parameters
190
+ ----------
191
+ lb : List[float]
192
+ Lower pagmo parameter bounds
193
+ ub : List[float]
194
+ Upper pagmo parameter bounds
195
+ unity_gradient : bool, optional
196
+ Uses an internal scaling that ensures that the derivative of pagmo parameters w.r.t.
197
+ Khan parameters are unity at (lb + ub)/2. By default True.
198
+ Otherwise, the original Khan method is used that can result in strongly modified
199
+ gradients
200
+ """
201
+ # call parent class constructor
202
+ super ().__init__ (lb , ub )
203
+
204
+ # determine coefficients inside the sin function
205
+ self ._a = 2 / (self ._ub_masked - self ._lb_masked ) if unity_gradient else 1.0
206
+ self ._b = (
207
+ - (self ._ub_masked + self ._lb_masked ) / (self ._ub_masked - self ._lb_masked )
208
+ if unity_gradient
209
+ else 0.0
210
+ )
211
+
212
+ def _eval (self , x_khan_masked : np .ndarray ) -> np .ndarray :
213
+ return (self ._ub_masked + self ._lb_masked ) / 2 + (
214
+ self ._ub_masked - self ._lb_masked
215
+ ) / 2 * np .sin (x_khan_masked * self ._a + self ._b )
216
+
217
+ def _eval_inv (self , x_masked : np .ndarray ) -> np .ndarray :
218
+ arg = (2 * x_masked - self ._ub_masked - self ._lb_masked ) / (
219
+ self ._ub_masked - self ._lb_masked
220
+ )
221
+
222
+ clip_value = 1.0 - 1e-8 # avoid boundaries
223
+ if np .any ((arg < - clip_value ) | (arg > clip_value )):
224
+ print (
225
+ "WARNING: Numerical inaccuracies encountered during khan_function inverse." ,
226
+ "Clipping parameters to valid range." ,
227
+ )
228
+ arg = np .clip (arg , - clip_value , clip_value )
229
+ return (np .arcsin (arg ) - self ._b ) / self ._a
230
+
231
+ def _eval_grad (self , x_khan_masked : np .ndarray ) -> np .ndarray :
232
+ return (
233
+ (self ._ub_masked - self ._lb_masked )
234
+ / 2
235
+ * np .cos (self ._a * x_khan_masked + self ._b )
236
+ * self ._a
237
+ )
238
+
239
+ def _eval_inv_grad (self , x_masked : np .ndarray ) -> np .ndarray :
240
+ return (
241
+ - 1
242
+ / self ._a
243
+ / (
244
+ (self ._lb_masked - self ._ub_masked )
245
+ * np .sqrt (
246
+ ((self ._lb_masked - x_masked ) * (x_masked - self ._ub_masked ))
247
+ / (self ._ub_masked - self ._lb_masked ) ** 2
248
+ )
249
+ )
250
+ )
251
+
252
+
253
+ class khan_function_tanh (base_khan_function ):
254
+ r"""Function to smothly enforce optimisation parameter bounds using the hyperbolic tangent:
255
+
256
+ .. math::
257
+
258
+ x = \frac{x_{max} + x_{min}}{2} + \frac{x_{max} - x_{min}}{2} \cdot \tanh(x_{khan})
259
+
260
+ Where :math:`x` is the pagmo decision vector and :math:`x_{khan}` is the decision vector
261
+ passed to OPTGRA. In this way parameter bounds are guaranteed to be satisfied, but the gradients
262
+ near the bounds approach zero.
263
+ """ # noqa: W605
264
+
265
+ def __init__ (self , lb : List [float ], ub : List [float ], unity_gradient : bool = True ):
266
+ """Constructor
267
+
268
+ Parameters
269
+ ----------
270
+ lb : List[float]
271
+ Lower pagmo parameter bounds
272
+ ub : List[float]
273
+ Upper pagmo parameter bounds
274
+ unity_gradient : bool, optional
275
+ Uses an internal scaling that ensures that the derivative of pagmo parameters w.r.t.
276
+ khan parameters are unity at (lb + ub)/2. By default True.
277
+ Otherwise, the original Khan method is used that can result in strongly modified
278
+ gradients
279
+ """
280
+ # call parent class constructor
281
+ super ().__init__ (lb , ub )
282
+
283
+ # define amplification factor to avoid bounds to be only reached at +/- infinity
284
+ amp = 1.0 + 1e-3
285
+
286
+ # define the clip value (we avoid the boundaries of the parameters by this much)
287
+ self .clip_value = 1.0 - 1e-6
288
+
289
+ # determine coefficients inside the tanh function
290
+ self ._diff_masked = amp * (self ._ub_masked - self ._lb_masked )
291
+ self ._sum_masked = self ._ub_masked + self ._lb_masked
292
+ self ._a = 2 / self ._diff_masked if unity_gradient else 1.0
293
+ self ._b = - self ._sum_masked / self ._diff_masked if unity_gradient else 0.0
294
+
295
+ def _eval (self , x_khan_masked : np .ndarray ) -> np .ndarray :
296
+ return self ._sum_masked / 2 + self ._diff_masked / 2 * np .tanh (
297
+ x_khan_masked * self ._a + self ._b
298
+ )
299
+
300
+ def _eval_inv (self , x_masked : np .ndarray ) -> np .ndarray :
301
+ arg = (2 * x_masked - self ._sum_masked ) / (self ._diff_masked )
302
+
303
+ if np .any ((arg < - self .clip_value ) | (arg > self .clip_value )):
304
+ print (
305
+ "WARNING: Numerical inaccuracies encountered during khan_function inverse." ,
306
+ "Clipping parameters to valid range." ,
307
+ )
308
+ arg = np .clip (arg , - self .clip_value , self .clip_value )
309
+ return (np .arctanh (arg ) - self ._b ) / self ._a
310
+
311
+ def _eval_grad (self , x_khan_masked : np .ndarray ) -> np .ndarray :
312
+ return self ._diff_masked / 2 / np .cosh (self ._a * x_khan_masked + self ._b ) ** 2 * self ._a
313
+
314
+ def _eval_inv_grad (self , x_masked : np .ndarray ) -> np .ndarray :
315
+
316
+ return (2 * self ._diff_masked ) / (
317
+ self ._a * (self ._diff_masked ** 2 - (2 * x_masked - self ._sum_masked ) ** 2 )
318
+ )
319
+
320
+
211
321
class optgra :
212
322
"""
213
323
This class is a user defined algorithm (UDA) providing a wrapper around OPTGRA, which is written
@@ -247,7 +357,7 @@ def _wrap_fitness_func(
247
357
problem ,
248
358
bounds_to_constraints : bool = True ,
249
359
force_bounds : bool = False ,
250
- khanf : Optional [khan_function ] = None ,
360
+ khanf : Optional [base_khan_function ] = None ,
251
361
):
252
362
# get problem parameters
253
363
lb , ub = problem .get_bounds ()
@@ -289,7 +399,7 @@ def _wrap_gradient_func(
289
399
problem ,
290
400
bounds_to_constraints : bool = True ,
291
401
force_bounds = False ,
292
- khanf : Optional [khan_function ] = None ,
402
+ khanf : Optional [base_khan_function ] = None ,
293
403
):
294
404
295
405
# get the sparsity pattern to index the sparse gradients
@@ -369,7 +479,7 @@ def __init__(
369
479
merit_function_threshold : float = 1e-6 ,
370
480
# bound_constraints_scalar: float = 1,
371
481
force_bounds : bool = False ,
372
- khan_bounds : bool = False ,
482
+ khan_bounds : Union [ str , bool ] = False ,
373
483
optimization_method : int = 2 ,
374
484
log_level : int = 0 ,
375
485
) -> None :
@@ -416,18 +526,21 @@ def __init__(
416
526
If active, the gradients evaluated near the bounds will be inacurate potentially
417
527
leading to convergence issues.
418
528
khan_bounds: optional - whether to gracefully enforce bounds on the decision vector
419
- using Michael Khan's method:
529
+ using Michael Khan's method, by default False. :
420
530
421
531
.. math::
422
532
423
533
x = \frac{x_{max} + x_{min}}{2} + \frac{x_{max} - x_{min}}{2} \cdot \sin(x_{Khan})
424
534
425
535
Where :math:`x` is the pagmo decision vector and :math:`x_{Khan}` is the decision
426
536
vector passed to OPTGRA. In this way parameter bounds are guaranteed to be
427
- satisfied, but the gradients near the bounds approach zero. By default False.
537
+ satisfied, but the gradients near the bounds approach zero.
428
538
Pyoptgra uses a variant of the above method that additionally scales the
429
539
argument of the :math:`\sin` function such that the derivatives
430
540
:math:`\frac{d x_{Khan}}{d x}` are unity in the center of the box bounds.
541
+ Alternatively, to a :math:`\sin` function, also a :math:`\tanh` can be
542
+ used as a Khan function.
543
+ Valid input values are: True (same as 'sin'),'sin', 'tanh' and False.
431
544
optimization_method: select 0 for steepest descent, 1 for modified spectral conjugate
432
545
gradient method, 2 for spectral conjugate gradient method and 3 for conjugate
433
546
gradient method
@@ -609,7 +722,17 @@ def evolve(self, population):
609
722
idx = list (population .get_ID ()).index (selected [0 ][0 ])
610
723
611
724
# optional Khan function to enforce parameter bounds
612
- khanf = khan_function (* problem .get_bounds ()) if self .khan_bounds else None
725
+ if self .khan_bounds in ("sin" , True ):
726
+ khanf = khan_function_sin (* problem .get_bounds ())
727
+ elif self .khan_bounds == "tanh" :
728
+ khanf = khan_function_tanh (* problem .get_bounds ())
729
+ elif self .khan_bounds :
730
+ raise ValueError (
731
+ f"Unrecognised option, { self .khan_bounds } , passed for 'khan_bounds'. "
732
+ "Supported options are 'sin', 'tanh' or None."
733
+ )
734
+ else :
735
+ khanf = None
613
736
614
737
fitness_func = optgra ._wrap_fitness_func (
615
738
problem , self .bounds_to_constraints , self .force_bounds , khanf
0 commit comments