Esempio n. 1
0
class Correlations(object):
    """
    The class defines a Nataf transformation. The input correlated marginals are mapped from their physical space to a new
    standard normal space, in which points are uncorrelated.

    :param Poly poly: A polynomial object.
    :param numpy.ndarray correlation_matrix: The correlation matrix associated with the joint distribution.

    **References**
        1. Melchers, R. E., (1945) Structural Reliability Analysis and Predictions. John Wiley and Sons, second edition.

    """
    def __init__(self, poly, correlation_matrix, verbose=False):
        self.poly = poly
        D = self.poly.get_parameters()
        self.D = D
        self.R = correlation_matrix
        self.std = Parameter(order=5,
                             distribution='normal',
                             shape_parameter_A=0.0,
                             shape_parameter_B=1.0)
        inf_lim = -8.0
        sup_lim = -inf_lim
        p1 = Parameter(distribution='uniform',
                       lower=inf_lim,
                       upper=sup_lim,
                       order=31)
        myBasis = Basis('tensor-grid')
        Pols = Poly([p1, p1], myBasis, method='numerical-integration')
        p = Pols.get_points()
        w = Pols.get_weights() * (sup_lim - inf_lim)**2
        p1 = p[:, 0]
        p2 = p[:, 1]
        R0 = np.eye((len(self.D)))
        for i in range(len(self.D)):
            for j in range(i + 1, len(self.D), 1):
                if self.R[i, j] == 0:
                    R0[i, j] = 0.0
                else:
                    tp11 = -(np.array(self.D[i].get_icdf(
                        self.std.get_cdf(points=p1))) -
                             self.D[i].mean) / np.sqrt(self.D[i].variance)
                    tp22 = -(np.array(self.D[j].get_icdf(
                        self.std.get_cdf(points=p2))) -
                             self.D[j].mean) / np.sqrt(self.D[j].variance)

                    rho_ij = self.R[i, j]
                    bivariateNormalPDF = (
                        1.0 / (2.0 * np.pi * np.sqrt(1.0 - rho_ij**2)) *
                        np.exp(-1.0 / (2.0 * (1.0 - rho_ij**2)) *
                               (p1**2 - 2.0 * rho_ij * p1 * p2 + p2**2)))
                    coefficientsIntegral = np.flipud(tp11 * tp22 * w)

                    def check_difference(rho_ij):
                        bivariateNormalPDF = (
                            1.0 / (2.0 * np.pi * np.sqrt(1.0 - rho_ij**2)) *
                            np.exp(-1.0 / (2.0 * (1.0 - rho_ij**2)) *
                                   (p1**2 - 2.0 * rho_ij * p1 * p2 + p2**2)))
                        diff = np.dot(coefficientsIntegral, bivariateNormalPDF)
                        return diff - self.R[i, j]

                    if (self.D[i].name != 'custom') or (self.D[j].name !=
                                                        'custom'):
                        rho = optimize.newton(check_difference,
                                              self.R[i, j],
                                              maxiter=50)
                    else:
                        res = optimize.least_squares(check_difference,
                                                     R[i, j],
                                                     bounds=(-0.999, 0.999),
                                                     ftol=1.e-03)
                        rho = res.x
                        print('A Custom Marginal is present')

                    R0[i, j] = rho
                    R0[j, i] = R0[i, j]

        self.A = np.linalg.cholesky(R0)
        if verbose is True:
            print('The Cholesky decomposition of fictive matrix R0 is:')
            print(self.A)
            print('The fictive matrix is:')
            print(R0)
        list_of_parameters = []
        for i in range(0, len(self.D)):
            standard_parameter = Parameter(order=self.D[i].order,
                                           distribution='gaussian',
                                           shape_parameter_A=0.,
                                           shape_parameter_B=1.)
            list_of_parameters.append(standard_parameter)
        self.polystandard = deepcopy(self.poly)
        self.polystandard._set_parameters(list_of_parameters)
        self.standard_samples = self.polystandard.get_points()
        self._points = self.get_correlated_from_uncorrelated(
            self.standard_samples)

    def get_points(self):
        """
        Returns the correlated samples based on the quadrature rules used in poly.

        :param Correlations self: An instance of the Correlations object.

        :return:
            **points**: A numpy.ndarray of sampled quadrature points with shape (number_of_samples, dimension).

        """
        return self._points

    def set_model(self, model=None, model_grads=None):
        """
        Computes the coefficients of the polynomial.

        :param Correlations self:
            An instance of the Correlations class.
        :param callable model:
            The function that needs to be approximated. In the absence of a callable function, the input can be the function evaluated at the quadrature points.
        :param callable model_grads:
            The gradient of the function that needs to be approximated. In the absence of a callable gradient function, the input can be a matrix of gradient evaluations at the quadrature points.
        """
        model_values = None
        model_grads_values = None
        if callable(model):
            model_values = evaluate_model(self._points, model)
        else:
            model_values = model
        if model_grads is not None:
            if callable(model_grads):
                model_grads_values = evaluate_model_gradients(
                    self._points, model_grads)
            else:
                model_grads_values = model_grads
        self.polystandard.set_model(model_values, model_grads_values)

    def get_transformed_poly(self):
        """
        Returns the transformed polynomial.

        :param Correlations self:
            An instance of the Correlations class.

        :return:
            **poly**: An instance of the Poly class.
        """
        return self.polystandard

    def get_correlated_from_uncorrelated(self, X):
        """
        Method for mapping uncorrelated variables from standard normal space to a new physical space in which variables are correlated.

        :param Correlations self: An instance of the Correlations object.
        :param numpy.ndarray X: Samples of uncorrelated points from the marginals; of shape (N,M)

        :return:
            **C**: A numpy.ndarray of shape (N, M), which contains the correlated samples.
        """
        X = X.T

        invA = np.linalg.inv(self.A)
        Z = np.linalg.solve(invA, X)
        Z = Z.T

        xc = np.zeros((len(Z[:, 0]), len(self.D)))
        for i in range(len(self.D)):
            for j in range(len(Z[:, 0])):
                xc[j, i] = self.std.get_cdf(points=Z[j, i])
        Xc = np.zeros((len(Z[:, 0]), len(self.D)))
        for i in range(len(self.D)):
            for j in range(len(Z[:, 0])):
                temporary = np.matrix(xc[j, i])
                temp = self.D[i].get_icdf(temporary)

                t = temp[0]
                Xc[j, i] = t
        return Xc

    def get_correlated_samples(self, N=None):
        """
        Method for generating correlated samples.

        :param int N: Number of correlated samples required.
        :param numpy.ndarray X: Samples of correlated points from the marginals; of shape (N,M)

        :return:
            **C**: A numpy.ndarray of shape (N, M), which contains the correlated samples.
        """
        if N is not None:

            distro = list()
            for i in range(len(self.D)):
                distro1 = self.std.get_samples(N)

                # check dimensions ------------------#
                distro1 = np.matrix(distro1)
                dimension = np.shape(distro1)
                if dimension[0] == N:
                    distro1 = distro1.T
                #------------------------------------#
                distro.append(distro1)

            distro = np.reshape(distro, (len(self.D), N))
            interm = np.dot(self.A, distro)
            correlated = np.zeros((len(self.D), N))
            for i in range(len(self.D)):
                for j in range(N):
                    correlated[i, j] = self.D[i].get_icdf(
                        self.std.get_cdf(interm[i, j]))
            correlated = correlated.T
            return correlated

        else:
            raise (
                ValueError,
                'One input must be given to "get Correlated Samples" method: please choose between sampling N points or giving an array of uncorrelated data '
            )
class Correlations(object):
    """
    The class defines methods for polynomial approximations with correlated inputs, including the Nataf transform and Gram-Schmidt process.

    :param numpy.ndarray correlation_matrix: The correlation matrix associated with the joint distribution.
    :param Poly poly: Polynomial defined with parameters with marginal distributions in uncorrelated space.
    :param list parameters: List of parameters with marginal distributions.
    :param str method: `nataf-transform` or `gram-schmidt`.
    :param bool verbose: Display Cholesky decomposition of the fictive matrix.

    **References**
        1. Melchers, R. E., (1945) Structural Reliability Analysis and Predictions. John Wiley and Sons, second edition.
        2. Jakeman, J. D. et al., (2019) Polynomial chaos expansions for dependent random variables.
    """
    def __init__(self,
                 correlation_matrix,
                 poly=None,
                 parameters=None,
                 method=None,
                 verbose=False):
        if (poly is None) and (method is not None):
            raise ValueError('Need to specify poly for probability transform.')
        if poly is not None:
            self.poly = poly
            D = self.poly.get_parameters()
        elif parameters is not None:
            D = parameters
        else:
            raise ValueError('Need to specify either poly or parameters.')
        self.D = D
        self.R = correlation_matrix
        self.std = Parameter(order=5,
                             distribution='normal',
                             shape_parameter_A=0.0,
                             shape_parameter_B=1.0)
        inf_lim = -8.0
        sup_lim = -inf_lim
        p1 = Parameter(distribution='uniform',
                       lower=inf_lim,
                       upper=sup_lim,
                       order=31)
        myBasis = Basis('tensor-grid')
        self.Pols = Poly([p1, p1], myBasis, method='numerical-integration')
        Pols = self.Pols
        p = Pols.get_points()
        # w = Pols.get_weights()
        w = Pols.get_weights() * (sup_lim - inf_lim)**2
        p1 = p[:, 0]
        p2 = p[:, 1]
        R0 = np.eye((len(self.D)))
        for i in range(len(self.D)):
            for j in range(i + 1, len(self.D), 1):
                if self.R[i, j] == 0:
                    R0[i, j] = 0.0
                else:
                    z1 = np.array(self.D[i].get_icdf(
                        self.std.get_cdf(points=p1)))
                    z2 = np.array(self.D[j].get_icdf(
                        self.std.get_cdf(points=p2)))

                    tp11 = (z1 - self.D[i].mean) / np.sqrt(self.D[i].variance)
                    tp22 = (z2 - self.D[j].mean) / np.sqrt(self.D[j].variance)

                    coefficientsIntegral = np.flipud(tp11 * tp22 * w)

                    def check_difference(rho_ij):
                        bivariateNormalPDF = (
                            1.0 / (2.0 * np.pi * np.sqrt(1.0 - rho_ij**2)) *
                            np.exp(-1.0 / (2.0 * (1.0 - rho_ij**2)) *
                                   (p1**2 - 2.0 * rho_ij * p1 * p2 + p2**2)))
                        diff = np.dot(coefficientsIntegral, bivariateNormalPDF)
                        return diff - self.R[i, j]

                    # if (self.D[i].name!='custom') or (self.D[j].name!='custom'):
                    rho = optimize.newton(check_difference,
                                          self.R[i, j],
                                          maxiter=50)
                    # else:
                    #     # ???
                    #     res = optimize.least_squares(check_difference, self.R[i,j], bounds=(-0.999,0.999), ftol=1.e-03)
                    #     rho = res.x
                    #     print('A Custom Marginal is present')

                    R0[i, j] = rho
                    R0[j, i] = R0[i, j]
        self.R0 = R0.copy()

        self.A = np.linalg.cholesky(R0)
        if verbose:
            print('The Cholesky decomposition of fictive matrix R0 is:')
            print(self.A)
            print('The fictive matrix is:')
            print(R0)

        if method is None:
            pass
        elif method.lower() == 'nataf-transform':
            list_of_parameters = []
            for i in range(0, len(self.D)):
                standard_parameter = Parameter(order=self.D[i].order,
                                               distribution='gaussian',
                                               shape_parameter_A=0.,
                                               shape_parameter_B=1.)
                list_of_parameters.append(standard_parameter)

            # have option so that we don't need to obtain
            self.corrected_poly = deepcopy(self.poly)

            if hasattr(self.corrected_poly, '_quadrature_points'):
                self.corrected_poly._set_parameters(list_of_parameters)
                self.standard_samples = self.corrected_poly._quadrature_points
                self._points = self.get_correlated_samples(
                    X=self.standard_samples)
                # self.corrected_poly._quadrature_points = self._points.copy()
        elif method.lower() == 'gram-schmidt':
            basis_card = poly.basis.cardinality
            oversampling = 10

            N_Psi = oversampling * basis_card
            S_samples = self.get_correlated_samples(N=N_Psi)
            w_weights = 1.0 / N_Psi * np.ones(N_Psi)
            Psi = poly.get_poly(S_samples).T
            WPsi = np.diag(np.sqrt(w_weights)) @ Psi
            self.WPsi = WPsi

            R_Psi = np.linalg.qr(WPsi)[1]

            self.R_Psi = R_Psi
            self.R_Psi[0, :] *= np.sign(self.R_Psi[0, 0])
            self.corrected_poly = deepcopy(poly)
            self.corrected_poly.inv_R_Psi = np.linalg.inv(self.R_Psi)
            self.corrected_poly.corr = self
            self.corrected_poly._set_points_and_weights()

            P = self.corrected_poly.get_poly(
                self.corrected_poly._quadrature_points)
            W = np.mat(
                np.diag(np.sqrt(self.corrected_poly._quadrature_weights)))
            A = W * P.T
            self.corrected_poly.A = A
            self.corrected_poly.P = P

            if hasattr(self.corrected_poly, '_quadrature_points'):
                # TODO: Correlated quadrature points?
                self._points = self.corrected_poly._quadrature_points
        else:
            raise ValueError('Invalid method for correlations.')

    def get_points(self):
        """
        Returns the correlated samples based on the quadrature rules used in poly.

        :param Correlations self: An instance of the Correlations object.

        :return:
            **points**: A numpy.ndarray of sampled quadrature points with shape (number_of_samples, dimension).

        """
        return self._points

    def set_model(self, model=None, model_grads=None):
        """
        Computes the coefficients of the polynomial.

        :param Correlations self:
            An instance of the Correlations class.
        :param callable model:
            The function that needs to be approximated. In the absence of a callable function, the input can be the function evaluated at the quadrature points.
        :param callable model_grads:
            The gradient of the function that needs to be approximated. In the absence of a callable gradient function, the input can be a matrix of gradient evaluations at the quadrature points.
        """
        # Need to account for the nataf transform here?
        model_values = None
        model_grads_values = None
        if callable(model):
            model_values = evaluate_model(self._points, model)
        else:
            model_values = model
        if model_grads is not None:
            if callable(model_grads):
                model_grads_values = evaluate_model_gradients(
                    self._points, model_grads)
            else:
                model_grads_values = model_grads
        self.corrected_poly.set_model(model_values, model_grads_values)

    def get_transformed_poly(self):
        """
        Returns the transformed polynomial.

        :param Correlations self:
            An instance of the Correlations class.

        :return:
            **poly**: An instance of the Poly class.
        """
        return self.corrected_poly

    def get_correlated_samples(self, N=None, X=None):
        """
        Method for generating correlated samples.

        :param int N: Number of correlated samples required.
        :param ndarray X: (Optional) Points in the uncorrelated space to map to the correlated space.

        :return:
            **C**: A numpy.ndarray of shape (N, M), which contains the correlated samples.
        """
        d = len(self.D)

        if X is None:
            if N is None:
                raise ValueError(
                    'Need to specify number of points to generate.')
            X = np.random.multivariate_normal(np.zeros(d), np.eye(d), size=N)

        X_test = X @ self.A.T

        U_test = np.zeros(X_test.shape)
        for i in range(d):
            U_test[:, i] = self.std.get_cdf(X_test[:, i])

        Z_test = np.zeros(U_test.shape)
        for i in range(d):
            Z_test[:, i] = self.D[i].get_icdf(U_test[:, i])
        return Z_test

    def get_pdf(self, X):
        """
        Evaluate PDF at the sample points.
        :param numpy.ndarray X: Sample points (Number of points by dimensions)
        :return:
            **C**: A numpy.ndarray of shape (N,) with evaluations of the PDF.
        """

        parameters = self.D
        if len(X.shape) == 1:
            X = X.reshape(-1, 1)
        d = X.shape[1]

        U = np.zeros(X.shape)
        for i in range(d):
            U[:, i] = norm.ppf(parameters[i].get_cdf(X[:, i]))
        cop_num = multivariate_normal(mean=np.zeros(d), cov=self.R0).pdf(U)
        cop_den = np.prod(np.array([norm.pdf(U[:, i]) for i in range(d)]),
                          axis=0)
        marginal_prod = np.prod(np.array(
            [parameters[i].get_pdf(X[:, i]) for i in range(d)]),
                                axis=0)
        return cop_num / cop_den * marginal_prod