Source code for gemseo.algos.opt.core.trust_updater

# -*- 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: Francois Gallard
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
#        :author: Benoît Pauwels - refactoring
"""
Updates a trust parameter according to a decreases ratio
********************************************************
"""

from __future__ import absolute_import, division, print_function, unicode_literals

from builtins import str, super

from future import standard_library
from numpy import divide, maximum, minimum, multiply, ones

standard_library.install_aliases()


from gemseo import LOGGER


[docs]class TrustUpdater(object): """Updates the trust parameter.""" def __init__(self, thresholds=None, multipliers=None, bound=None): """Initializer. :param thresholds: thresholds for the decreases ratio :type thresholds: tuple :param multipliers: multipliers for the trust parameter :type multipliers: tuple :param bound: (lower or upper) bound for the trust parameter """ if not isinstance(thresholds, tuple): raise ValueError( "The thresholds must be input as a tuple; " "input of type " + type(thresholds) + " was provided." ) self._ratio_thresholds = thresholds if not isinstance(multipliers, tuple): raise ValueError( "The multipliers must be input as a tuple; " "input of type " + type(multipliers) + " was provided." ) self._param_multipliers = multipliers self._param_bound = bound # bound for the trust parameter def _check(self): """Checks attributes consistency.""" raise NotImplementedError()
[docs] def update(self, ratio, parameter): """Updates the trust parameter relative to the decreases ratio value. Method to be overidden by subclasses. :param ratio: decreases ratio :param parameter: trust parameter (radius or penalty) :returns: new trust parameter, iteration success :rtype: bool """ raise NotImplementedError()
[docs]class PenaltyUpdater(TrustUpdater): """Updates the penalty parameter.""" def __init__(self, thresholds=(0.0, 0.25), multipliers=(0.5, 2.0), bound=1e-6): """Initializer. :param thresholds: thresholds for the decreases ratio :type thresholds: tuple :param multipliers: multipliers for the penalty parameter :type multipliers: tuple :param bound: lower bound for the penalty parameter """ super(PenaltyUpdater, self).__init__(thresholds, multipliers, bound) self._check() def _check(self): """Checks the attributes of the penalty updater.""" # Check the thresholds: if len(self._ratio_thresholds) != 2: raise ValueError( "There must be exactly two thresholds for the " "decreases ratio; " + str(len(self._ratio_thresholds)) + " were given." ) update_thresh = self._ratio_thresholds[0] nonexp_thresh = self._ratio_thresholds[1] if update_thresh > nonexp_thresh: raise ValueError( "The update threshold (" + str(update_thresh) + ") must be lower than or equal to " + "the non-expansion threshold (" + str(nonexp_thresh) + ")." ) # Check the multipliers: if len(self._param_multipliers) != 2: raise ValueError( "There must be exactly two multipliers for the " "penalty parameter; " + str(len(self._ratio_thresholds)) + " were given." ) contract_fact = self._param_multipliers[0] expan_fact = self._param_multipliers[1] if contract_fact >= 1.0: raise ValueError( "The contraction factor (" + str(contract_fact) + ") must be lower than one." ) if expan_fact < 1.0: raise ValueError( "The expansion factor (" + str(expan_fact) + ") must be greater than or equal to one." )
[docs] def update(self, ratio, parameter): """Updates the penalty parameter. :param ratio: decreases ratio :param parameter: penalty parameter :returns: new penalty parameter, iteration success (boolean) """ # The iteration is declared successful if and only if the ratio is # greater than or equal to the lower threshold. success = ratio >= self._ratio_thresholds[0] # If the ratio is greater than or equal to the upper threshold, the # penalty parameter is not increased, otherwise it is increased. if ratio >= self._ratio_thresholds[1]: new_penalty = parameter * self._param_multipliers[0] if self._param_bound is not None and new_penalty < self._param_bound: new_penalty = 0.0 else: if self._param_bound is not None and parameter == 0.0: new_penalty = self._param_bound else: new_penalty = parameter * self._param_multipliers[1] return new_penalty, success
[docs]class RadiusUpdater(TrustUpdater): """Updates the trust region radius.""" def __init__(self, thresholds=(0.0, 0.25), multipliers=(0.5, 2.0), bound=1000.0): """Initializer. :param thresholds: thresholds for the decreases ratio :type thresholds: tuple :param multipliers: multipliers for the region radius :type multipliers: tuple :param bound: lower bound for the region radius """ super(RadiusUpdater, self).__init__(thresholds, multipliers, bound) self._check() def _check(self): """Checks the attributes of the radius updater.""" # Check the thresholds: if len(self._ratio_thresholds) != 2: raise ValueError( "There must be exactly two thresholds for the " "decreases ratio; " + str(len(self._ratio_thresholds)) + " were given." ) update_thresh = self._ratio_thresholds[0] noncontract_thresh = self._ratio_thresholds[1] if update_thresh > noncontract_thresh: raise ValueError( "The update threshold (" + str(update_thresh) + ") must be lower than or equal to the " "non-contraction threshold (" + str(noncontract_thresh) + ")." ) # Check the multipliers: if len(self._param_multipliers) != 2: raise ValueError( "There must be exactly two multipliers for the " "region radius; " + str(len(self._ratio_thresholds)) + " were given." ) contract_fact = self._param_multipliers[0] expan_fact = self._param_multipliers[1] if contract_fact > 1.0: raise ValueError( "The contraction factor (" + str(contract_fact) + ") must be lower than or equal to one." ) if expan_fact <= 1.0: raise ValueError( "The expansion factor (" + str(expan_fact) + ") must be greater than one." )
[docs] def update(self, ratio, parameter): """Updates the trust radius. :param ratio: decreases ratio :param parameter: region radius :returns: new region radius, iteration success (boolean) """ # The iteration is declared successful if and only if the ratio is # greater than or equal to the lower threshold. success = ratio >= self._ratio_thresholds[0] # If the ratio is greater than or equal to the upper threshold, the # region radius is not decreased, otherwise it is decreased. if ratio >= self._ratio_thresholds[1]: new_radius = parameter * self._param_multipliers[1] if self._param_bound is not None: new_radius = min(new_radius, self._param_bound) else: new_radius = parameter * self._param_multipliers[0] return new_radius, success
[docs]class BoundsUpdater(object): """Updates trust bounds, i.e. trust ball w.r.t. the infinity norm.""" def __init__(self, lower_bounds, upper_bounds, normalize=False): """Initializer. :param lower_bounds: reference lower bounds :type lower_bounds: ndarray :param upper_bounds: reference upper bounds :type upper_bounds: ndarray :param normalize: if True the radius is applied to the normalized bounds :param normalize: bool, optional """ self._lower_bounds = lower_bounds self._upper_bounds = upper_bounds self._normalized_update = normalize
[docs] def update(self, radius, center): """ Updates the trust bounds. :param radius: region radius w.r.t. the infinity norm :type ratio: float :param center: region center :type center: ndarray :returns: new region radius, iteration success (boolean) """ if not self._normalized_update: low_bnds, upp_bnds = self._compute_trust_bounds( self._lower_bounds, self._upper_bounds, center, radius ) else: norm_center = self._normalize(center) ones_vect = ones(center.size) low_bnds, upp_bnds = self._compute_trust_bounds( -ones_vect, ones_vect, norm_center, radius ) low_bnds = self._unnormalize(low_bnds) upp_bnds = self._unnormalize(upp_bnds) return low_bnds, upp_bnds
@staticmethod def _compute_trust_bounds(lower_bounds, upper_bounds, center, radius): """Updates bounds based on a ball center and ball radius w.r.t. the infinity norm. :param lower_bounds: lower bounds to be updated :type lower_bounds: ndarray :param upper_bounds: upper bounds to be updated :type upper_bounds: ndarray :param center: ball center :type center: ndarray :param radius: ball radius :type radius: float :returns: updated lower bounds, updated upper bounds :rtype: ndarray, ndarray """ lower_trust_bounds = minimum( maximum(center - radius, lower_bounds), upper_bounds ) upper_trust_bounds = maximum( minimum(center + radius, upper_bounds), lower_bounds ) return lower_trust_bounds, upper_trust_bounds def _normalize(self, x_vect): """ Normalize a vector coordinates to [-1, 1]. """ x_norm = 2.0 * x_vect - self._upper_bounds - self._lower_bounds x_norm = divide(x_norm, self._upper_bounds - self._lower_bounds) return x_norm def _unnormalize(self, x_norm): """ Unnormalize a vector coordinates from [-1, 1]. """ x_vect = multiply(x_norm, (self._upper_bounds - self._lower_bounds)) x_vect = (x_vect + self._upper_bounds + self._lower_bounds) / 2.0 return x_vect