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