"Definition of a univariate parameter."
import equadratures.plot as plot
import numpy as np
import scipy as sc
[docs]
class ParentParameter(object):
""" This class defines a univariate parameter.
Parameters
----------
lower : float, optional
Lower bound for the parameter.
upper : float, optional
Upper bound for the parameter.
order : int, optional
Order of the parameter.
param_type : str, optional
The type of distribution that characterizes the parameter (see [1, 2]). Options include `chebyshev (arcsine) <https://en.wikipedia.org/wiki/Arcsine_distribution>`_, `gaussian <https://en.wikipedia.org/wiki/Normal_distribution>`_,
`truncated-gaussian <https://en.wikipedia.org/wiki/Truncated_normal_distribution>`_, `beta <https://en.wikipedia.org/wiki/Beta_distribution>`_,
`cauchy <https://en.wikipedia.org/wiki/Cauchy_distribution>`_, `exponential <https://en.wikipedia.org/wiki/Exponential_distribution>`_,
`uniform <https://en.wikipedia.org/wiki/Uniform_distribution_(continuous)>`_, `triangular <https://en.wikipedia.org/wiki/Triangular_distribution>`_, `gamma <https://en.wikipedia.org/wiki/Gamma_distribution>`_,
`weibull <https://en.wikipedia.org/wiki/Weibull_distribution>`_, `rayleigh <https://en.wikipedia.org/wiki/Rayleigh_distribution>`_,
`pareto <https://en.wikipedia.org/wiki/Pareto_distribution>`_, `lognormal <https://en.wikipedia.org/wiki/Log-normal_distribution>`_,
`students-t <https://en.wikipedia.org/wiki/Student%27s_t-distribution>`_, `logistic <https://en.wikipedia.org/wiki/Log-normal_distribution>`_,
`gumbel <https://en.wikipedia.org/wiki/Gumbel_distribution>`_, `chi <https://en.wikipedia.org/wiki/Chi_distribution>`_ and `chi-squared <https://en.wikipedia.org/wiki/Chi-squared_distribution>`_.
If no string is provided, a ``uniform`` distribution is assumed. Data-driven and custom analytical parameters can also be constructed by setting this option to ``data`` and ``analytical`` and providing a **weight_function** (see examples).
shape_parameter_A : float, optional
Most of the aforementioned distributions are characterized by two shape parameters. For instance, in the case of a ``gaussian`` (or ``truncated-gaussian``), this represents the mean. In the case of a beta distribution this represents the alpha value. For a ``uniform`` distribution this input is not required.
shape_parameter_B : float, optional
This is the second shape parameter that characterizes the distribution selected. In the case of a ``gaussian`` or ``truncated-gaussian``, this is the variance.
data : numpy.ndarray, optional
A data-set with shape (number_of_data_points, 2), where the first column comprises of parameter values, while the second column corresponds to the data observations. This input should only be used with the ``Analytical`` distribution.
endpoints : str, optional
If set to ``both``, then the quadrature points and weights will have end-points, based on Gauss-Lobatto quadrature rules. If set to ``upper`` or ``lower`` a Gauss-Radau rule is used to compute one end-point at either the upper or lower bound.
weight_function: Weight, optional
An instance of Weight, which contains a bespoke analytical or data-driven weight (probability density) function.
Examples
--------
A uniform parameter
>>> param = eq.Parameter(distribution='uniform', lower=-2, upper=2., order=3)
A beta parameter
>>> param = eq.Parameter(distribution='beta', lower=-2., upper=15., order=4,
>>> shape_parameter_A=3.2, shape_parameter_B=1.7)
A data-driven parameter
>>> pdf = eq.Weight( stats.gaussian_kde(data, bw_method='silverman'),
>>> support=[-3, 3.2])
>>> param = eq.Parameter(distribution='analytical',
>>> weight_function=pdf, order=2)
References
----------
1. Xiu, D., Karniadakis, G. E., (2002) The Wiener-Askey Polynomial Chaos for Stochastic Differential Equations. SIAM Journal on Scientific Computing, 24(2), `Paper <https://epubs.siam.org/doi/abs/10.1137/S1064827501387826?journalCode=sjoce3>`__
2. Gautschi, W., (1985) Orthogonal Polynomials-Constructive Theory and Applications. Journal of Computational and Applied Mathematics 12 (1985), pp. 61-76. `Paper <https://www.sciencedirect.com/science/article/pii/037704278590007X>`__
"""
def __init__(self, distribution, order, variable, endpoints):
self.distribution = distribution
self.variable = variable
self.order = order
self.endpoints = endpoints
self.weight_function = weight_function
self._set_distribution()
self._set_bounds()
self._set_moments()
if self.endpoints is not None:
if (self.distribution.bounds[0] == -np.inf) and (self.distribution.bounds[1] == np.inf) and (self.endpoints.lower() == 'both'):
raise(ValueError, 'Parameter: The lower bound for your distribution is -infinity and the upper bound is infinity. Furthermore, you have selected the to have both endpoints. These options are incompatible!')
if (self.distribution.bounds[0] == -np.inf) and (self.endpoints.lower() == 'lower'):
raise(ValueError, 'Parameter: The lower bound for your distribution is -infinity and you have selected the lower bound option in the endpoints. These options are incompatible!')
if (self.distribution.bounds[1] == np.inf) and (self.endpoints.lower() == 'upper'):
raise(ValueError, 'Parameter: The upper bound for your distribution is infinity and you have selected the upper bound option in the endpoints. These options are incompatible!')
def _set_distribution(self):
""" Private function that sets the distribution. """
if self.name.lower() == 'gaussian' or self.name.lower() == 'normal':
self.distribution = Gaussian(self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'uniform':
self.distribution = Uniform(self.lower, self.upper)
elif self.name.lower() == 'triangular':
self.distribution = Triangular(self.lower, self.upper, self.shape_parameter_A)
elif self.name.lower() == 'analytical' or self.name.lower() == 'data':
self.distribution = Analytical(self.weight_function)
elif self.name.lower() == 'beta':
self.distribution = Beta(self.lower, self.upper, self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'truncated-gaussian':
self.distribution = TruncatedGaussian(self.shape_parameter_A, self.shape_parameter_B, self.lower, self.upper)
elif self.name.lower() == 'cauchy':
self.distribution = Cauchy(self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'exponential':
self.distribution = Exponential(self.shape_parameter_A)
elif self.name.lower() == 'gamma':
self.distribution = Gamma(self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'weibull':
self.distribution = Weibull(self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'arcsine' or self.name.lower() == 'chebyshev':
self.distribution = Chebyshev(self.lower, self.upper)
elif self.name.lower() == 'rayleigh':
self.distribution = Rayleigh(self.shape_parameter_A)
elif self.name.lower() == 'chi-squared':
self.distribution = Chisquared(self.shape_parameter_A)
elif self.name.lower() == 'chi':
self.distribution = Chi(self.shape_parameter_A)
elif self.name.lower() == 'pareto':
self.distribution = Pareto(self.shape_parameter_A)
elif self.name.lower() == 'gumbel':
self.distribution = Gumbel(self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'logistic':
self.distribution = Logistic(self.shape_parameter_A, self.shape_parameter_B)
elif self.name.lower() == 'students-t' or self.name.lower() == 't' or self.name.lower() == 'studentt':
self.distribution = Studentst(self.shape_parameter_A)
elif self.name.lower() == 'lognormal' or self.name.lower() == 'log-normal':
self.distribution = Lognormal(self.shape_parameter_A)
else:
distribution_error()
self.mean = self.distribution.mean
self.variance = self.distribution.variance
[docs]
def plot_orthogonal_polynomials(self, ax=None, order_limit=None, number_of_points=200, show=True):
""" Plots the first few orthogonal polynomials. See :meth:`~equadratures.plot.plot_orthogonal_polynomials` for full description. """
return plot.plot_orthogonal_polynomials(self,ax,order_limit,number_of_points,show)
[docs]
def plot_pdf(self, ax=None, data=None, show=True, lim_range=True):
""" Plots the probability density function for a Parameter. See :meth:`~equadratures.plot.plot_pdf` for full description. """
return plot.plot_pdf(self,ax, data, show, lim_range)
[docs]
def plot_cdf(self, ax=None, show=True, lim_range=True):
""" Plots the cumulative density function for a Parameter. See :meth:`~equadratures.plot.plot_cdf` for full description. """
return plot.plot_cdf(self,ax, show, lim_range)
[docs]
def get_pdf(self, points=None):
""" Computes the probability density function associated with the Parameter.
Parameters
----------
points : numpy.ndarray, optional
Values of the parameter at which the PDF must be evaluated.
Returns
-------
numpy.ndarray
If ``points!=None``. ndarray containing the probability density function evaluated at the points in ``points``.
tuple
If ``points=None``. A tuple (`x`, `pdf`), where `pdf` is the probability density function evaluated at the points in `x`.
"""
if points is None:
x = self.x_range_for_pdf
return x, self.get_pdf(x)
else:
return self.get_pdf(points)
[docs]
def get_cdf(self, points=None):
""" Computes the cumulative density function associated with the Parameter.
Parameters
----------
points : numpy.ndarray, optional
Values of the parameter at which the CDF must be evaluated.
Returns
-------
numpy.ndarray
If ``points!=None``. ndarray containing the cumulative density function evaluated at the points in ``points``.
tuple
If ``points=None``. A tuple (`x`, `cdf`), where `cdf` is the cumulative density function evaluated at the points in `x`.
"""
if points is None:
x = self.distribution.x_range_for_pdf
return x, self.distribution.get_cdf(x)
else:
return self.distribution.get_cdf(points)
[docs]
def get_icdf(self, cdf_values):
""" Computes the inverse cumulative density function associated with the Parameter.
Parameters
----------
cdf_values : numpy.ndarray
Values of the cumulative density function for which its inverse needs to be computed.
Returns
-------
numpy.ndarray
The inverse cumulative density function.
"""
return self.distribution.get_icdf(cdf_values)
[docs]
def get_samples(self, number_of_samples_required):
""" Generates samples from the distribution associated with the Parameter.
Parameters
----------
number_of_samples_required : int
Number of samples that are required.
Returns
-------
numpy.ndarray
The generated samples.
"""
return self.distribution.get_samples(number_of_samples_required)
[docs]
def get_description(self):
""" Provides a description of the Parameter.
Returns
-------
str
A description of the parameter.
"""
return self.distribution.get_description()
[docs]
def get_recurrence_coefficients(self, order=None):
""" Generates the recurrence coefficients.
Parameters
----------
order : int, optional
Order of the recurrence coefficients.
Returns
-------
numpy.ndarray
Array of recurrence coefficients.
"""
return self.distribution.get_recurrence_coefficients(order)
[docs]
def get_jacobi_eigenvectors(self, order=None):
""" Computes the eigenvectors of the Jacobi matrix.
Parameters
----------
order : int
Order of the recurrence coefficients.
Returns
-------
numpy.ndarray
Array of eigenvectors.
"""
if order is None:
order = self.order + 1
JacobiMat = self.get_jacobi_matrix(order)
if order == 1:
V = [1.0]
else:
#D,V = np.linalg.eig(self.get_jacobi_matrix(order))
D, V = sc.linalg.eigh(self.get_jacobi_matrix(order))
idx = D.argsort()[::-1]
eigs = D[idx]
eigVecs = V[:, idx]
#V = np.asmatrix(V) # convert to matrix
#i = np.argsort(D) # get the sorted indices
#i = np.array(i) # convert to array
#V = V[:,i]
return eigVecs
[docs]
def get_jacobi_matrix(self, order=None, ab=None):
""" Computes the Jacobi matrix---a tridiagonal matrix of the recurrence coefficients.
Parameters
----------
order : int
Order of the recurrence coefficients.
Returns
-------
numpy.ndarray
2D array containing the Jacobi matrix.
"""
if order is None and ab is None:
ab = self.get_recurrence_coefficients()
order = self.order + 1
elif ab is None:
ab = self.get_recurrence_coefficients(order)
else:
ab = ab[0:order, :]
order = int(order)
# The case of order 1~
if int(order) == 1:
JacobiMatrix = ab[0, 0]
# For everything else~
else:
JacobiMatrix = np.zeros((int(order), int(order))) # allocate space
JacobiMatrix[0,0] = ab[0,0]
JacobiMatrix[0,1] = np.sqrt(ab[1,1])
k = order - 1
for u in range(1, int(k)):
JacobiMatrix[u,u] = ab[u,0]
JacobiMatrix[u,u-1] = np.sqrt(ab[u,1])
JacobiMatrix[u,u+1] = np.sqrt(ab[u+1,1])
JacobiMatrix[order-1, order-1] = ab[order-1,0]
JacobiMatrix[order-1, order-2] = np.sqrt(ab[order-1,1])
return JacobiMatrix
def _get_orthogonal_polynomial(self, points, order=None):
"""
Private function that evaluates the univariate orthogonal polynomial at quadrature points.
:param Parameter self:
An instance of the Parameter object.
:param numpy.ndarray points:
Points at which the orthogonal polynomial must be evaluated.
:param int order:
Order up to which the orthogonal polynomial must be obtained.
"""
if order is None:
order = self.order + 1
else:
order = order + 1
gridPoints = np.asarray(points).copy()
ab = self.get_recurrence_coefficients(order)
"""
print('Before:')
print(gridPoints)
for q in range(0, gridPoints.shape[0]):
if (gridPoints[q] < self.bounds[0]) or (gridPoints[q] > self.bounds[1]):
grid_flag = 1
if grid_flag == 1:
for r in range(0, gridPoints.shape[0]):
gridPoints[r] = (self.bounds[1] - self.bounds[0]) * ( (gridPoints[r] - self.lower) / (self.upper - self.lower) ) + self.bounds[0]
#print(gridPoints)
print('After:')
print(gridPoints)
"""
orthopoly = np.zeros((order, len(gridPoints))) # create a matrix full of zeros
derivative_orthopoly = np.zeros((order, len(gridPoints)))
dderivative_orthopoly = np.zeros((order, len(gridPoints)))
# Convert the grid points to a numpy array -- simplfy life!
gridPointsII = gridPoints.reshape((-1, 1))
orthopoly[0, :] = 1.0
# Cases
if order == 1: # CHANGED 2/2/18
return orthopoly, derivative_orthopoly, dderivative_orthopoly
orthopoly[1, :] = ((gridPointsII[:, 0] - ab[0, 0]) * orthopoly[0, :]) * (1.0) / (1.0 * np.sqrt(ab[1, 1]))
derivative_orthopoly[1, :] = orthopoly[0, :] / (np.sqrt(ab[1, 1]))
if order == 2: # CHANGED 2/2/18
return orthopoly, derivative_orthopoly, dderivative_orthopoly
if order >= 3: # CHANGED 2/2/18
for u in range(2, order): # CHANGED 2/2/18
# Three-term recurrence rule in action!
orthopoly[u, :] = (((gridPointsII[:, 0] - ab[u - 1, 0]) * orthopoly[u - 1, :]) - np.sqrt(
ab[u - 1, 1]) * orthopoly[u - 2, :]) / (1.0 * np.sqrt(ab[u, 1]))
for u in range(2, order): # CHANGED 2/2/18
# Four-term recurrence formula for derivatives of orthogonal polynomials!
derivative_orthopoly[u,:] = ( ((gridPointsII[:,0] - ab[u-1,0]) * derivative_orthopoly[u-1,:]) - ( np.sqrt(ab[u-1,1]) * derivative_orthopoly[u-2,:] ) + orthopoly[u-1,:] )/(1.0 * np.sqrt(ab[u,1]))
for u in range(2,order):
# Four-term recurrence formula for second derivatives of orthogonal polynomials!
dderivative_orthopoly[u,:] = ( ((gridPointsII[:,0] - ab[u-1,0]) * dderivative_orthopoly[u-1,:]) - ( np.sqrt(ab[u-1,1]) * dderivative_orthopoly[u-2,:] ) + 2.0 * derivative_orthopoly[u-1,:] )/(1.0 * np.sqrt(ab[u,1]))
return orthopoly, derivative_orthopoly, dderivative_orthopoly
def _get_local_quadrature(self, order=None, ab=None):
"""
Returns the 1D quadrature points and weights for the parameter. WARNING: Should not be called under normal circumstances.
:param Parameter self:
An instance of the Parameter class
:param int N:
Number of quadrature points and weights required. If order is not specified, then by default the method will return the number of points defined in the parameter itself.
:return:
A N-by-1 matrix that contains the quadrature points
:return:
A 1-by-N matrix that contains the quadrature weights
"""
if self.endpoints is None:
return get_local_quadrature(self, order, ab)
elif self.endpoints.lower() == 'lower' or self.endpoints.lower() == 'upper':
return get_local_quadrature_radau(self, order, ab)
elif self.endpoints.lower() == 'both':
return get_local_quadrature_lobatto(self, order, ab)
else:
raise(ValueError, 'Error in endpoints specification.')
def get_local_quadrature(self, order=None, ab=None):
# Check for extra input argument!
if order is None:
order = self.order + 1
else:
order = order + 1
if ab is None:
# Get the recurrence coefficients & the jacobi matrix
JacobiMat = self.get_jacobi_matrix(order)
ab = self.get_recurrence_coefficients(order+1)
else:
ab = ab[0:order+1,:]
JacobiMat = self.get_jacobi_matrix(order, ab)
# If statement to handle the case where order = 1
if order == 1:
# Check to see whether upper and lower bound are defined:
if np.isinf(self.distribution.lower) or np.isinf(self.distribution.upper):
p = np.asarray(self.distribution.mean).reshape((1,1))
else:
print('see below!')
print(self.distribution.lower, self.distribution.upper)
print(self.distribution.lower(), self.distribution.upper())
p = np.asarray((self.distribution.upper - self.distribution.lower)/(2.0) + self.distribution.lower).reshape((1,1))
w = [1.0]
else:
# Compute eigenvalues & eigenvectors of Jacobi matrix
#D,V = np.linalg.eig(JacobiMat)
D, V = sc.linalg.eigh(JacobiMat)
#V = np.asmatrix(V) # convert to matrix
#local_points = np.sort(D) # sort by the eigenvalues
#i = np.argsort(D) # get the sorted indices
#i = np.array(i) # convert to array
idx = D.argsort()[::-1]
eigs = D[idx]
eigVecs = V[:, idx]
w = np.linspace(1,order+1,order) # create space for weights
p = np.ones((int(order),1))
for u in range(0, len(idx) ):
w[u] = float(ab[0,1]) * (eigVecs[0,idx[u]]**2) # replace weights with right value
p[u,0] = eigs[u]
#if (p[u,0] < 1e-16) and (-1e-16 < p[u,0]):
# p[u,0] = np.abs(p[u,0])
p = p[::-1]
return p, w
def get_local_quadrature_radau(self, order=None, ab=None):
if self.endpoints.lower() == 'lower':
end0 = self.distribution.lower
elif self.endpoints.lower() == 'upper':
end0 = self.distribution.upper
if order is None:
order = self.order - 1
else:
order = order - 1
N = order
if ab is None:
ab = self.get_recurrence_coefficients(order+1)
else:
ab = ab[0:order+1, :]
p0 = 0.
p1 = 1.
for i in range(0, N+1):
pm1 = p0
p0 = p1
p1 = (end0 - ab[i, 0]) * p0 - ab[i, 1]*pm1
ab[N+1, 0] = end0 - ab[N+1, 1] * p0/p1
return get_local_quadrature(self, order=order+1, ab=ab)
def get_local_quadrature_lobatto(self, order=None, ab=None):
if order is None:
order = self.order - 2
else:
order = order - 2
N = order
endl = self.distribution.lower
endr = self.distribution.upper
if ab is None:
ab = self.get_recurrence_coefficients(order+2)
else:
ab = ab[0:order+2, :]
p0l = 0.
p0r = 0.
p1l = 1.
p1r = 1.
for i in range(0, N+2):
pm1l = p0l
p0l = p1l
pm1r = p0r
p0r = p1r
p1l = (endl - ab[i, 0]) * p0l - ab[i, 1] * pm1l
p1r = (endr - ab[i, 0]) * p0r - ab[i, 1] * pm1r
det = p1l * p0r - p1r * p0l
ab[N+2, 0] = (endl*p1l*p0r-endr*p1r*p0l)/det
ab[N+2, 1] = (endr - endl) * p1l * p1r/det
return get_local_quadrature(self, order=order+2, ab=ab)