Source code for gemseo.utils.derivatives.gradient_approximator

# -*- 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.

"""Gradient approximation."""
from __future__ import division, unicode_literals

import logging
from typing import Any, Callable, List, Optional, Sequence, Tuple, Union

import six
from custom_inherit import DocInheritMeta
from numpy import array, finfo, float64, ndarray

from gemseo.algos.design_space import DesignSpace
from gemseo.core.factory import Factory
from gemseo.utils.py23_compat import xrange

EPSILON = finfo(float).eps
LOGGER = logging.getLogger(__name__)


[docs]@six.add_metaclass( DocInheritMeta( abstract_base_class=True, style="google_with_merge", include_special_methods=True, ) ) class GradientApproximator(object): """A gradient approximator. Attributes: f_pointer (Callable): The pointer to the function to derive. """ ALIAS = None def __init__( self, f_pointer, # type: Callable[[ndarray],ndarray] step=1e-6, # type: float parallel=False, # type: bool design_space=None, # type: Optional[DesignSpace] normalize=True, # type: bool **parallel_args # type: Union[int,bool,float] ): # type: (...) -> None """ Args: f_pointer: The pointer to the function to derive. step: The default differentiation step. parallel: Whether to differentiate the function in parallel. design_space: The design space containing the upper bounds of the input variables. If None, consider that the input variables are unbounded. normalize: If True, then the functions are normalized. **parallel_args: The parallel execution options, see :mod:`gemseo.core.parallel_execution`. """ self.f_pointer = f_pointer self.__par_args = parallel_args self.__parallel = parallel self._step = None self.step = step self._design_space = design_space self._normalize = normalize @property def _parallel(self): # type: (...) -> bool """Whether to differentiate the function in parallel.""" return self.__parallel @property def _par_args(self): # type: (...) -> Union[int,bool,float] """The parallel execution options.""" return self.__par_args @property def step(self): # type: (...) -> float """The default approximation step.""" return self._step @step.setter def step( self, value, # type: float ): # type: (...) -> None self._step = value
[docs] def f_gradient( self, x_vect, # type: ndarray step=None, # type: Optional[float] x_indices=None, # type: Optional[Sequence[int]] **kwargs # type: Any ): # type: (...) -> ndarray """Approximate the gradient of the function for a given input vector. Args: x_vect: The input vector. step: The differentiation step. If None, use the default differentiation step. x_indices: The components of the input vector to be used for the differentiation. If None, use all the components. **kwargs: The optional arguments for the function. Returns: The approximated gradient. """ input_dimension = len(x_vect) input_perturbations, steps = self.generate_perturbations( input_dimension, x_vect, x_indices=x_indices, step=step ) n_perturbations = input_perturbations.shape[1] if self._parallel: grad = self._compute_parallel_grad( x_vect, n_perturbations, input_perturbations, steps, **kwargs ) else: grad = self._compute_grad( x_vect, n_perturbations, input_perturbations, steps, **kwargs ) return array(grad, dtype=float64).T
def _compute_parallel_grad( self, input_values, # type: ndarray n_perturbations, # type: int input_perturbations, # type: ndarray step, # type: float **kwargs # type: Any ): # type: (...) -> ndarray """Approximate the gradient in parallel. Args: input_values: The input values. n_perturbations: The number of perturbations. step: The differentiation step, either one global step or one step by input component. **kwargs: The optional arguments for the function. Returns: The approximated gradient. """ raise NotImplementedError def _compute_grad( self, input_values, # type: ndarray n_perturbations, # type: int input_perturbations, # type: ndarray step, # type: Union[float,ndarray] **kwargs # type: Any ): # type: (...) -> ndarray """Approximate the gradient. Args: input_values: The input values. n_perturbations: The number of perturbations. input_perturbations: The input perturbations. step: The differentiation step, either one global step or one step by input component. **kwargs: The optional arguments for the function. Returns: The approximated gradient. """ raise NotImplementedError
[docs] def generate_perturbations( self, n_dim, # type: int x_vect, # type: ndarray x_indices=None, # type: Optional[Sequence[int]] step=None, # type: Optional[float] ): # type: (...) -> Tuple[ndarray,Union[float,ndarray]] """Generate the input perturbations from the differentiation step. These perturbations will be used to compute the output ones. Args: n_dim: The input dimension. x_vect: The input vector. x_indices: The components of the input vector to be used for the differentiation. If None, use all the components. step: The differentiation step. If None, use the default differentiation step. Returns: * The input perturbations. * The differentiation step, either one global step or one step by input component. """ if step is None: step = self.step if x_indices is None: x_indices = xrange(n_dim) return self._generate_perturbations(x_vect, x_indices, step)
def _generate_perturbations( self, input_values, # type: ndarray input_indices, # type: List[int] step, # type: float ): # type: (...) -> Tuple[ndarray,Union[float,ndarray]] """Generate the input perturbations from the differentiation step. These perturbations will be used to compute the output ones. Args: input_values: The input vector. input_indices: The components of the input vector to be used for the differentiation. step: The differentiation step. Returns: * The input perturbations. * The differentiation step, either one global step or one step by input component. """ raise NotImplementedError
[docs]class GradientApproximationFactory(object): """A factory to create gradient approximators.""" def __init__(self): # type: (...) -> None self.factory = Factory(GradientApproximator, ("gemseo.utils.derivatives",)) self.__aliases = { self.factory.get_class(class_name).ALIAS: class_name for class_name in self.factory.classes }
[docs] def create( self, name, # type:str f_pointer, # type: Callable step=None, # type: Optional[float] parallel=False, # type: bool **parallel_args ): # type: (...) -> GradientApproximator """Create a gradient approximator. Args: name: Either the name of the class implementing the gradient approximator or its alias. f_pointer: The pointer to the function to differentiate. step: The differentiation step. If None, use a default differentiation step. parallel: Whether to differentiate the function in parallel. **parallel_args: The parallel execution options, see :mod:`gemseo.core.parallel_execution`. Returns: The gradient approximator. """ if name in self.__aliases: name = self.__aliases[name] if step is None: return self.factory.create( name, f_pointer=f_pointer, parallel=parallel, **parallel_args ) else: return self.factory.create( name, f_pointer=f_pointer, step=step, parallel=parallel, **parallel_args )
@property def gradient_approximators(self): # type: (...) -> List[str] """The gradient approximators.""" return self.factory.classes
[docs] def is_available(self, class_name): # type:(...) -> bool """Whether a gradient approximator is available. Args: class_name: The name of the class implementing the gradient approximator. Return: Whether the gradient approximator is available. """ return self.factory.is_available(class_name)