1# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
2#
3# This source code is licensed under the MIT license found in the
4# LICENSE file in the root directory of this source tree.
5
6import numpy as np
7from scipy import optimize as scipyoptimize
8import nevergrad.common.typing as tp
9from nevergrad.parametrization import parameter as p
10from . import base
11from .base import IntOrParameter
12from . import recaster
13
14
15class _ScipyMinimizeBase(recaster.SequentialRecastOptimizer):
16    def __init__(
17        self,
18        parametrization: IntOrParameter,
19        budget: tp.Optional[int] = None,
20        num_workers: int = 1,
21        *,
22        method: str = "Nelder-Mead",
23        random_restart: bool = False,
24    ) -> None:
25        super().__init__(parametrization, budget=budget, num_workers=num_workers)
26        self.multirun = 1  # work in progress
27        self.initial_guess: tp.Optional[tp.ArrayLike] = None
28        # configuration
29        assert method in ["Nelder-Mead", "COBYLA", "SLSQP", "Powell"], f"Unknown method '{method}'"
30        self.method = method
31        self.random_restart = random_restart
32
33    #    def _internal_tell_not_asked(self, x: base.ArrayLike, value: float) -> None:
34    def _internal_tell_not_asked(self, candidate: p.Parameter, value: float) -> None:
35        """Called whenever calling "tell" on a candidate that was not "asked".
36        Defaults to the standard tell pipeline.
37        """  # We do not do anything; this just updates the current best.
38
39    def get_optimization_function(self) -> tp.Callable[[tp.Callable[[tp.ArrayLike], float]], tp.ArrayLike]:
40        # create a different sub-instance, so that the current instance is not referenced by the thread
41        # (consequence: do not create a thread at initialization, or we get a thread explosion)
42        subinstance = self.__class__(
43            parametrization=self.parametrization,
44            budget=self.budget,
45            num_workers=self.num_workers,
46            method=self.method,
47            random_restart=self.random_restart,
48        )
49        subinstance.archive = self.archive
50        subinstance.current_bests = self.current_bests
51        return subinstance._optimization_function
52
53    def _optimization_function(self, objective_function: tp.Callable[[tp.ArrayLike], float]) -> tp.ArrayLike:
54        # pylint:disable=unused-argument
55        budget = np.inf if self.budget is None else self.budget
56        best_res = np.inf
57        best_x: np.ndarray = self.current_bests["average"].x  # np.zeros(self.dimension)
58        if self.initial_guess is not None:
59            best_x = np.array(self.initial_guess, copy=True)  # copy, just to make sure it is not modified
60        remaining = budget - self._num_ask
61        while remaining > 0:  # try to restart if budget is not elapsed
62            options: tp.Dict[str, int] = {} if self.budget is None else {"maxiter": remaining}
63            res = scipyoptimize.minimize(
64                objective_function,
65                best_x if not self.random_restart else self._rng.normal(0.0, 1.0, self.dimension),
66                method=self.method,
67                options=options,
68                tol=0,
69            )
70            if res.fun < best_res:
71                best_res = res.fun
72                best_x = res.x
73            remaining = budget - self._num_ask
74        return best_x
75
76
77class ScipyOptimizer(base.ConfiguredOptimizer):
78    """Wrapper over Scipy optimizer implementations, in standard ask and tell format.
79    This is actually an import from scipy-optimize, including Sequential Quadratic Programming,
80
81    Parameters
82    ----------
83    method: str
84        Name of the method to use among:
85
86        - Nelder-Mead
87        - COBYLA
88        - SQP (or SLSQP): very powerful e.g. in continuous noisy optimization. It is based on
89          approximating the objective function by quadratic models.
90        - Powell
91    random_restart: bool
92        whether to restart at a random point if the optimizer converged but the budget is not entirely
93        spent yet (otherwise, restarts from best point)
94
95    Note
96    ----
97    These optimizers do not support asking several candidates in a row
98    """
99
100    recast = True
101    no_parallelization = True
102
103    # pylint: disable=unused-argument
104    def __init__(self, *, method: str = "Nelder-Mead", random_restart: bool = False) -> None:
105        super().__init__(_ScipyMinimizeBase, locals())
106
107
108NelderMead = ScipyOptimizer(method="Nelder-Mead").set_name("NelderMead", register=True)
109Powell = ScipyOptimizer(method="Powell").set_name("Powell", register=True)
110RPowell = ScipyOptimizer(method="Powell", random_restart=True).set_name("RPowell", register=True)
111Cobyla = ScipyOptimizer(method="COBYLA").set_name("Cobyla", register=True)
112RCobyla = ScipyOptimizer(method="COBYLA", random_restart=True).set_name("RCobyla", register=True)
113SQP = ScipyOptimizer(method="SLSQP").set_name("SQP", register=True)
114SLSQP = SQP  # Just so that people who are familiar with SLSQP naming are not lost.
115RSQP = ScipyOptimizer(method="SLSQP", random_restart=True).set_name("RSQP", register=True)
116RSLSQP = RSQP  # Just so that people who are familiar with SLSQP naming are not lost.
117