2727)
2828
2929
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:
3233
3334 .. math::
3435
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})
3637
3738 Where :math:`x` is the pagmo decision vector and :math:`x_{khan}` is the decision vector
3839 passed to OPTGRA. In this way parameter bounds are guaranteed to be satisfied, but the gradients
3940 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`
4044 """ # noqa: W605
4145
42- def __init__ (self , lb : List [float ], ub : List [float ], unity_gradient : bool = True ):
46+ def __init__ (self , lb : List [float ], ub : List [float ]):
4347 """Constructor
4448
4549 Parameters
@@ -48,11 +52,6 @@ def __init__(self, lb: List[float], ub: List[float], unity_gradient: bool = True
4852 Lower pagmo parameter bounds
4953 ub : List[float]
5054 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
5655 """
5756 self ._lb = np .asarray (lb )
5857 self ._ub = np .asarray (ub )
@@ -86,54 +85,6 @@ def _isfinite(a: np.ndarray):
8685 self ._lb_masked = self ._lb [self .mask ]
8786 self ._ub_masked = self ._ub [self .mask ]
8887
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-
13788 def _apply_to_subset (
13889 self , x : np .ndarray , func : Callable , default_result : Optional [np .ndarray ] = None
13990 ) -> np .ndarray :
@@ -144,6 +95,18 @@ def _apply_to_subset(
14495 result [self .mask ] = func (x [self .mask ])
14596 return result
14697
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+
147110 def eval (self , x_khan : np .ndarray ) -> np .ndarray :
148111 """Convert :math:`x_{optgra}` to :math:`x`.
149112
@@ -208,6 +171,153 @@ def eval_inv_grad(self, x: np.ndarray) -> np.ndarray:
208171 return np .diag (self ._apply_to_subset (np .asarray (x ), self ._eval_inv_grad , np .ones (self ._nx )))
209172
210173
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+
211321class optgra :
212322 """
213323 This class is a user defined algorithm (UDA) providing a wrapper around OPTGRA, which is written
@@ -247,7 +357,7 @@ def _wrap_fitness_func(
247357 problem ,
248358 bounds_to_constraints : bool = True ,
249359 force_bounds : bool = False ,
250- khanf : Optional [khan_function ] = None ,
360+ khanf : Optional [base_khan_function ] = None ,
251361 ):
252362 # get problem parameters
253363 lb , ub = problem .get_bounds ()
@@ -289,7 +399,7 @@ def _wrap_gradient_func(
289399 problem ,
290400 bounds_to_constraints : bool = True ,
291401 force_bounds = False ,
292- khanf : Optional [khan_function ] = None ,
402+ khanf : Optional [base_khan_function ] = None ,
293403 ):
294404
295405 # get the sparsity pattern to index the sparse gradients
@@ -369,7 +479,7 @@ def __init__(
369479 merit_function_threshold : float = 1e-6 ,
370480 # bound_constraints_scalar: float = 1,
371481 force_bounds : bool = False ,
372- khan_bounds : bool = False ,
482+ khan_bounds : Union [ str , bool ] = False ,
373483 optimization_method : int = 2 ,
374484 log_level : int = 0 ,
375485 ) -> None :
@@ -416,18 +526,21 @@ def __init__(
416526 If active, the gradients evaluated near the bounds will be inacurate potentially
417527 leading to convergence issues.
418528 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. :
420530
421531 .. math::
422532
423533 x = \frac{x_{max} + x_{min}}{2} + \frac{x_{max} - x_{min}}{2} \cdot \sin(x_{Khan})
424534
425535 Where :math:`x` is the pagmo decision vector and :math:`x_{Khan}` is the decision
426536 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.
428538 Pyoptgra uses a variant of the above method that additionally scales the
429539 argument of the :math:`\sin` function such that the derivatives
430540 :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.
431544 optimization_method: select 0 for steepest descent, 1 for modified spectral conjugate
432545 gradient method, 2 for spectral conjugate gradient method and 3 for conjugate
433546 gradient method
@@ -609,7 +722,17 @@ def evolve(self, population):
609722 idx = list (population .get_ID ()).index (selected [0 ][0 ])
610723
611724 # 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
613736
614737 fitness_func = optgra ._wrap_fitness_func (
615738 problem , self .bounds_to_constraints , self .force_bounds , khanf
0 commit comments