# -*- coding: utf-8 -*-
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Contributors:
# INITIAL AUTHORS - initial API and implementation and/or initial
# documentation
# :author: Matthias De Lozzo
# OTHER AUTHORS - MACROSCOPIC CHANGES
"""scipy.optimize global optimization library wrapper."""
from __future__ import division, unicode_literals
import logging
from distutils.version import LooseVersion
from typing import Any, Dict, Mapping, Tuple, Union
import scipy
from numpy import inf as np_inf
from numpy import isfinite, ndarray, real
from scipy import optimize
from scipy.optimize import NonlinearConstraint
from gemseo.algos.opt.opt_lib import OptimizationLibrary
from gemseo.algos.opt_result import OptimizationResult
from gemseo.utils.py23_compat import PY2
LOGGER = logging.getLogger(__name__)
[docs]class ScipyGlobalOpt(OptimizationLibrary):
"""Scipy optimization library interface.
See OptimizationLibrary.
"""
LIB_COMPUTE_GRAD = True
def __init__(self):
"""Constructor.
Generate the library dict, contains the list
of algorithms with their characteristics:
- does it require gradient
- does it handle equality constraints
- does it handle inequality constraints
"""
super(ScipyGlobalOpt, self).__init__()
doc = "https://docs.scipy.org/doc/scipy/reference/generated/"
self.lib_dict = {
"DUAL_ANNEALING": {
self.INTERNAL_NAME: "dual_annealing",
self.REQUIRE_GRAD: False,
self.POSITIVE_CONSTRAINTS: False,
self.HANDLE_EQ_CONS: False,
self.HANDLE_INEQ_CONS: False,
self.DESCRIPTION: "Dual annealing",
self.WEBSITE: doc + "scipy.optimize.dual_annealing.html",
},
"SHGO": {
self.INTERNAL_NAME: "shgo",
self.REQUIRE_GRAD: False,
self.POSITIVE_CONSTRAINTS: True,
self.HANDLE_EQ_CONS: True,
self.HANDLE_INEQ_CONS: True,
self.DESCRIPTION: "Simplicial homology global optimization",
self.WEBSITE: doc + "scipy.optimize.shgo.html",
},
"DIFFERENTIAL_EVOLUTION": {
self.INTERNAL_NAME: "differential_evolution",
self.REQUIRE_GRAD: False,
self.POSITIVE_CONSTRAINTS: False,
self.HANDLE_EQ_CONS: True,
self.HANDLE_INEQ_CONS: True,
self.DESCRIPTION: "Differential Evolution algorithm",
self.WEBSITE: doc + "scipy.optimize.differential_evolution.html",
},
}
# maximum function calls option passed to the algorithm
self.max_func_calls = 1000000000
scipy_version_ok = LooseVersion(scipy.__version__) >= LooseVersion("1.4.0")
if PY2 or not scipy_version_ok:
for algo in ["DIFFERENTIAL_EVOLUTION", "SHGO"]:
self.lib_dict[algo][self.HANDLE_EQ_CONS] = False
self.lib_dict[algo][self.HANDLE_INEQ_CONS] = False
# Causes overflow due to py2
self.max_func_calls = 1000000
def _get_options(
self,
max_iter=999, # type: int
ftol_rel=1e-9, # type: float
ftol_abs=1e-9, # type: float
xtol_rel=1e-9, # type: float
xtol_abs=1e-9, # type: float
workers=1, # type: int
updating="immediate", # type: str
atol=0.0, # type: float
init="latinhypercube", # type: str
recombination=0.7, # type: float
tol=0.01, # type: float
popsize=15, # type: int
strategy="best1bin", # type: str
sampling_method="simplicial", # type: str
niters=1, # type: int
n=100, # type: int
seed=1, # type: int
polish=True, # type: bool
iters=1, # type: int
eq_tolerance=1e-6, # type: float
ineq_tolerance=1e-6, # type: float
normalize_design_space=True, # type: bool
local_options=None, # type: Mapping[str, Any]
**kwargs # type: Any
): # type: (...) -> Dict[str, Any] # pylint: disable=W0221
r"""Set the options default values.
To get the best and up to date information about algorithms options,
go to scipy.optimize documentation:
https://docs.scipy.org/doc/scipy/reference/tutorial/optimize.html
Args:
max_iter: The maximum number of iterations, i.e. unique calls to f(x).
ftol_rel: A stop criteria, the relative tolerance on the
objective function.
If abs(f(xk)-f(xk+1))/abs(f(xk))<= ftol_rel: stop.
ftol_abs: A stop criteria, the absolute tolerance on the objective
function. If abs(f(xk)-f(xk+1))<= ftol_rel: stop.
xtol_rel: A stop criteria, the relative tolerance on the
design variables. If norm(xk-xk+1)/norm(xk)<= xtol_rel: stop.
xtol_abs: A stop criteria, the absolute tolerance on the
design variables. If norm(xk-xk+1)<= xtol_abs: stop.
workers: The number of processes for parallel execution.
updating: The strategy to update the solution vector.
If ``"immediate"``, the best solution vector is continuously
updated within a single generation.
With 'deferred',
the best solution vector is updated once per generation.
Only 'deferred' is compatible with parallelization,
and the ``workers`` keyword can over-ride this option.
atol: The absolute tolerance for convergence.
init: Either the type of population initialization to be used
or an array specifying the initial population.
recombination: The recombination constant, should be in the
range [0, 1].
tol: The relative tolerance for convergence.
popsize: A multiplier for setting the total population size.
The population has popsize * len(x) individuals.
strategy: The differential evolution strategy to use.
sampling_method: The method to compute the initial points.
Current built in sampling method options
are ``halton``, ``sobol`` and ``simplicial``.
n: The number of sampling points
used in the construction of the simplicial complex.
seed: The seed to be used for repeatable minimizations.
If None, the ``numpy.random.RandomState`` singleton is used.
polish: Whether to use the L-BFGS-B algorithm
to polish the best population member at the end.
mutation: The mutation constant.
recombination: The recombination constant.
initial_temp: The initial temperature.
visit: The parameter for the visiting distribution.
accept: The parameter for the acceptance distribution.
niters: The number of iterations
used in the construction of the simplicial complex.
restart_temp_ratio: During the annealing process,
temperature is decreasing,
when it reaches ``initial_temp * restart_temp_ratio``,
the reannealing process is triggered.
normalize_design_space: If True, variables are scaled in [0, 1].
eq_tolerance: The tolerance on equality constraints.
ineq_tolerance: The tolerance on inequality constraints.
n: The number of sampling points used in the construction
of the simplicial complex.
iters: The number of iterations used in the construction
of the simplicial complex.
local_options: The options for the local optimization algorithm,
only for shgo, see scipy.optimize doc.
**kwargs: The other algorithms options.
"""
return self._process_options(
max_iter=max_iter,
ftol_rel=ftol_rel,
ftol_abs=ftol_abs,
xtol_rel=xtol_rel,
xtol_abs=xtol_abs,
workers=workers,
updating=updating,
init=init,
tol=tol,
atol=atol,
recombination=recombination,
seed=seed,
popsize=popsize,
strategy=strategy,
sampling_method=sampling_method,
iters=iters,
n=n,
polish=polish,
eq_tolerance=eq_tolerance,
ineq_tolerance=ineq_tolerance,
normalize_design_space=normalize_design_space,
local_options=local_options,
**kwargs
)
[docs] def iter_callback(
self,
x_vect, # type: ndarray
): # type: (...) -> None
"""Call the objective and constraints functions.
Args:
x_vect: The input data with which to call the functions.
"""
if self.normalize_ds:
x_vect = self.problem.design_space.normalize_vect(x_vect)
self.problem.objective(x_vect)
for constraint in self.problem.constraints:
constraint(x_vect)
[docs] def real_part_obj_fun(
self,
x, # type: ndarray
): # type: (...) -> Union[int, float]
"""Wrap the function and return the real part.
Args:
x: The values to be given to the function.
Returns:
The real part of the evaluation of the objective function.
"""
return real(self.problem.objective.func(x))
def _run(
self, **options # type: Any
): # type: (...) -> OptimizationResult
"""Run the algorithm, to be overloaded by subclasses.
Args:
**options: The options for the algorithm.
Returns:
The optimization result.
"""
# remove normalization from options for algo
self.normalize_ds = options.pop(self.NORMALIZE_DESIGN_SPACE_OPTION, True)
# Get the normalized bounds:
_, l_b, u_b = self.get_x0_and_bounds_vects(self.normalize_ds)
# Replace infinite values with None:
l_b = [val if isfinite(val) else None for val in l_b]
u_b = [val if isfinite(val) else None for val in u_b]
bounds = list(zip(l_b, u_b))
# This is required because some algorithms do not
# call the objective very often when the problem
# is very constrained (Power2) and OptProblem may fail
# to detect the optimum.
if self.problem.has_constraints():
self.problem.add_callback(self.iter_callback)
local_options = options.get("local_options")
if self.internal_algo_name == "dual_annealing":
opt_result = optimize.dual_annealing(
func=self.real_part_obj_fun,
bounds=bounds,
maxiter=self.max_func_calls,
local_search_options={},
initial_temp=5230.0,
restart_temp_ratio=2e-05,
visit=2.62,
accept=-5.0,
maxfun=self.max_func_calls,
seed=options["seed"],
no_local_search=False,
callback=None,
x0=None,
)
elif self.internal_algo_name == "shgo":
constraints = self.__get_constraints_as_scipy_dictionary()
opt_result = optimize.shgo(
func=self.real_part_obj_fun,
bounds=bounds,
args=(),
n=options["n"],
iters=options["iters"],
callback=None,
minimizer_kwargs=None,
sampling_method=options["sampling_method"],
constraints=constraints,
options=local_options,
)
elif self.internal_algo_name == "differential_evolution":
if PY2:
opts = {}
else:
opts = {"constraints": self.__get_non_linear_constraints()}
opt_result = optimize.differential_evolution(
func=self.real_part_obj_fun,
bounds=bounds,
args=(),
strategy=options["strategy"],
maxiter=self.max_func_calls,
popsize=options["popsize"],
tol=options["tol"],
atol=options["atol"],
mutation=options.get("mutation", (0.5, 1)),
recombination=options["recombination"],
seed=options["seed"],
polish=options["polish"],
init=options["init"],
updating=options["updating"],
workers=options["workers"],
**opts
)
else: # pragma: no cover
raise ValueError("Unknown algorithm: {}.".format(self.internal_algo_name))
return self.get_optimum_from_database(opt_result.message, opt_result.success)
def __get_non_linear_constraints(self): # type: (...) -> Tuple[NonlinearConstraint]
"""Create the constraints to be passed to a SciPy algorithm as
NonLinearConstraints.
:return: The constraints.
:rtype: tuple(NonLinearConstraint)
"""
constraints = []
ineq_tolerance = self.problem.ineq_tolerance
eq_tolerance = self.problem.eq_tolerance
for constr in self.problem.get_eq_constraints():
constraints.append(
NonlinearConstraint(constr, -eq_tolerance, eq_tolerance, jac=constr.jac)
)
for constr in self.problem.get_ineq_constraints():
constraints.append(
NonlinearConstraint(constr, -np_inf, ineq_tolerance, jac=constr.jac)
)
return tuple(constraints)
def __get_constraints_as_scipy_dictionary(self):
"""Create the constraints to be passed to a SciPy algorithm as a dictionary.
:return: The constraints.
:rtype: list(dict)
"""
constraints = self.get_right_sign_constraints()
scipy_constraints = []
for constraint in constraints:
scipy_constraint = {
"type": constraint.f_type,
"fun": constraint.func,
"jac": constraint.jac,
}
scipy_constraints.append(scipy_constraint)
return scipy_constraints