"""Parametric optimization function and loss function interfaces.
This module includes the interface to implement any parameterized function for
optimization purposes. This interface offers convenient methods to handle
optimization parameters, namely their specification (named or sequential),
their bounds, and their normalization/denormalization.
This module also includes the interface of a general loss function defined
between a given set of values of parametric and reference solutions, allowing
the implementation of common loss functions such as the Relative Root Mean
Squared Error (RRMSE).
Classes
-------
OptimizationFunction
Optimization function interface.
Loss
Loss function interface.
RelativeRootMeanSquaredError
Relative Root Mean Squared Error (RRMSE).
"""
#
# Modules
# =============================================================================
# Standard
from abc import ABC, abstractmethod
import copy
# Third-party
import numpy as np
#
# Authorship & Credits
# =============================================================================
__author__ = 'Bernardo Ferreira (bernardo_ferreira@brown.edu)'
__credits__ = ['Bernardo Ferreira', ]
__status__ = 'Stable'
# =============================================================================
#
# =============================================================================
#
# Interface: Optimization function
# =============================================================================
[docs]class OptimizationFunction(ABC):
"""Optimization function interface.
Methods
-------
opt_function(self, parameters)
*abstract*: Optimization function.
get_parameters_names(self)
Get optimization parameters names.
get_bounds(self, is_normalized=False)
Get optimization parameters lower and upper bounds.
get_init_shot(self, is_normalized=False)
Get optimization parameters initial guess.
set_norm_bounds(self, norm_min=-1.0, norm_max=1.0)
Set optimization parameters normalization bounds.
get_norm_bounds(self)
Get optimization parameters normalization bounds.
normalize(self, parameters)
Normalize optimization parameters between min and max values.
denormalize(self, norm_parameters)
Recover optimization parameters from normalized values.
norm_opt_function(self, norm_parameters)
Wrapper of optimization function with normalized parameters.
opt_function_seq(self, parameters_seq)
Wrapper of optimization function with sequential parameters.
norm_opt_function_seq(self, parameters_seq)
Wrapper of optimization function with norm. sequential parameters.
"""
[docs] @abstractmethod
def __init__(self, lower_bounds, upper_bounds, init_shot=None,
weights=None):
"""Constructor.
Parameters
----------
lower_bounds : dict
Optimization parameters (key, str) lower bounds (item, float).
upper_bounds : dict
Optimization parameters (key, str) upper bounds (item, float).
init_shot : dict, default=None
Optimization parameters (key, str) initial guess (item, float).
weights : tuple, default=None
Weights attributed to each data point.
"""
pass
# -------------------------------------------------------------------------
[docs] @abstractmethod
def opt_function(self, parameters):
"""Optimization function.
Parameters
----------
parameters : dict
Optimization parameters names (key, str) and values (item, float).
Returns
-------
value : float
Optimization function value.
"""
pass
# -------------------------------------------------------------------------
[docs] def get_parameters_names(self):
"""Get optimization parameters names.
Returns
-------
parameters_names : tuple[str]
Optimization parameters names (str).
"""
return copy.deepcopy(self._parameters_names)
# -------------------------------------------------------------------------
[docs] def get_bounds(self, is_normalized=False):
"""Get optimization parameters lower and upper bounds.
Parameters
----------
is_normalized : bool, default=False
Whether optimization parameters are normalized or not.
Returns
-------
lower_bounds : dict
Optimization parameters (key, str) lower bounds (item, float).
upper_bounds : dict
Optimization parameters (key, str) upper bounds (item, float).
"""
# Get optimization function parameters lower and upper bounds
if is_normalized:
# Get normalized lower and upper bounds
lower_bounds = {str(param): self._norm_bounds[0]
for param in self._parameters_names}
upper_bounds = {str(param): self._norm_bounds[1]
for param in self._parameters_names}
else:
# Get lower and upper bounds
lower_bounds = copy.deepcopy(self._lower_bounds)
upper_bounds = copy.deepcopy(self._upper_bounds)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return lower_bounds, upper_bounds
# -------------------------------------------------------------------------
[docs] def get_init_shot(self, is_normalized=False):
"""Get optimization parameters initial guess.
Parameters
----------
is_normalized : bool, default=False
Whether optimization parameters are normalized or not.
Returns
-------
init_shot : dict
Optimization parameters (key, str) initial guess (item, float).
"""
# Check if optimization parameters initial guess is defined
if self._init_shot is None:
raise RuntimeError('Optimization parameters initial guess is not '
'defined.')
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Get optimization parameters initial guess
if is_normalized:
init_shot = self.normalize(self._init_shot)
else:
init_shot = copy.deepcopy(self._init_shot)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return init_shot
# -------------------------------------------------------------------------
[docs] def set_norm_bounds(self, norm_min=-1.0, norm_max=1.0):
"""Set optimization parameters normalization bounds.
Parameters
----------
norm_min : float, default=-1.0
Normalized optimization parameter lower bound.
norm_max : float, default=1.0
Normalized optimization parameter upper bound.
"""
# Set normalization bounds
self._norm_bounds = (norm_min, norm_max)
# -------------------------------------------------------------------------
[docs] def get_norm_bounds(self):
"""Get optimization parameters normalization bounds.
Returns
-------
norm_bounds : tuple
Normalization bounds (lower, upper) used to perform the
normalization of the optimization parameters.
"""
return copy.deepcopy(self._norm_bounds)
# -------------------------------------------------------------------------
[docs] def normalize(self, parameters):
"""Normalize optimization parameters between min and max values.
Parameters
----------
parameters : dict
Optimization parameters names (key, str) and values (item, float).
norm_min : float, default=-1.0
Normalized optimization parameter lower bound.
norm_max : float, default=1.0
Normalized optimization parameter upper bound.
Returns
-------
norm_parameters : dict
Normalized optimization parameters names (key, str) and values
(item, float).
"""
# Get optimization parameters normalization bounds
norm_min = self._norm_bounds[0]
norm_max = self._norm_bounds[1]
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Initialize normalized optimization parameters
norm_parameters = {}
# Loop over optimization parameters
for param in self._parameters_names:
# Get optimization parameter value
value = parameters[str(param)]
# Get optimization parameter lower and upper bounds
lbound = self._lower_bounds[str(param)]
ubound = self._upper_bounds[str(param)]
# Normalize optimization parameter
norm_parameters[str(param)] = norm_min \
+ ((value - lbound)/(ubound - lbound))*(norm_max - norm_min)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return norm_parameters
# -------------------------------------------------------------------------
[docs] def denormalize(self, norm_parameters):
"""Recover optimization parameters from normalized values.
Parameters
----------
norm_parameters : dict
Normalized optimization parameters names (key, str) and values
(item, float).
Returns
-------
parameters : dict
Optimization parameters names (key, str) and values (item, float).
"""
# Get optimization parameters normalization bounds
norm_min, norm_max = self._norm_bounds
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Initialize optimization function parameters
parameters = {}
# Loop over optimization parameters
for param in self._parameters_names:
# Get optimization parameter value
norm_value = norm_parameters[str(param)]
# Get optimization parameter lower and upper bounds
lbound = self._lower_bounds[str(param)]
ubound = self._upper_bounds[str(param)]
# Recover optimization parameter
parameters[str(param)] = lbound \
+ ((norm_value - norm_min)/(norm_max - norm_min))*(ubound
- lbound)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return parameters
# -------------------------------------------------------------------------
[docs] def norm_opt_function(self, norm_parameters):
"""Wrapper of optimization function with normalized parameters.
Parameters
----------
norm_parameters : dict
Normalized optimization parameters names (key, str) and values
(item, float).
Returns
-------
value : float
Optimization function value.
"""
# Recover optimization parameters from normalized values
parameters = self.denormalize(norm_parameters)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Compute optimization function
value = self.opt_function(parameters)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return value
# -------------------------------------------------------------------------
[docs] def opt_function_seq(self, parameters_seq):
"""Wrapper of optimization function with sequential parameters.
Parameters
----------
parameters_seq : tuple[float]
Optimization parameters values.
Returns
-------
value : float
Optimization function value.
"""
# Build optimization parameters dictionary
parameters = {str(param): parameters_seq[i]
for i, param in enumerate(self._parameters_names)}
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Compute optimization function
value = self.opt_function(parameters)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return value
# -------------------------------------------------------------------------
[docs] def norm_opt_function_seq(self, parameters_seq):
"""Wrapper of optimization function with norm. sequential parameters.
Parameters
----------
parameters_seq : tuple[float]
Optimization parameters values.
Returns
-------
value : float
Optimization function value.
"""
# Build normalized optimization parameters dictionary
norm_parameters = {str(param): parameters_seq[i]
for i, param in enumerate(self._parameters_names)}
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Recover optimization parameters from normalized values
parameters = self.denormalize(norm_parameters)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Compute optimization function
value = self.opt_function(parameters)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return value
#
# Interface: Loss function
# =============================================================================
[docs]class Loss(ABC):
"""Loss function interface.
Methods
-------
loss(self, y, y_ref, type='minimization')
*abstract*: Loss function.
"""
[docs] @abstractmethod
def __init__(self):
"""Constructor."""
pass
# -------------------------------------------------------------------------
[docs] @abstractmethod
def loss(self, y, y_ref, type='minimization'):
"""Loss function.
Parameters
----------
y : tuple[float]
Values of parametric solution.
y_ref : tuple[float]
Values of reference solution.
type : {'minimization', 'maximization'}, default='minimization'
Type of optimization problem. The option 'maximization' negates the
loss function evaluation.
Returns
-------
loss : float
Loss function value.
"""
pass
#
# Loss functions
# =============================================================================
[docs]class RelativeRootMeanSquaredError(Loss):
"""Relative Root Mean Squared Error (RRMSE).
Methods
-------
loss(self, y, y_ref, type='minimization')
Loss function.
"""
[docs] def __init__(self):
"""Constructor."""
pass
# -------------------------------------------------------------------------
[docs] def loss(self, y, y_ref, type='minimization'):
"""Loss function.
The Relative Root Mean Squared Error (RRMSE) is defined as
.. math::
\\text{RRMSE} (\\boldsymbol{y}, \\hat{\\boldsymbol{y}}) =
\\sqrt{\\dfrac{\\dfrac{1}{n} \\sum_{i=1}^{n}(y_{i} -
\\hat{y}_{i})^{2}}{ \\sum_{i=1}^{n} \\hat{y}_{i}^{2}}}
where :math:`\\boldsymbol{y}` is the vector of predicted values,
:math:`\\hat{\\boldsymbol{y}}` is the vector of reference values,
and :math:`n` is the number of data points.
----
Parameters
----------
y : tuple[float]
Values of parametric solution.
y_ref : tuple[float]
Values of reference solution.
type : {'minimization', 'maximization'}, default='minimization'
Type of optimization problem. The option 'maximization' negates the
loss function evaluation.
Returns
-------
loss : float
Loss function value.
"""
# Get number of data points
n = len(y)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Compute required summations
sum_1 = sum([(y[i] - y_ref[i])**2 for i in range(n)])
sum_2 = sum([y_ref[i]**2 for i in range(n)])
# Compute relative root mean squared error
loss = np.sqrt(((1.0/n)*sum_1)/sum_2)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Negate loss function if maximization optimization
if type == 'maximization':
loss = -loss
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Return
return loss