# -*- 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
"""
Parameter space including both deterministic and uncertain parameters
=====================================================================
Overview
--------
The :class:`.ParameterSpace` class describes a set of parameters of
interest which can be either deterministic or uncertain. This class
inherits from :class:`.DesignSpace`.
Capabilities
------------
The :meth:`.DesignSpace.add_variable` aims to add deterministic
variables from:
- a variable name,
- a variable size (default: 1),
- a variable type (default: float),
- a lower bound (default: - infinity),
- an upper bound (default: + infinity),
- a current value (default: None).
The :meth:`.add_random_variable` aims to add uncertain
variables (a.k.a. random variables) from:
- a variable name,
- a distribution name
(see :meth:`~gemseo.uncertainty.api.get_available_distributions`),
- a variable size,
- distribution parameters (:code:`parameters` set as
a tuple of positional arguments for :class:`.OTDistribution`
or a dictionary of keyword arguments for :class:`.SPDistribution`,
or keyword arguments for standard probability distribution such
as :class:`.OTNormalDistribution` and :class:`.OTNormalDistribution`).
The :class:`.ParameterSpace` also provides the following methods:
- :meth:`.get_cdf`: evaluate the cumulative density function
for the different variables and their different
- :meth:`.get_composed_distribution`: returns the probability distribution
of an uncertain variable,
- :meth:`.get_marginal_distributions`: returns the marginal probability
distributions of an uncertain variable,
- :meth:`.get_range` returns the numerical range
of the different uncertain parameters,
- :meth:`.ParameterSpace.get_sample`: returns several sample
of the uncertain variables,
- :meth:`.get_support`: returns the mathematical support
of the different uncertain variables,
- :meth:`.is_uncertain`: checks if a parameter is uncertain,
- :meth:`.is_deterministic`: checks if a parameter is deterministic.
"""
from __future__ import absolute_import, division, unicode_literals
from future import standard_library
from numpy import array, ndarray
from sympy import simplify
from gemseo.algos.design_space import DesignSpace
from gemseo.uncertainty.distributions.distribution import ComposedDistribution
from gemseo.uncertainty.distributions.factory import DistributionFactory
from gemseo.utils.data_conversion import DataConversion
standard_library.install_aliases()
from gemseo import LOGGER
[docs]class ParameterSpace(DesignSpace):
""" Parameter space. """
INITIAL_DISTRIBUTION = "Initial distribution"
TRANSFORMATION = "Transformation"
SUPPORT = "Support"
MEAN = "Mean"
STANDARD_DEVIATION = "Standard deviation"
RANGE = "Range"
BLANK = ""
PARAMETER_SPACE = "Parameter space"
def __init__(
self,
print_decimals=2,
shorten=True,
copula=ComposedDistribution.INDEPENDENT_COPULA,
):
"""Constructor
:param int print_decimals: number of decimals to print. Default: 2.
:param bool shorten: if True, simplify the expressions
of variable transformations. Default: True.
:param str copula: copula name.
Default: ComposedDistribution.INDEPENDENT_COPULA.
"""
LOGGER.info("Create a new parameter space. ")
super(ParameterSpace, self).__init__()
self.uncertain_variables = []
self.marginals = {}
self.distribution = None
self._ndecimals = print_decimals
self._shorten = shorten
if copula not in ComposedDistribution.AVAILABLE_COPULA:
raise ValueError("%s is not an available copula." % (copula))
self.copula = copula
[docs] def is_uncertain(self, variable):
"""Check if a variable is uncertain.
:param str variable: variable name.
"""
return variable in self.uncertain_variables
[docs] def is_deterministic(self, variable):
"""Check if a variable is deterministic.
:param str variable: variable name.
"""
determistic = set(self.variables_names) - set(self.uncertain_variables)
return variable in determistic
def __update_parameter_space(self, variable):
"""Update parameter space.
:param variable: variable name.
:type variable: str
"""
if variable not in self.variables_names:
l_b = self.marginals[variable].math_lower_bound
u_b = self.marginals[variable].math_upper_bound
value = self.marginals[variable].mean
size = self.marginals[variable].dimension
self.add_variable(variable, size, "float", l_b, u_b, value)
else:
l_b = self.marginals[variable].math_lower_bound
u_b = self.marginals[variable].math_upper_bound
value = self.marginals[variable].mean
self.set_lower_bound(variable, l_b)
self.set_upper_bound(variable, u_b)
self.set_current_variable(variable, value)
[docs] def add_random_variable(self, name, distribution, size=1, **parameters):
"""Add a random variable from a distribution
:param str name: name of the random variable.
:param str distribution: distribution name.
:param int size: variable size.
:param parameters: parameters of the distribution.
"""
factory = DistributionFactory()
distribution = factory.create(
distribution, variable=name, dimension=size, **parameters
)
variable = distribution.variable_name
LOGGER.info("Add the random variable: %s", variable)
self.marginals[variable] = distribution
self.uncertain_variables.append(variable)
self._build_composed_distribution()
self.__update_parameter_space(variable)
def _build_composed_distribution(self):
""" Build the composed distribution from the marginal ones. """
tmp_marginal = self.marginals[self.uncertain_variables[0]]
marginals = [self.marginals[name] for name in self.uncertain_variables]
self.distribution = tmp_marginal.COMPOSED_DISTRIBUTION(marginals, self.copula)
[docs] def get_composed_distribution(self, variable):
"""Get the composed distribution of a random variable.
:param str variable: variable name.
"""
return self.marginals[variable].distribution
[docs] def get_marginal_distributions(self, variable):
"""Get the marginal distributions of a random variable.
:param str variable: variable name.
"""
return self.marginals[variable].marginals
[docs] def get_range(self, variable):
"""Get the numerical range of a random variable.
:param str variable: variable name.
"""
return self.marginals[variable].range
[docs] def get_support(self, variable):
"""Get the mathematical support of a random variable.
:param str variable: variable name.
"""
return self.marginals[variable].support
[docs] def remove_variable(self, name):
"""Remove a variable from the probability space.
:param str name: variable name.
"""
if name in self.uncertain_variables:
del self.marginals[name]
self.uncertain_variables.remove(name)
self._build_composed_distribution()
super(ParameterSpace, self).remove_variable(name)
[docs] def set_dependence(self, variables, copula, **options):
"""Set dependence relation between random variables.
:param list(str) variables: list of variables names.
:param str copula: copula name.
:param options: copula options.
"""
raise NotImplementedError
[docs] def get_sample(self, n_samples=1, as_dict=False):
"""Get sample.
:param int n_samples: number of samples.
:param bool as_dict: return a dictionary.
:return: samples
:rtype: list(array) or list(dict)
"""
sample = self.distribution.get_sample(n_samples)
if as_dict:
sample = [
DataConversion.array_to_dict(
data_array, self.uncertain_variables, self.variables_sizes
)
for data_array in sample
]
return sample
[docs] def get_cdf(self, value, inverse=False):
"""Get the inverse Cumulative Density Function
values of the different marginals.
:param dict(array) value: values
:return: (inverse) CDF values
:rtype: dict(array)
"""
if inverse:
self.__check_dict_of_array(value)
values = {}
for name in self.uncertain_variables:
val = value[name]
distribution = self.marginals[name]
if inverse:
current_v = distribution.inverse_cdf(val)
else:
current_v = distribution.cdf(val)
values[name] = array(current_v)
return values
def __check_dict_of_array(self, obj):
"""Check if the object is a dictionary of array.
:param obj: object to test
"""
error_msg = "obj must be a dictionary whose keys are the variables "
error_msg += "names and values are arrays whose dimensions are the "
error_msg += "variables ones and components are in [0, 1]."
if not isinstance(obj, dict):
raise TypeError(error_msg)
for variable, value in obj.items():
if variable not in self.uncertain_variables:
LOGGER.debug(
"%s is not defined in the probability space."
" Available variables are [%s]."
" Use uniform distribution for %s.",
variable,
", ".join(self.uncertain_variables),
variable,
)
else:
if not isinstance(value, ndarray):
raise TypeError(error_msg)
if len(value.flatten()) != self.variables_sizes[variable]:
raise ValueError(error_msg)
if any(value.flatten() > 1.0) or any(value.flatten() < 0.0):
raise ValueError(error_msg)
def __str__(self, *args, **kwargs):
"""String representation.
:return: description
:rtype: str
"""
table = self.get_pretty_table()
distribution = []
transformation = []
support = []
mean = []
std = []
rnge = []
for variable in self.variables_names:
if variable in self.uncertain_variables:
dist = self.marginals[variable]
tmp_mean = dist.mean
tmp_std = dist.standard_deviation
tmp_range = dist.range
tmp_support = dist.support
for dim in range(dist.dimension):
distribution.append(str(dist))
transformation.append(dist.transformation)
if self._shorten:
transformation[-1] = str(simplify(transformation[-1]))
mean.append(tmp_mean[dim])
mean[-1] = round(mean[-1], self._ndecimals)
std.append(tmp_std[dim])
std[-1] = round(std[-1], self._ndecimals)
rnge.append(tmp_range[dim])
support.append(tmp_support[dim])
else:
for dim in range(self.variables_sizes[variable]):
distribution.append(self.BLANK)
transformation.append(self.BLANK)
mean.append(self.BLANK)
std.append(self.BLANK)
support.append(self.BLANK)
rnge.append(self.BLANK)
table.add_column(self.INITIAL_DISTRIBUTION, distribution)
table.add_column(self.TRANSFORMATION, transformation)
table.add_column(self.SUPPORT, support)
table.add_column(self.MEAN, mean)
table.add_column(self.STANDARD_DEVIATION, std)
table.add_column(self.RANGE, rnge)
table.title = self.PARAMETER_SPACE
desc = str(table)
return desc
[docs] def unnormalize_vect(self, x_vect, minus_lb=True, no_check=False, use_dist=True):
"""Inverse transformation from a unit design vector.
Unnormalizes a normalized vector of the design space.
:param array x_vect: design variables.
:param bool minus_lb: if True, remove lower bounds at normalization.
:param bool no_check: if True, don't check that values are in [0,1].
:param bool use_dist: if True, rescale wrt the stats law.
:return: normalized vector
:rtype: array
"""
if not use_dist:
return super(ParameterSpace, self).unnormalize_vect(x_vect)
data_names = self.variables_names
data_sizes = self.variables_sizes
dict_sample = DataConversion.array_to_dict(x_vect, data_names, data_sizes)
x_u_geom = super(ParameterSpace, self).unnormalize_vect(x_vect)
x_u = self.get_cdf(dict_sample, inverse=True)
x_u_geom = DataConversion.array_to_dict(x_u_geom, data_names, data_sizes)
missing_names = list(set(data_names) - set(x_u.keys()))
for name in missing_names:
x_u[name] = x_u_geom[name]
x_u = DataConversion.dict_to_array(x_u, data_names)
return x_u
[docs] def normalize_vect(self, x_vect, minus_lb=True, use_dist=False):
"""Normalizes a vector of the design space.
Unbounded variables are not normalized.
:param array x_vect: design variables.
:param bool minus_lb: if True, remove lower bounds at normalization.
:param bool no_check: if True, don't check that values are in [0,1].
:param bool use_dist: if True, rescale wrt the stats law.
:return: normalized vector
:rtype: array
"""
if use_dist:
return super(ParameterSpace, self).normalize_vect(x_vect)
data_names = self.variables_names
data_sizes = self.variables_sizes
dict_sample = DataConversion.array_to_dict(x_vect, data_names, data_sizes)
x_u_geom = super(ParameterSpace, self).normalize_vect(x_vect)
x_u = self.get_cdf(dict_sample, inverse=False)
x_u_geom = DataConversion.array_to_dict(x_u_geom, data_names, data_sizes)
missing_names = list(set(data_names) - set(x_u.keys()))
for name in missing_names:
x_u[name] = x_u_geom[name]
x_u = DataConversion.dict_to_array(x_u, data_names)
return x_u