1# Copyright 2020, 2021 PaGMO development team 2# 3# This file is part of the pygmo library. 4# 5# This Source Code Form is subject to the terms of the Mozilla 6# Public License v. 2.0. If a copy of the MPL was not distributed 7# with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 9# Import the unconstrain meta-problem so that we can re-use 10# the docstring of its inner_problem property in the documentation 11# of the inner_problem property of decorator_problem. 12from .core import unconstrain as _unconstrain 13 14 15def _with_decorator(f): 16 # A decorator that will decorate the input method f of a decorator_problem 17 # with one of the decorators stored inside the problem itself, in the _decors 18 # dictionary. 19 from functools import wraps 20 21 @wraps(f) 22 def wrapper(self, *args, **kwds): 23 dec = self._decors.get(f.__name__) 24 if dec is None: 25 return f(self, *args, **kwds) 26 else: 27 return dec(f)(self, *args, **kwds) 28 return wrapper 29 30 31def _add_doc(value): 32 # Small decorator for changing the docstring 33 # of a function to 'value'. See: 34 # https://stackoverflow.com/questions/4056983/how-do-i-programmatically-set-the-docstring 35 def _doc(func): 36 func.__doc__ = value 37 return func 38 return _doc 39 40 41class decorator_problem(object): 42 """Decorator meta-problem. 43 44 .. versionadded:: 2.9 45 46 This meta-problem allows to apply arbitrary transformations to the functions 47 of a PyGMO :class:`~pygmo.problem` via Python decorators. 48 49 The decorators are passed as keyword arguments during initialisation, and they 50 must be named after the function they are meant to decorate plus the 51 ``_decorator`` suffix. For instance, we can define a minimal decorator 52 for the fitness function as follows: 53 54 >>> def f_decor(orig_fitness_function): 55 ... def new_fitness_function(self, dv): 56 ... print("Evaluating dv: {}".format(dv)) 57 ... return orig_fitness_function(self, dv) 58 ... return new_fitness_function 59 60 This decorator will print the input decision vector *dv* before invoking the 61 original fitness function. We can then construct a decorated Rosenbrock problem 62 as follows: 63 64 >>> from pygmo import decorator_problem, problem, rosenbrock 65 >>> dprob = problem(decorator_problem(rosenbrock(), fitness_decorator=f_decor)) 66 67 We can then verify that calling the fitness function of *dprob* will print 68 the decision vector before returning the fitness value: 69 70 >>> fv = dprob.fitness([1, 2]) 71 Evaluating dv: [1. 2.] 72 >>> print(fv) 73 [100.] 74 75 An extended :ref:`tutorial <py_tutorial_udp_meta_decorator>` on the usage of this class is available 76 in PyGMO's documentation. 77 78 All the functions in the public API of a UDP can be decorated (see the documentation 79 of :class:`pygmo.problem` for the full list). Note that the public API of :class:`~pygmo.decorator_problem` 80 includes the UDP public API: there is a ``fitness()`` method, methods to query the problem's properties, 81 sparsity-related methods, etc. In order to avoid duplication, we do not repeat here the documentation of 82 the UDP API and we document instead only the few methods which are specific to :class:`~pygmo.decorator_problem`. 83 Users can refer to the documentation of :class:`pygmo.problem` for detailed information on the UDP API. 84 85 Both *prob* and the decorators will be deep-copied inside the instance upon construction. As 86 usually done in meta-problems, this class will store as an internal data member a :class:`~pygmo.problem` 87 containing a copy of *prob* (this is commonly referred to as the *inner problem* of the 88 meta-problem). The inner problem is accessible via the :attr:`~pygmo.decorator_problem.inner_problem` 89 read-only property. 90 91 """ 92 93 def __init__(self, prob=None, **kwargs): 94 """ 95 Args: 96 97 prob: a :class:`~pygmo.problem` or a user-defined problem, either C++ or Python (if 98 *prob* is :data:`None`, a :class:`~pygmo.null_problem` will be used in its stead) 99 kwargs: the dictionary of decorators to be applied to the functions of the input problem 100 101 Raises: 102 103 TypeError: if at least one of the values in *kwargs* is not callable 104 unspecified: any exception thrown by the constructor of :class:`~pygmo.problem` or the deep copy 105 of *prob* or *kwargs* 106 107 """ 108 from . import problem, null_problem 109 from warnings import warn 110 from copy import deepcopy 111 if prob is None: 112 prob = null_problem() 113 if type(prob) == problem: 114 # If prob is a pygmo problem, we will make a copy 115 # and store it. The copy is to ensure consistent behaviour 116 # with the other meta problems and with the constructor 117 # from a UDP (which will end up making a deep copy of 118 # the input object). 119 self._prob = deepcopy(prob) 120 else: 121 # Otherwise, we attempt to create a problem from it. This will 122 # work if prob is an exposed C++ problem or a Python UDP. 123 self._prob = problem(prob) 124 self._decors = {} 125 for k in kwargs: 126 if k.endswith("_decorator"): 127 if not callable(kwargs[k]): 128 raise TypeError( 129 "Cannot register the decorator for the '{}' method: the supplied object " 130 "'{}' is not callable.".format(k[:-10], kwargs[k])) 131 self._decors[k[:-10]] = deepcopy(kwargs[k]) 132 else: 133 warn("A keyword argument without the '_decorator' suffix, '{}', was used in the " 134 "construction of a decorator problem. This keyword argument will be ignored.".format(k)) 135 136 @_with_decorator 137 def fitness(self, dv): 138 return self._prob.fitness(dv) 139 140 @_with_decorator 141 def batch_fitness(self, dvs): 142 return self._prob.batch_fitness(dvs) 143 144 @_with_decorator 145 def has_batch_fitness(self): 146 return self._prob.has_batch_fitness() 147 148 @_with_decorator 149 def get_bounds(self): 150 return self._prob.get_bounds() 151 152 @_with_decorator 153 def get_nobj(self): 154 return self._prob.get_nobj() 155 156 @_with_decorator 157 def get_nec(self): 158 return self._prob.get_nec() 159 160 @_with_decorator 161 def get_nic(self): 162 return self._prob.get_nic() 163 164 @_with_decorator 165 def get_nix(self): 166 return self._prob.get_nix() 167 168 @_with_decorator 169 def has_gradient(self): 170 return self._prob.has_gradient() 171 172 @_with_decorator 173 def gradient(self, dv): 174 return self._prob.gradient(dv) 175 176 @_with_decorator 177 def has_gradient_sparsity(self): 178 return self._prob.has_gradient_sparsity() 179 180 @_with_decorator 181 def gradient_sparsity(self): 182 return self._prob.gradient_sparsity() 183 184 @_with_decorator 185 def has_hessians(self): 186 return self._prob.has_hessians() 187 188 @_with_decorator 189 def hessians(self, dv): 190 return self._prob.hessians(dv) 191 192 @_with_decorator 193 def has_hessians_sparsity(self): 194 return self._prob.has_hessians_sparsity() 195 196 @_with_decorator 197 def hessians_sparsity(self): 198 return self._prob.hessians_sparsity() 199 200 @_with_decorator 201 def has_set_seed(self): 202 return self._prob.has_set_seed() 203 204 @_with_decorator 205 def set_seed(self, s): 206 return self._prob.set_seed(s) 207 208 @_with_decorator 209 def get_name(self): 210 return self._prob.get_name() + " [decorated]" 211 212 @_with_decorator 213 def get_extra_info(self): 214 retval = self._prob.get_extra_info() 215 if len(self._decors) == 0: 216 retval += "\tNo registered decorators.\n" 217 else: 218 retval += "\tRegistered decorators:\n" 219 for i, k in enumerate(self._decors): 220 retval += "\t\t" + k + \ 221 (",\n" if i < len(self._decors) - 1 else "") 222 retval += '\n' 223 return retval 224 225 @property 226 @_add_doc(_unconstrain.inner_problem.__doc__) 227 def inner_problem(self): 228 return self._prob 229 230 def get_decorator(self, fname): 231 """Get the decorator for the function called *fname*. 232 233 This method will return a copy of the decorator that has been registered upon construction 234 for the function called *fname*. If no decorator for *fname* has been specified during 235 construction, :data:`None` will be returned. 236 237 >>> from pygmo import decorator_problem, problem, rosenbrock 238 >>> def f_decor(orig_fitness_function): 239 ... def new_fitness_function(self, dv): 240 ... print("Evaluating dv: {}".format(dv)) 241 ... return orig_fitness_function(self, dv) 242 ... return new_fitness_function 243 >>> dprob = decorator_problem(rosenbrock(), fitness_decorator=f_decor) 244 >>> dprob.get_decorator("fitness") # doctest: +ELLIPSIS 245 <function ...> 246 >>> dprob.get_decorator("gradient") is None 247 True 248 249 Args: 250 251 fname(str): the name of the function whose decorator will be returned 252 253 Returns: 254 255 a copy of the decorator registered for *fname*, or :data:`None` if no decorator for *fname* has been registered 256 257 Raises: 258 259 TypeError: if *fname* is not a string 260 unspecified: any exception thrown by the deep copying of the decorator for *fname* 261 262 """ 263 if not isinstance(fname, str): 264 raise TypeError( 265 "The input parameter 'fname' must be a string, but it is of type '{}' instead.".format(type(fname))) 266 from copy import deepcopy 267 return deepcopy(self._decors.get(fname)) 268