From 87626d228e623ce3f5fac677ede40f47e2672f2e Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 29 May 2020 08:57:16 -0400 Subject: [PATCH 01/43] Add coordinate promolecular transformation (#15) Added the transformation from real space to unit cube using the Promolecular density function of a single coordinate. Added the inverse transformation from unit-cube to real space using a root-solver of a single coordinate. --- src/grid/protransform.py | 133 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/grid/protransform.py diff --git a/src/grid/protransform.py b/src/grid/protransform.py new file mode 100644 index 000000000..c3d604540 --- /dev/null +++ b/src/grid/protransform.py @@ -0,0 +1,133 @@ +# GRID is a numerical integration module for quantum chemistry. +# +# Copyright (C) 2011-2019 The GRID Development Team +# +# This file is part of GRID. +# +# GRID is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# GRID 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see +# -- +r"""Promolecular Grid Transformation""" + + +from collections import namedtuple + +from grid.rtransform import BaseTransform + +import numpy as np +from scipy.optimize import root_scalar +from scipy.special import erf + + +PromolParams = namedtuple("PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"]) + + +def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=False): + r""" + Transform the `i_var` coordinate in a real point to [0, 1]^D using promolecular. + + Parameters + ---------- + real_pt : np.ndarray(D,) + Real point being transformed. + i_var : int + Index that is being tranformed. Less than D. + promol_params : namedTuple + Data about the Promolecular density. + deriv : bool + If true, return the derivative of transformation wrt `i_var` real variable. + Default is False. + sderiv : bool + If true, return the second derivative of transformation wrt `i_var` real variable. + Default is False. + + Returns + ------- + unit_pt, deriv, sderiv : (float, float, float) + The transformed point in [0,1]^D and its derivative with respect to real point and + the second derivative with respect to real point are returned. + + """ + c_m, e_m, coords, dim, pi_over_exps = promol_params + + # Distance to centers/nuclei`s and Prefactors. + diff_coords = real_pt[:i_var + 1] - coords[:, :i_var + 1] + diff_squared = diff_coords**2. + distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] + # If i_var is zero, then distance is just all zeros. + + # Gaussian Integrals Over Entire Space For Numerator and Denomator. + gaussian_integrals = np.exp(-e_m * distance) * pi_over_exps**(dim - i_var) + coeff_num = c_m * gaussian_integrals + + # Get the integral of Gaussian till a point. + coord_ivar = diff_coords[:, i_var][:, np.newaxis] + integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.) / 2. + + # Final Result. + transf_num = np.sum(coeff_num * integrate_till_pt_x) + transf_den = np.sum(coeff_num) + transform_value = transf_num / transf_den + + if deriv: + inner_term = coeff_num * np.exp(-e_m * diff_squared[:, i_var][:, np.newaxis]) + deriv = np.sum(inner_term) / transf_den + + if sderiv: + sderiv = np.sum(inner_term * -e_m * 2. * coord_ivar) / transf_den + return transform_value, deriv, sderiv + return transform_value, deriv + return transform_value + + +def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): + all_points = np.append(prev_trans_pts, init_guess) + transf_pt = transform_coordinate(all_points, i_var, params) + return theta_pt - transf_pt + + +def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): + r""" + + Parameters + ---------- + theta_pt : float + Point in [0, 1]. + i_var : int + Index that is being tranformed. Less than D. + promol_params : namedTuple + Data about the Promolecular density. + transformed : list(`i_var` - 1) + The set of previous points before index `i_var` that were transformed to real space. + bracket : (float, float) + Interval where root is suspected to be in Reals. Used for "brentq" root-finding method. + Default is (-10, 10). + + Returns + ------- + real_pt : float + Return the transformed real point. + + Raises + ------ + AssertionError : If the root did not converge, or brackets did not have opposite sign. + + """ + # The [:i_var] is needed because of the way I've set-up transformed attribute. + if np.isnan(bracket[0]) or np.nan in transformed[:i_var]: + return np.nan + args = (transformed[:i_var], theta_pt, i_var, params) + root_result = root_scalar(_root_equation, args=args, method="brentq", + bracket=[bracket[0], bracket[1]], maxiter=50, xtol=2e-15) + assert root_result.converged + return root_result.root From fbe5355d68d6a148cb4929b53ec17fce1fdb299f Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 29 May 2020 09:37:35 -0400 Subject: [PATCH 02/43] Add promol cubic class and grid transform (#15) * Promolecular Cubic Grid Transformation is added. * Bracket initalization is added as private func. * Padding multiple arrays with zeros is added st have array size. * Full Grid transformation from unit-cube to real space. --- src/grid/protransform.py | 80 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index c3d604540..693a9996d 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -32,9 +32,77 @@ PromolParams = namedtuple("PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"]) +class ProCubicTransform: + def __init__(self, stepsize, weights, coeffs, exps, coords): + self.stepsizes = stepsize + self.num_pts = (int(1 / stepsize[0]) + 1, + int(1. / stepsize[1]) + 1, + int(1. / stepsize[2]) + 1) + + # pad coefficients and exponents with zeros to have the same size. + coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) + + # Rather than computing this repeatedly. It is fixed. + with np.errstate(divide='ignore'): + pi_over_exponents = np.sqrt(np.pi / exps) + pi_over_exponents[exps == 0] = 0 + + self.promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) + self.points = np.empty((np.prod(self.num_pts), 3), dtype=np.float64) + self._transform() # Fill out self.points. + self.weights = weights + + def _transform(self): + counter = 0 + for ix in range(self.num_pts[0]): + cart_pt = [None, None, None] + unit_x = self.stepsizes[0] * ix + + initx = self._get_bracket((ix,), 0) + transformx = inverse_coordinate(unit_x, 0, self.promol, cart_pt, initx) + cart_pt[0] = transformx + + for iy in range(self.num_pts[1]): + unit_y = self.stepsizes[1] * iy + + inity = self._get_bracket((ix, iy), 1) + transformy = inverse_coordinate(unit_y, 1, self.promol, cart_pt, inity) + cart_pt[1] = transformy + + for iz in range(self.num_pts[2]): + unit_z = self.stepsizes[2] * iz + + initz = self._get_bracket((ix, iy, iz), 2) + transformz = inverse_coordinate(unit_z, 2, self.promol, cart_pt, initz) + cart_pt[2] = transformz + self.points[counter] = cart_pt.copy() + counter += 1 + + def _get_bracket(self, coord, i_var): + # If it is a boundary point, then return nan. + if 0. in coord[:i_var + 1] or (self.num_pts[i_var] - 1) in coord[:i_var + 1]: + return np.nan, np.nan + # If it is a new point, with no nearby point, get a large initial guess. + elif coord[i_var] == 1: + min = (np.min(self.promol.coords[:, i_var]) - 3.) * 20. + max = (np.max(self.promol.coords[:, i_var]) + 3.) * 20. + return min, max + # If the previous point has been converted, use that as a initial guess. + if i_var == 0: + index = (coord[0] - 1) * self.num_pts[1] * self.num_pts[2] + elif i_var == 1: + index = coord[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * (coord[1] - 1) + elif i_var == 2: + index = (coord[0] * self.num_pts[1] * self.num_pts[2] + + self.num_pts[2] * coord[1] + coord[2] - 1) + + # FIXME : Rather than using fixed +10., use truncated taylor series. + return self.points[index, i_var], self.points[index, i_var] + 10. + + def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=False): r""" - Transform the `i_var` coordinate in a real point to [0, 1]^D using promolecular. + Transform the `i_var` coordinate in a real point to [0, 1] using promolecular density. Parameters ---------- @@ -98,6 +166,7 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): r""" + Transform a point in [0, 1] to the real space corresponding to the `i_var` variable. Parameters ---------- @@ -131,3 +200,12 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): bracket=[bracket[0], bracket[1]], maxiter=50, xtol=2e-15) assert root_result.converged return root_result.root + + +def _pad_coeffs_exps_with_zeros(coeffs, exps): + max_numb_of_gauss = max(len(c) for c in coeffs) + coeffs = np.array([np.pad(a, (0, max_numb_of_gauss - len(a)), 'constant', + constant_values=0.) for a in coeffs], dtype=np.float64) + exps = np.array([np.pad(a, (0, max_numb_of_gauss - len(a)), 'constant', + constant_values=0.) for a in exps], dtype=np.float64) + return coeffs, exps From 8b1e12be2d89fe51a9f4eef178d090a42f92d120 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 29 May 2020 10:21:21 -0400 Subject: [PATCH 03/43] Add promolecular density evaluation (#15) Need this for integration --- src/grid/protransform.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 693a9996d..b8f44f9b1 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -34,7 +34,7 @@ class ProCubicTransform: def __init__(self, stepsize, weights, coeffs, exps, coords): - self.stepsizes = stepsize + self.ss = stepsize self.num_pts = (int(1 / stepsize[0]) + 1, int(1. / stepsize[1]) + 1, int(1. / stepsize[2]) + 1) @@ -52,25 +52,55 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): self._transform() # Fill out self.points. self.weights = weights + def _promolecular(self, grid): + r""" + Evaluate the promolecular density over a grid. + + Parameters + ---------- + grid : np.ndarray(M,) + Grid points. + + Returns + ------- + np.ndarray(M,) : + Promolecular density evaluated at the grid points. + + """ + # TODO: For Design, Store this or constantly re-evaluate it? + # N is the number of grid points. + # M is the number of centers/atoms. + # D is the number of dimensions, usually 3. + # K is maximum number of gaussian functions over all M atoms. + cm, em, coords, _, _ = self.promol + # Shape (N, M, D), then Summing gives (N, M, 1) + distance = np.sum((grid - coords[:, np.newaxis])**2., axis=2, keepdims=True) + # At each center, multiply Each Distance of a Coordinate, with its exponents. + exponen = np.exp(-np.einsum("MND, MK-> MNK" , distance, em)) + # At each center, multiply the exponential with its coefficients. + gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) + # At each point, summing the gaussians for each center, then summing all centers together. + return np.einsum("MNK -> N", gaussian) + def _transform(self): counter = 0 for ix in range(self.num_pts[0]): cart_pt = [None, None, None] - unit_x = self.stepsizes[0] * ix + unit_x = self.ss[0] * ix initx = self._get_bracket((ix,), 0) transformx = inverse_coordinate(unit_x, 0, self.promol, cart_pt, initx) cart_pt[0] = transformx for iy in range(self.num_pts[1]): - unit_y = self.stepsizes[1] * iy + unit_y = self.ss[1] * iy inity = self._get_bracket((ix, iy), 1) transformy = inverse_coordinate(unit_y, 1, self.promol, cart_pt, inity) cart_pt[1] = transformy for iz in range(self.num_pts[2]): - unit_z = self.stepsizes[2] * iz + unit_z = self.ss[2] * iz initz = self._get_bracket((ix, iy, iz), 2) transformz = inverse_coordinate(unit_z, 2, self.promol, cart_pt, initz) From 815096e994ed3d55b6a4ccc5fc1a4227be94d38d Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 29 May 2020 10:51:40 -0400 Subject: [PATCH 04/43] Add integration method to promol transform (#15) Also added the promolecular trick. --- src/grid/protransform.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index b8f44f9b1..251ae5e7f 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -22,7 +22,7 @@ from collections import namedtuple -from grid.rtransform import BaseTransform +from grid.basegrid import Grid import numpy as np from scipy.optimize import root_scalar @@ -32,7 +32,7 @@ PromolParams = namedtuple("PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"]) -class ProCubicTransform: +class ProCubicTransform(Grid): def __init__(self, stepsize, weights, coeffs, exps, coords): self.ss = stepsize self.num_pts = (int(1 / stepsize[0]) + 1, @@ -41,16 +41,32 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): # pad coefficients and exponents with zeros to have the same size. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) - # Rather than computing this repeatedly. It is fixed. with np.errstate(divide='ignore'): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 - + self.prointegral = np.sum(coeffs * pi_over_exponents**(1.5)) self.promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) - self.points = np.empty((np.prod(self.num_pts), 3), dtype=np.float64) - self._transform() # Fill out self.points. - self.weights = weights + + # initialize parent class + empty_points = np.empty((np.prod(self.num_pts), 3), dtype=np.float64) + super().__init__(empty_points, weights * self.prointegral) + self._transform() + + def integrate(self, *value_arrays, promol_trick=False): + promolecular = self._promolecular(self.points) + integrands = [] + with np.errstate(divide='ignore'): + for arr in value_arrays: + if promol_trick: + integrand = (arr - promolecular) / promolecular + else: + integrand = arr / promolecular + integrand[np.isnan(self.points).any(axis=1)] = 0. + integrands.append(arr) + if promol_trick: + return self.prointegral + super().integrate(*integrands) + return super().integrate(*integrands) def _promolecular(self, grid): r""" From 42efb075f9a03e3f9a2743b460874bd8087fd679 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 29 May 2020 11:22:34 -0400 Subject: [PATCH 05/43] Add docs to promolecular grid transform (#15) --- src/grid/protransform.py | 122 ++++++++++++++++++++++++++++++++++----- 1 file changed, 107 insertions(+), 15 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 251ae5e7f..53dbf9107 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -33,11 +33,61 @@ class ProCubicTransform(Grid): + r""" + Promolecular Grid Transformation of a Cubic Grid in [0,1]^3. + + Attributes + ---------- + num_pts : (int, int, int) + The number of points in x, y, and z direction. + ss : (float, float, float) + The step-size in each x, y, and z direction. + points : np.ndarray(N, 3) + The transformed points in real space. + prointegral : float + The integration value of the promolecular density over Euclidean space. + weights : np.ndarray(N,) + The weights multiplied by `prointegral`. + promol : namedTuple + Data about the Promolecular density. + + Methods + ------- + integrate(trick=False) + Integral of a real-valued function over Euclidean space. + + Examples + -------- + Define information of the Promolecular Density. + >> c = np.array([[5.], [10.]]) + >> e = np.array([[2.], [3.]]) + >> coord = np.array([[0., 0., 0.], [2., 2., 2.]]) + + Define information of the grid and its weights. + >> stepsize = 0.01 + >> weights = np.array([0.01] * 101**3) # Simple Riemannian weights. + >> promol = ProCubicTransform([ss] * 3, weights, c, e, coord) + + To integrate some function f. + >> def func(pt): + >> return np.exp(-0.1 * np.linalg.norm(pt, axis=1)**2.) + >> func_values = func(promol.points) + >> print("The integral is %.4f" % promol.integrate(func_values, trick=False) + + References + ---------- + .. [1] J. I. Rodríguez, D. C. Thompson, P. W. Ayers, and A. M. Koster, "Numerical integration + of exchange-correlation energies and potentials using transformed sparse grids." + + Notes + ----- + + """ def __init__(self, stepsize, weights, coeffs, exps, coords): - self.ss = stepsize - self.num_pts = (int(1 / stepsize[0]) + 1, - int(1. / stepsize[1]) + 1, - int(1. / stepsize[2]) + 1) + self._ss = stepsize + self._num_pts = (int(1 / stepsize[0]) + 1, + int(1. / stepsize[1]) + 1, + int(1. / stepsize[2]) + 1) # pad coefficients and exponents with zeros to have the same size. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) @@ -45,27 +95,70 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): with np.errstate(divide='ignore'): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 - self.prointegral = np.sum(coeffs * pi_over_exponents**(1.5)) - self.promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) + self._prointegral = np.sum(coeffs * pi_over_exponents ** (1.5)) + self._promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) # initialize parent class - empty_points = np.empty((np.prod(self.num_pts), 3), dtype=np.float64) - super().__init__(empty_points, weights * self.prointegral) + empty_points = np.empty((np.prod(self._num_pts), 3), dtype=np.float64) + super().__init__(empty_points, weights * self._prointegral) self._transform() - def integrate(self, *value_arrays, promol_trick=False): + @property + def num_pts(self): + r"""Number of points in each direction.""" + return self._num_pts + + @property + def ss(self): + r"""Stepsize of the cubic grid.""" + return self._ss + + @property + def prointegral(self): + r"""Integration of Promolecular density.""" + return self._prointegral + + @property + def promol(self): + r"""PromolParams namedTuple.""" + return self._promol + + def integrate(self, *value_arrays, trick=False): + r""" + Integrate any function. + + Parameters + ---------- + *value_arrays : np.ndarray(N, ) + One or multiple value array to integrate. + trick : bool + If true, uses the promolecular trick. + + Returns + ------- + float : + Return the integration of the function. + + Raises + ------ + TypeError + Input integrand is not of type np.ndarray. + ValueError + Input integrand array is given or not of proper shape. + + """ promolecular = self._promolecular(self.points) integrands = [] with np.errstate(divide='ignore'): for arr in value_arrays: - if promol_trick: + if trick: integrand = (arr - promolecular) / promolecular else: integrand = arr / promolecular integrand[np.isnan(self.points).any(axis=1)] = 0. integrands.append(arr) - if promol_trick: - return self.prointegral + super().integrate(*integrands) + if trick: + return self._prointegral + super().integrate(*integrands) return super().integrate(*integrands) def _promolecular(self, grid): @@ -74,17 +167,16 @@ def _promolecular(self, grid): Parameters ---------- - grid : np.ndarray(M,) + grid : np.ndarray(N,) Grid points. Returns ------- - np.ndarray(M,) : + np.ndarray(N,) : Promolecular density evaluated at the grid points. """ # TODO: For Design, Store this or constantly re-evaluate it? - # N is the number of grid points. # M is the number of centers/atoms. # D is the number of dimensions, usually 3. # K is maximum number of gaussian functions over all M atoms. From aebc5193e3646c17b0cd3763f26ebc51369ecfea Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 29 May 2020 13:39:52 -0400 Subject: [PATCH 06/43] Fix black complaints --- src/grid/protransform.py | 111 +++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 53dbf9107..32064d0a8 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -29,7 +29,9 @@ from scipy.special import erf -PromolParams = namedtuple("PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"]) +PromolParams = namedtuple( + "PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"] +) class ProCubicTransform(Grid): @@ -38,10 +40,10 @@ class ProCubicTransform(Grid): Attributes ---------- - num_pts : (int, int, int) - The number of points in x, y, and z direction. ss : (float, float, float) The step-size in each x, y, and z direction. + num_pts : (int, int, int) + The number of points in x, y, and z direction. This is calculated as `int(1. / ss[i]) + 1` points : np.ndarray(N, 3) The transformed points in real space. prointegral : float @@ -69,9 +71,9 @@ class ProCubicTransform(Grid): >> promol = ProCubicTransform([ss] * 3, weights, c, e, coord) To integrate some function f. - >> def func(pt): + >> def f(pt): >> return np.exp(-0.1 * np.linalg.norm(pt, axis=1)**2.) - >> func_values = func(promol.points) + >> func_values = f(promol.points) >> print("The integral is %.4f" % promol.integrate(func_values, trick=False) References @@ -83,16 +85,27 @@ class ProCubicTransform(Grid): ----- """ + def __init__(self, stepsize, weights, coeffs, exps, coords): + if not isinstance(stepsize, tuple): + pass + if not isinstance(coeffs, (list, np.ndarray)): + pass + if not isinstance(exps, (list, np.ndarray)): + pass + if not isinstance(coords, (list, np.ndarray)): + pass self._ss = stepsize - self._num_pts = (int(1 / stepsize[0]) + 1, - int(1. / stepsize[1]) + 1, - int(1. / stepsize[2]) + 1) + self._num_pts = ( + int(1 / stepsize[0]) + 1, + int(1 / stepsize[1]) + 1, + int(1 / stepsize[2]) + 1, + ) # pad coefficients and exponents with zeros to have the same size. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) # Rather than computing this repeatedly. It is fixed. - with np.errstate(divide='ignore'): + with np.errstate(divide="ignore"): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 self._prointegral = np.sum(coeffs * pi_over_exponents ** (1.5)) @@ -149,13 +162,13 @@ def integrate(self, *value_arrays, trick=False): """ promolecular = self._promolecular(self.points) integrands = [] - with np.errstate(divide='ignore'): + with np.errstate(divide="ignore"): for arr in value_arrays: if trick: integrand = (arr - promolecular) / promolecular else: integrand = arr / promolecular - integrand[np.isnan(self.points).any(axis=1)] = 0. + integrand[np.isnan(self.points).any(axis=1)] = 0.0 integrands.append(arr) if trick: return self._prointegral + super().integrate(*integrands) @@ -182,12 +195,12 @@ def _promolecular(self, grid): # K is maximum number of gaussian functions over all M atoms. cm, em, coords, _, _ = self.promol # Shape (N, M, D), then Summing gives (N, M, 1) - distance = np.sum((grid - coords[:, np.newaxis])**2., axis=2, keepdims=True) + distance = np.sum((grid - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) # At each center, multiply Each Distance of a Coordinate, with its exponents. - exponen = np.exp(-np.einsum("MND, MK-> MNK" , distance, em)) + exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) # At each center, multiply the exponential with its coefficients. gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) - # At each point, summing the gaussians for each center, then summing all centers together. + # At each point, sum for each center, then sum all centers together. return np.einsum("MNK -> N", gaussian) def _transform(self): @@ -197,45 +210,51 @@ def _transform(self): unit_x = self.ss[0] * ix initx = self._get_bracket((ix,), 0) - transformx = inverse_coordinate(unit_x, 0, self.promol, cart_pt, initx) - cart_pt[0] = transformx + transfx = inverse_coordinate(unit_x, 0, self.promol, cart_pt, initx) + cart_pt[0] = transfx for iy in range(self.num_pts[1]): unit_y = self.ss[1] * iy inity = self._get_bracket((ix, iy), 1) - transformy = inverse_coordinate(unit_y, 1, self.promol, cart_pt, inity) - cart_pt[1] = transformy + transfy = inverse_coordinate(unit_y, 1, self.promol, cart_pt, inity) + cart_pt[1] = transfy for iz in range(self.num_pts[2]): unit_z = self.ss[2] * iz initz = self._get_bracket((ix, iy, iz), 2) - transformz = inverse_coordinate(unit_z, 2, self.promol, cart_pt, initz) - cart_pt[2] = transformz + transfz = inverse_coordinate(unit_z, 2, self.promol, cart_pt, initz) + cart_pt[2] = transfz self.points[counter] = cart_pt.copy() counter += 1 def _get_bracket(self, coord, i_var): # If it is a boundary point, then return nan. - if 0. in coord[:i_var + 1] or (self.num_pts[i_var] - 1) in coord[:i_var + 1]: + if 0.0 in coord[: i_var + 1] or (self.num_pts[i_var] - 1) in coord[: i_var + 1]: return np.nan, np.nan # If it is a new point, with no nearby point, get a large initial guess. elif coord[i_var] == 1: - min = (np.min(self.promol.coords[:, i_var]) - 3.) * 20. - max = (np.max(self.promol.coords[:, i_var]) + 3.) * 20. + min = (np.min(self.promol.coords[:, i_var]) - 3.0) * 20.0 + max = (np.max(self.promol.coords[:, i_var]) + 3.0) * 20.0 return min, max # If the previous point has been converted, use that as a initial guess. if i_var == 0: index = (coord[0] - 1) * self.num_pts[1] * self.num_pts[2] elif i_var == 1: - index = coord[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * (coord[1] - 1) + index = coord[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * ( + coord[1] - 1 + ) elif i_var == 2: - index = (coord[0] * self.num_pts[1] * self.num_pts[2] + - self.num_pts[2] * coord[1] + coord[2] - 1) + index = ( + coord[0] * self.num_pts[1] * self.num_pts[2] + + self.num_pts[2] * coord[1] + + coord[2] + - 1 + ) # FIXME : Rather than using fixed +10., use truncated taylor series. - return self.points[index, i_var], self.points[index, i_var] + 10. + return self.points[index, i_var], self.points[index, i_var] + 10.0 def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=False): @@ -267,18 +286,18 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals c_m, e_m, coords, dim, pi_over_exps = promol_params # Distance to centers/nuclei`s and Prefactors. - diff_coords = real_pt[:i_var + 1] - coords[:, :i_var + 1] - diff_squared = diff_coords**2. + diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] + diff_squared = diff_coords ** 2.0 distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] # If i_var is zero, then distance is just all zeros. # Gaussian Integrals Over Entire Space For Numerator and Denomator. - gaussian_integrals = np.exp(-e_m * distance) * pi_over_exps**(dim - i_var) + gaussian_integrals = np.exp(-e_m * distance) * pi_over_exps ** (dim - i_var) coeff_num = c_m * gaussian_integrals # Get the integral of Gaussian till a point. coord_ivar = diff_coords[:, i_var][:, np.newaxis] - integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.) / 2. + integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.0) / 2.0 # Final Result. transf_num = np.sum(coeff_num * integrate_till_pt_x) @@ -290,7 +309,7 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals deriv = np.sum(inner_term) / transf_den if sderiv: - sderiv = np.sum(inner_term * -e_m * 2. * coord_ivar) / transf_den + sderiv = np.sum(inner_term * -e_m * 2.0 * coord_ivar) / transf_den return transform_value, deriv, sderiv return transform_value, deriv return transform_value @@ -334,16 +353,32 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): if np.isnan(bracket[0]) or np.nan in transformed[:i_var]: return np.nan args = (transformed[:i_var], theta_pt, i_var, params) - root_result = root_scalar(_root_equation, args=args, method="brentq", - bracket=[bracket[0], bracket[1]], maxiter=50, xtol=2e-15) + root_result = root_scalar( + _root_equation, + args=args, + method="brentq", + bracket=[bracket[0], bracket[1]], + maxiter=50, + xtol=2e-15, + ) assert root_result.converged return root_result.root def _pad_coeffs_exps_with_zeros(coeffs, exps): max_numb_of_gauss = max(len(c) for c in coeffs) - coeffs = np.array([np.pad(a, (0, max_numb_of_gauss - len(a)), 'constant', - constant_values=0.) for a in coeffs], dtype=np.float64) - exps = np.array([np.pad(a, (0, max_numb_of_gauss - len(a)), 'constant', - constant_values=0.) for a in exps], dtype=np.float64) + coeffs = np.array( + [ + np.pad(a, (0, max_numb_of_gauss - len(a)), "constant", constant_values=0.0) + for a in coeffs + ], + dtype=np.float64, + ) + exps = np.array( + [ + np.pad(a, (0, max_numb_of_gauss - len(a)), "constant", constant_values=0.0) + for a in exps + ], + dtype=np.float64, + ) return coeffs, exps From 8e37b8c9a0a5eb86b58a8c5cc891cdfbc7ebeea5 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Sun, 31 May 2020 12:24:54 -0400 Subject: [PATCH 07/43] Fix integration method for promol transform (#15) --- src/grid/protransform.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 32064d0a8..70a45024d 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -67,7 +67,8 @@ class ProCubicTransform(Grid): Define information of the grid and its weights. >> stepsize = 0.01 - >> weights = np.array([0.01] * 101**3) # Simple Riemannian weights. + >> numb_x = int(1. / stepsize) + 1 + >> weights = np.array([0.01] * numb_x**3) # Simple Riemannian weights. >> promol = ProCubicTransform([ss] * 3, weights, c, e, coord) To integrate some function f. @@ -108,7 +109,7 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): with np.errstate(divide="ignore"): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 - self._prointegral = np.sum(coeffs * pi_over_exponents ** (1.5)) + self._prointegral = np.sum(coeffs * pi_over_exponents ** 3.) self._promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) # initialize parent class @@ -142,7 +143,7 @@ def integrate(self, *value_arrays, trick=False): Parameters ---------- - *value_arrays : np.ndarray(N, ) + *value_arrays : (np.ndarray(N, dtype=float),) One or multiple value array to integrate. trick : bool If true, uses the promolecular trick. @@ -164,14 +165,15 @@ def integrate(self, *value_arrays, trick=False): integrands = [] with np.errstate(divide="ignore"): for arr in value_arrays: + assert arr.dtype != object, "Array dtype should not be object." if trick: integrand = (arr - promolecular) / promolecular else: integrand = arr / promolecular integrand[np.isnan(self.points).any(axis=1)] = 0.0 - integrands.append(arr) + integrands.append(integrand) if trick: - return self._prointegral + super().integrate(*integrands) + return self.prointegral + super().integrate(*integrands) return super().integrate(*integrands) def _promolecular(self, grid): @@ -201,7 +203,7 @@ def _promolecular(self, grid): # At each center, multiply the exponential with its coefficients. gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) # At each point, sum for each center, then sum all centers together. - return np.einsum("MNK -> N", gaussian) + return np.einsum("MNK -> N", gaussian, dtype=np.float64) def _transform(self): counter = 0 @@ -252,7 +254,6 @@ def _get_bracket(self, coord, i_var): + coord[2] - 1 ) - # FIXME : Rather than using fixed +10., use truncated taylor series. return self.points[index, i_var], self.points[index, i_var] + 10.0 From 775466fe84adf6f7fe47b53302c51c661be995cd Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 12 Jun 2020 15:00:48 -0400 Subject: [PATCH 08/43] Add tests to promolecular grid transformation --- src/grid/tests/test_protransform.py | 415 ++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 src/grid/tests/test_protransform.py diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py new file mode 100644 index 000000000..b83340268 --- /dev/null +++ b/src/grid/tests/test_protransform.py @@ -0,0 +1,415 @@ +# GRID is a numerical integration module for quantum chemistry. +# +# Copyright (C) 2011-2019 The GRID Development Team +# +# This file is part of GRID. +# +# GRID is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# GRID 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see +# -- +r""" +Tests for Cubic Promolecular transformation. + +Tests +----- +TestTwoGaussianDiffCenters : + Test Transformation of Two Gaussian promolecular against different methods both analytical + and numerics. +TestOneGaussianAgainstNumerics : + Test a single Gaussian against numerical integration/differentiation. + +""" + +import numpy as np +from scipy.special import erf +from scipy.optimize import approx_fprime + +import pytest + +from grid.protransform import ( + CubicProTransform, PromolParams, transform_coordinate, _pad_coeffs_exps_with_zeros +) + + +class TestTwoGaussianDiffCenters: + r""" + Test a Sum of Two Gaussian function against analytic formulas and numerical procedures. + """ + def setUp(self, ss=0.1, return_obj=False): + c = np.array([[5.], [10.]]) + e = np.array([[2.], [3.]]) + coord = np.array([[1., 2., 3.], [2., 2., 2.]]) + params = PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + if return_obj: + num_pts = int(1 / ss) + 1 + weights = np.array([((1. / (num_pts - 2)))**3.] * num_pts**3) + obj = CubicProTransform([ss] * 3, weights, params.c_m, params.e_m, params.coords) + return params, obj + return params + + def promolecular(self, x, y, z, params): + # Promolecular in CubicProTransform class uses einsum, this tests it against that. + # Also could be used for integration tests. + cm, em, coords, _, _ = params + promol = 0. + for i, coeffs in enumerate(cm): + xc, yc, zc = coords[i] + for j, coeff in enumerate(coeffs): + distance = ((x - xc)**2. + (y - yc)**2. + (z - zc)**2.) + promol += np.sum(coeff * np.exp(- em[i, j] * distance)) + return promol + + def test_promolecular_density(self): + grid = np.array([[-1., 2., 3.], [5., 10., -1.], [0., 0., 0.], + [3., 2., 1.], [10., 20., 30.], [0., 10., 0.2]]) + params, obj = self.setUp(return_obj=True) + + true_ans = [] + for pt in grid: + x, y, z = pt + true_ans.append(self.promolecular(x, y, z, params)) + + desired = obj._promolecular(grid) + assert np.all(np.abs(np.array(true_ans) - desired) < 1e-8) + + @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.5)) + def test_transforming_x_against_formula(self, pt): + def formula_transforming_x(x): + r"""Closed form formula for transforming x coordinate.""" + first_factor = (5. * np.pi ** 1.5 / (4 * 2 ** 0.5)) * (erf(2 ** 0.5 * (x - 1)) + 1.) + sec_fac = ((10. * np.pi ** 1.5) / (6. * 3 ** 0.5)) * (erf(3. ** 0.5 * (x - 2)) + 1.) + return (first_factor + sec_fac) / (5. * (np.pi / 2) ** 1.5 + 10. * (np.pi / 3.) ** 1.5) + + true_ans = transform_coordinate([pt], 0, self.setUp()) + assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 + + @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.75)) + def test_derivative_transforming_x_with_finite_difference(self, pt): + # Unfortunately, pnly one decimal place is obtained. + params = self.setUp() + _, actual = transform_coordinate([pt], 0, params, deriv=True) + def func(x): + return transform_coordinate([x], 0, params) + desired = approx_fprime([pt], func, epsilon=1.49e-08) + assert np.abs(desired - actual) < 1e-1 + + @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) + @pytest.mark.parametrize("y", [-3, 2., -10.2321, 20.232109]) + def test_transforming_y_against_formula(self, x, y): + def formula_transforming_y(x, y): + r"""Closed form formula for transforming y coordinate.""" + fac1 = 5. * np.sqrt(np.pi / 2.) * np.exp(-2. * (x - 1.) ** 2.) + fac1 *= (np.sqrt(np.pi) * (erf(2. ** 0.5 * (y - 2)) + 1.) / (2. * np.sqrt(2.))) + fac2 = 10. * np.sqrt(np.pi / 3.) * np.exp(-3. * (x - 2.) ** 2.) + fac2 *= (np.sqrt(np.pi) * (erf(3. ** 0.5 * (y - 2)) + 1.) / (2. * np.sqrt(3.))) + num = fac1 + fac2 + + dac1 = 5. * (np.pi / 2.) * np.exp(-2. * (x - 1.) ** 2.) + dac2 = 10. * (np.pi / 3.) * np.exp(-3. * (x - 2.) ** 2.) + den = dac1 + dac2 + return num / den + true_ans = transform_coordinate([x, y], 1, self.setUp()) + assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 + + @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) + @pytest.mark.parametrize("y", [-3, 2., -10.2321]) + @pytest.mark.parametrize("z", [-10., 0., 2.343432]) + def test_transforming_z_against_formula(self, x, y, z): + def formula_transforming_z(x, y, z): + r"""Closed form formula for transforming z coordinate.""" + a1, a2, a3 = (x - 1.), (y - 2.), (z - 3.) + erfx = erf(2. ** 0.5 * a3) + 1. + fac1 = 5. * np.exp(-2. * (a1 ** 2. + a2 ** 2.)) * erfx * np.pi ** 0.5 / (2. * 2. ** 0.5) + + b1, b2, b3 = (x - 2.), (y - 2.), (z - 2.) + erfy = erf(3. ** 0.5 * b3) + 1. + fac2 = 10. * np.exp(-3. * (b1 ** 2. + b2 ** 2.)) * erfy * np.pi ** 0.5 / ( + 2. * 3. ** 0.5) + + den = 5. * (np.pi / 2.) ** 0.5 * np.exp(-2. * (a1 ** 2. + a2 ** 2.)) + den += 10. * (np.pi / 3.) ** 0.5 * np.exp(-3. * (b1 ** 2. + b2 ** 2.)) + return (fac1 + fac2) / den + + params, obj = self.setUp(ss=0.5, return_obj=True) + true_ans = formula_transforming_z(x, y, z) + # Test function + actual = transform_coordinate([x, y, z], 2, params) + assert np.abs(true_ans - actual) < 1e-8 + + # Test Method + actual = obj.transform(np.array([x, y, z]))[2] + assert np.abs(true_ans - actual) < 1e-8 + + def test_transforming_simple_grid(self): + r"""Test transforming a grid that only contains one non-boundary point.""" + ss = 0.5 + params, obj = self.setUp(ss, return_obj=True) + num_pt = int(1 / ss) + 1 # number of points in one-direction. + assert obj.points.shape == (num_pt**3, 3) + non_boundary_pt_index = num_pt**2 + num_pt + 1 + real_pt = obj.points[non_boundary_pt_index] + # Test that this point is not the boundary. + assert real_pt[0] != np.nan + assert real_pt[1] != np.nan + assert real_pt[2] != np.nan + # Test that converting the point back to unit cube gives [0.5, 0.5, 0.5]. + for i_var in range(0, 3): + transf = transform_coordinate(real_pt, i_var, obj.promol) + assert np.abs(transf - 0.5) < 1e-5 + # Test that all other points are indeed boundary points. + all_nans = np.delete(obj.points, non_boundary_pt_index, axis=0) + assert np.all(np.any(np.isnan(all_nans), axis=1)) + + # @pytest.mark.parametrize("x", [-2, -2, 0, 2.2]) + # @pytest.mark.parametrize("y", [-3, 2., -3.2321]) + # @pytest.mark.parametrize("z", [-2., 1.5, 2.343432]) + def test_transforming_with_inverse_transformation_is_identity(self): + # Note that for points far away from the promolecular gets mapped to nan. + # So this isn't really the inverse, in the mathematical sense. + param, obj = self.setUp(0.5, return_obj=True) + + pt = np.array([1, 2, 3], dtype=np.float64) + transf = obj.transform(pt) + reverse = obj.inverse(transf) + assert np.all(np.abs(reverse - pt) < 1e-10) + + def test_integrating_itself(self): + r"""Test integrating the very same promolecular density""" + params, obj = self.setUp(ss=0.2, return_obj=True) + promol = [] + for pt in obj.points: + promol.append(self.promolecular(pt[0], pt[1], pt[2], params)) + promol = np.array(promol, dtype=np.float64) + desired = obj.prointegral + actual = obj.integrate(promol) + assert np.abs(actual - desired) < 1e-8 + + actual = obj.integrate(promol, trick=True) + assert np.abs(actual - desired) < 1e-8 + + @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.75)) + def test_derivative_tranformation_x_finite_difference(self, pt): + params, obj = self.setUp(ss=0.2, return_obj=True) + pt = np.array([pt, 2., 3.]) + + actual = obj.jacobian(pt) + def tranformation_x(pt): + return transform_coordinate(pt, 0, params) + + grad = approx_fprime([pt[0]], tranformation_x, 1e-6) + assert np.abs(grad - actual[0, 0]) < 1e-4 + + @pytest.mark.parametrize("x", np.arange(-5., 5., 0.75)) + @pytest.mark.parametrize("y", [-2.5, -1.5, 0, 1.5]) + def test_derivative_tranformation_y_finite_difference(self, x, y): + params, obj = self.setUp(ss=0.2, return_obj=True) + actual = obj.jacobian(np.array([x, y, 3.])) + + def tranformation_y(pt): + return transform_coordinate([x, pt[0]], 1, params) + + grad = approx_fprime([y], tranformation_y, 1e-8) + assert np.abs(grad - actual[1, 1]) < 1e-5 + + def transformation_y_wrt_x(pt): + return transform_coordinate([pt[0], y], 1, params) + h = 1e-8 + deriv = np.imag(transformation_y_wrt_x([complex(x, h)])) / h + assert np.abs(deriv - actual[1, 0]) < 1e-4 + + @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) + @pytest.mark.parametrize("y", [-3, 2., -2.2321]) + @pytest.mark.parametrize("z", [-1.5, 0., 2.343432]) + def test_derivative_tranformation_z_finite_difference(self, x, y, z): + params, obj = self.setUp(ss=0.2, return_obj=True) + actual = obj.jacobian(np.array([x, y, z])) + + def tranformation_z(pt): + return transform_coordinate([x, y, pt[0]], 2, params) + + grad = approx_fprime([z], tranformation_z, 1e-8) + assert np.abs(grad - actual[2, 2]) < 1e-5 + + def transformation_z_wrt_y(pt): + return transform_coordinate([x, pt[0], z], 2, params) + + deriv = approx_fprime([y], transformation_z_wrt_y, 1e-8) + assert np.abs(deriv - actual[2, 1]) < 1e-4 + + def transformation_z_wrt_x(pt): + a = transform_coordinate([pt[0], y, z], 2, params) + return a + + h = 1e-8 + deriv = np.imag(transformation_z_wrt_x([complex(x, h)])) / h + + assert np.abs(deriv - actual[2, 0]) < 1e-4 + + @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) + @pytest.mark.parametrize("y", [-3, 2., -2.2321]) + @pytest.mark.parametrize("z", [-1.5, 0., 2.343432]) + def test_steepest_ascent_direction_with_numerics(self, x, y, z): + r"""Test steepest-ascent direction match in real and theta space. + + The function to test is x^2 + y^2 + z^2. + """ + def grad(pt): + # Gradient of x^2 + y^2 + z^2. + return np.array([2. * pt[0], 2. * pt[1], 2. * pt[2]]) + + params, obj = self.setUp(ss=0.2, return_obj=True) + + # Take a step in real-space. + pt = np.array([x, y, z]) + grad_pt = grad(pt) + step = 1e-8 + pt_step = pt + grad_pt * step + + # Convert the steps in theta space and calculate finite-difference gradient. + transf = obj.transform(pt) + transf_step = obj.transform(pt_step) + grad_finite = (transf_step - transf) / step + + # Test the actual actual. + actual = obj.steepest_ascent_theta(pt, grad_pt) + assert np.all(np.abs(actual - grad_finite) < 1e-4) + + +class TestOneGaussianAgainstNumerics(): + r""" + Tests Using Numerical Integration of a One Gaussian function to match transformation function. + + """ + def setUp(self, ss=0.1, return_obj=False): + c = np.array([[5.]]) + e = np.array([[2.]]) + coord = np.array([[1., 2., 3.]]) + params = PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + if return_obj: + num_pts = int(1 / ss) + 1 + weights = np.array([(1. / (num_pts - 2))**3.] * num_pts**3) + obj = CubicProTransform([ss] * 3, weights, params.c_m, params.e_m, params.coords) + return params, obj + return params + + @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.5)) + def test_transforming_x_against_numerics(self, pt): + def promolecular_in_x(grid, every_grid): + r"""Constructs the formula of promolecular for integration.""" + promol_x = 5. * np.exp(-2. * (grid - 1.)**2.) + promol_x_all = 5. * np.exp(-2. * (every_grid - 1.)**2.) + return promol_x, promol_x_all + + true_ans = transform_coordinate([pt], 0, self.setUp()) + grid = np.arange(-10., pt, 0.00001) # Integration till a x point + every_grid = np.arange(-10., 10., 0.00001) # Full Integration + promol_x, promol_x_all = promolecular_in_x(grid, every_grid) + + # Integration over y and z cancel out from numerator and denominator. + actual = np.trapz(promol_x, grid) / np.trapz(promol_x_all, every_grid) + assert np.abs(true_ans - actual) < 1e-5 + + @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) + @pytest.mark.parametrize("y", [-3, 2., -10.2321, 20.232109]) + def test_transforming_y_against_numerics(self, x, y): + def promolecular_in_y(grid, every_grid): + r"""Constructs the formula of promolecular for integration.""" + promol_y = 5. * np.exp(-2. * (grid - 2.) ** 2.) + promol_y_all = 5. * np.exp(-2. * (every_grid - 2.) ** 2.) + return promol_y_all, promol_y + + true_ans = transform_coordinate([x, y], 1, self.setUp()) + grid = np.arange(-10., y, 0.00001) # Integration till a x point + every_grid = np.arange(-10., 10., 0.00001) # Full Integration + promol_y_all, promol_y = promolecular_in_y(grid, every_grid) + + # Integration over z cancel out from numerator and denominator. + # Further, gaussian at a point does too. + actual = np.trapz(promol_y, grid) / np.trapz(promol_y_all, every_grid) + assert np.abs(true_ans - actual) < 1e-5 + + @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) + @pytest.mark.parametrize("y", [-3, 2., -10.2321]) + @pytest.mark.parametrize("z", [-10., 0., 2.343432]) + def test_transforming_z_against_numerics(self, x, y, z): + def promolecular_in_z(grid, every_grid): + r"""Constructs the formula of promolecular for integration.""" + promol_z = 5. * np.exp(-2. * (grid - 3.) ** 2.) + promol_z_all = 5. * np.exp(-2. * (every_grid - 3.) ** 2.) + return promol_z_all, promol_z + + grid = np.arange(-10., z, 0.00001) # Integration till a x point + every_grid = np.arange(-10., 10., 0.00001) # Full Integration + promol_z_all, promol_z = promolecular_in_z(grid, every_grid) + + actual = np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) + true_ans = transform_coordinate([x, y, z], 2, self.setUp()) + assert np.abs(true_ans - actual) < 1e-5 + + @pytest.mark.parametrize("x", [0., 0.25, 1.1, 0.5, 1.5]) + @pytest.mark.parametrize("y", [0., 1.25, 2.2, 2.25, 2.5]) + @pytest.mark.parametrize("z", [0., 2.25, 3.2, 3.25, 4.5]) + def test_jacobian_is_diagonal(self, x, y, z): + params, obj = self.setUp(ss=0.2, return_obj=True) + actual = obj.jacobian(np.array([x, y, z])) + + # assert lower-triangular component is zero. + assert np.abs(actual[1, 0]) < 1e-5 + assert np.abs(actual[2, 0]) < 1e-5 + assert np.abs(actual[2, 1]) < 1e-5 + + # test derivative wrt to x + def tranformation_x(pt): + return transform_coordinate([pt[0], y, z], 0, params) + + grad = approx_fprime([x], tranformation_x, 1e-8) + assert np.abs(grad - actual[0, 0]) < 1e-5 + + # test derivative wrt to y + def tranformation_y(pt): + return transform_coordinate([x, pt[0]], 1, params) + + grad = approx_fprime([y], tranformation_y, 1e-8) + assert np.abs(grad - actual[1, 1]) < 1e-5 + + # Test derivative wrt to z + def tranformation_z(pt): + return transform_coordinate([x, y, pt[0]], 2, params) + grad = approx_fprime([z], tranformation_z, 1e-8) + assert np.abs(grad - actual[2, 2]) < 1e-5 + + def test_integration_slightly_perturbed_gaussian(self): + # Only Measured against one decimal place and very similar exponent. + params, obj = self.setUp(ss=0.03, return_obj=True) + + # Gaussian exponent is slightly perturbed from 2. + exponent = 2.001 + + def gaussian(grid): + return 5. * np.exp(-exponent * np.sum((grid - np.array([1., 2., 3.]))**2., axis=1)) + + func_vals = gaussian(obj.points) + desired = 5. * np.sqrt(np.pi / exponent)**3. + actual = obj.integrate(func_vals, trick=True) + assert np.abs(actual - desired) < 1e-2 + + +def test_padding_arrays(): + r"""Test different array sizes are correctly padded.""" + coeff = np.array([[1., 2.], [1., 2., 3., 4.], [5.]]) + exps = np.array([[4., 5.], [5., 6., 7., 8.], [9.]]) + coeff_pad, exps_pad = _pad_coeffs_exps_with_zeros(coeff, exps) + coeff_desired = np.array([[1., 2., 0., 0.], [1., 2., 3., 4.], [5., 0., 0., 0.]]) + np.testing.assert_array_equal(coeff_desired, coeff_pad) + exp_desired = np.array([[4., 5., 0., 0.], [5., 6., 7., 8.], [9., 0., 0., 0.]]) + np.testing.assert_array_equal(exp_desired, exps_pad) From a79ee45f2baf2dfcd4d72c079a9ea0aefa11994c Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 12 Jun 2020 15:02:05 -0400 Subject: [PATCH 09/43] Add jacobian and steepest ascent to promol transf Follows changes were made - Added and Fixed docuentation. - Added jacobian of transformation - Added steepest-ascent - Added utility to transform individual points --- src/grid/protransform.py | 240 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 223 insertions(+), 17 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 70a45024d..45bc8318e 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -25,18 +25,20 @@ from grid.basegrid import Grid import numpy as np +from scipy.linalg import solve_triangular from scipy.optimize import root_scalar from scipy.special import erf +__all__ = ["CubicProTransform"] PromolParams = namedtuple( "PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"] ) -class ProCubicTransform(Grid): +class CubicProTransform(Grid): r""" - Promolecular Grid Transformation of a Cubic Grid in [0,1]^3. + Promolecular Grid Transformation of a Cubic, Uniform Grid in [0,1]^3 to Real space. Attributes ---------- @@ -57,6 +59,14 @@ class ProCubicTransform(Grid): ------- integrate(trick=False) Integral of a real-valued function over Euclidean space. + jacobian() + Jacobian of the transformation from Real space to Theta/Unit cube space. + steepest_ascent_theta() + Direction of steepest-ascent of a function in theta space from gradient in real space. + transform(): + Transform Real point to theta/unit-cube point :math:`[0,1]^3`. + inverse(bracket=(-10, 10)) + Transform theta/unit-cube point to Real space :math:`\mathbb{R}^3`. Examples -------- @@ -69,7 +79,7 @@ class ProCubicTransform(Grid): >> stepsize = 0.01 >> numb_x = int(1. / stepsize) + 1 >> weights = np.array([0.01] * numb_x**3) # Simple Riemannian weights. - >> promol = ProCubicTransform([ss] * 3, weights, c, e, coord) + >> promol = CubicProTransform([ss] * 3, weights, c, e, coord) To integrate some function f. >> def f(pt): @@ -141,6 +151,8 @@ def integrate(self, *value_arrays, trick=False): r""" Integrate any function. + Assumes integrand decays faster than the promolecular density. + Parameters ---------- *value_arrays : (np.ndarray(N, dtype=float),) @@ -170,12 +182,173 @@ def integrate(self, *value_arrays, trick=False): integrand = (arr - promolecular) / promolecular else: integrand = arr / promolecular + # Functions evaluated at points on the boundary is set to zero. integrand[np.isnan(self.points).any(axis=1)] = 0.0 integrands.append(integrand) if trick: return self.prointegral + super().integrate(*integrands) return super().integrate(*integrands) + def jacobian(self, real_pt): + r""" + Jacobian of the transformation from real space to unit-cube/theta space. + + Precisely, it is the lower-triangular matrix + .. math:: + \begin{bmatrix} + \frac{\partial \theta_x}{\partial X} & 0 & 0 \\ + \frac{\partial \theta_y}{\partial X} & \frac{\partial \theta_y}{\partial Y} & 0 \\ + \frac{\partial \theta_z}{\partial X} & \frac{\partial \theta_Z}{\partial Y} & + \frac{\partial \theta_Z}{\partial Z} + \end{bmatrix}. + + Parameters + ---------- + real_pt : np.ndarray(3,) + Point in :math:`\mathbb{R}^3`. + + Returns + ------- + np.ndarray(3, 3) : + Jacobian of transformation. + + """ + jacobian = np.zeros((3, 3), dtype=np.float64) + + c_m, e_m, coords, dim, pi_over_exps = self.promol + + # Code is duplicated from `transform_coordinate` due to effiency reasons. + for i_var in range(0, 3): + # Distance to centers/nuclei`s and Prefactors. + diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] + diff_squared = diff_coords ** 2.0 + distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] + # If i_var is zero, then distance is just all zeros. + + # Gaussian Integrals Over Entire Space For Numerator and Denomator. + coeff_num = c_m * np.exp(-e_m * distance) * pi_over_exps ** (dim - i_var) + + # Get integral of Gaussian till a point. + coord_ivar = diff_coords[:, i_var][:, np.newaxis] + # (pi / exponent)^0.5 is factored and absorbed in `coeff_num`. + integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.0) / 2.0 + + # Final Result. + transf_num = np.sum(coeff_num * integrate_till_pt_x) + transf_den = np.sum(coeff_num) + + for j_deriv in range(0, i_var + 1): + if i_var == j_deriv: + # Derivative eliminates `integrate_till_pt_x`, and adds a Gaussian. + inner_term = coeff_num * np.exp(-e_m * diff_squared[:, i_var][:, np.newaxis]) + # Needed because coeff_num has additional (pi / exponent)^0.5 term. + inner_term /= pi_over_exps + jacobian[i_var, i_var] = np.sum(inner_term) / transf_den + elif j_deriv < i_var: + # Derivative of inside of Gaussian. + deriv_quadratic = -e_m * 2. * diff_coords[:, j_deriv][:, np.newaxis] + deriv_num = np.sum(coeff_num * integrate_till_pt_x * deriv_quadratic) + deriv_den = np.sum(coeff_num * deriv_quadratic) + # Quotient Rule + jacobian[i_var, j_deriv] = (deriv_num * transf_den - transf_num * deriv_den) + jacobian[i_var, j_deriv] /= transf_den**2. + + return jacobian + + def transform(self, real_pt): + r""" + Transform a real point in three-dimensional Reals to theta/unit cube. + + Parameters + ---------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + + Returns + ------- + theta_pt : np.ndarray(3) + Point in :math:`[0, 1]^3`. + + """ + return np.array([transform_coordinate(real_pt, i, self.promol) for i in range(0, 3)]) + + def inverse(self, theta_pt, bracket=(-10, 10)): + r""" + Transform a theta/unit-cube point to three-dimensional Real space. + + Parameters + ---------- + theta_pt : np.ndarray(3) + Point in :math:`[0, 1]^3` + bracket : (float, float), optional + Interval where root is suspected to be in Reals. + Used for "brentq" root-finding method. Default is (-10, 10). + + Returns + ------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + + Notes + ----- + - If a point is far away from the promolecular density, then it will be mapped + to `np.nan`. + + """ + real_pt = [] + for i in range(0, 3): + scalar = inverse_coordinate(theta_pt[i], i, self.promol, real_pt[:i], bracket) + real_pt.append(scalar) + return np.array(real_pt) + + def derivative(self, real_pt, real_derivative): + r""" + Directional derivative in theta space. + + Parameters + ---------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + real_derivative : np.ndarray(3) + Derivative of a function in real space with respect to x, y, z coordinates. + + Returns + ------- + theta_derivative : np.ndarray(3) + Derivative of a function in theta space with respect to theta coordinates. + + Notes + ----- + This does not preserve the direction of steepest-ascent/gradient. + + """ + jacobian = self.jacobian(real_pt) + return solve_triangular(jacobian.T, real_derivative) + + def steepest_ascent_theta(self, real_pt, real_grad): + r""" + Steepest ascent direction of a function in theta/unit-cube space. + + Steepest ascent is the gradient ie direction of maximum change of a function. + This guarantees moving in direction of steepest ascent in real-space + corresponds to moving in the direction of the gradient in theta-space. + + Parameters + ---------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + real_grad : np.ndarray(3) + Gradient of a function in real space. + + Returns + ------- + theta_grad : np.ndarray(3) + Gradient of a function in theta/unit-cube space. + + """ + jacobian = self.jacobian(real_pt) + return jacobian.dot(real_grad) + def _promolecular(self, grid): r""" Evaluate the promolecular density over a grid. @@ -206,32 +379,47 @@ def _promolecular(self, grid): return np.einsum("MNK -> N", gaussian, dtype=np.float64) def _transform(self): + # Coordinates (i, j, k) start from bottom, left-most corner of the unit cube. counter = 0 for ix in range(self.num_pts[0]): cart_pt = [None, None, None] unit_x = self.ss[0] * ix - initx = self._get_bracket((ix,), 0) - transfx = inverse_coordinate(unit_x, 0, self.promol, cart_pt, initx) - cart_pt[0] = transfx + bracx = self._get_bracket((ix,), 0) + cart_pt[0] = inverse_coordinate(unit_x, 0, self.promol, cart_pt, bracx) for iy in range(self.num_pts[1]): unit_y = self.ss[1] * iy - inity = self._get_bracket((ix, iy), 1) - transfy = inverse_coordinate(unit_y, 1, self.promol, cart_pt, inity) - cart_pt[1] = transfy + bracy = self._get_bracket((ix, iy), 1) + cart_pt[1] = inverse_coordinate(unit_y, 1, self.promol, cart_pt, bracy) for iz in range(self.num_pts[2]): unit_z = self.ss[2] * iz - initz = self._get_bracket((ix, iy, iz), 2) - transfz = inverse_coordinate(unit_z, 2, self.promol, cart_pt, initz) - cart_pt[2] = transfz + bracz = self._get_bracket((ix, iy, iz), 2) + cart_pt[2] = inverse_coordinate(unit_z, 2, self.promol, cart_pt, bracz) + self.points[counter] = cart_pt.copy() counter += 1 def _get_bracket(self, coord, i_var): + r""" + Obtain brackets for root-finder based on the coordinate of the point. + + Parameters + ---------- + coord : tuple(int, int, int) + The coordinate of a point. + i_var : int + Index of point being transformed. + + Returns + ------- + (float, float) : + The bracket for the root-finder solver. + + """ # If it is a boundary point, then return nan. if 0.0 in coord[: i_var + 1] or (self.num_pts[i_var] - 1) in coord[: i_var + 1]: return np.nan, np.nan @@ -260,7 +448,7 @@ def _get_bracket(self, coord, i_var): def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=False): r""" - Transform the `i_var` coordinate in a real point to [0, 1] using promolecular density. + Transform the `i_var` coordinate of a real point to [0, 1] using promolecular density. Parameters ---------- @@ -296,7 +484,8 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals gaussian_integrals = np.exp(-e_m * distance) * pi_over_exps ** (dim - i_var) coeff_num = c_m * gaussian_integrals - # Get the integral of Gaussian till a point. + # Get the integral of Gaussian till a point excluding a prefactor. + # This prefactor (pi / exponents) is included in `gaussian_integrals`. coord_ivar = diff_coords[:, i_var][:, np.newaxis] integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.0) / 2.0 @@ -324,7 +513,7 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): r""" - Transform a point in [0, 1] to the real space corresponding to the `i_var` variable. + Transform a point in [0, 1] to the real space corresponding to `i_var` variable. Parameters ---------- @@ -349,10 +538,27 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): ------ AssertionError : If the root did not converge, or brackets did not have opposite sign. + Notes + ----- + - If the theta point is on the boundary or it is itself a nan, then it get's mapped to nan. + Further, if nan is in `transformed[:i_var]` then this function will return nan. + """ - # The [:i_var] is needed because of the way I've set-up transformed attribute. - if np.isnan(bracket[0]) or np.nan in transformed[:i_var]: + def _is_boundary_pt(theta, prev_transformed, bound1=0., bound2=1.): + # Check's if the boundary points are there. + if np.abs(theta - bound1) < 1e-10: + return True + if np.abs(theta - bound2) < 1e-10: + return True + # Check's if there is NAN in here. + # The [:i_var] is needed because of the way I've set-up transforming points in _transform. + if np.isnan(bracket[0]) or np.nan in prev_transformed: + return np.nan + return False + + if _is_boundary_pt(theta_pt, transformed[:i_var]): return np.nan + args = (transformed[:i_var], theta_pt, i_var, params) root_result = root_scalar( _root_equation, From 809f68c3146a6cfc5051ceabd3eef0194a8f5207 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 12 Jun 2020 15:53:37 -0400 Subject: [PATCH 10/43] Fix black issue --- src/grid/protransform.py | 49 +++-- src/grid/tests/test_protransform.py | 280 +++++++++++++++++----------- 2 files changed, 204 insertions(+), 125 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 45bc8318e..587926393 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -r"""Promolecular Grid Transformation""" +r"""Promolecular Grid Transformation.""" from collections import namedtuple @@ -25,6 +25,7 @@ from grid.basegrid import Grid import numpy as np + from scipy.linalg import solve_triangular from scipy.optimize import root_scalar from scipy.special import erf @@ -94,7 +95,7 @@ class CubicProTransform(Grid): Notes ----- - + TODO: Insert Info About Conditional Distribution Method. """ def __init__(self, stepsize, weights, coeffs, exps, coords): @@ -119,7 +120,7 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): with np.errstate(divide="ignore"): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 - self._prointegral = np.sum(coeffs * pi_over_exponents ** 3.) + self._prointegral = np.sum(coeffs * pi_over_exponents ** 3.0) self._promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) # initialize parent class @@ -129,22 +130,22 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): @property def num_pts(self): - r"""Number of points in each direction.""" + r"""Return number of points in each direction.""" return self._num_pts @property def ss(self): - r"""Stepsize of the cubic grid.""" + r"""Return stepsize of the cubic grid.""" return self._ss @property def prointegral(self): - r"""Integration of Promolecular density.""" + r"""Return integration of Promolecular density.""" return self._prointegral @property def promol(self): - r"""PromolParams namedTuple.""" + r"""Return `PromolParams` namedTuple.""" return self._promol def integrate(self, *value_arrays, trick=False): @@ -240,18 +241,26 @@ def jacobian(self, real_pt): for j_deriv in range(0, i_var + 1): if i_var == j_deriv: # Derivative eliminates `integrate_till_pt_x`, and adds a Gaussian. - inner_term = coeff_num * np.exp(-e_m * diff_squared[:, i_var][:, np.newaxis]) + inner_term = coeff_num * np.exp( + -e_m * diff_squared[:, i_var][:, np.newaxis] + ) # Needed because coeff_num has additional (pi / exponent)^0.5 term. inner_term /= pi_over_exps jacobian[i_var, i_var] = np.sum(inner_term) / transf_den elif j_deriv < i_var: # Derivative of inside of Gaussian. - deriv_quadratic = -e_m * 2. * diff_coords[:, j_deriv][:, np.newaxis] - deriv_num = np.sum(coeff_num * integrate_till_pt_x * deriv_quadratic) + deriv_quadratic = ( + -e_m * 2.0 * diff_coords[:, j_deriv][:, np.newaxis] + ) + deriv_num = np.sum( + coeff_num * integrate_till_pt_x * deriv_quadratic + ) deriv_den = np.sum(coeff_num * deriv_quadratic) # Quotient Rule - jacobian[i_var, j_deriv] = (deriv_num * transf_den - transf_num * deriv_den) - jacobian[i_var, j_deriv] /= transf_den**2. + jacobian[i_var, j_deriv] = ( + deriv_num * transf_den - transf_num * deriv_den + ) + jacobian[i_var, j_deriv] /= transf_den ** 2.0 return jacobian @@ -270,7 +279,9 @@ def transform(self, real_pt): Point in :math:`[0, 1]^3`. """ - return np.array([transform_coordinate(real_pt, i, self.promol) for i in range(0, 3)]) + return np.array( + [transform_coordinate(real_pt, i, self.promol) for i in range(0, 3)] + ) def inverse(self, theta_pt, bracket=(-10, 10)): r""" @@ -297,7 +308,9 @@ def inverse(self, theta_pt, bracket=(-10, 10)): """ real_pt = [] for i in range(0, 3): - scalar = inverse_coordinate(theta_pt[i], i, self.promol, real_pt[:i], bracket) + scalar = inverse_coordinate( + theta_pt[i], i, self.promol, real_pt[:i], bracket + ) real_pt.append(scalar) return np.array(real_pt) @@ -398,7 +411,9 @@ def _transform(self): unit_z = self.ss[2] * iz bracz = self._get_bracket((ix, iy, iz), 2) - cart_pt[2] = inverse_coordinate(unit_z, 2, self.promol, cart_pt, bracz) + cart_pt[2] = inverse_coordinate( + unit_z, 2, self.promol, cart_pt, bracz + ) self.points[counter] = cart_pt.copy() counter += 1 @@ -544,7 +559,8 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): Further, if nan is in `transformed[:i_var]` then this function will return nan. """ - def _is_boundary_pt(theta, prev_transformed, bound1=0., bound2=1.): + + def _is_boundary_pt(theta, prev_transformed, bound1=0.0, bound2=1.0): # Check's if the boundary points are there. if np.abs(theta - bound1) < 1e-10: return True @@ -573,6 +589,7 @@ def _is_boundary_pt(theta, prev_transformed, bound1=0., bound2=1.): def _pad_coeffs_exps_with_zeros(coeffs, exps): + r"""Pad Promolecular coefficients and exponents with zero. Results in same size array.""" max_numb_of_gauss = max(len(c) for c in coeffs) coeffs = np.array( [ diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index b83340268..c607d9f56 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -30,48 +30,65 @@ """ + +from grid.protransform import ( + CubicProTransform, + PromolParams, + _pad_coeffs_exps_with_zeros, + transform_coordinate, +) + import numpy as np -from scipy.special import erf -from scipy.optimize import approx_fprime import pytest -from grid.protransform import ( - CubicProTransform, PromolParams, transform_coordinate, _pad_coeffs_exps_with_zeros -) +from scipy.optimize import approx_fprime +from scipy.special import erf class TestTwoGaussianDiffCenters: - r""" - Test a Sum of Two Gaussian function against analytic formulas and numerical procedures. - """ + r"""Test a Sum of Two Gaussian function against analytic formulas and numerical procedures.""" + def setUp(self, ss=0.1, return_obj=False): - c = np.array([[5.], [10.]]) - e = np.array([[2.], [3.]]) - coord = np.array([[1., 2., 3.], [2., 2., 2.]]) + r"""Set up a two parameter Gaussian function.""" + c = np.array([[5.0], [10.0]]) + e = np.array([[2.0], [3.0]]) + coord = np.array([[1.0, 2.0, 3.0], [2.0, 2.0, 2.0]]) params = PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([((1. / (num_pts - 2)))**3.] * num_pts**3) - obj = CubicProTransform([ss] * 3, weights, params.c_m, params.e_m, params.coords) - return params, obj + weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) + obj = CubicProTransform( + [ss] * 3, weights, params.c_m, params.e_m, params.coords + ) + return params, obj return params def promolecular(self, x, y, z, params): + r"""Hard-Code the promolecular density.""" # Promolecular in CubicProTransform class uses einsum, this tests it against that. # Also could be used for integration tests. cm, em, coords, _, _ = params - promol = 0. + promol = 0.0 for i, coeffs in enumerate(cm): xc, yc, zc = coords[i] for j, coeff in enumerate(coeffs): - distance = ((x - xc)**2. + (y - yc)**2. + (z - zc)**2.) - promol += np.sum(coeff * np.exp(- em[i, j] * distance)) + distance = (x - xc) ** 2.0 + (y - yc) ** 2.0 + (z - zc) ** 2.0 + promol += np.sum(coeff * np.exp(-em[i, j] * distance)) return promol def test_promolecular_density(self): - grid = np.array([[-1., 2., 3.], [5., 10., -1.], [0., 0., 0.], - [3., 2., 1.], [10., 20., 30.], [0., 10., 0.2]]) + r"""Test Promolecular Density function against hard-coded.""" + grid = np.array( + [ + [-1.0, 2.0, 3.0], + [5.0, 10.0, -1.0], + [0.0, 0.0, 0.0], + [3.0, 2.0, 1.0], + [10.0, 20.0, 30.0], + [0.0, 10.0, 0.2], + ] + ) params, obj = self.setUp(return_obj=True) true_ans = [] @@ -82,62 +99,84 @@ def test_promolecular_density(self): desired = obj._promolecular(grid) assert np.all(np.abs(np.array(true_ans) - desired) < 1e-8) - @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.5)) + @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) def test_transforming_x_against_formula(self, pt): + r"""Test transformming the X-transformation against analytic formula.""" + def formula_transforming_x(x): - r"""Closed form formula for transforming x coordinate.""" - first_factor = (5. * np.pi ** 1.5 / (4 * 2 ** 0.5)) * (erf(2 ** 0.5 * (x - 1)) + 1.) - sec_fac = ((10. * np.pi ** 1.5) / (6. * 3 ** 0.5)) * (erf(3. ** 0.5 * (x - 2)) + 1.) - return (first_factor + sec_fac) / (5. * (np.pi / 2) ** 1.5 + 10. * (np.pi / 3.) ** 1.5) + r"""Return closed form formula for transforming x coordinate.""" + first_factor = (5.0 * np.pi ** 1.5 / (4 * 2 ** 0.5)) * ( + erf(2 ** 0.5 * (x - 1)) + 1.0 + ) + + sec_fac = ((10.0 * np.pi ** 1.5) / (6.0 * 3 ** 0.5)) * ( + erf(3.0 ** 0.5 * (x - 2)) + 1.0 + ) + + return (first_factor + sec_fac) / ( + 5.0 * (np.pi / 2) ** 1.5 + 10.0 * (np.pi / 3.0) ** 1.5 + ) true_ans = transform_coordinate([pt], 0, self.setUp()) assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 - @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.75)) - def test_derivative_transforming_x_with_finite_difference(self, pt): - # Unfortunately, pnly one decimal place is obtained. - params = self.setUp() - _, actual = transform_coordinate([pt], 0, params, deriv=True) - def func(x): - return transform_coordinate([x], 0, params) - desired = approx_fprime([pt], func, epsilon=1.49e-08) - assert np.abs(desired - actual) < 1e-1 - @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) - @pytest.mark.parametrize("y", [-3, 2., -10.2321, 20.232109]) + @pytest.mark.parametrize("y", [-3, 2, -10.2321, 20.232109]) def test_transforming_y_against_formula(self, x, y): + r"""Test transforming the Y-transformation against analytic formula.""" + def formula_transforming_y(x, y): - r"""Closed form formula for transforming y coordinate.""" - fac1 = 5. * np.sqrt(np.pi / 2.) * np.exp(-2. * (x - 1.) ** 2.) - fac1 *= (np.sqrt(np.pi) * (erf(2. ** 0.5 * (y - 2)) + 1.) / (2. * np.sqrt(2.))) - fac2 = 10. * np.sqrt(np.pi / 3.) * np.exp(-3. * (x - 2.) ** 2.) - fac2 *= (np.sqrt(np.pi) * (erf(3. ** 0.5 * (y - 2)) + 1.) / (2. * np.sqrt(3.))) + r"""Return closed form formula for transforming y coordinate.""" + fac1 = 5.0 * np.sqrt(np.pi / 2.0) * np.exp(-2.0 * (x - 1) ** 2) + fac1 *= ( + np.sqrt(np.pi) + * (erf(2.0 ** 0.5 * (y - 2)) + 1.0) + / (2.0 * np.sqrt(2.0)) + ) + fac2 = 10.0 * np.sqrt(np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) + fac2 *= ( + np.sqrt(np.pi) + * (erf(3.0 ** 0.5 * (y - 2)) + 1.0) + / (2.0 * np.sqrt(3.0)) + ) num = fac1 + fac2 - dac1 = 5. * (np.pi / 2.) * np.exp(-2. * (x - 1.) ** 2.) - dac2 = 10. * (np.pi / 3.) * np.exp(-3. * (x - 2.) ** 2.) + dac1 = 5.0 * (np.pi / 2.0) * np.exp(-2.0 * (x - 1.0) ** 2.0) + dac2 = 10.0 * (np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) den = dac1 + dac2 return num / den + true_ans = transform_coordinate([x, y], 1, self.setUp()) assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) - @pytest.mark.parametrize("y", [-3, 2., -10.2321]) - @pytest.mark.parametrize("z", [-10., 0., 2.343432]) + @pytest.mark.parametrize("y", [-3, 2, -10.2321]) + @pytest.mark.parametrize("z", [-10, 0, 2.343432]) def test_transforming_z_against_formula(self, x, y, z): + r"""Test transforming the Z-transformation against analytic formula.""" + def formula_transforming_z(x, y, z): - r"""Closed form formula for transforming z coordinate.""" - a1, a2, a3 = (x - 1.), (y - 2.), (z - 3.) - erfx = erf(2. ** 0.5 * a3) + 1. - fac1 = 5. * np.exp(-2. * (a1 ** 2. + a2 ** 2.)) * erfx * np.pi ** 0.5 / (2. * 2. ** 0.5) - - b1, b2, b3 = (x - 2.), (y - 2.), (z - 2.) - erfy = erf(3. ** 0.5 * b3) + 1. - fac2 = 10. * np.exp(-3. * (b1 ** 2. + b2 ** 2.)) * erfy * np.pi ** 0.5 / ( - 2. * 3. ** 0.5) - - den = 5. * (np.pi / 2.) ** 0.5 * np.exp(-2. * (a1 ** 2. + a2 ** 2.)) - den += 10. * (np.pi / 3.) ** 0.5 * np.exp(-3. * (b1 ** 2. + b2 ** 2.)) + r"""Return closed form formula for transforming z coordinate.""" + a1, a2, a3 = (x - 1.0), (y - 2.0), (z - 3.0) + erfx = erf(2.0 ** 0.5 * a3) + 1.0 + fac1 = ( + 5.0 + * np.exp(-2.0 * (a1 ** 2.0 + a2 ** 2.0)) + * erfx + * np.pi ** 0.5 + / (2.0 * 2.0 ** 0.5) + ) + b1, b2, b3 = (x - 2.0), (y - 2.0), (z - 2.0) + erfy = erf(3.0 ** 0.5 * b3) + 1.0 + fac2 = ( + 10.0 + * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) + * erfy + * np.pi ** 0.5 + / (2.0 * 3.0 ** 0.5) + ) + den = 5.0 * (np.pi / 2.0) ** 0.5 * np.exp(-2.0 * (a1 ** 2.0 + a2 ** 2.0)) + den += 10.0 * (np.pi / 3.0) ** 0.5 * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) return (fac1 + fac2) / den params, obj = self.setUp(ss=0.5, return_obj=True) @@ -155,8 +194,8 @@ def test_transforming_simple_grid(self): ss = 0.5 params, obj = self.setUp(ss, return_obj=True) num_pt = int(1 / ss) + 1 # number of points in one-direction. - assert obj.points.shape == (num_pt**3, 3) - non_boundary_pt_index = num_pt**2 + num_pt + 1 + assert obj.points.shape == (num_pt ** 3, 3) + non_boundary_pt_index = num_pt ** 2 + num_pt + 1 real_pt = obj.points[non_boundary_pt_index] # Test that this point is not the boundary. assert real_pt[0] != np.nan @@ -174,6 +213,7 @@ def test_transforming_simple_grid(self): # @pytest.mark.parametrize("y", [-3, 2., -3.2321]) # @pytest.mark.parametrize("z", [-2., 1.5, 2.343432]) def test_transforming_with_inverse_transformation_is_identity(self): + r"""Test transforming with inverse transformation is identity.""" # Note that for points far away from the promolecular gets mapped to nan. # So this isn't really the inverse, in the mathematical sense. param, obj = self.setUp(0.5, return_obj=True) @@ -184,7 +224,7 @@ def test_transforming_with_inverse_transformation_is_identity(self): assert np.all(np.abs(reverse - pt) < 1e-10) def test_integrating_itself(self): - r"""Test integrating the very same promolecular density""" + r"""Test integrating the very same promolecular density.""" params, obj = self.setUp(ss=0.2, return_obj=True) promol = [] for pt in obj.points: @@ -197,23 +237,26 @@ def test_integrating_itself(self): actual = obj.integrate(promol, trick=True) assert np.abs(actual - desired) < 1e-8 - @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.75)) + @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.75)) def test_derivative_tranformation_x_finite_difference(self, pt): + r"""Test the derivative of X-transformation against finite-difference.""" params, obj = self.setUp(ss=0.2, return_obj=True) - pt = np.array([pt, 2., 3.]) + pt = np.array([pt, 2.0, 3.0]) actual = obj.jacobian(pt) + def tranformation_x(pt): return transform_coordinate(pt, 0, params) grad = approx_fprime([pt[0]], tranformation_x, 1e-6) assert np.abs(grad - actual[0, 0]) < 1e-4 - @pytest.mark.parametrize("x", np.arange(-5., 5., 0.75)) + @pytest.mark.parametrize("x", np.arange(-5.0, 5.0, 0.75)) @pytest.mark.parametrize("y", [-2.5, -1.5, 0, 1.5]) def test_derivative_tranformation_y_finite_difference(self, x, y): + r"""Test the derivative of Y-transformation against finite-difference.""" params, obj = self.setUp(ss=0.2, return_obj=True) - actual = obj.jacobian(np.array([x, y, 3.])) + actual = obj.jacobian(np.array([x, y, 3.0])) def tranformation_y(pt): return transform_coordinate([x, pt[0]], 1, params) @@ -223,14 +266,16 @@ def tranformation_y(pt): def transformation_y_wrt_x(pt): return transform_coordinate([pt[0], y], 1, params) + h = 1e-8 deriv = np.imag(transformation_y_wrt_x([complex(x, h)])) / h assert np.abs(deriv - actual[1, 0]) < 1e-4 @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) - @pytest.mark.parametrize("y", [-3, 2., -2.2321]) - @pytest.mark.parametrize("z", [-1.5, 0., 2.343432]) + @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) + @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) def test_derivative_tranformation_z_finite_difference(self, x, y, z): + r"""Test the derivative of Z-transformation against finite-difference.""" params, obj = self.setUp(ss=0.2, return_obj=True) actual = obj.jacobian(np.array([x, y, z])) @@ -256,16 +301,17 @@ def transformation_z_wrt_x(pt): assert np.abs(deriv - actual[2, 0]) < 1e-4 @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) - @pytest.mark.parametrize("y", [-3, 2., -2.2321]) - @pytest.mark.parametrize("z", [-1.5, 0., 2.343432]) + @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) + @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) def test_steepest_ascent_direction_with_numerics(self, x, y, z): r"""Test steepest-ascent direction match in real and theta space. The function to test is x^2 + y^2 + z^2. """ + def grad(pt): # Gradient of x^2 + y^2 + z^2. - return np.array([2. * pt[0], 2. * pt[1], 2. * pt[2]]) + return np.array([2.0 * pt[0], 2.0 * pt[1], 2.0 * pt[2]]) params, obj = self.setUp(ss=0.2, return_obj=True) @@ -285,52 +331,57 @@ def grad(pt): assert np.all(np.abs(actual - grad_finite) < 1e-4) -class TestOneGaussianAgainstNumerics(): - r""" - Tests Using Numerical Integration of a One Gaussian function to match transformation function. +class TestOneGaussianAgainstNumerics: + r"""Tests With Numerical Integration of a One Gaussian function.""" - """ def setUp(self, ss=0.1, return_obj=False): - c = np.array([[5.]]) - e = np.array([[2.]]) - coord = np.array([[1., 2., 3.]]) + r"""Return a one Gaussian example and its CubicProTransform object.""" + c = np.array([[5.0]]) + e = np.array([[2.0]]) + coord = np.array([[1.0, 2.0, 3.0]]) params = PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([(1. / (num_pts - 2))**3.] * num_pts**3) - obj = CubicProTransform([ss] * 3, weights, params.c_m, params.e_m, params.coords) - return params, obj + weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) + obj = CubicProTransform( + [ss] * 3, weights, params.c_m, params.e_m, params.coords + ) + return params, obj return params - @pytest.mark.parametrize("pt", np.arange(-5., 5., 0.5)) + @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) def test_transforming_x_against_numerics(self, pt): + r"""Test transforming X against numerical algorithms.""" + def promolecular_in_x(grid, every_grid): - r"""Constructs the formula of promolecular for integration.""" - promol_x = 5. * np.exp(-2. * (grid - 1.)**2.) - promol_x_all = 5. * np.exp(-2. * (every_grid - 1.)**2.) + r"""Construct the formula of promolecular for integration.""" + promol_x = 5.0 * np.exp(-2.0 * (grid - 1.0) ** 2.0) + promol_x_all = 5.0 * np.exp(-2.0 * (every_grid - 1.0) ** 2.0) return promol_x, promol_x_all true_ans = transform_coordinate([pt], 0, self.setUp()) - grid = np.arange(-10., pt, 0.00001) # Integration till a x point - every_grid = np.arange(-10., 10., 0.00001) # Full Integration + grid = np.arange(-10.0, pt, 0.00001) # Integration till a x point + every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration promol_x, promol_x_all = promolecular_in_x(grid, every_grid) # Integration over y and z cancel out from numerator and denominator. actual = np.trapz(promol_x, grid) / np.trapz(promol_x_all, every_grid) assert np.abs(true_ans - actual) < 1e-5 - @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) - @pytest.mark.parametrize("y", [-3, 2., -10.2321, 20.232109]) + @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2, 1.23]) + @pytest.mark.parametrize("y", [-3.0, 2.0, -10.2321, 20.232109]) def test_transforming_y_against_numerics(self, x, y): + r"""Test transformation y against numerical algorithms.""" + def promolecular_in_y(grid, every_grid): - r"""Constructs the formula of promolecular for integration.""" - promol_y = 5. * np.exp(-2. * (grid - 2.) ** 2.) - promol_y_all = 5. * np.exp(-2. * (every_grid - 2.) ** 2.) + r"""Construct the formula of promolecular for integration.""" + promol_y = 5.0 * np.exp(-2.0 * (grid - 2.0) ** 2.0) + promol_y_all = 5.0 * np.exp(-2.0 * (every_grid - 2.0) ** 2.0) return promol_y_all, promol_y true_ans = transform_coordinate([x, y], 1, self.setUp()) - grid = np.arange(-10., y, 0.00001) # Integration till a x point - every_grid = np.arange(-10., 10., 0.00001) # Full Integration + grid = np.arange(-10.0, y, 0.00001) # Integration till a x point + every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration promol_y_all, promol_y = promolecular_in_y(grid, every_grid) # Integration over z cancel out from numerator and denominator. @@ -338,28 +389,31 @@ def promolecular_in_y(grid, every_grid): actual = np.trapz(promol_y, grid) / np.trapz(promol_y_all, every_grid) assert np.abs(true_ans - actual) < 1e-5 - @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) - @pytest.mark.parametrize("y", [-3, 2., -10.2321]) - @pytest.mark.parametrize("z", [-10., 0., 2.343432]) + @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2]) + @pytest.mark.parametrize("y", [-3.0, 2.0, -10.2321]) + @pytest.mark.parametrize("z", [-10.0, 0.0, 2.343432]) def test_transforming_z_against_numerics(self, x, y, z): + r"""Test transforming Z against numerical algorithms.""" + def promolecular_in_z(grid, every_grid): - r"""Constructs the formula of promolecular for integration.""" - promol_z = 5. * np.exp(-2. * (grid - 3.) ** 2.) - promol_z_all = 5. * np.exp(-2. * (every_grid - 3.) ** 2.) + r"""Construct the formula of promolecular for integration.""" + promol_z = 5.0 * np.exp(-2.0 * (grid - 3.0) ** 2.0) + promol_z_all = 5.0 * np.exp(-2.0 * (every_grid - 3.0) ** 2.0) return promol_z_all, promol_z - grid = np.arange(-10., z, 0.00001) # Integration till a x point - every_grid = np.arange(-10., 10., 0.00001) # Full Integration + grid = np.arange(-10.0, z, 0.00001) # Integration till a x point + every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration promol_z_all, promol_z = promolecular_in_z(grid, every_grid) actual = np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) true_ans = transform_coordinate([x, y, z], 2, self.setUp()) assert np.abs(true_ans - actual) < 1e-5 - @pytest.mark.parametrize("x", [0., 0.25, 1.1, 0.5, 1.5]) - @pytest.mark.parametrize("y", [0., 1.25, 2.2, 2.25, 2.5]) - @pytest.mark.parametrize("z", [0., 2.25, 3.2, 3.25, 4.5]) - def test_jacobian_is_diagonal(self, x, y, z): + @pytest.mark.parametrize("x", [0.0, 0.25, 1.1, 0.5, 1.5]) + @pytest.mark.parametrize("y", [0.0, 1.25, 2.2, 2.25, 2.5]) + @pytest.mark.parametrize("z", [0.0, 2.25, 3.2, 3.25, 4.5]) + def test_jacobian(self, x, y, z): + r"""Test that the jacobian of the transformation.""" params, obj = self.setUp(ss=0.2, return_obj=True) actual = obj.jacobian(np.array([x, y, z])) @@ -385,10 +439,12 @@ def tranformation_y(pt): # Test derivative wrt to z def tranformation_z(pt): return transform_coordinate([x, y, pt[0]], 2, params) + grad = approx_fprime([z], tranformation_z, 1e-8) assert np.abs(grad - actual[2, 2]) < 1e-5 def test_integration_slightly_perturbed_gaussian(self): + r"""Test integration of a slightly perturbed function.""" # Only Measured against one decimal place and very similar exponent. params, obj = self.setUp(ss=0.03, return_obj=True) @@ -396,20 +452,26 @@ def test_integration_slightly_perturbed_gaussian(self): exponent = 2.001 def gaussian(grid): - return 5. * np.exp(-exponent * np.sum((grid - np.array([1., 2., 3.]))**2., axis=1)) + return 5.0 * np.exp( + -exponent * np.sum((grid - np.array([1.0, 2.0, 3.0])) ** 2.0, axis=1) + ) func_vals = gaussian(obj.points) - desired = 5. * np.sqrt(np.pi / exponent)**3. + desired = 5.0 * np.sqrt(np.pi / exponent) ** 3.0 actual = obj.integrate(func_vals, trick=True) assert np.abs(actual - desired) < 1e-2 def test_padding_arrays(): r"""Test different array sizes are correctly padded.""" - coeff = np.array([[1., 2.], [1., 2., 3., 4.], [5.]]) - exps = np.array([[4., 5.], [5., 6., 7., 8.], [9.]]) + coeff = np.array([[1.0, 2.0], [1.0, 2.0, 3.0, 4.0], [5.0]]) + exps = np.array([[4.0, 5.0], [5.0, 6.0, 7.0, 8.0], [9.0]]) coeff_pad, exps_pad = _pad_coeffs_exps_with_zeros(coeff, exps) - coeff_desired = np.array([[1., 2., 0., 0.], [1., 2., 3., 4.], [5., 0., 0., 0.]]) + coeff_desired = np.array( + [[1.0, 2.0, 0.0, 0.0], [1.0, 2.0, 3.0, 4.0], [5.0, 0.0, 0.0, 0.0]] + ) np.testing.assert_array_equal(coeff_desired, coeff_pad) - exp_desired = np.array([[4., 5., 0., 0.], [5., 6., 7., 8.], [9., 0., 0., 0.]]) + exp_desired = np.array( + [[4.0, 5.0, 0.0, 0.0], [5.0, 6.0, 7.0, 8.0], [9.0, 0.0, 0.0, 0.0]] + ) np.testing.assert_array_equal(exp_desired, exps_pad) From 50f9a73271d9e853cbe4c0e47e2ee1e5a6d96728 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 13:16:23 -0400 Subject: [PATCH 11/43] Add promolecular density dataclass Private data class that holds the coefficients, exponents, dimensions and compute its density. --- src/grid/protransform.py | 104 ++++++++++++++++------------ src/grid/tests/test_protransform.py | 12 ++-- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 587926393..a800ef041 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -20,7 +20,7 @@ r"""Promolecular Grid Transformation.""" -from collections import namedtuple +from dataclasses import dataclass, astuple from grid.basegrid import Grid @@ -32,9 +32,43 @@ __all__ = ["CubicProTransform"] -PromolParams = namedtuple( - "PromolParams", ["c_m", "e_m", "coords", "dim", "pi_over_exponents"] -) + +@dataclass +class _PromolParams: + r"""Private class for Promolecular Density information.""" + c_m: np.ndarray + e_m: np.ndarray + coords: np.ndarray + pi_over_exponents: np.ndarray + dim: int = 3 + + def promolecular(self, grid): + r""" + Evaluate the promolecular density over a grid. + + Parameters + ---------- + grid : np.ndarray(N,) + Grid points. + + Returns + ------- + np.ndarray(N,) : + Promolecular density evaluated at the grid points. + + """ + # M is the number of centers/atoms. + # D is the number of dimensions, usually 3. + # K is maximum number of gaussian functions over all M atoms. + cm, em, coords = self.c_m, self.e_m, self.coords + # Shape (N, M, D), then Summing gives (N, M, 1) + distance = np.sum((grid - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) + # At each center, multiply Each Distance of a Coordinate, with its exponents. + exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) + # At each center, multiply the exponential with its coefficients. + gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) + # At each point, sum for each center, then sum all centers together. + return np.einsum("MNK -> N", gaussian, dtype=np.float64) class CubicProTransform(Grid): @@ -77,10 +111,10 @@ class CubicProTransform(Grid): >> coord = np.array([[0., 0., 0.], [2., 2., 2.]]) Define information of the grid and its weights. - >> stepsize = 0.01 + >> stepsize = 0.01 #TODO: Remove >> numb_x = int(1. / stepsize) + 1 >> weights = np.array([0.01] * numb_x**3) # Simple Riemannian weights. - >> promol = CubicProTransform([ss] * 3, weights, c, e, coord) + >> promol = CubicProTransform([ss, ss, ss], weights, c, e, coord) To integrate some function f. >> def f(pt): @@ -99,6 +133,7 @@ class CubicProTransform(Grid): """ def __init__(self, stepsize, weights, coeffs, exps, coords): + # TODO: Add Types if not isinstance(stepsize, tuple): pass if not isinstance(coeffs, (list, np.ndarray)): @@ -108,6 +143,7 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): if not isinstance(coords, (list, np.ndarray)): pass self._ss = stepsize + # TODO: Add boundary info in docs self._num_pts = ( int(1 / stepsize[0]) + 1, int(1 / stepsize[1]) + 1, @@ -115,13 +151,14 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): ) # pad coefficients and exponents with zeros to have the same size. + # Make it easier to use numpy operations. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) # Rather than computing this repeatedly. It is fixed. with np.errstate(divide="ignore"): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 self._prointegral = np.sum(coeffs * pi_over_exponents ** 3.0) - self._promol = PromolParams(coeffs, exps, coords, 3, pi_over_exponents) + self._promol = _PromolParams(coeffs, exps, coords, pi_over_exponents) # initialize parent class empty_points = np.empty((np.prod(self._num_pts), 3), dtype=np.float64) @@ -174,11 +211,13 @@ def integrate(self, *value_arrays, trick=False): Input integrand array is given or not of proper shape. """ - promolecular = self._promolecular(self.points) + promolecular = self.promol.promolecular(self.points) integrands = [] + # TODO: Use Mask Array with nan's with user-defined tol. with np.errstate(divide="ignore"): for arr in value_arrays: assert arr.dtype != object, "Array dtype should not be object." + # This may be refactored to fit in the general promolecular trick in `grid`. if trick: integrand = (arr - promolecular) / promolecular else: @@ -216,10 +255,12 @@ def jacobian(self, real_pt): """ jacobian = np.zeros((3, 3), dtype=np.float64) - c_m, e_m, coords, dim, pi_over_exps = self.promol + c_m, e_m, coords, pi_over_exps, dim = astuple(self.promol) # Code is duplicated from `transform_coordinate` due to effiency reasons. + # TODO: Reduce the number of computation with `i_var`. for i_var in range(0, 3): + # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] diff_squared = diff_coords ** 2.0 @@ -362,37 +403,10 @@ def steepest_ascent_theta(self, real_pt, real_grad): jacobian = self.jacobian(real_pt) return jacobian.dot(real_grad) - def _promolecular(self, grid): - r""" - Evaluate the promolecular density over a grid. - - Parameters - ---------- - grid : np.ndarray(N,) - Grid points. - - Returns - ------- - np.ndarray(N,) : - Promolecular density evaluated at the grid points. - - """ - # TODO: For Design, Store this or constantly re-evaluate it? - # M is the number of centers/atoms. - # D is the number of dimensions, usually 3. - # K is maximum number of gaussian functions over all M atoms. - cm, em, coords, _, _ = self.promol - # Shape (N, M, D), then Summing gives (N, M, 1) - distance = np.sum((grid - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) - # At each center, multiply Each Distance of a Coordinate, with its exponents. - exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) - # At each center, multiply the exponential with its coefficients. - gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) - # At each point, sum for each center, then sum all centers together. - return np.einsum("MNK -> N", gaussian, dtype=np.float64) - def _transform(self): - # Coordinates (i, j, k) start from bottom, left-most corner of the unit cube. + # Indices (i, j, k) start from bottom, left-most corner of the unit cube. + # TODO: Add i, j, k integer info. + # TODO: Rename coordinates to indices. counter = 0 for ix in range(self.num_pts[0]): cart_pt = [None, None, None] @@ -425,7 +439,7 @@ def _get_bracket(self, coord, i_var): Parameters ---------- coord : tuple(int, int, int) - The coordinate of a point. + The coordinate of a point. # TODO: rename indices. i_var : int Index of point being transformed. @@ -436,7 +450,7 @@ def _get_bracket(self, coord, i_var): """ # If it is a boundary point, then return nan. - if 0.0 in coord[: i_var + 1] or (self.num_pts[i_var] - 1) in coord[: i_var + 1]: + if 0 in coord[: i_var + 1] or (self.num_pts[i_var] - 1) in coord[: i_var + 1]: return np.nan, np.nan # If it is a new point, with no nearby point, get a large initial guess. elif coord[i_var] == 1: @@ -457,7 +471,8 @@ def _get_bracket(self, coord, i_var): + coord[2] - 1 ) - # FIXME : Rather than using fixed +10., use truncated taylor series. + + # TODO: Add dynamic bracketing methods with +5. return self.points[index, i_var], self.points[index, i_var] + 10.0 @@ -487,7 +502,7 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals the second derivative with respect to real point are returned. """ - c_m, e_m, coords, dim, pi_over_exps = promol_params + c_m, e_m, coords, pi_over_exps, dim = astuple(promol_params) # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] @@ -521,6 +536,7 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): + # TODO: Add docs. all_points = np.append(prev_trans_pts, init_guess) transf_pt = transform_coordinate(all_points, i_var, params) return theta_pt - transf_pt @@ -562,6 +578,8 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): def _is_boundary_pt(theta, prev_transformed, bound1=0.0, bound2=1.0): # Check's if the boundary points are there. + # TODO: Remove isnan and keep the following boundary condition. + # TODO: Remove helper function. if np.abs(theta - bound1) < 1e-10: return True if np.abs(theta - bound2) < 1e-10: diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index c607d9f56..752c54cba 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -33,7 +33,7 @@ from grid.protransform import ( CubicProTransform, - PromolParams, + _PromolParams, _pad_coeffs_exps_with_zeros, transform_coordinate, ) @@ -54,7 +54,7 @@ def setUp(self, ss=0.1, return_obj=False): c = np.array([[5.0], [10.0]]) e = np.array([[2.0], [3.0]]) coord = np.array([[1.0, 2.0, 3.0], [2.0, 2.0, 2.0]]) - params = PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + params = _PromolParams(c, e, coord, pi_over_exponents=np.sqrt(np.pi / e), dim=3) if return_obj: num_pts = int(1 / ss) + 1 weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) @@ -68,7 +68,7 @@ def promolecular(self, x, y, z, params): r"""Hard-Code the promolecular density.""" # Promolecular in CubicProTransform class uses einsum, this tests it against that. # Also could be used for integration tests. - cm, em, coords, _, _ = params + cm, em, coords = params.c_m, params.e_m, params.coords promol = 0.0 for i, coeffs in enumerate(cm): xc, yc, zc = coords[i] @@ -89,14 +89,14 @@ def test_promolecular_density(self): [0.0, 10.0, 0.2], ] ) - params, obj = self.setUp(return_obj=True) + params= self.setUp() true_ans = [] for pt in grid: x, y, z = pt true_ans.append(self.promolecular(x, y, z, params)) - desired = obj._promolecular(grid) + desired = params.promolecular(grid) assert np.all(np.abs(np.array(true_ans) - desired) < 1e-8) @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) @@ -339,7 +339,7 @@ def setUp(self, ss=0.1, return_obj=False): c = np.array([[5.0]]) e = np.array([[2.0]]) coord = np.array([[1.0, 2.0, 3.0]]) - params = PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + params = _PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) if return_obj: num_pts = int(1 / ss) + 1 weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) From ad01358addd0400f97dc0271997c836f3bc798b7 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 13:23:11 -0400 Subject: [PATCH 12/43] Add docs to _root_equation for promol transform --- src/grid/protransform.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index a800ef041..67d4651b6 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -536,7 +536,29 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): - # TODO: Add docs. + r""" + Equation to solve for the root to find inverse coordinate from theta space to Real space. + + Parameters + ---------- + init_guess : float + Initial guess of Real point that transforms to `theta_pt`. + prev_trans_pts : list[Float, i_var - 1] + The previous points in real-space that were already transformed. + theta_pt : float + The point in [0, 1] being transformed to the Real space. + i_var : int + Index of variable being transformed. + params : _PromolParams + Promolecular density data class. + + Returns + ------- + float : + The difference between `theta_pt` and the transformed point based on + `init_guess` and `prev_trans_pts`. + + """ all_points = np.append(prev_trans_pts, init_guess) transf_pt = transform_coordinate(all_points, i_var, params) return theta_pt - transf_pt From 11b603aa4ee5e351e7e8ae22e6e37f5e24e046ea Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 13:36:29 -0400 Subject: [PATCH 13/43] Remove helper function in inverse_coordinate From code review, it was not necessary. --- src/grid/protransform.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 67d4651b6..75b3b94e6 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -130,6 +130,7 @@ class CubicProTransform(Grid): Notes ----- TODO: Insert Info About Conditional Distribution Method. + TODO: Add Infor about how boundarys on theta-space are mapped to np.nan. """ def __init__(self, stepsize, weights, coeffs, exps, coords): @@ -543,7 +544,7 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): ---------- init_guess : float Initial guess of Real point that transforms to `theta_pt`. - prev_trans_pts : list[Float, i_var - 1] + prev_trans_pts : list[`i_var` - 1] The previous points in real-space that were already transformed. theta_pt : float The point in [0, 1] being transformed to the Real space. @@ -576,7 +577,7 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): Index that is being tranformed. Less than D. promol_params : namedTuple Data about the Promolecular density. - transformed : list(`i_var` - 1) + transformed : list[`i_var` - 1] The set of previous points before index `i_var` that were transformed to real space. bracket : (float, float) Interval where root is suspected to be in Reals. Used for "brentq" root-finding method. @@ -597,22 +598,16 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): Further, if nan is in `transformed[:i_var]` then this function will return nan. """ - - def _is_boundary_pt(theta, prev_transformed, bound1=0.0, bound2=1.0): - # Check's if the boundary points are there. - # TODO: Remove isnan and keep the following boundary condition. - # TODO: Remove helper function. - if np.abs(theta - bound1) < 1e-10: - return True - if np.abs(theta - bound2) < 1e-10: - return True - # Check's if there is NAN in here. - # The [:i_var] is needed because of the way I've set-up transforming points in _transform. - if np.isnan(bracket[0]) or np.nan in prev_transformed: - return np.nan - return False - - if _is_boundary_pt(theta_pt, transformed[:i_var]): + # Check's if this is a boundary points which is mapped to np.nan + # These two conditions are added for individual point transformation. + if np.abs(theta_pt - 0.0) < 1e-10: + return np.nan + if np.abs(theta_pt - 1.0) < 1e-10: + return np.nan + # This condition is added for transformation of the entire grid. + # The [:i_var] is needed because of the way I've set-up transforming points in _transform. + # Likewise for the bracket, see the function `get_bracket`. + if np.nan in bracket or np.nan in transformed[:i_var]: return np.nan args = (transformed[:i_var], theta_pt, i_var, params) From 239858b98ca50d29f3f463fc51c763db5dbe391b Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 15:30:33 -0400 Subject: [PATCH 14/43] Change stepsize to number of points for input Providing number of points seems more natural than providing the stepsize for Promolecular Cubic, Uniform Grid Transformation. --- src/grid/protransform.py | 29 ++++++++++++++--------------- src/grid/tests/test_protransform.py | 4 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 75b3b94e6..7b32d5bc0 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -77,10 +77,11 @@ class CubicProTransform(Grid): Attributes ---------- + num_pts : (int, int, int) + The number of points, including both of the end/boundary point, in x, y, and z direction. + This is calculated as `int(1. / ss[i]) + 1`. ss : (float, float, float) The step-size in each x, y, and z direction. - num_pts : (int, int, int) - The number of points in x, y, and z direction. This is calculated as `int(1. / ss[i]) + 1` points : np.ndarray(N, 3) The transformed points in real space. prointegral : float @@ -111,10 +112,9 @@ class CubicProTransform(Grid): >> coord = np.array([[0., 0., 0.], [2., 2., 2.]]) Define information of the grid and its weights. - >> stepsize = 0.01 #TODO: Remove - >> numb_x = int(1. / stepsize) + 1 + >> numb_x = 50 >> weights = np.array([0.01] * numb_x**3) # Simple Riemannian weights. - >> promol = CubicProTransform([ss, ss, ss], weights, c, e, coord) + >> promol = CubicProTransform([numb_x, numb_x, numb_x], weights, c, e, coord) To integrate some function f. >> def f(pt): @@ -131,11 +131,12 @@ class CubicProTransform(Grid): ----- TODO: Insert Info About Conditional Distribution Method. TODO: Add Infor about how boundarys on theta-space are mapped to np.nan. + """ - def __init__(self, stepsize, weights, coeffs, exps, coords): + def __init__(self, num_pts, weights, coeffs, exps, coords): # TODO: Add Types - if not isinstance(stepsize, tuple): + if not isinstance(num_pts, (tuple, list)): pass if not isinstance(coeffs, (list, np.ndarray)): pass @@ -143,16 +144,14 @@ def __init__(self, stepsize, weights, coeffs, exps, coords): pass if not isinstance(coords, (list, np.ndarray)): pass - self._ss = stepsize - # TODO: Add boundary info in docs - self._num_pts = ( - int(1 / stepsize[0]) + 1, - int(1 / stepsize[1]) + 1, - int(1 / stepsize[2]) + 1, + self._ss = ( + 1. / (num_pts[0] - 1), + 1. / (num_pts[1] - 1), + 1. / (num_pts[2] - 1), ) + self._num_pts = num_pts - # pad coefficients and exponents with zeros to have the same size. - # Make it easier to use numpy operations. + # pad coefficients and exponents with zeros to have the same size, easier to use numpy. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) # Rather than computing this repeatedly. It is fixed. with np.errstate(divide="ignore"): diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 752c54cba..35be0557e 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -59,7 +59,7 @@ def setUp(self, ss=0.1, return_obj=False): num_pts = int(1 / ss) + 1 weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) obj = CubicProTransform( - [ss] * 3, weights, params.c_m, params.e_m, params.coords + [num_pts] * 3, weights, params.c_m, params.e_m, params.coords ) return params, obj return params @@ -344,7 +344,7 @@ def setUp(self, ss=0.1, return_obj=False): num_pts = int(1 / ss) + 1 weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) obj = CubicProTransform( - [ss] * 3, weights, params.c_m, params.e_m, params.coords + [num_pts] * 3, weights, params.c_m, params.e_m, params.coords ) return params, obj return params From eb328973bb78e537157f2c3a91cae2ef6aacc43a Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 16:11:50 -0400 Subject: [PATCH 15/43] Add mask to small/nan values for integrate promol Code review suggestion to use masked arrays for boundary points and small values of promolecular with a user-defined tolerance. --- src/grid/protransform.py | 44 +++++++++++++++++++---------- src/grid/tests/test_protransform.py | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 7b32d5bc0..05cf26d73 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -78,7 +78,7 @@ class CubicProTransform(Grid): Attributes ---------- num_pts : (int, int, int) - The number of points, including both of the end/boundary point, in x, y, and z direction. + The number of points, including both of the end/boundary points, in x, y, and z direction. This is calculated as `int(1. / ss[i]) + 1`. ss : (float, float, float) The step-size in each x, y, and z direction. @@ -185,7 +185,7 @@ def promol(self): r"""Return `PromolParams` namedTuple.""" return self._promol - def integrate(self, *value_arrays, trick=False): + def integrate(self, *value_arrays, trick=False, tol=1e-10): r""" Integrate any function. @@ -195,8 +195,11 @@ def integrate(self, *value_arrays, trick=False): ---------- *value_arrays : (np.ndarray(N, dtype=float),) One or multiple value array to integrate. - trick : bool + trick : bool, optional If true, uses the promolecular trick. + tol : float, optional + Integrand is set to zero whenever promolecular density is less than tolerance. + Default value is 1e-10. Returns ------- @@ -210,21 +213,32 @@ def integrate(self, *value_arrays, trick=False): ValueError Input integrand array is given or not of proper shape. + Notes + ----- + - TODO: Insert formula for integration. + - This method assumes the integrand decays faster than the promolecular density. + """ promolecular = self.promol.promolecular(self.points) + # Integrand is set to zero when promolecular is less than certain value and, + # When on the boundary (hence when promolecular is nan). + cond = (promolecular <= tol) | (np.isnan(promolecular)) + promolecular = np.ma.masked_where(cond, promolecular, copy=False) + integrands = [] - # TODO: Use Mask Array with nan's with user-defined tol. - with np.errstate(divide="ignore"): - for arr in value_arrays: - assert arr.dtype != object, "Array dtype should not be object." - # This may be refactored to fit in the general promolecular trick in `grid`. - if trick: - integrand = (arr - promolecular) / promolecular - else: - integrand = arr / promolecular - # Functions evaluated at points on the boundary is set to zero. - integrand[np.isnan(self.points).any(axis=1)] = 0.0 - integrands.append(integrand) + for arr in value_arrays: + # This is needed as it gives incorrect results when arr.dtype isn't object. + assert arr.dtype != object, "Array dtype should not be object." + # This may be refactored to fit in the general promolecular trick in `grid`. + # Masked array is needed since division by promolecular contains nan. + if trick: + integrand = np.ma.divide(arr - promolecular, promolecular) + else: + integrand = np.ma.divide(arr, promolecular) + # Function/Integrand evaluated at points on the boundary is set to zero. + np.ma.fix_invalid(integrand, copy=False, fill_value=0) + integrands.append(integrand) + if trick: return self.prointegral + super().integrate(*integrands) return super().integrate(*integrands) diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 35be0557e..31e01a5a6 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -443,7 +443,7 @@ def tranformation_z(pt): grad = approx_fprime([z], tranformation_z, 1e-8) assert np.abs(grad - actual[2, 2]) < 1e-5 - def test_integration_slightly_perturbed_gaussian(self): + def test_integration_slightly_perturbed_gaussian_with_promolecular_trick(self): r"""Test integration of a slightly perturbed function.""" # Only Measured against one decimal place and very similar exponent. params, obj = self.setUp(ss=0.03, return_obj=True) From 40a04875f009bc0b79d9819bf9b12227af229275 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 16:21:06 -0400 Subject: [PATCH 16/43] Rename coords to indices --- src/grid/protransform.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 05cf26d73..31e49df36 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -419,8 +419,6 @@ def steepest_ascent_theta(self, real_pt, real_grad): def _transform(self): # Indices (i, j, k) start from bottom, left-most corner of the unit cube. - # TODO: Add i, j, k integer info. - # TODO: Rename coordinates to indices. counter = 0 for ix in range(self.num_pts[0]): cart_pt = [None, None, None] @@ -446,14 +444,15 @@ def _transform(self): self.points[counter] = cart_pt.copy() counter += 1 - def _get_bracket(self, coord, i_var): + def _get_bracket(self, indices, i_var): r""" Obtain brackets for root-finder based on the coordinate of the point. Parameters ---------- - coord : tuple(int, int, int) - The coordinate of a point. # TODO: rename indices. + indices : tuple(int, int, int) + The indices of a point, where (0, 0, 0) is the bottom, left-most, down point + of the cube. i_var : int Index of point being transformed. @@ -464,25 +463,25 @@ def _get_bracket(self, coord, i_var): """ # If it is a boundary point, then return nan. - if 0 in coord[: i_var + 1] or (self.num_pts[i_var] - 1) in coord[: i_var + 1]: + if 0 in indices[: i_var + 1] or (self.num_pts[i_var] - 1) in indices[: i_var + 1]: return np.nan, np.nan # If it is a new point, with no nearby point, get a large initial guess. - elif coord[i_var] == 1: + elif indices[i_var] == 1: min = (np.min(self.promol.coords[:, i_var]) - 3.0) * 20.0 max = (np.max(self.promol.coords[:, i_var]) + 3.0) * 20.0 return min, max # If the previous point has been converted, use that as a initial guess. if i_var == 0: - index = (coord[0] - 1) * self.num_pts[1] * self.num_pts[2] + index = (indices[0] - 1) * self.num_pts[1] * self.num_pts[2] elif i_var == 1: - index = coord[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * ( - coord[1] - 1 + index = indices[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * ( + indices[1] - 1 ) elif i_var == 2: index = ( - coord[0] * self.num_pts[1] * self.num_pts[2] - + self.num_pts[2] * coord[1] - + coord[2] + indices[0] * self.num_pts[1] * self.num_pts[2] + + self.num_pts[2] * indices[1] + + indices[2] - 1 ) From b8fc2b2a826d29afdca4771528c41486b98c32a0 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 17 Jun 2020 17:36:26 -0400 Subject: [PATCH 17/43] Add dynamic bracketing method to root eqn Suggested in code review, able to handle insufficient brackets. --- src/grid/protransform.py | 52 ++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 31e49df36..9ed0ef295 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -19,7 +19,6 @@ # -- r"""Promolecular Grid Transformation.""" - from dataclasses import dataclass, astuple from grid.basegrid import Grid @@ -305,7 +304,7 @@ def jacobian(self, real_pt): elif j_deriv < i_var: # Derivative of inside of Gaussian. deriv_quadratic = ( - -e_m * 2.0 * diff_coords[:, j_deriv][:, np.newaxis] + -e_m * 2.0 * diff_coords[:, j_deriv][:, np.newaxis] ) deriv_num = np.sum( coeff_num * integrate_till_pt_x * deriv_quadratic @@ -313,7 +312,7 @@ def jacobian(self, real_pt): deriv_den = np.sum(coeff_num * deriv_quadratic) # Quotient Rule jacobian[i_var, j_deriv] = ( - deriv_num * transf_den - transf_num * deriv_den + deriv_num * transf_den - transf_num * deriv_den ) jacobian[i_var, j_deriv] /= transf_den ** 2.0 @@ -475,17 +474,16 @@ def _get_bracket(self, indices, i_var): index = (indices[0] - 1) * self.num_pts[1] * self.num_pts[2] elif i_var == 1: index = indices[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * ( - indices[1] - 1 + indices[1] - 1 ) elif i_var == 2: index = ( - indices[0] * self.num_pts[1] * self.num_pts[2] - + self.num_pts[2] * indices[1] - + indices[2] - - 1 + indices[0] * self.num_pts[1] * self.num_pts[2] + + self.num_pts[2] * indices[1] + + indices[2] + - 1 ) - # TODO: Add dynamic bracketing methods with +5. return self.points[index, i_var], self.points[index, i_var] + 10.0 @@ -602,14 +600,46 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): Raises ------ - AssertionError : If the root did not converge, or brackets did not have opposite sign. + AssertionError : If the root did not converge, or brackets did not have opposite sign. + RuntimeError : If dynamic bracketing reached maximum iteration. Notes ----- - If the theta point is on the boundary or it is itself a nan, then it get's mapped to nan. Further, if nan is in `transformed[:i_var]` then this function will return nan. + - If Brackets do not have the opposite sign, will change the brackets by adding/subtracting + the value 10 to the lower or upper bound that is closest to zero. """ + def _dynamic_bracketing(l_bnd, u_bnd, maxiter=50): + r"""Dynamically changes either the lower or upper bound to have different sign values.""" + def is_same_sign(x, y): return (x >= 0 and y >= 0) or (x < 0 and y < 0) + + bounds = [l_bnd, u_bnd] + f_l_bnd = _root_equation(l_bnd, *args) + f_u_bnd = _root_equation(u_bnd, *args) + # Get Index of the one that is closest to zero, the one that needs to change. + f_bnds = np.abs([f_l_bnd, f_u_bnd]) + idx = f_bnds.argmin() + # Check if they have the same sign. + same_sign = is_same_sign(*bounds) + counter = 0 + while same_sign and counter < maxiter: + # Add 10 to the upper bound or subtract 10 to the lower bound to the one that + # is closest to zero. This is done based on the sign. + bounds[idx] = np.sign(idx - 0.5) * 10 + bracket[idx] + # Update info for next iteration. + if idx == 0: + f_l_bnd = _root_equation(bracket[0], *args) + else: + f_u_bnd = _root_equation(bracket[1], *args) + same_sign = is_same_sign(f_l_bnd, f_u_bnd) + counter += 1 + + if counter == maxiter: + raise RuntimeError("Dynamic Bracketing did not converge.") + return tuple(bounds) + # Check's if this is a boundary points which is mapped to np.nan # These two conditions are added for individual point transformation. if np.abs(theta_pt - 0.0) < 1e-10: @@ -622,7 +652,9 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): if np.nan in bracket or np.nan in transformed[:i_var]: return np.nan + # Set up Arguments for root_equation with dynamic bracketing. args = (transformed[:i_var], theta_pt, i_var, params) + bracket = _dynamic_bracketing(bracket[0], bracket[1]) root_result = root_scalar( _root_equation, args=args, From c104ac68194834e26eb71c1e66465d8a753e55cb Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Sat, 20 Jun 2020 09:34:58 -0400 Subject: [PATCH 18/43] Add cubic grid support for protransform Cubic grid is a tensor product of one-dimensional grids. Promolecular transform is modeled on a cubic grid. --- src/grid/protransform.py | 243 ++++++++++++++-------------- src/grid/tests/test_protransform.py | 53 +++--- 2 files changed, 149 insertions(+), 147 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 9ed0ef295..60d1ae4d3 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -21,7 +21,8 @@ from dataclasses import dataclass, astuple -from grid.basegrid import Grid +from grid.basegrid import Grid, OneDGrid + import numpy as np @@ -41,6 +42,10 @@ class _PromolParams: pi_over_exponents: np.ndarray dim: int = 3 + def integrate_all(self): + r"""Integration of Gaussian over Entire Real space ie :math:`\mathbb{R}^D`.""" + return np.sum(self.c_m * self.pi_over_exponents ** self.dim) + def promolecular(self, grid): r""" Evaluate the promolecular density over a grid. @@ -72,15 +77,15 @@ def promolecular(self, grid): class CubicProTransform(Grid): r""" - Promolecular Grid Transformation of a Cubic, Uniform Grid in [0,1]^3 to Real space. + Promolecular Grid Transformation of a Cubic Grid. + + Grid is three dimensional and modeled as Tensor Product of Three, one dimensional grids. Attributes ---------- num_pts : (int, int, int) The number of points, including both of the end/boundary points, in x, y, and z direction. This is calculated as `int(1. / ss[i]) + 1`. - ss : (float, float, float) - The step-size in each x, y, and z direction. points : np.ndarray(N, 3) The transformed points in real space. prointegral : float @@ -112,8 +117,10 @@ class CubicProTransform(Grid): Define information of the grid and its weights. >> numb_x = 50 - >> weights = np.array([0.01] * numb_x**3) # Simple Riemannian weights. - >> promol = CubicProTransform([numb_x, numb_x, numb_x], weights, c, e, coord) + >> weights = np.array([(1.0 / (numb_x - 2))] * numb_x) # Simple Riemannian weights. + >> grid = np.linspace(0, 1, num=num_pts, endpoint=True) # Uniform 1D Grid TODO 0,1 + >> oned = OneDGrid(grid, weights, domain=(0,1)) TODo 0,1 + >> promol = CubicProTransform([oned, oned, oned], params.c_m, params.e_m, params.coords) To integrate some function f. >> def f(pt): @@ -131,24 +138,20 @@ class CubicProTransform(Grid): TODO: Insert Info About Conditional Distribution Method. TODO: Add Infor about how boundarys on theta-space are mapped to np.nan. - """ - def __init__(self, num_pts, weights, coeffs, exps, coords): - # TODO: Add Types - if not isinstance(num_pts, (tuple, list)): - pass - if not isinstance(coeffs, (list, np.ndarray)): + """ + def __init__(self, oned_grids, coeffs, exps, coords): + if not isinstance(oned_grids, list): pass - if not isinstance(exps, (list, np.ndarray)): + if not np.all([isinstance(grid, OneDGrid) for grid in oned_grids]): pass - if not isinstance(coords, (list, np.ndarray)): + if not np.all([grid.domain == (0., 1.) for grid in oned_grids]): pass - self._ss = ( - 1. / (num_pts[0] - 1), - 1. / (num_pts[1] - 1), - 1. / (num_pts[2] - 1), - ) - self._num_pts = num_pts + if not len(oned_grids) == 3: + raise ValueError("There should be three One-Dimensional grids in `oned_grids`.") + + self._num_pts = tuple([grid.size for grid in oned_grids]) + self._dim = len(oned_grids) # pad coefficients and exponents with zeros to have the same size, easier to use numpy. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) @@ -156,23 +159,26 @@ def __init__(self, num_pts, weights, coeffs, exps, coords): with np.errstate(divide="ignore"): pi_over_exponents = np.sqrt(np.pi / exps) pi_over_exponents[exps == 0] = 0 - self._prointegral = np.sum(coeffs * pi_over_exponents ** 3.0) - self._promol = _PromolParams(coeffs, exps, coords, pi_over_exponents) + self._promol = _PromolParams(coeffs, exps, coords, pi_over_exponents, self._dim) + self._prointegral = self._promol.integrate_all() + + empty_pts = np.empty((np.prod(self._num_pts), self._dim), dtype=np.float64) + weights = np.kron(np.kron(oned_grids[0].weights, oned_grids[1].weights), + oned_grids[2].weights) + super().__init__(empty_pts, weights * self._prointegral) + # Transform Cubic Grid in Theta-Space to Real-space. + self._transform(oned_grids) - # initialize parent class - empty_points = np.empty((np.prod(self._num_pts), 3), dtype=np.float64) - super().__init__(empty_points, weights * self._prointegral) - self._transform() + @property + def dim(self): + r"""Return the dimension of the cubic grid.""" + return self._dim @property def num_pts(self): r"""Return number of points in each direction.""" return self._num_pts - @property - def ss(self): - r"""Return stepsize of the cubic grid.""" - return self._ss @property def prointegral(self): @@ -184,6 +190,57 @@ def promol(self): r"""Return `PromolParams` namedTuple.""" return self._promol + def transform(self, real_pt): + r""" + Transform a real point in three-dimensional Reals to theta/unit cube. + + Parameters + ---------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + + Returns + ------- + theta_pt : np.ndarray(3) + Point in :math:`[0, 1]^3`. + + """ + return np.array( + [_transform_coordinate(real_pt, i, self.promol) + for i in range(0, self.promol.dim)] + ) + + def inverse(self, theta_pt, bracket=(-10, 10)): + r""" + Transform a theta/unit-cube point to three-dimensional Real space. + + Parameters + ---------- + theta_pt : np.ndarray(3) + Point in :math:`[0, 1]^3` + bracket : (float, float), optional + Interval where root is suspected to be in Reals. + Used for "brentq" root-finding method. Default is (-10, 10). + + Returns + ------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + + Notes + ----- + - If a point is far away from the promolecular density, then it will be mapped + to `np.nan`. + + """ + real_pt = [] + for i in range(0, self.promol.dim): + scalar = _inverse_coordinate( + theta_pt[i], i, real_pt[:i], self.promol, bracket + ) + real_pt.append(scalar) + return np.array(real_pt) + def integrate(self, *value_arrays, trick=False, tol=1e-10): r""" Integrate any function. @@ -272,7 +329,7 @@ def jacobian(self, real_pt): # Code is duplicated from `transform_coordinate` due to effiency reasons. # TODO: Reduce the number of computation with `i_var`. - for i_var in range(0, 3): + for i_var in range(0, self.promol.dim): # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] @@ -318,56 +375,6 @@ def jacobian(self, real_pt): return jacobian - def transform(self, real_pt): - r""" - Transform a real point in three-dimensional Reals to theta/unit cube. - - Parameters - ---------- - real_pt : np.ndarray(3) - Point in :math:`\mathbb{R}^3` - - Returns - ------- - theta_pt : np.ndarray(3) - Point in :math:`[0, 1]^3`. - - """ - return np.array( - [transform_coordinate(real_pt, i, self.promol) for i in range(0, 3)] - ) - - def inverse(self, theta_pt, bracket=(-10, 10)): - r""" - Transform a theta/unit-cube point to three-dimensional Real space. - - Parameters - ---------- - theta_pt : np.ndarray(3) - Point in :math:`[0, 1]^3` - bracket : (float, float), optional - Interval where root is suspected to be in Reals. - Used for "brentq" root-finding method. Default is (-10, 10). - - Returns - ------- - real_pt : np.ndarray(3) - Point in :math:`\mathbb{R}^3` - - Notes - ----- - - If a point is far away from the promolecular density, then it will be mapped - to `np.nan`. - - """ - real_pt = [] - for i in range(0, 3): - scalar = inverse_coordinate( - theta_pt[i], i, self.promol, real_pt[:i], bracket - ) - real_pt.append(scalar) - return np.array(real_pt) - def derivative(self, real_pt, real_derivative): r""" Directional derivative in theta space. @@ -416,28 +423,28 @@ def steepest_ascent_theta(self, real_pt, real_grad): jacobian = self.jacobian(real_pt) return jacobian.dot(real_grad) - def _transform(self): + def _transform(self, oned_grids): # Indices (i, j, k) start from bottom, left-most corner of the unit cube. counter = 0 for ix in range(self.num_pts[0]): cart_pt = [None, None, None] - unit_x = self.ss[0] * ix + unit_x = oned_grids[0].points[ix] - bracx = self._get_bracket((ix,), 0) - cart_pt[0] = inverse_coordinate(unit_x, 0, self.promol, cart_pt, bracx) + brack_x = self._get_bracket((ix,), 0) + cart_pt[0] = _inverse_coordinate(unit_x, 0, cart_pt, self.promol, brack_x) for iy in range(self.num_pts[1]): - unit_y = self.ss[1] * iy + unit_y = oned_grids[1].points[iy] - bracy = self._get_bracket((ix, iy), 1) - cart_pt[1] = inverse_coordinate(unit_y, 1, self.promol, cart_pt, bracy) + brack_y = self._get_bracket((ix, iy), 1) + cart_pt[1] = _inverse_coordinate(unit_y, 1, cart_pt, self.promol, brack_y) for iz in range(self.num_pts[2]): - unit_z = self.ss[2] * iz + unit_z = oned_grids[2].points[iz] - bracz = self._get_bracket((ix, iy, iz), 2) - cart_pt[2] = inverse_coordinate( - unit_z, 2, self.promol, cart_pt, bracz + brack_z = self._get_bracket((ix, iy, iz), 2) + cart_pt[2] = _inverse_coordinate( + unit_z, 2, cart_pt, self.promol, brack_z ) self.points[counter] = cart_pt.copy() @@ -461,7 +468,7 @@ def _get_bracket(self, indices, i_var): The bracket for the root-finder solver. """ - # If it is a boundary point, then return nan. + # If it is a boundary point, then return nan. Done by indices. if 0 in indices[: i_var + 1] or (self.num_pts[i_var] - 1) in indices[: i_var + 1]: return np.nan, np.nan # If it is a new point, with no nearby point, get a large initial guess. @@ -487,7 +494,7 @@ def _get_bracket(self, indices, i_var): return self.points[index, i_var], self.points[index, i_var] + 10.0 -def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=False): +def _transform_coordinate(real_pt, i_var, promol): r""" Transform the `i_var` coordinate of a real point to [0, 1] using promolecular density. @@ -497,23 +504,16 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals Real point being transformed. i_var : int Index that is being tranformed. Less than D. - promol_params : namedTuple - Data about the Promolecular density. - deriv : bool - If true, return the derivative of transformation wrt `i_var` real variable. - Default is False. - sderiv : bool - If true, return the second derivative of transformation wrt `i_var` real variable. - Default is False. + promol : _PromolParams + Promolecular Data Class. Returns ------- - unit_pt, deriv, sderiv : (float, float, float) - The transformed point in [0,1]^D and its derivative with respect to real point and - the second derivative with respect to real point are returned. + unit_pt : float + The transformed point in [0,1]^D. """ - c_m, e_m, coords, pi_over_exps, dim = astuple(promol_params) + c_m, e_m, coords, pi_over_exps, dim = astuple(promol) # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] @@ -535,18 +535,10 @@ def transform_coordinate(real_pt, i_var, promol_params, deriv=False, sderiv=Fals transf_den = np.sum(coeff_num) transform_value = transf_num / transf_den - if deriv: - inner_term = coeff_num * np.exp(-e_m * diff_squared[:, i_var][:, np.newaxis]) - deriv = np.sum(inner_term) / transf_den - - if sderiv: - sderiv = np.sum(inner_term * -e_m * 2.0 * coord_ivar) / transf_den - return transform_value, deriv, sderiv - return transform_value, deriv return transform_value -def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): +def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, promol): r""" Equation to solve for the root to find inverse coordinate from theta space to Real space. @@ -560,8 +552,8 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): The point in [0, 1] being transformed to the Real space. i_var : int Index of variable being transformed. - params : _PromolParams - Promolecular density data class. + promol : _PromolParams + Promolecular Data Class. Returns ------- @@ -571,11 +563,11 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, params): """ all_points = np.append(prev_trans_pts, init_guess) - transf_pt = transform_coordinate(all_points, i_var, params) + transf_pt = _transform_coordinate(all_points, i_var, promol) return theta_pt - transf_pt -def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): +def _inverse_coordinate(theta_pt, i_var, transformed, promol, bracket=(-10, 10)): r""" Transform a point in [0, 1] to the real space corresponding to `i_var` variable. @@ -585,10 +577,10 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): Point in [0, 1]. i_var : int Index that is being tranformed. Less than D. - promol_params : namedTuple - Data about the Promolecular density. transformed : list[`i_var` - 1] The set of previous points before index `i_var` that were transformed to real space. + promol : _PromolParams + Promolecular Data Class. bracket : (float, float) Interval where root is suspected to be in Reals. Used for "brentq" root-finding method. Default is (-10, 10). @@ -611,9 +603,12 @@ def inverse_coordinate(theta_pt, i_var, params, transformed, bracket=(-10, 10)): the value 10 to the lower or upper bound that is closest to zero. """ + def _dynamic_bracketing(l_bnd, u_bnd, maxiter=50): - r"""Dynamically changes either the lower or upper bound to have different sign values.""" - def is_same_sign(x, y): return (x >= 0 and y >= 0) or (x < 0 and y < 0) + r"""Dynamically changes the lower (or upper bound) to have different sign values.""" + + def is_same_sign(x, y): + return (x >= 0 and y >= 0) or (x < 0 and y < 0) bounds = [l_bnd, u_bnd] f_l_bnd = _root_equation(l_bnd, *args) @@ -653,7 +648,7 @@ def is_same_sign(x, y): return (x >= 0 and y >= 0) or (x < 0 and y < 0) return np.nan # Set up Arguments for root_equation with dynamic bracketing. - args = (transformed[:i_var], theta_pt, i_var, params) + args = (transformed[:i_var], theta_pt, i_var, promol) bracket = _dynamic_bracketing(bracket[0], bracket[1]) root_result = root_scalar( _root_equation, diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 31e01a5a6..619b2a04f 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -31,11 +31,12 @@ """ +from grid.basegrid import OneDGrid from grid.protransform import ( CubicProTransform, _PromolParams, _pad_coeffs_exps_with_zeros, - transform_coordinate, + _transform_coordinate, ) import numpy as np @@ -57,16 +58,18 @@ def setUp(self, ss=0.1, return_obj=False): params = _PromolParams(c, e, coord, pi_over_exponents=np.sqrt(np.pi / e), dim=3) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) + weights = np.array([(1.0 / (num_pts - 2))] * num_pts) + oned = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0,1)) + obj = CubicProTransform( - [num_pts] * 3, weights, params.c_m, params.e_m, params.coords + [oned, oned, oned], params.c_m, params.e_m, params.coords ) return params, obj return params def promolecular(self, x, y, z, params): r"""Hard-Code the promolecular density.""" - # Promolecular in CubicProTransform class uses einsum, this tests it against that. + # Promolecular in UniformProTransform class uses einsum, this tests it against that. # Also could be used for integration tests. cm, em, coords = params.c_m, params.e_m, params.coords promol = 0.0 @@ -117,7 +120,7 @@ def formula_transforming_x(x): 5.0 * (np.pi / 2) ** 1.5 + 10.0 * (np.pi / 3.0) ** 1.5 ) - true_ans = transform_coordinate([pt], 0, self.setUp()) + true_ans = _transform_coordinate([pt], 0, self.setUp()) assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) @@ -146,7 +149,7 @@ def formula_transforming_y(x, y): den = dac1 + dac2 return num / den - true_ans = transform_coordinate([x, y], 1, self.setUp()) + true_ans = _transform_coordinate([x, y], 1, self.setUp()) assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) @@ -182,7 +185,7 @@ def formula_transforming_z(x, y, z): params, obj = self.setUp(ss=0.5, return_obj=True) true_ans = formula_transforming_z(x, y, z) # Test function - actual = transform_coordinate([x, y, z], 2, params) + actual = _transform_coordinate([x, y, z], 2, params) assert np.abs(true_ans - actual) < 1e-8 # Test Method @@ -203,7 +206,7 @@ def test_transforming_simple_grid(self): assert real_pt[2] != np.nan # Test that converting the point back to unit cube gives [0.5, 0.5, 0.5]. for i_var in range(0, 3): - transf = transform_coordinate(real_pt, i_var, obj.promol) + transf = _transform_coordinate(real_pt, i_var, obj.promol) assert np.abs(transf - 0.5) < 1e-5 # Test that all other points are indeed boundary points. all_nans = np.delete(obj.points, non_boundary_pt_index, axis=0) @@ -246,7 +249,7 @@ def test_derivative_tranformation_x_finite_difference(self, pt): actual = obj.jacobian(pt) def tranformation_x(pt): - return transform_coordinate(pt, 0, params) + return _transform_coordinate(pt, 0, params) grad = approx_fprime([pt[0]], tranformation_x, 1e-6) assert np.abs(grad - actual[0, 0]) < 1e-4 @@ -259,13 +262,13 @@ def test_derivative_tranformation_y_finite_difference(self, x, y): actual = obj.jacobian(np.array([x, y, 3.0])) def tranformation_y(pt): - return transform_coordinate([x, pt[0]], 1, params) + return _transform_coordinate([x, pt[0]], 1, params) grad = approx_fprime([y], tranformation_y, 1e-8) assert np.abs(grad - actual[1, 1]) < 1e-5 def transformation_y_wrt_x(pt): - return transform_coordinate([pt[0], y], 1, params) + return _transform_coordinate([pt[0], y], 1, params) h = 1e-8 deriv = np.imag(transformation_y_wrt_x([complex(x, h)])) / h @@ -280,19 +283,19 @@ def test_derivative_tranformation_z_finite_difference(self, x, y, z): actual = obj.jacobian(np.array([x, y, z])) def tranformation_z(pt): - return transform_coordinate([x, y, pt[0]], 2, params) + return _transform_coordinate([x, y, pt[0]], 2, params) grad = approx_fprime([z], tranformation_z, 1e-8) assert np.abs(grad - actual[2, 2]) < 1e-5 def transformation_z_wrt_y(pt): - return transform_coordinate([x, pt[0], z], 2, params) + return _transform_coordinate([x, pt[0], z], 2, params) deriv = approx_fprime([y], transformation_z_wrt_y, 1e-8) assert np.abs(deriv - actual[2, 1]) < 1e-4 def transformation_z_wrt_x(pt): - a = transform_coordinate([pt[0], y, z], 2, params) + a = _transform_coordinate([pt[0], y, z], 2, params) return a h = 1e-8 @@ -335,16 +338,20 @@ class TestOneGaussianAgainstNumerics: r"""Tests With Numerical Integration of a One Gaussian function.""" def setUp(self, ss=0.1, return_obj=False): - r"""Return a one Gaussian example and its CubicProTransform object.""" + r"""Return a one Gaussian example and its UniformProTransform object.""" c = np.array([[5.0]]) e = np.array([[2.0]]) coord = np.array([[1.0, 2.0, 3.0]]) params = _PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([(1.0 / (num_pts - 2)) ** 3.0] * num_pts ** 3) + weights = np.array([(1.0 / (num_pts - 2))] * num_pts) + oned_x = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0, 1)) + oned_y = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0, 1)) + oned_z = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0, 1)) + obj = CubicProTransform( - [num_pts] * 3, weights, params.c_m, params.e_m, params.coords + [oned_x, oned_y, oned_z], params.c_m, params.e_m, params.coords ) return params, obj return params @@ -359,7 +366,7 @@ def promolecular_in_x(grid, every_grid): promol_x_all = 5.0 * np.exp(-2.0 * (every_grid - 1.0) ** 2.0) return promol_x, promol_x_all - true_ans = transform_coordinate([pt], 0, self.setUp()) + true_ans = _transform_coordinate([pt], 0, self.setUp()) grid = np.arange(-10.0, pt, 0.00001) # Integration till a x point every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration promol_x, promol_x_all = promolecular_in_x(grid, every_grid) @@ -379,7 +386,7 @@ def promolecular_in_y(grid, every_grid): promol_y_all = 5.0 * np.exp(-2.0 * (every_grid - 2.0) ** 2.0) return promol_y_all, promol_y - true_ans = transform_coordinate([x, y], 1, self.setUp()) + true_ans = _transform_coordinate([x, y], 1, self.setUp()) grid = np.arange(-10.0, y, 0.00001) # Integration till a x point every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration promol_y_all, promol_y = promolecular_in_y(grid, every_grid) @@ -406,7 +413,7 @@ def promolecular_in_z(grid, every_grid): promol_z_all, promol_z = promolecular_in_z(grid, every_grid) actual = np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) - true_ans = transform_coordinate([x, y, z], 2, self.setUp()) + true_ans = _transform_coordinate([x, y, z], 2, self.setUp()) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("x", [0.0, 0.25, 1.1, 0.5, 1.5]) @@ -424,21 +431,21 @@ def test_jacobian(self, x, y, z): # test derivative wrt to x def tranformation_x(pt): - return transform_coordinate([pt[0], y, z], 0, params) + return _transform_coordinate([pt[0], y, z], 0, params) grad = approx_fprime([x], tranformation_x, 1e-8) assert np.abs(grad - actual[0, 0]) < 1e-5 # test derivative wrt to y def tranformation_y(pt): - return transform_coordinate([x, pt[0]], 1, params) + return _transform_coordinate([x, pt[0]], 1, params) grad = approx_fprime([y], tranformation_y, 1e-8) assert np.abs(grad - actual[1, 1]) < 1e-5 # Test derivative wrt to z def tranformation_z(pt): - return transform_coordinate([x, y, pt[0]], 2, params) + return _transform_coordinate([x, y, pt[0]], 2, params) grad = approx_fprime([z], tranformation_z, 1e-8) assert np.abs(grad - actual[2, 2]) < 1e-5 From a35a07eaf8b03614802afc2848eb5393c30c1239 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Sat, 20 Jun 2020 11:13:12 -0400 Subject: [PATCH 19/43] Change default [0, 1] to [-1, 1] for Promolecular Since the bounds [-1, 1] is used in onedgrids.py, the default bounds is changed to be consistent. --- src/grid/protransform.py | 80 ++++++++++++++++------------- src/grid/tests/test_protransform.py | 50 +++++++++--------- 2 files changed, 68 insertions(+), 62 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 60d1ae4d3..b837b2a7f 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -80,6 +80,8 @@ class CubicProTransform(Grid): Promolecular Grid Transformation of a Cubic Grid. Grid is three dimensional and modeled as Tensor Product of Three, one dimensional grids. + Theta space is defined to be :math:`[-1, 1]^3`. + Real space is defined to be :math:`\mathbb{R}^3.` Attributes ---------- @@ -100,13 +102,13 @@ class CubicProTransform(Grid): integrate(trick=False) Integral of a real-valued function over Euclidean space. jacobian() - Jacobian of the transformation from Real space to Theta/Unit cube space. + Jacobian of the transformation from Real space to Theta space :math:`[-1, 1]^3`. steepest_ascent_theta() Direction of steepest-ascent of a function in theta space from gradient in real space. transform(): - Transform Real point to theta/unit-cube point :math:`[0,1]^3`. + Transform Real point to theta point :math:`[-1, 1]^3`. inverse(bracket=(-10, 10)) - Transform theta/unit-cube point to Real space :math:`\mathbb{R}^3`. + Transform theta point to Real space :math:`\mathbb{R}^3`. Examples -------- @@ -116,10 +118,10 @@ class CubicProTransform(Grid): >> coord = np.array([[0., 0., 0.], [2., 2., 2.]]) Define information of the grid and its weights. + >> from grid.onedgrid import GaussChebyshev + >> numb_x = 50 - >> weights = np.array([(1.0 / (numb_x - 2))] * numb_x) # Simple Riemannian weights. - >> grid = np.linspace(0, 1, num=num_pts, endpoint=True) # Uniform 1D Grid TODO 0,1 - >> oned = OneDGrid(grid, weights, domain=(0,1)) TODo 0,1 + >> oned = GaussChebyshev(numb_x) # Grid is the same in all x, y, z directions. >> promol = CubicProTransform([oned, oned, oned], params.c_m, params.e_m, params.coords) To integrate some function f. @@ -138,15 +140,14 @@ class CubicProTransform(Grid): TODO: Insert Info About Conditional Distribution Method. TODO: Add Infor about how boundarys on theta-space are mapped to np.nan. - """ def __init__(self, oned_grids, coeffs, exps, coords): if not isinstance(oned_grids, list): - pass + raise TypeError("oned_grid should be of type list.") if not np.all([isinstance(grid, OneDGrid) for grid in oned_grids]): - pass - if not np.all([grid.domain == (0., 1.) for grid in oned_grids]): - pass + raise TypeError("Grid in oned_grids should be of type `OneDGrid`.") + if not np.all([grid.domain == (-1, 1.) for grid in oned_grids]): + raise ValueError("One Dimensional grid domain should be (-1, 1).") if not len(oned_grids) == 3: raise ValueError("There should be three One-Dimensional grids in `oned_grids`.") @@ -163,9 +164,13 @@ def __init__(self, oned_grids, coeffs, exps, coords): self._prointegral = self._promol.integrate_all() empty_pts = np.empty((np.prod(self._num_pts), self._dim), dtype=np.float64) - weights = np.kron(np.kron(oned_grids[0].weights, oned_grids[1].weights), - oned_grids[2].weights) - super().__init__(empty_pts, weights * self._prointegral) + weights = np.kron( + np.kron(oned_grids[0].weights, oned_grids[1].weights), + oned_grids[2].weights + ) + # The prointegral is needed because of promolecular integration. + # Divide by 8 needed because the grid is in [-1, 1] rather than [0, 1]. + super().__init__(empty_pts, weights * self._prointegral / 2.0**self._dim) # Transform Cubic Grid in Theta-Space to Real-space. self._transform(oned_grids) @@ -192,7 +197,7 @@ def promol(self): def transform(self, real_pt): r""" - Transform a real point in three-dimensional Reals to theta/unit cube. + Transform a real point in three-dimensional Reals to theta space. Parameters ---------- @@ -202,7 +207,7 @@ def transform(self, real_pt): Returns ------- theta_pt : np.ndarray(3) - Point in :math:`[0, 1]^3`. + Point in :math:`[-1, 1]^3`. """ return np.array( @@ -212,12 +217,12 @@ def transform(self, real_pt): def inverse(self, theta_pt, bracket=(-10, 10)): r""" - Transform a theta/unit-cube point to three-dimensional Real space. + Transform a theta space point to three-dimensional Real space. Parameters ---------- theta_pt : np.ndarray(3) - Point in :math:`[0, 1]^3` + Point in :math:`[-1, 1]^3` bracket : (float, float), optional Interval where root is suspected to be in Reals. Used for "brentq" root-finding method. Default is (-10, 10). @@ -301,7 +306,7 @@ def integrate(self, *value_arrays, trick=False, tol=1e-10): def jacobian(self, real_pt): r""" - Jacobian of the transformation from real space to unit-cube/theta space. + Jacobian of the transformation from real space to theta space. Precisely, it is the lower-triangular matrix .. math:: @@ -373,7 +378,7 @@ def jacobian(self, real_pt): ) jacobian[i_var, j_deriv] /= transf_den ** 2.0 - return jacobian + return 2.0 * jacobian def derivative(self, real_pt, real_derivative): r""" @@ -401,7 +406,7 @@ def derivative(self, real_pt, real_derivative): def steepest_ascent_theta(self, real_pt, real_grad): r""" - Steepest ascent direction of a function in theta/unit-cube space. + Steepest ascent direction of a function in theta space. Steepest ascent is the gradient ie direction of maximum change of a function. This guarantees moving in direction of steepest ascent in real-space @@ -417,34 +422,34 @@ def steepest_ascent_theta(self, real_pt, real_grad): Returns ------- theta_grad : np.ndarray(3) - Gradient of a function in theta/unit-cube space. + Gradient of a function in theta space. """ jacobian = self.jacobian(real_pt) return jacobian.dot(real_grad) def _transform(self, oned_grids): - # Indices (i, j, k) start from bottom, left-most corner of the unit cube. + # Indices (i, j, k) start from bottom, left-most corner of the [-1, 1]^3 cube. counter = 0 for ix in range(self.num_pts[0]): cart_pt = [None, None, None] - unit_x = oned_grids[0].points[ix] + theta_x = oned_grids[0].points[ix] brack_x = self._get_bracket((ix,), 0) - cart_pt[0] = _inverse_coordinate(unit_x, 0, cart_pt, self.promol, brack_x) + cart_pt[0] = _inverse_coordinate(theta_x, 0, cart_pt, self.promol, brack_x) for iy in range(self.num_pts[1]): - unit_y = oned_grids[1].points[iy] + theta_y = oned_grids[1].points[iy] brack_y = self._get_bracket((ix, iy), 1) - cart_pt[1] = _inverse_coordinate(unit_y, 1, cart_pt, self.promol, brack_y) + cart_pt[1] = _inverse_coordinate(theta_y, 1, cart_pt, self.promol, brack_y) for iz in range(self.num_pts[2]): - unit_z = oned_grids[2].points[iz] + theta_z = oned_grids[2].points[iz] brack_z = self._get_bracket((ix, iy, iz), 2) cart_pt[2] = _inverse_coordinate( - unit_z, 2, cart_pt, self.promol, brack_z + theta_z, 2, cart_pt, self.promol, brack_z ) self.points[counter] = cart_pt.copy() @@ -496,7 +501,7 @@ def _get_bracket(self, indices, i_var): def _transform_coordinate(real_pt, i_var, promol): r""" - Transform the `i_var` coordinate of a real point to [0, 1] using promolecular density. + Transform the `i_var` coordinate of a real point to [-1, 1] using promolecular density. Parameters ---------- @@ -509,8 +514,8 @@ def _transform_coordinate(real_pt, i_var, promol): Returns ------- - unit_pt : float - The transformed point in [0,1]^D. + theta_pt : float + The transformed point in :math:`[-1, 1]`. """ c_m, e_m, coords, pi_over_exps, dim = astuple(promol) @@ -535,7 +540,8 @@ def _transform_coordinate(real_pt, i_var, promol): transf_den = np.sum(coeff_num) transform_value = transf_num / transf_den - return transform_value + # -1. + 2. is needed to transform to [-1, 1], rather than [0, 1]. + return -1.0 + 2.0 * transform_value def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, promol): @@ -549,7 +555,7 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, promol): prev_trans_pts : list[`i_var` - 1] The previous points in real-space that were already transformed. theta_pt : float - The point in [0, 1] being transformed to the Real space. + The point in [-1, 1] being transformed to the Real space. i_var : int Index of variable being transformed. promol : _PromolParams @@ -569,12 +575,12 @@ def _root_equation(init_guess, prev_trans_pts, theta_pt, i_var, promol): def _inverse_coordinate(theta_pt, i_var, transformed, promol, bracket=(-10, 10)): r""" - Transform a point in [0, 1] to the real space corresponding to `i_var` variable. + Transform a point in [-1, 1] to the real space corresponding to `i_var` variable. Parameters ---------- theta_pt : float - Point in [0, 1]. + Point in [-1, 1]. i_var : int Index that is being tranformed. Less than D. transformed : list[`i_var` - 1] @@ -637,7 +643,7 @@ def is_same_sign(x, y): # Check's if this is a boundary points which is mapped to np.nan # These two conditions are added for individual point transformation. - if np.abs(theta_pt - 0.0) < 1e-10: + if np.abs(theta_pt - -1.0) < 1e-10: return np.nan if np.abs(theta_pt - 1.0) < 1e-10: return np.nan diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 619b2a04f..5ad239280 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -58,11 +58,13 @@ def setUp(self, ss=0.1, return_obj=False): params = _PromolParams(c, e, coord, pi_over_exponents=np.sqrt(np.pi / e), dim=3) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([(1.0 / (num_pts - 2))] * num_pts) - oned = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0,1)) + weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + oned = OneDGrid( + np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1), + ) obj = CubicProTransform( - [oned, oned, oned], params.c_m, params.e_m, params.coords + [oned, oned, oned], params.c_m, params.e_m, params.coords, ) return params, obj return params @@ -116,9 +118,10 @@ def formula_transforming_x(x): erf(3.0 ** 0.5 * (x - 2)) + 1.0 ) - return (first_factor + sec_fac) / ( + ans = (first_factor + sec_fac) / ( 5.0 * (np.pi / 2) ** 1.5 + 10.0 * (np.pi / 3.0) ** 1.5 ) + return -1.0 + 2.0 * ans true_ans = _transform_coordinate([pt], 0, self.setUp()) assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 @@ -147,7 +150,7 @@ def formula_transforming_y(x, y): dac1 = 5.0 * (np.pi / 2.0) * np.exp(-2.0 * (x - 1.0) ** 2.0) dac2 = 10.0 * (np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) den = dac1 + dac2 - return num / den + return -1.0 + 2.0 * (num / den) true_ans = _transform_coordinate([x, y], 1, self.setUp()) assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 @@ -180,7 +183,7 @@ def formula_transforming_z(x, y, z): ) den = 5.0 * (np.pi / 2.0) ** 0.5 * np.exp(-2.0 * (a1 ** 2.0 + a2 ** 2.0)) den += 10.0 * (np.pi / 3.0) ** 0.5 * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) - return (fac1 + fac2) / den + return -1.0 + 2.0 * (fac1 + fac2) / den params, obj = self.setUp(ss=0.5, return_obj=True) true_ans = formula_transforming_z(x, y, z) @@ -207,14 +210,11 @@ def test_transforming_simple_grid(self): # Test that converting the point back to unit cube gives [0.5, 0.5, 0.5]. for i_var in range(0, 3): transf = _transform_coordinate(real_pt, i_var, obj.promol) - assert np.abs(transf - 0.5) < 1e-5 + assert np.abs(transf - 0.) < 1e-5 # Test that all other points are indeed boundary points. all_nans = np.delete(obj.points, non_boundary_pt_index, axis=0) assert np.all(np.any(np.isnan(all_nans), axis=1)) - # @pytest.mark.parametrize("x", [-2, -2, 0, 2.2]) - # @pytest.mark.parametrize("y", [-3, 2., -3.2321]) - # @pytest.mark.parametrize("z", [-2., 1.5, 2.343432]) def test_transforming_with_inverse_transformation_is_identity(self): r"""Test transforming with inverse transformation is identity.""" # Note that for points far away from the promolecular gets mapped to nan. @@ -227,7 +227,7 @@ def test_transforming_with_inverse_transformation_is_identity(self): assert np.all(np.abs(reverse - pt) < 1e-10) def test_integrating_itself(self): - r"""Test integrating the very same promolecular density.""" + r"""Test integrating the very same promolecular density used for transformation.""" params, obj = self.setUp(ss=0.2, return_obj=True) promol = [] for pt in obj.points: @@ -345,13 +345,13 @@ def setUp(self, ss=0.1, return_obj=False): params = _PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([(1.0 / (num_pts - 2))] * num_pts) - oned_x = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0, 1)) - oned_y = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0, 1)) - oned_z = OneDGrid(np.linspace(0, 1, num=num_pts, endpoint=True), weights, domain=(0, 1)) + weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + oned_x = OneDGrid( + np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1) + ) obj = CubicProTransform( - [oned_x, oned_y, oned_z], params.c_m, params.e_m, params.coords + [oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords ) return params, obj return params @@ -367,12 +367,12 @@ def promolecular_in_x(grid, every_grid): return promol_x, promol_x_all true_ans = _transform_coordinate([pt], 0, self.setUp()) - grid = np.arange(-10.0, pt, 0.00001) # Integration till a x point - every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration + grid = np.arange(-4.0, pt, 0.000005) # Integration till a x point + every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_x, promol_x_all = promolecular_in_x(grid, every_grid) # Integration over y and z cancel out from numerator and denominator. - actual = np.trapz(promol_x, grid) / np.trapz(promol_x_all, every_grid) + actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz(promol_x_all, every_grid) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2, 1.23]) @@ -387,13 +387,13 @@ def promolecular_in_y(grid, every_grid): return promol_y_all, promol_y true_ans = _transform_coordinate([x, y], 1, self.setUp()) - grid = np.arange(-10.0, y, 0.00001) # Integration till a x point - every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration + grid = np.arange(-5.0, y, 0.000001) # Integration till a x point + every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_y_all, promol_y = promolecular_in_y(grid, every_grid) # Integration over z cancel out from numerator and denominator. # Further, gaussian at a point does too. - actual = np.trapz(promol_y, grid) / np.trapz(promol_y_all, every_grid) + actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz(promol_y_all, every_grid) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2]) @@ -408,11 +408,11 @@ def promolecular_in_z(grid, every_grid): promol_z_all = 5.0 * np.exp(-2.0 * (every_grid - 3.0) ** 2.0) return promol_z_all, promol_z - grid = np.arange(-10.0, z, 0.00001) # Integration till a x point - every_grid = np.arange(-10.0, 10.0, 0.00001) # Full Integration + grid = np.arange(-5.0, z, 0.00001) # Integration till a x point + every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_z_all, promol_z = promolecular_in_z(grid, every_grid) - actual = np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) + actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) true_ans = _transform_coordinate([x, y, z], 2, self.setUp()) assert np.abs(true_ans - actual) < 1e-5 From ab7cd82493d286da512a75aae98a7e8d647eb62d Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Sat, 20 Jun 2020 11:31:27 -0400 Subject: [PATCH 20/43] Add integration test for promolecular transform Since cubic grids are not supported, I tested integration with GaussChebslev oned grid. --- src/grid/protransform.py | 3 ++- src/grid/tests/test_protransform.py | 29 +++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index b837b2a7f..41f1681b1 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -121,7 +121,8 @@ class CubicProTransform(Grid): >> from grid.onedgrid import GaussChebyshev >> numb_x = 50 - >> oned = GaussChebyshev(numb_x) # Grid is the same in all x, y, z directions. + >> oned = GaussChebyshev(numb_x) + One dimensional grid is the same in all x, y, z directions. >> promol = CubicProTransform([oned, oned, oned], params.c_m, params.e_m, params.coords) To integrate some function f. diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 5ad239280..30f86d3ea 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -32,6 +32,7 @@ from grid.basegrid import OneDGrid +from grid.onedgrid import GaussChebyshevLobatto from grid.protransform import ( CubicProTransform, _PromolParams, @@ -450,13 +451,32 @@ def tranformation_z(pt): grad = approx_fprime([z], tranformation_z, 1e-8) assert np.abs(grad - actual[2, 2]) < 1e-5 - def test_integration_slightly_perturbed_gaussian_with_promolecular_trick(self): + +class TestIntegration: + r""" + Only one integration test as of this moment. Choose to make it, it's own seperate + class since many choices of oned grid is possible. + """ + def setUp_one_gaussian(self, ss=0.03): + r"""Return a one Gaussian example and its UniformProTransform object.""" + c = np.array([[5.0]]) + e = np.array([[2.0]]) + coord = np.array([[1.0, 2.0, 3.0]]) + params = _PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + num_pts = int(1 / ss) + 1 + oned_x = GaussChebyshevLobatto(num_pts) + obj = CubicProTransform( + [oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords + ) + return params, obj + + def test_integration_perturbed_gaussian_with_promolecular_trick(self): r"""Test integration of a slightly perturbed function.""" # Only Measured against one decimal place and very similar exponent. - params, obj = self.setUp(ss=0.03, return_obj=True) + _, obj = self.setUp_one_gaussian(ss=0.03) # Gaussian exponent is slightly perturbed from 2. - exponent = 2.001 + exponent = 2.01 def gaussian(grid): return 5.0 * np.exp( @@ -466,7 +486,8 @@ def gaussian(grid): func_vals = gaussian(obj.points) desired = 5.0 * np.sqrt(np.pi / exponent) ** 3.0 actual = obj.integrate(func_vals, trick=True) - assert np.abs(actual - desired) < 1e-2 + + assert np.abs(actual - desired) < 1e-3 def test_padding_arrays(): From 9e734007a91851558da3079928766a8b3f7949c4 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Fri, 26 Jun 2020 18:12:11 -0400 Subject: [PATCH 21/43] Add Hessian and tests promolecular --- src/grid/protransform.py | 416 +++++++++++++++++++--------- src/grid/tests/test_protransform.py | 138 ++++++++- 2 files changed, 423 insertions(+), 131 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 41f1681b1..b7e73557f 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -19,13 +19,11 @@ # -- r"""Promolecular Grid Transformation.""" -from dataclasses import dataclass, astuple +from dataclasses import astuple, dataclass, field from grid.basegrid import Grid, OneDGrid - import numpy as np - from scipy.linalg import solve_triangular from scipy.optimize import root_scalar from scipy.special import erf @@ -33,48 +31,6 @@ __all__ = ["CubicProTransform"] -@dataclass -class _PromolParams: - r"""Private class for Promolecular Density information.""" - c_m: np.ndarray - e_m: np.ndarray - coords: np.ndarray - pi_over_exponents: np.ndarray - dim: int = 3 - - def integrate_all(self): - r"""Integration of Gaussian over Entire Real space ie :math:`\mathbb{R}^D`.""" - return np.sum(self.c_m * self.pi_over_exponents ** self.dim) - - def promolecular(self, grid): - r""" - Evaluate the promolecular density over a grid. - - Parameters - ---------- - grid : np.ndarray(N,) - Grid points. - - Returns - ------- - np.ndarray(N,) : - Promolecular density evaluated at the grid points. - - """ - # M is the number of centers/atoms. - # D is the number of dimensions, usually 3. - # K is maximum number of gaussian functions over all M atoms. - cm, em, coords = self.c_m, self.e_m, self.coords - # Shape (N, M, D), then Summing gives (N, M, 1) - distance = np.sum((grid - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) - # At each center, multiply Each Distance of a Coordinate, with its exponents. - exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) - # At each center, multiply the exponential with its coefficients. - gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) - # At each point, sum for each center, then sum all centers together. - return np.einsum("MNK -> N", gaussian, dtype=np.float64) - - class CubicProTransform(Grid): r""" Promolecular Grid Transformation of a Cubic Grid. @@ -157,11 +113,7 @@ def __init__(self, oned_grids, coeffs, exps, coords): # pad coefficients and exponents with zeros to have the same size, easier to use numpy. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) - # Rather than computing this repeatedly. It is fixed. - with np.errstate(divide="ignore"): - pi_over_exponents = np.sqrt(np.pi / exps) - pi_over_exponents[exps == 0] = 0 - self._promol = _PromolParams(coeffs, exps, coords, pi_over_exponents, self._dim) + self._promol = _PromolParams(coeffs, exps, coords, self._dim) self._prointegral = self._promol.integrate_all() empty_pts = np.empty((np.prod(self._num_pts), self._dim), dtype=np.float64) @@ -185,7 +137,6 @@ def num_pts(self): r"""Return number of points in each direction.""" return self._num_pts - @property def prointegral(self): r"""Return integration of Promolecular density.""" @@ -193,7 +144,7 @@ def prointegral(self): @property def promol(self): - r"""Return `PromolParams` namedTuple.""" + r"""Return `PromolParams` data class.""" return self._promol def transform(self, real_pt): @@ -305,6 +256,77 @@ def integrate(self, *value_arrays, trick=False, tol=1e-10): return self.prointegral + super().integrate(*integrands) return super().integrate(*integrands) + def derivative(self, real_pt, real_derivative): + r""" + Directional derivative in theta space. + + Parameters + ---------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + real_derivative : np.ndarray(3) + Derivative of a function in real space with respect to x, y, z coordinates. + + Returns + ------- + theta_derivative : np.ndarray(3) + Derivative of a function in theta space with respect to theta coordinates. + + Notes + ----- + This does not preserve the direction of steepest-ascent/gradient. + + """ + jacobian = self.jacobian(real_pt) + return solve_triangular(jacobian.T, real_derivative) + + def steepest_ascent_theta(self, real_pt, real_grad): + r""" + Steepest ascent direction of a function in theta space. + + Steepest ascent is the gradient ie direction of maximum change of a function. + This guarantees moving in direction of steepest ascent in real-space + corresponds to moving in the direction of the gradient in theta-space. + + Parameters + ---------- + real_pt : np.ndarray(3) + Point in :math:`\mathbb{R}^3` + real_grad : np.ndarray(3) + Gradient of a function in real space. + + Returns + ------- + theta_grad : np.ndarray(3) + Gradient of a function in theta space. + + """ + jacobian = self.jacobian(real_pt) + return jacobian.dot(real_grad) + + def differentiation_interpolation(self, real_pt, func_val, use_log=False): + r""" + Differentiate a point in Real-space using interpolation. + + Parameters + ---------- + real_pt : ndarray(3,) + + func_val : float + + use_log : bool + + """ + # Map to theta space. + + # Construct Cubic Splines and differentiate. + + # Convert back to Real-Space. + pass + + def integration_interpolation(self): + pass + def jacobian(self, real_pt): r""" Jacobian of the transformation from real space to theta space. @@ -314,8 +336,8 @@ def jacobian(self, real_pt): \begin{bmatrix} \frac{\partial \theta_x}{\partial X} & 0 & 0 \\ \frac{\partial \theta_y}{\partial X} & \frac{\partial \theta_y}{\partial Y} & 0 \\ - \frac{\partial \theta_z}{\partial X} & \frac{\partial \theta_Z}{\partial Y} & - \frac{\partial \theta_Z}{\partial Z} + \frac{\partial \theta_z}{\partial X} & \frac{\partial \theta_z}{\partial Y} & + \frac{\partial \theta_z}{\partial Z} \end{bmatrix}. Parameters @@ -333,46 +355,39 @@ def jacobian(self, real_pt): c_m, e_m, coords, pi_over_exps, dim = astuple(self.promol) - # Code is duplicated from `transform_coordinate` due to effiency reasons. - # TODO: Reduce the number of computation with `i_var`. + # Distance to centers/nuclei`s and Prefactors. + diff_coords = real_pt - coords + diff_squared = diff_coords ** 2.0 + # If i_var is zero, then distance is just all zeros. for i_var in range(0, self.promol.dim): - - # Distance to centers/nuclei`s and Prefactors. - diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] - diff_squared = diff_coords ** 2.0 distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] - # If i_var is zero, then distance is just all zeros. # Gaussian Integrals Over Entire Space For Numerator and Denomator. - coeff_num = c_m * np.exp(-e_m * distance) * pi_over_exps ** (dim - i_var) + single_gauss = self.promol.single_gaussians(distance) + single_gauss *= pi_over_exps ** (dim - i_var - 1) # Get integral of Gaussian till a point. - coord_ivar = diff_coords[:, i_var][:, np.newaxis] - # (pi / exponent)^0.5 is factored and absorbed in `coeff_num`. - integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.0) / 2.0 - - # Final Result. - transf_num = np.sum(coeff_num * integrate_till_pt_x) - transf_den = np.sum(coeff_num) + integrate_till_pt_x = self.promol.integration_gaussian_till_point(diff_coords, + i_var, + with_factor=True) + # Numerator and Denominator of Original Transformation. + transf_num = np.sum(single_gauss * integrate_till_pt_x) + transf_den = np.sum(single_gauss * pi_over_exps) for j_deriv in range(0, i_var + 1): if i_var == j_deriv: # Derivative eliminates `integrate_till_pt_x`, and adds a Gaussian. - inner_term = coeff_num * np.exp( + inner_term = single_gauss * np.exp( -e_m * diff_squared[:, i_var][:, np.newaxis] ) - # Needed because coeff_num has additional (pi / exponent)^0.5 term. - inner_term /= pi_over_exps jacobian[i_var, i_var] = np.sum(inner_term) / transf_den elif j_deriv < i_var: # Derivative of inside of Gaussian. - deriv_quadratic = ( - -e_m * 2.0 * diff_coords[:, j_deriv][:, np.newaxis] - ) + deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) deriv_num = np.sum( - coeff_num * integrate_till_pt_x * deriv_quadratic + single_gauss * integrate_till_pt_x * deriv_inside ) - deriv_den = np.sum(coeff_num * deriv_quadratic) + deriv_den = np.sum(single_gauss * deriv_inside * pi_over_exps) # Quotient Rule jacobian[i_var, j_deriv] = ( deriv_num * transf_den - transf_num * deriv_den @@ -381,55 +396,122 @@ def jacobian(self, real_pt): return 2.0 * jacobian - def derivative(self, real_pt, real_derivative): - r""" - Directional derivative in theta space. - - Parameters - ---------- - real_pt : np.ndarray(3) - Point in :math:`\mathbb{R}^3` - real_derivative : np.ndarray(3) - Derivative of a function in real space with respect to x, y, z coordinates. - - Returns - ------- - theta_derivative : np.ndarray(3) - Derivative of a function in theta space with respect to theta coordinates. + def hessian(self, real_pt): + hessian = np.zeros((self.dim, self.dim, self.dim), dtype=np.float64) - Notes - ----- - This does not preserve the direction of steepest-ascent/gradient. - - """ - jacobian = self.jacobian(real_pt) - return solve_triangular(jacobian.T, real_derivative) + c_m, e_m, coords, pi_over_exps, dim = astuple(self.promol) - def steepest_ascent_theta(self, real_pt, real_grad): - r""" - Steepest ascent direction of a function in theta space. + # Distance to centers/nuclei`s and Prefactors. + diff_coords = real_pt - coords + diff_squared = diff_coords ** 2.0 - Steepest ascent is the gradient ie direction of maximum change of a function. - This guarantees moving in direction of steepest ascent in real-space - corresponds to moving in the direction of the gradient in theta-space. - - Parameters - ---------- - real_pt : np.ndarray(3) - Point in :math:`\mathbb{R}^3` - real_grad : np.ndarray(3) - Gradient of a function in real space. + # i_var is the transformation to theta-space. + # j_deriv is the first partial derivative wrt x, y, z. + # k_deriv is the second partial derivative wrt x, y, z. + for i_var in range(0, 3): + distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] - Returns - ------- - theta_grad : np.ndarray(3) - Gradient of a function in theta space. + # Gaussian Integrals Over Entire Space For Numerator and Denomator. + single_gauss = self.promol.single_gaussians(distance) + single_gauss *= pi_over_exps ** (dim - i_var - 1) - """ - jacobian = self.jacobian(real_pt) - return jacobian.dot(real_grad) + # Get integral of Gaussian till a point. + integrate_till_pt_x = self.promol.integration_gaussian_till_point(diff_coords, + i_var, + with_factor=True) + # Numerator and Denominator of Original Transformation. + transf_num = np.sum(single_gauss * integrate_till_pt_x) + transf_den = np.sum(single_gauss * pi_over_exps) + for j_deriv in range(0, i_var + 1): + for k_deriv in range(0, i_var + 1): + derivative = 0. + + if i_var == j_deriv: + gauss_extra = single_gauss * np.exp( + -e_m * diff_squared[:, j_deriv][:, np.newaxis] + ) + if j_deriv == k_deriv: + # Diagonal derivative e.g. d(theta_X)(dx dx) + gauss_extra *= self.promol.derivative_gaussian(diff_coords, j_deriv) + derivative = np.sum(gauss_extra) / transf_den + else: + # Partial derivative of diagonal derivative e.g. d^2(theta_y)(dy dx). + deriv_inside = self.promol.derivative_gaussian(diff_coords, k_deriv) + deriv_num = np.sum( + gauss_extra * deriv_inside + ) + deriv_den = np.sum(single_gauss * deriv_inside * pi_over_exps) + # Numerator is different from `transf_num` since Gaussian is added. + new_numerator = np.sum(gauss_extra) + # Quotient Rule + derivative = ( + deriv_num * transf_den - new_numerator * deriv_den + ) + derivative /= transf_den ** 2.0 + + # Here, quotient rule all the way down. + elif j_deriv < i_var: + if k_deriv == i_var: + gauss_extra = single_gauss * np.exp( + -e_m * diff_squared[:, k_deriv][:, np.newaxis] + ) + + deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) + ddnum_djdi = np.sum( + gauss_extra * deriv_inside + ) + + dden_dj = np.sum(single_gauss * deriv_inside * pi_over_exps) + # Quotient Rule + dnum_dj = np.sum(gauss_extra) + derivative = ddnum_djdi * transf_den - dnum_dj * dden_dj + derivative /= transf_den ** 2.0 + + elif k_deriv == j_deriv: + # Double Quotient Rule. + # See wikipedia "Quotient Rules Higher order formulas". + deriv_inside = self.promol.derivative_gaussian(diff_coords, k_deriv) + dnum_dj = np.sum(single_gauss * integrate_till_pt_x * deriv_inside) + dden_dj = np.sum(single_gauss * pi_over_exps * deriv_inside) + + prod_rule = deriv_inside ** 2.0 - 2.0 * e_m + sec_deriv_num = np.sum(single_gauss * integrate_till_pt_x * prod_rule) + sec_deriv_den = np.sum(single_gauss * pi_over_exps * prod_rule) + + output = (sec_deriv_num * transf_den - dnum_dj * dden_dj) + output /= transf_den ** 2.0 + quot = transf_den * (dnum_dj * dden_dj + transf_num * sec_deriv_den) + quot -= 2.0 * transf_num * dden_dj * dden_dj + derivative = output - quot / transf_den ** 3.0 + + elif k_deriv != j_deriv: + # K is i_Sec_diff and i is i_diff + deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) + deriv_inside_sec = self.promol.derivative_gaussian(diff_coords, k_deriv) + + dnum_di = np.sum(single_gauss * integrate_till_pt_x * deriv_inside) + dden_di = np.sum(single_gauss * pi_over_exps * deriv_inside) + + dnum_dk = np.sum(single_gauss * integrate_till_pt_x * deriv_inside_sec) + dden_dk = np.sum(single_gauss * pi_over_exps * deriv_inside_sec) + + ddnum_dkdk = np.sum(single_gauss * deriv_inside * deriv_inside_sec * integrate_till_pt_x) + ddden_dkdk = np.sum(single_gauss * deriv_inside * deriv_inside_sec * pi_over_exps) + + output = ddnum_dkdk / transf_den + output -= (dnum_di * dden_dk / transf_den ** 2.0) + product = dnum_dk * dden_di + transf_num * ddden_dkdk + derivative = output + derivative -= product * transf_den / transf_den ** 3. + derivative += 2.0 * transf_num * dden_di * dden_dk / transf_den ** 3. + + # The 2.0 is needed because we're in [-1, 1] rather than [0, 1]. + hessian[i_var, j_deriv, k_deriv] = 2.0 * derivative + + return hessian def _transform(self, oned_grids): + # Transform the entire grid. # Indices (i, j, k) start from bottom, left-most corner of the [-1, 1]^3 cube. counter = 0 for ix in range(self.num_pts[0]): @@ -500,6 +582,85 @@ def _get_bracket(self, indices, i_var): return self.points[index, i_var], self.points[index, i_var] + 10.0 +@dataclass +class _PromolParams: + r""" + Private class for Promolecular Density information. + + Contains helper-functions for Promolecular Transformation. + They are coded as pipe-lines for this special purpose and + the reason why "diff_coords" is chosen as a attribute rather + than a generic "[x, y, z]" point. + + """ + c_m: np.ndarray # Coefficients of Promolecular. + e_m: np.ndarray # Exponents of Promolecular. + coords: np.ndarray # Centers/Coordinates of Each Gaussian. + pi_over_exponents: np.ndarray = field(init=False) + dim: int = 3 + + def __post_init__(self): + r"""Initialize pi_over_exponents.""" + # Rather than computing this repeatedly. It is fixed. + with np.errstate(divide="ignore"): + self.pi_over_exponents = np.sqrt(np.pi / self.e_m) + self.pi_over_exponents[self.e_m == 0.0] = 0.0 + + def integrate_all(self): + r"""Integration of Gaussian over Entire Real space ie :math:`\mathbb{R}^D`.""" + return np.sum(self.c_m * self.pi_over_exponents ** self.dim) + + def integrate_all_certain_variables(self): + r"""""" + return self.pi_over_exponents ** () + + def derivative_gaussian(self, diff_coords, j_deriv): + r"""Derivative of single Gaussian but without exponential.""" + return -self.e_m * 2.0 * diff_coords[:, j_deriv][:, np.newaxis] + + def integration_gaussian_till_point(self, diff_coords, i_var, with_factor=False): + r"""Integration of Gaussian wrt to `i_var` variable till a point (inside diff_coords).""" + coord_ivar = diff_coords[:, i_var][:, np.newaxis] + integration = (erf(np.sqrt(self.e_m) * coord_ivar) + 1.0) / 2.0 + if with_factor: + # Included the (pi / exponents), this is the actual integral here. + # Not including the (pi / exponents) increasing computation slightly faster. + return integration * self.pi_over_exponents + return integration + + def single_gaussians(self, distance): + r"""Return matrix with entries a single gaussian evaluated at the float distance.""" + return self.c_m * np.exp(-self.e_m * distance) + + def promolecular(self, points): + r""" + Evaluate the promolecular density over a grid. + + Parameters + ---------- + points : np.ndarray(N, D) + Points in :math:`\mathbb{R}^D`. + + Returns + ------- + np.ndarray(N,) : + Promolecular density evaluated at the points. + + """ + # M is the number of centers/atoms. + # D is the number of dimensions, usually 3. + # K is maximum number of gaussian functions over all M atoms. + cm, em, coords = self.c_m, self.e_m, self.coords + # Shape (N, M, D), then Summing gives (N, M, 1) + distance = np.sum((points - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) + # At each center, multiply Each Distance of a Coordinate, with its exponents. + exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) + # At each center, multiply the exponential with its coefficients. + gaussian = np.einsum("MNK, MK -> MNK", exponen, cm) + # At each point, sum for each center, then sum all centers together. + return np.einsum("MNK -> N", gaussian, dtype=np.float64) + + def _transform_coordinate(real_pt, i_var, promol): r""" Transform the `i_var` coordinate of a real point to [-1, 1] using promolecular density. @@ -519,7 +680,7 @@ def _transform_coordinate(real_pt, i_var, promol): The transformed point in :math:`[-1, 1]`. """ - c_m, e_m, coords, pi_over_exps, dim = astuple(promol) + _, _, coords, pi_over_exps, dim = astuple(promol) # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] @@ -527,18 +688,17 @@ def _transform_coordinate(real_pt, i_var, promol): distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] # If i_var is zero, then distance is just all zeros. - # Gaussian Integrals Over Entire Space For Numerator and Denomator. - gaussian_integrals = np.exp(-e_m * distance) * pi_over_exps ** (dim - i_var) - coeff_num = c_m * gaussian_integrals + # Single Gaussians Including Integration of Exponential over `(dim - i_var)` variables. + single_gauss = promol.single_gaussians(distance) * pi_over_exps ** (dim - i_var) # Get the integral of Gaussian till a point excluding a prefactor. - # This prefactor (pi / exponents) is included in `gaussian_integrals`. - coord_ivar = diff_coords[:, i_var][:, np.newaxis] - integrate_till_pt_x = (erf(np.sqrt(e_m) * coord_ivar) + 1.0) / 2.0 + # prefactor (pi / exponents) is included in `gaussian_integrals`. + cdf_gauss = promol.integration_gaussian_till_point(diff_coords, i_var, + with_factor=False) # Final Result. - transf_num = np.sum(coeff_num * integrate_till_pt_x) - transf_den = np.sum(coeff_num) + transf_num = np.sum(single_gauss * cdf_gauss) + transf_den = np.sum(single_gauss) transform_value = transf_num / transf_den # -1. + 2. is needed to transform to [-1, 1], rather than [0, 1]. diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 30f86d3ea..b021b1606 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -56,7 +56,7 @@ def setUp(self, ss=0.1, return_obj=False): c = np.array([[5.0], [10.0]]) e = np.array([[2.0], [3.0]]) coord = np.array([[1.0, 2.0, 3.0], [2.0, 2.0, 2.0]]) - params = _PromolParams(c, e, coord, pi_over_exponents=np.sqrt(np.pi / e), dim=3) + params = _PromolParams(c, e, coord, dim=3) if return_obj: num_pts = int(1 / ss) + 1 weights = np.array([(2.0 / (num_pts - 2))] * num_pts) @@ -334,6 +334,138 @@ def grad(pt): actual = obj.steepest_ascent_theta(pt, grad_pt) assert np.all(np.abs(actual - grad_finite) < 1e-4) + def test_second_derivative_of_theta_x_dx(self): + r"""Test the second derivative of d(theta_x)/d(x) .""" + params, obj = self.setUp(ss=0.2, return_obj=True) + x, y, z = 0., 0., 0. + actual = obj.hessian(np.array([x, y, z])) + + # test second derivative of theta_x wrt to dx dx. + def tranformation_x(pt): + return obj.jacobian([pt[0], y, z])[0, 0] + + dthetax_dx_dx = approx_fprime([x], tranformation_x, 1e-8) + assert np.abs(dthetax_dx_dx - actual[0, 0, 0]) < 1e-5 + # Since theta_x only depends on x, then all other elements + # are zero. + for i in range(0, 3): + for j in range(0, 3): + if i != j: + assert np.abs(actual[0, i, j]) < 1e-10 + + @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) + @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) + @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) + def test_second_derivative_of_theta_y(self, x, y, z): + r"""Test the second derivative of d(theta_y).""" + params, obj = self.setUp(ss=0.2, return_obj=True) + actual = obj.hessian(np.array([x, y, z])) + + # test second derivative of theta_y wrt to dy dy. + def dtheta_y_dy(pt): + return obj.jacobian([x, pt[0], z])[1, 1] + + dthetay_dy_dy = approx_fprime([y], dtheta_y_dy, 1e-8) + assert np.abs(dthetay_dy_dy - actual[1, 1, 1]) < 1e-5 + + # test second derivative of d(theta_y)/d(y) wrt dx. + def dtheta_y_dy(pt): + return obj.jacobian([pt[0], y, z])[1, 1] + + dthetay_dy_dx = approx_fprime([x], dtheta_y_dy, 1e-8) + assert np.abs(dthetay_dy_dx - actual[1, 1, 0]) < 1e-5 + + # test second derivative of d(theta_y)/(dx) wrt dy + def dtheta_y_dx(pt): + return obj.jacobian([x, pt[0], z])[1, 0] + + dtheta_y_dx_dy = approx_fprime([y], dtheta_y_dx, 1e-8) + assert np.abs(dtheta_y_dx_dy - actual[1, 0, 1]) < 1e-4 + + # test second derivative of d(theta_y)/(dx) wrt dx + def dtheta_y_dx(pt): + return obj.jacobian([pt[0], y, z])[1, 0] + + dtheta_y_dx_dx = approx_fprime([x], dtheta_y_dx, 1e-8) + assert np.abs(dtheta_y_dx_dx - actual[1, 0, 0]) < 1e-5 + + # Test other elements are all zeros. + for i in range(0, 3): + assert np.abs(actual[1, 2, i]) < 1e-10 + assert np.abs(actual[1, 0, 2]) < 1e-10 + assert np.abs(actual[1, 1, 2]) < 1e-10 + + @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) + @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) + @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) + def test_second_derivative_of_theta_z(self, x, y, z): + r"""Test the second derivative of d(theta_z).""" + params, obj = self.setUp(ss=0.2, return_obj=True) + actual = obj.hessian(np.array([x, y, z])) + + # test second derivative of theta_z wrt to dz dz. + def dtheta_z_dz(pt): + return obj.jacobian([x, y, pt[0]])[2, 2] + + dthetaz_dz_dz = approx_fprime([z], dtheta_z_dz, 1e-8) + assert np.abs(dthetaz_dz_dz - actual[2, 2, 2]) < 1e-5 + + # test second derivative of theta_z wrt to dz dy. + def dtheta_z_dz(pt): + return obj.jacobian([x, pt[0], z])[2, 2] + + dthetaz_dz_dy = approx_fprime([y], dtheta_z_dz, 1e-8) + assert np.abs(dthetaz_dz_dy - actual[2, 2, 1]) < 1e-5 + + # test second derivative of theta_z wrt to dz dx. + def dtheta_z_dz(pt): + return obj.jacobian([pt[0], y, z])[2, 2] + + dthetaz_dz_dx = approx_fprime([x], dtheta_z_dz, 1e-8) + assert np.abs(dthetaz_dz_dx - actual[2, 2, 0]) < 1e-5 + + # test second derivative of theta_z wrt to dy dz. + def dtheta_z_dy(pt): + return obj.jacobian([x, y, pt[0]])[2, 1] + + dthetaz_dy_dz = approx_fprime([z], dtheta_z_dy, 1e-8) + assert np.abs(dthetaz_dy_dz - actual[2, 1, 2]) < 1e-5 + + # test second derivative of theta_z wrt to dy dy. + def dtheta_z_dy(pt): + return obj.jacobian([x, pt[0], z])[2, 1] + + dthetaz_dy_dy = approx_fprime([y], dtheta_z_dy, 1e-8) + assert np.abs(dthetaz_dy_dy - actual[2, 1, 1]) < 1e-5 + + # test second derivative of theta_z wrt to dy dx. + def dtheta_z_dy(pt): + return obj.jacobian([pt[0], y, z])[2, 1] + + dthetaz_dy_dx = approx_fprime([x], dtheta_z_dy, 1e-8) + assert np.abs(dthetaz_dy_dx - actual[2, 1, 0]) < 1e-5 + + # test second derivative of theta_z wrt to dx dz. + def dtheta_z_dx(pt): + return obj.jacobian([x, y, pt[0]])[2, 0] + + dthetaz_dx_dz = approx_fprime([z], dtheta_z_dx, 1e-8) + assert np.abs(dthetaz_dx_dz - actual[2, 0, 2]) < 1e-5 + + # test second derivative of theta_z wrt to dx dy. + def dtheta_z_dx(pt): + return obj.jacobian([x, pt[0], z])[2, 0] + + dthetaz_dx_dy = approx_fprime([y], dtheta_z_dx, 1e-8) + assert np.abs(dthetaz_dx_dy - actual[2, 0, 1]) < 1e-5 + + # test second derivative of theta_z wrt to dx dx. + def dtheta_z_dx(pt): + return obj.jacobian([pt[0], y, z])[2, 0] + + dthetaz_dx_dx = approx_fprime([x], dtheta_z_dx, 1e-8) + assert np.abs(dthetaz_dx_dx - actual[2, 0, 0]) < 1e-5 + class TestOneGaussianAgainstNumerics: r"""Tests With Numerical Integration of a One Gaussian function.""" @@ -343,7 +475,7 @@ def setUp(self, ss=0.1, return_obj=False): c = np.array([[5.0]]) e = np.array([[2.0]]) coord = np.array([[1.0, 2.0, 3.0]]) - params = _PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + params = _PromolParams(c, e, coord, dim=3) if return_obj: num_pts = int(1 / ss) + 1 weights = np.array([(2.0 / (num_pts - 2))] * num_pts) @@ -462,7 +594,7 @@ def setUp_one_gaussian(self, ss=0.03): c = np.array([[5.0]]) e = np.array([[2.0]]) coord = np.array([[1.0, 2.0, 3.0]]) - params = _PromolParams(c, e, coord, dim=3, pi_over_exponents=np.sqrt(np.pi / e)) + params = _PromolParams(c, e, coord, dim=3) num_pts = int(1 / ss) + 1 oned_x = GaussChebyshevLobatto(num_pts) obj = CubicProTransform( From 8727c0ac46bdfb972613f7df5e450af387a50fc2 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 6 Jul 2020 16:33:22 -0400 Subject: [PATCH 22/43] Add hessian and interpolation to grid transform Added - convert index to indices to grid transform - convert indices to index to grid transform - interpolation and its derivative - hessian --- src/grid/protransform.py | 454 +++++++++++++++++++++++++++++---------- 1 file changed, 339 insertions(+), 115 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index b7e73557f..e27ce478c 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -24,6 +24,8 @@ from grid.basegrid import Grid, OneDGrid import numpy as np + +from scipy.interpolate import CubicSpline from scipy.linalg import solve_triangular from scipy.optimize import root_scalar from scipy.special import erf @@ -43,15 +45,14 @@ class CubicProTransform(Grid): ---------- num_pts : (int, int, int) The number of points, including both of the end/boundary points, in x, y, and z direction. - This is calculated as `int(1. / ss[i]) + 1`. - points : np.ndarray(N, 3) - The transformed points in real space. prointegral : float The integration value of the promolecular density over Euclidean space. - weights : np.ndarray(N,) - The weights multiplied by `prointegral`. promol : namedTuple Data about the Promolecular density. + points : np.ndarray(N, 3) + The transformed points in real space. + weights : np.ndarray(N,) + The weights multiplied by `prointegral`. Methods ------- @@ -59,12 +60,16 @@ class CubicProTransform(Grid): Integral of a real-valued function over Euclidean space. jacobian() Jacobian of the transformation from Real space to Theta space :math:`[-1, 1]^3`. + hessian() + Hessian of the transformation from Real space to Theta space :math:`[-1, 1]^3`. steepest_ascent_theta() Direction of steepest-ascent of a function in theta space from gradient in real space. transform(): Transform Real point to theta point :math:`[-1, 1]^3`. inverse(bracket=(-10, 10)) Transform theta point to Real space :math:`\mathbb{R}^3`. + interpolate_function(use_log=False, nu=0) + Interpolate a function (or its derivative) at a real point. Examples -------- @@ -98,15 +103,18 @@ class CubicProTransform(Grid): TODO: Add Infor about how boundarys on theta-space are mapped to np.nan. """ + def __init__(self, oned_grids, coeffs, exps, coords): if not isinstance(oned_grids, list): raise TypeError("oned_grid should be of type list.") if not np.all([isinstance(grid, OneDGrid) for grid in oned_grids]): raise TypeError("Grid in oned_grids should be of type `OneDGrid`.") - if not np.all([grid.domain == (-1, 1.) for grid in oned_grids]): + if not np.all([grid.domain == (-1, 1) for grid in oned_grids]): raise ValueError("One Dimensional grid domain should be (-1, 1).") if not len(oned_grids) == 3: - raise ValueError("There should be three One-Dimensional grids in `oned_grids`.") + raise ValueError( + "There should be three One-Dimensional grids in `oned_grids`." + ) self._num_pts = tuple([grid.size for grid in oned_grids]) self._dim = len(oned_grids) @@ -118,20 +126,14 @@ def __init__(self, oned_grids, coeffs, exps, coords): empty_pts = np.empty((np.prod(self._num_pts), self._dim), dtype=np.float64) weights = np.kron( - np.kron(oned_grids[0].weights, oned_grids[1].weights), - oned_grids[2].weights + np.kron(oned_grids[0].weights, oned_grids[1].weights), oned_grids[2].weights ) # The prointegral is needed because of promolecular integration. # Divide by 8 needed because the grid is in [-1, 1] rather than [0, 1]. - super().__init__(empty_pts, weights * self._prointegral / 2.0**self._dim) + super().__init__(empty_pts, weights * self._prointegral / 2.0 ** self._dim) # Transform Cubic Grid in Theta-Space to Real-space. self._transform(oned_grids) - @property - def dim(self): - r"""Return the dimension of the cubic grid.""" - return self._dim - @property def num_pts(self): r"""Return number of points in each direction.""" @@ -147,6 +149,11 @@ def promol(self): r"""Return `PromolParams` data class.""" return self._promol + @property + def dim(self): + r"""Return the dimension of the cubic grid.""" + return self._dim + def transform(self, real_pt): r""" Transform a real point in three-dimensional Reals to theta space. @@ -163,8 +170,10 @@ def transform(self, real_pt): """ return np.array( - [_transform_coordinate(real_pt, i, self.promol) - for i in range(0, self.promol.dim)] + [ + _transform_coordinate(real_pt, i, self.promol) + for i in range(0, self.promol.dim) + ] ) def inverse(self, theta_pt, bracket=(-10, 10)): @@ -304,28 +313,90 @@ def steepest_ascent_theta(self, real_pt, real_grad): jacobian = self.jacobian(real_pt) return jacobian.dot(real_grad) - def differentiation_interpolation(self, real_pt, func_val, use_log=False): + def interpolate_function( + self, real_pt, func_values, oned_grids, use_log=False, nu=0 + ): + # TODO: Should oned_grids be stored as class attribute when only this method requires it. r""" - Differentiate a point in Real-space using interpolation. + Interpolate function at a point. Parameters ---------- - real_pt : ndarray(3,) - - func_val : float - + real_pt : np.ndarray(3,) + Point in :math:`\mathbb{R}^3` that needs to be interpolated. + func_values : np.ndarray(N,) + Function values at each point of the grid `points`. + oned_grids = list(3,) + List Containing Three One-Dimensional grid corresponding to x, y, z direction. use_log : bool + If true, then logarithm is applied before interpolating to the function values, + including `func_values`. + + Returns + ------- + float : + If nu is 0: Returns the interpolated of a function at a real point. + If nu is 1: Returns the interpolated derivative of a function at a real point. """ + # TODO: Ask about use_log and derivative. # Map to theta space. + theta_pt = self.transform(real_pt) - # Construct Cubic Splines and differentiate. + if use_log: + func_values = np.log(func_values) - # Convert back to Real-Space. - pass + jac = self.jacobian(real_pt).T - def integration_interpolation(self): - pass + # Interpolate the Z-Axis based on x, y coordinates in grid. + def z_spline(z, x_index, y_index): + # x_index, y_index is assumed to be in the grid while z is not assumed. + # Get smallest and largest index for selecting func vals on this specific z-slice. + # The `1` and `self.num_puts[2] - 2` is needed because I don't want the boundary. + small_index = self._indices_to_index((x_index, y_index, 1)) + large_index = self._indices_to_index( + (x_index, y_index, self.num_pts[2] - 2) + ) + val = CubicSpline( + oned_grids[2].points[1 : self.num_pts[2] - 2], + func_values[small_index:large_index], + )(z, nu) + + if nu == 1: + # Derivative in real-space with respect to z. + return (jac[:, 2] * val)[2] + return val + + # Interpolate the Y-Axis based on x coordinate in grid. + def y_splines(y, x_index, z): + # The `1` and `self.num_puts[1] - 2` is needed because I don't want the boundary. + # Assumes x_index is in the grid while y, z may not be. + val = CubicSpline( + oned_grids[1].points[1 : self.num_pts[2] - 2], + [ + z_spline(z, x_index, y_index) + for y_index in range(1, self.num_pts[1] - 2) + ], + )(y, nu) + if nu == 1: + # Derivative in real-space with respect to y. + return (jac[:, 1] * val)[1] + return val + + # Interpolate the X-Axis. + def x_spline(x, y, z): + # x, y, z may not be in the grid. + val = CubicSpline( + oned_grids[0].points[1 : self.num_pts[2] - 2], + [y_splines(y, x_index, z) for x_index in range(1, self.num_pts[0] - 2)], + )(x, nu) + if nu == 1: + # Derivative in real-space with respect to x. + return (jac[:, 0] * val)[0] + return val + + interpolated = x_spline(theta_pt[0], theta_pt[1], theta_pt[2]) + return interpolated def jacobian(self, real_pt): r""" @@ -360,19 +431,15 @@ def jacobian(self, real_pt): diff_squared = diff_coords ** 2.0 # If i_var is zero, then distance is just all zeros. for i_var in range(0, self.promol.dim): - distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] - # Gaussian Integrals Over Entire Space For Numerator and Denomator. - single_gauss = self.promol.single_gaussians(distance) - single_gauss *= pi_over_exps ** (dim - i_var - 1) - - # Get integral of Gaussian till a point. - integrate_till_pt_x = self.promol.integration_gaussian_till_point(diff_coords, - i_var, - with_factor=True) - # Numerator and Denominator of Original Transformation. - transf_num = np.sum(single_gauss * integrate_till_pt_x) - transf_den = np.sum(single_gauss * pi_over_exps) + # Basic-Level arrays for integration and derivatives. + ( + distance, + single_gauss, + integrate_till_pt_x, + transf_num, + transf_den, + ) = self.promol.helper_for_derivatives(diff_squared, diff_coords, i_var) for j_deriv in range(0, i_var + 1): if i_var == j_deriv: @@ -390,13 +457,29 @@ def jacobian(self, real_pt): deriv_den = np.sum(single_gauss * deriv_inside * pi_over_exps) # Quotient Rule jacobian[i_var, j_deriv] = ( - deriv_num * transf_den - transf_num * deriv_den + deriv_num * transf_den - transf_num * deriv_den ) jacobian[i_var, j_deriv] /= transf_den ** 2.0 return 2.0 * jacobian def hessian(self, real_pt): + r""" + Hessian of the transformation. + + Parameters + ---------- + real_pt : np.ndarray(3,) + Real point in :math:`\mathbb{R}^3`. + + Returns + ------- + hessian : np.ndarray(3, 3, 3) + The (i, j, k)th entry is the partial derivative of the ith transformation function + with respect to the jth, kth coordinate. e.g. when i = 0, then hessian entry at + (i, j, k) is zero unless j = k = 0. + + """ hessian = np.zeros((self.dim, self.dim, self.dim), dtype=np.float64) c_m, e_m, coords, pi_over_exps, dim = astuple(self.promol) @@ -409,22 +492,24 @@ def hessian(self, real_pt): # j_deriv is the first partial derivative wrt x, y, z. # k_deriv is the second partial derivative wrt x, y, z. for i_var in range(0, 3): - distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] - - # Gaussian Integrals Over Entire Space For Numerator and Denomator. - single_gauss = self.promol.single_gaussians(distance) - single_gauss *= pi_over_exps ** (dim - i_var - 1) - - # Get integral of Gaussian till a point. - integrate_till_pt_x = self.promol.integration_gaussian_till_point(diff_coords, - i_var, - with_factor=True) - # Numerator and Denominator of Original Transformation. - transf_num = np.sum(single_gauss * integrate_till_pt_x) - transf_den = np.sum(single_gauss * pi_over_exps) + + # Basic-Level arrays for integration and derivatives. + ( + distance, + single_gauss, + integrate_till_pt_x, + transf_num, + transf_den, + ) = self.promol.helper_for_derivatives(diff_squared, diff_coords, i_var) + for j_deriv in range(0, i_var + 1): for k_deriv in range(0, i_var + 1): - derivative = 0. + # num is the numerator of transformation function. + # den is the denominator of transformation function. + # dnum_dk is the derivative of numerator wrt to k_deriv. + # dnum_dkdj is the derivative of num wrt to j_deriv then k_deriv. + # The derivative will store the result and pass it to the Hessian. + derivative = 0.0 if i_var == j_deriv: gauss_extra = single_gauss * np.exp( @@ -432,21 +517,21 @@ def hessian(self, real_pt): ) if j_deriv == k_deriv: # Diagonal derivative e.g. d(theta_X)(dx dx) - gauss_extra *= self.promol.derivative_gaussian(diff_coords, j_deriv) + gauss_extra *= self.promol.derivative_gaussian( + diff_coords, j_deriv + ) derivative = np.sum(gauss_extra) / transf_den else: # Partial derivative of diagonal derivative e.g. d^2(theta_y)(dy dx). - deriv_inside = self.promol.derivative_gaussian(diff_coords, k_deriv) - deriv_num = np.sum( - gauss_extra * deriv_inside + deriv_inside = self.promol.derivative_gaussian( + diff_coords, k_deriv ) - deriv_den = np.sum(single_gauss * deriv_inside * pi_over_exps) + dnum_dkdj = np.sum(gauss_extra * deriv_inside) + dden_dk = np.sum(single_gauss * deriv_inside * pi_over_exps) # Numerator is different from `transf_num` since Gaussian is added. - new_numerator = np.sum(gauss_extra) + dnum_dj = np.sum(gauss_extra) # Quotient Rule - derivative = ( - deriv_num * transf_den - new_numerator * deriv_den - ) + derivative = dnum_dkdj * transf_den - dnum_dj * dden_dk derivative /= transf_den ** 2.0 # Here, quotient rule all the way down. @@ -455,12 +540,10 @@ def hessian(self, real_pt): gauss_extra = single_gauss * np.exp( -e_m * diff_squared[:, k_deriv][:, np.newaxis] ) - - deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) - ddnum_djdi = np.sum( - gauss_extra * deriv_inside + deriv_inside = self.promol.derivative_gaussian( + diff_coords, j_deriv ) - + ddnum_djdi = np.sum(gauss_extra * deriv_inside) dden_dj = np.sum(single_gauss * deriv_inside * pi_over_exps) # Quotient Rule dnum_dj = np.sum(gauss_extra) @@ -470,40 +553,62 @@ def hessian(self, real_pt): elif k_deriv == j_deriv: # Double Quotient Rule. # See wikipedia "Quotient Rules Higher order formulas". - deriv_inside = self.promol.derivative_gaussian(diff_coords, k_deriv) - dnum_dj = np.sum(single_gauss * integrate_till_pt_x * deriv_inside) + deriv_inside = self.promol.derivative_gaussian( + diff_coords, k_deriv + ) + dnum_dj = np.sum( + single_gauss * integrate_till_pt_x * deriv_inside + ) dden_dj = np.sum(single_gauss * pi_over_exps * deriv_inside) prod_rule = deriv_inside ** 2.0 - 2.0 * e_m - sec_deriv_num = np.sum(single_gauss * integrate_till_pt_x * prod_rule) - sec_deriv_den = np.sum(single_gauss * pi_over_exps * prod_rule) + sec_deriv_num = np.sum( + single_gauss * integrate_till_pt_x * prod_rule + ) + sec_deriv_den = np.sum( + single_gauss * pi_over_exps * prod_rule + ) - output = (sec_deriv_num * transf_den - dnum_dj * dden_dj) + output = sec_deriv_num * transf_den - dnum_dj * dden_dj output /= transf_den ** 2.0 - quot = transf_den * (dnum_dj * dden_dj + transf_num * sec_deriv_den) + quot = transf_den * ( + dnum_dj * dden_dj + transf_num * sec_deriv_den + ) quot -= 2.0 * transf_num * dden_dj * dden_dj derivative = output - quot / transf_den ** 3.0 elif k_deriv != j_deriv: # K is i_Sec_diff and i is i_diff - deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) - deriv_inside_sec = self.promol.derivative_gaussian(diff_coords, k_deriv) + deriv_inside = self.promol.derivative_gaussian( + diff_coords, j_deriv + ) + deriv_inside_sec = self.promol.derivative_gaussian( + diff_coords, k_deriv + ) + gauss_and_inte_x = single_gauss * integrate_till_pt_x + gauss_and_inte = single_gauss * pi_over_exps - dnum_di = np.sum(single_gauss * integrate_till_pt_x * deriv_inside) - dden_di = np.sum(single_gauss * pi_over_exps * deriv_inside) + dnum_di = np.sum(gauss_and_inte_x * deriv_inside) + dden_di = np.sum(gauss_and_inte * deriv_inside) - dnum_dk = np.sum(single_gauss * integrate_till_pt_x * deriv_inside_sec) - dden_dk = np.sum(single_gauss * pi_over_exps * deriv_inside_sec) + dnum_dk = np.sum(gauss_and_inte_x * deriv_inside_sec) + dden_dk = np.sum(gauss_and_inte * deriv_inside_sec) - ddnum_dkdk = np.sum(single_gauss * deriv_inside * deriv_inside_sec * integrate_till_pt_x) - ddden_dkdk = np.sum(single_gauss * deriv_inside * deriv_inside_sec * pi_over_exps) + ddnum_dkdk = np.sum( + gauss_and_inte_x * deriv_inside * deriv_inside_sec + ) + ddden_dkdk = np.sum( + gauss_and_inte * deriv_inside * deriv_inside_sec + ) output = ddnum_dkdk / transf_den - output -= (dnum_di * dden_dk / transf_den ** 2.0) + output -= dnum_di * dden_dk / transf_den ** 2.0 product = dnum_dk * dden_di + transf_num * ddden_dkdk derivative = output - derivative -= product * transf_den / transf_den ** 3. - derivative += 2.0 * transf_num * dden_di * dden_dk / transf_den ** 3. + derivative -= product / transf_den ** 2.0 + derivative += ( + 2.0 * transf_num * dden_di * dden_dk / transf_den ** 3.0 + ) # The 2.0 is needed because we're in [-1, 1] rather than [0, 1]. hessian[i_var, j_deriv, k_deriv] = 2.0 * derivative @@ -525,7 +630,9 @@ def _transform(self, oned_grids): theta_y = oned_grids[1].points[iy] brack_y = self._get_bracket((ix, iy), 1) - cart_pt[1] = _inverse_coordinate(theta_y, 1, cart_pt, self.promol, brack_y) + cart_pt[1] = _inverse_coordinate( + theta_y, 1, cart_pt, self.promol, brack_y + ) for iz in range(self.num_pts[2]): theta_z = oned_grids[2].points[iz] @@ -557,7 +664,10 @@ def _get_bracket(self, indices, i_var): """ # If it is a boundary point, then return nan. Done by indices. - if 0 in indices[: i_var + 1] or (self.num_pts[i_var] - 1) in indices[: i_var + 1]: + if ( + 0 in indices[: i_var + 1] + or (self.num_pts[i_var] - 1) in indices[: i_var + 1] + ): return np.nan, np.nan # If it is a new point, with no nearby point, get a large initial guess. elif indices[i_var] == 1: @@ -569,18 +679,89 @@ def _get_bracket(self, indices, i_var): index = (indices[0] - 1) * self.num_pts[1] * self.num_pts[2] elif i_var == 1: index = indices[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * ( - indices[1] - 1 + indices[1] - 1 ) elif i_var == 2: index = ( - indices[0] * self.num_pts[1] * self.num_pts[2] - + self.num_pts[2] * indices[1] - + indices[2] - - 1 + indices[0] * self.num_pts[1] * self.num_pts[2] + + self.num_pts[2] * indices[1] + + indices[2] + - 1 ) return self.points[index, i_var], self.points[index, i_var] + 10.0 + def _closest_point(self, point): + r""" + Return closest index of the grid point to a point. + + Imagine a point inside a small sub-cube. If `closest` is selected, it will + pick the corner in the sub-cube that is closest to that point. + if `origin` is selected, it will pick the corner that is the bottom, + left-most, down-most in the sub-cube. + + Parameters + ---------- + point : np.ndarray(3,) + Point in :math:`\mathbb{R}^3`. + which : str + If "closest", returns the closest index of the grid point. + If "origin", return the bottom, left-most, down-most closest index of the grid point. + + Returns + ------- + index : int + Index of the point in `points` closest to the grid point. + + """ + # O(n) operations, Index of closest point. + idx = np.nanargmin(np.sum((self.points - point) ** 2.0, axis=1)) + return int(idx) + + def _index_to_indices(self, index): + r""" + Convert Index to Indices, ie integer m to (i, j, k) position of the Cubic Grid. + + Cubic Grid has shape (N_x, N_y, N_z) where N_x is the number of points + in the x-direction, etc. Then 0 <= i <= N_x - 1, 0 <= j <= N_y - 1, etc. + + Parameters + ---------- + index : int + Index of the grid point. + + Returns + ------- + indices : (int, int, int) + The ith, jth, kth position of the grid point. + + """ + assert index >= 0, "Index should be positive. %r" % index + n_1d, n_2d = self.num_pts[2], self.num_pts[1] * self.num_pts[2] + i = index // n_2d + j = (index - n_2d * i) // n_1d + k = index - n_2d * i - n_1d * j + return i, j, k + + def _indices_to_index(self, indices): + r""" + Convert Indices to Index, ie (i, j, k) to a index/integer m. + + Parameters + ---------- + indices : (int, int, int) + The ith, jth, kth position of the grid point. + + Returns + ------- + index : int + Index of the grid point. + + """ + n_1d, n_2d = self.num_pts[2], self.num_pts[1] * self.num_pts[2] + index = n_2d * indices[0] + n_1d * indices[1] + indices[2] + return index + @dataclass class _PromolParams: @@ -593,6 +774,7 @@ class _PromolParams: than a generic "[x, y, z]" point. """ + c_m: np.ndarray # Coefficients of Promolecular. e_m: np.ndarray # Exponents of Promolecular. coords: np.ndarray # Centers/Coordinates of Each Gaussian. @@ -610,12 +792,8 @@ def integrate_all(self): r"""Integration of Gaussian over Entire Real space ie :math:`\mathbb{R}^D`.""" return np.sum(self.c_m * self.pi_over_exponents ** self.dim) - def integrate_all_certain_variables(self): - r"""""" - return self.pi_over_exponents ** () - def derivative_gaussian(self, diff_coords, j_deriv): - r"""Derivative of single Gaussian but without exponential.""" + r"""Return derivative of single Gaussian but without exponential.""" return -self.e_m * 2.0 * diff_coords[:, j_deriv][:, np.newaxis] def integration_gaussian_till_point(self, diff_coords, i_var, with_factor=False): @@ -623,7 +801,7 @@ def integration_gaussian_till_point(self, diff_coords, i_var, with_factor=False) coord_ivar = diff_coords[:, i_var][:, np.newaxis] integration = (erf(np.sqrt(self.e_m) * coord_ivar) + 1.0) / 2.0 if with_factor: - # Included the (pi / exponents), this is the actual integral here. + # Included the (pi / exponents), this becomes the actual integral. # Not including the (pi / exponents) increasing computation slightly faster. return integration * self.pi_over_exponents return integration @@ -652,7 +830,9 @@ def promolecular(self, points): # K is maximum number of gaussian functions over all M atoms. cm, em, coords = self.c_m, self.e_m, self.coords # Shape (N, M, D), then Summing gives (N, M, 1) - distance = np.sum((points - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) + distance = np.sum( + (points - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True + ) # At each center, multiply Each Distance of a Coordinate, with its exponents. exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) # At each center, multiply the exponential with its coefficients. @@ -660,6 +840,50 @@ def promolecular(self, points): # At each point, sum for each center, then sum all centers together. return np.einsum("MNK -> N", gaussian, dtype=np.float64) + def helper_for_derivatives(self, diff_squared, diff_coords, i_var): + r""" + Return Arrays for computing the derivative of transformation functions wrt x, y, z. + + Parameters + ---------- + diff_squared : np.ndarray + The squared of difference of position to the center of the Promoleculars. + diff_coords : np.ndarray + The difference of position to the center of the Promoleculars. This is the square + root of `diff_squared`. + i_var : int + Index of one of x, y, z. + + Returns + ------- + distance : np.ndarray + The squared distance from the position to the center of the Promoleculars. + single_gauss : np.ndarray + Array with entries of a single Gaussian e^(-a distance) with factor (pi / a). + integrate_till_pt_x : np.ndarray + Integration of a Gaussian from -inf to x, ie + (pi / a)^0.5 * (erf(a^0.5 (x - center of Gaussian) + 1) / 2 + transf_num : float + The numerator of the transformation. Mostly used for quotient rule. + transf_den : float + The denominator of the transformation. Mostly used for quotient rule. + + """ + distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] + + # Gaussian Integrals Over Entire Space For Numerator and Denomator. + single_gauss = self.single_gaussians(distance) + single_gauss *= self.pi_over_exponents ** (self.dim - i_var - 1) + + # Get integral of Gaussian till a point. + integrate_till_pt_x = self.integration_gaussian_till_point( + diff_coords, i_var, with_factor=True + ) + # Numerator and Denominator of Original Transformation. + transf_num = np.sum(single_gauss * integrate_till_pt_x) + transf_den = np.sum(single_gauss * self.pi_over_exponents) + return distance, single_gauss, integrate_till_pt_x, transf_num, transf_den + def _transform_coordinate(real_pt, i_var, promol): r""" @@ -693,8 +917,9 @@ def _transform_coordinate(real_pt, i_var, promol): # Get the integral of Gaussian till a point excluding a prefactor. # prefactor (pi / exponents) is included in `gaussian_integrals`. - cdf_gauss = promol.integration_gaussian_till_point(diff_coords, i_var, - with_factor=False) + cdf_gauss = promol.integration_gaussian_till_point( + diff_coords, i_var, with_factor=False + ) # Final Result. transf_num = np.sum(single_gauss * cdf_gauss) @@ -770,14 +995,25 @@ def _inverse_coordinate(theta_pt, i_var, transformed, promol, bracket=(-10, 10)) the value 10 to the lower or upper bound that is closest to zero. """ + # Check's if this is a boundary points which is mapped to np.nan + # These two conditions are added for individual point transformation. + if np.abs(theta_pt - -1.0) < 1e-10: + return np.nan + if np.abs(theta_pt - 1.0) < 1e-10: + return np.nan + # This condition is added for transformation of the entire grid. + # The [:i_var] is needed because of the way I've set-up transforming points in _transform. + # Likewise for the bracket, see the function `get_bracket`. + if np.nan in bracket or np.nan in transformed[:i_var]: + return np.nan def _dynamic_bracketing(l_bnd, u_bnd, maxiter=50): r"""Dynamically changes the lower (or upper bound) to have different sign values.""" + bounds = [l_bnd, u_bnd] def is_same_sign(x, y): return (x >= 0 and y >= 0) or (x < 0 and y < 0) - bounds = [l_bnd, u_bnd] f_l_bnd = _root_equation(l_bnd, *args) f_u_bnd = _root_equation(u_bnd, *args) # Get Index of the one that is closest to zero, the one that needs to change. @@ -802,18 +1038,6 @@ def is_same_sign(x, y): raise RuntimeError("Dynamic Bracketing did not converge.") return tuple(bounds) - # Check's if this is a boundary points which is mapped to np.nan - # These two conditions are added for individual point transformation. - if np.abs(theta_pt - -1.0) < 1e-10: - return np.nan - if np.abs(theta_pt - 1.0) < 1e-10: - return np.nan - # This condition is added for transformation of the entire grid. - # The [:i_var] is needed because of the way I've set-up transforming points in _transform. - # Likewise for the bracket, see the function `get_bracket`. - if np.nan in bracket or np.nan in transformed[:i_var]: - return np.nan - # Set up Arguments for root_equation with dynamic bracketing. args = (transformed[:i_var], theta_pt, i_var, promol) bracket = _dynamic_bracketing(bracket[0], bracket[1]) From f21aa78beac89797dfe6c639cd064929cf94c299 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 6 Jul 2020 16:34:27 -0400 Subject: [PATCH 23/43] Add test for hessian and interpolation promol --- src/grid/tests/test_protransform.py | 180 +++++++++++++++++++++++++--- 1 file changed, 161 insertions(+), 19 deletions(-) diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index b021b1606..4f408e5c0 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -58,7 +58,7 @@ def setUp(self, ss=0.1, return_obj=False): coord = np.array([[1.0, 2.0, 3.0], [2.0, 2.0, 2.0]]) params = _PromolParams(c, e, coord, dim=3) if return_obj: - num_pts = int(1 / ss) + 1 + num_pts = int(2 / ss) + 1 weights = np.array([(2.0 / (num_pts - 2))] * num_pts) oned = OneDGrid( np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1), @@ -95,7 +95,7 @@ def test_promolecular_density(self): [0.0, 10.0, 0.2], ] ) - params= self.setUp() + params = self.setUp() true_ans = [] for pt in grid: @@ -108,6 +108,7 @@ def test_promolecular_density(self): @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) def test_transforming_x_against_formula(self, pt): r"""Test transformming the X-transformation against analytic formula.""" + true_ans = _transform_coordinate([pt], 0, self.setUp()) def formula_transforming_x(x): r"""Return closed form formula for transforming x coordinate.""" @@ -124,13 +125,13 @@ def formula_transforming_x(x): ) return -1.0 + 2.0 * ans - true_ans = _transform_coordinate([pt], 0, self.setUp()) assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) @pytest.mark.parametrize("y", [-3, 2, -10.2321, 20.232109]) def test_transforming_y_against_formula(self, x, y): r"""Test transforming the Y-transformation against analytic formula.""" + true_ans = _transform_coordinate([x, y], 1, self.setUp()) def formula_transforming_y(x, y): r"""Return closed form formula for transforming y coordinate.""" @@ -153,7 +154,6 @@ def formula_transforming_y(x, y): den = dac1 + dac2 return -1.0 + 2.0 * (num / den) - true_ans = _transform_coordinate([x, y], 1, self.setUp()) assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) @@ -161,6 +161,7 @@ def formula_transforming_y(x, y): @pytest.mark.parametrize("z", [-10, 0, 2.343432]) def test_transforming_z_against_formula(self, x, y, z): r"""Test transforming the Z-transformation against analytic formula.""" + params, obj = self.setUp(ss=0.5, return_obj=True) def formula_transforming_z(x, y, z): r"""Return closed form formula for transforming z coordinate.""" @@ -186,7 +187,6 @@ def formula_transforming_z(x, y, z): den += 10.0 * (np.pi / 3.0) ** 0.5 * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) return -1.0 + 2.0 * (fac1 + fac2) / den - params, obj = self.setUp(ss=0.5, return_obj=True) true_ans = formula_transforming_z(x, y, z) # Test function actual = _transform_coordinate([x, y, z], 2, params) @@ -198,9 +198,9 @@ def formula_transforming_z(x, y, z): def test_transforming_simple_grid(self): r"""Test transforming a grid that only contains one non-boundary point.""" - ss = 0.5 + ss = 1.0 params, obj = self.setUp(ss, return_obj=True) - num_pt = int(1 / ss) + 1 # number of points in one-direction. + num_pt = int(2.0 / ss) + 1 # number of points in one-direction. assert obj.points.shape == (num_pt ** 3, 3) non_boundary_pt_index = num_pt ** 2 + num_pt + 1 real_pt = obj.points[non_boundary_pt_index] @@ -211,7 +211,7 @@ def test_transforming_simple_grid(self): # Test that converting the point back to unit cube gives [0.5, 0.5, 0.5]. for i_var in range(0, 3): transf = _transform_coordinate(real_pt, i_var, obj.promol) - assert np.abs(transf - 0.) < 1e-5 + assert np.abs(transf) < 1e-5 # Test that all other points are indeed boundary points. all_nans = np.delete(obj.points, non_boundary_pt_index, axis=0) assert np.all(np.any(np.isnan(all_nans), axis=1)) @@ -312,13 +312,12 @@ def test_steepest_ascent_direction_with_numerics(self, x, y, z): The function to test is x^2 + y^2 + z^2. """ + params, obj = self.setUp(ss=0.2, return_obj=True) def grad(pt): # Gradient of x^2 + y^2 + z^2. return np.array([2.0 * pt[0], 2.0 * pt[1], 2.0 * pt[2]]) - params, obj = self.setUp(ss=0.2, return_obj=True) - # Take a step in real-space. pt = np.array([x, y, z]) grad_pt = grad(pt) @@ -337,7 +336,7 @@ def grad(pt): def test_second_derivative_of_theta_x_dx(self): r"""Test the second derivative of d(theta_x)/d(x) .""" params, obj = self.setUp(ss=0.2, return_obj=True) - x, y, z = 0., 0., 0. + x, y, z = 0.0, 0.0, 0.0 actual = obj.hessian(np.array([x, y, z])) # test second derivative of theta_x wrt to dx dx. @@ -492,6 +491,7 @@ def setUp(self, ss=0.1, return_obj=False): @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) def test_transforming_x_against_numerics(self, pt): r"""Test transforming X against numerical algorithms.""" + true_ans = _transform_coordinate([pt], 0, self.setUp()) def promolecular_in_x(grid, every_grid): r"""Construct the formula of promolecular for integration.""" @@ -499,19 +499,21 @@ def promolecular_in_x(grid, every_grid): promol_x_all = 5.0 * np.exp(-2.0 * (every_grid - 1.0) ** 2.0) return promol_x, promol_x_all - true_ans = _transform_coordinate([pt], 0, self.setUp()) grid = np.arange(-4.0, pt, 0.000005) # Integration till a x point every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_x, promol_x_all = promolecular_in_x(grid, every_grid) # Integration over y and z cancel out from numerator and denominator. - actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz(promol_x_all, every_grid) + actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz( + promol_x_all, every_grid + ) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2, 1.23]) @pytest.mark.parametrize("y", [-3.0, 2.0, -10.2321, 20.232109]) def test_transforming_y_against_numerics(self, x, y): r"""Test transformation y against numerical algorithms.""" + true_ans = _transform_coordinate([x, y], 1, self.setUp()) def promolecular_in_y(grid, every_grid): r"""Construct the formula of promolecular for integration.""" @@ -519,14 +521,15 @@ def promolecular_in_y(grid, every_grid): promol_y_all = 5.0 * np.exp(-2.0 * (every_grid - 2.0) ** 2.0) return promol_y_all, promol_y - true_ans = _transform_coordinate([x, y], 1, self.setUp()) grid = np.arange(-5.0, y, 0.000001) # Integration till a x point every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_y_all, promol_y = promolecular_in_y(grid, every_grid) # Integration over z cancel out from numerator and denominator. # Further, gaussian at a point does too. - actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz(promol_y_all, every_grid) + actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz( + promol_y_all, every_grid + ) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2]) @@ -534,6 +537,7 @@ def promolecular_in_y(grid, every_grid): @pytest.mark.parametrize("z", [-10.0, 0.0, 2.343432]) def test_transforming_z_against_numerics(self, x, y, z): r"""Test transforming Z against numerical algorithms.""" + grid = np.arange(-5.0, z, 0.00001) # Integration till a x point def promolecular_in_z(grid, every_grid): r"""Construct the formula of promolecular for integration.""" @@ -541,11 +545,12 @@ def promolecular_in_z(grid, every_grid): promol_z_all = 5.0 * np.exp(-2.0 * (every_grid - 3.0) ** 2.0) return promol_z_all, promol_z - grid = np.arange(-5.0, z, 0.00001) # Integration till a x point every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_z_all, promol_z = promolecular_in_z(grid, every_grid) - actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) + actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz( + promol_z_all, every_grid + ) true_ans = _transform_coordinate([x, y, z], 2, self.setUp()) assert np.abs(true_ans - actual) < 1e-5 @@ -584,11 +589,148 @@ def tranformation_z(pt): assert np.abs(grad - actual[2, 2]) < 1e-5 +class TestInterpolation: + r"""Test Interpolation Methods and Cubic Grid Utility Functions.""" + + def setUp(self, ss=0.1): + r"""Set up a simple one Gaussian function with uniform cubic grid.""" + c = np.array([[5.0]]) + e = np.array([[2.0]]) + coord = np.array([[1.0, 2.0, 3.0]]) + params = _PromolParams(c, e, coord, dim=3) + + num_pts = int(2 / ss) + 1 + weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + oned_x = OneDGrid( + np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1) + ) + + obj = CubicProTransform( + [oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords + ) + return params, obj, [oned_x, oned_x, oned_x] + + def test_interpolate_cubic_function(self): + r"""Interpolate a cubic function.""" + param, obj, oned_grids = self.setUp(ss=0.08) + + # Function to interpolate. + def func(x, y, z): + return (x - 1.0) ** 3.0 + (y - 2.0) ** 3.0 + (z - 3.0) ** 3.0 + + # Set up function values on transformed grid for interpolate function. + func_grid = [] + for pt in obj.points: + func_grid.append(func(pt[0], pt[1], pt[2])) + func_grid = np.array(func_grid) + + # Test over a grid. Pytest isn't used for effiency reasons. + grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] + for real_pt in grid: + # Desired Point + desired = func(real_pt[0], real_pt[1], real_pt[2]) + + print(desired) + actual = obj.interpolate_function(real_pt, func_grid, oned_grids) + print(actual) + assert np.abs(desired - actual) < 1e-5 + + # Test on a exact point in the grid. + real_pt = obj.points[5001] + desired = func(real_pt[0], real_pt[1], real_pt[2]) + actual = obj.interpolate_function(real_pt, func_grid, oned_grids) + assert np.abs(desired - actual) < 1e-10 + + def test_interpolate_derivative_cubic_function(self): + r"""Interpolate the derivative of some simple function.""" + param, obj, oned_grids = self.setUp(ss=0.08) + + # Function to interpolate. + def func(x, y, z): + return (x - 1.0) * (y - 2.0) * (z - 3.0) + + def derivative(x, y, z): + return 1.0 + + # Set up function values on transformed grid for interpolate function. + func_grid = [] + for pt in obj.points: + func_grid.append(func(pt[0], pt[1], pt[2])) + func_grid = np.array(func_grid) + + # Test over a grid. Pytest isn't used for effiency reasons. + # Had trouble interpolating points far away from the Gaussian 5 e^(-x(...)^2). + grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] + for real_pt in grid: + # Desired Point + desired = derivative(real_pt[0], real_pt[1], real_pt[2]) + + actual = obj.interpolate_function(real_pt, func_grid, oned_grids, nu=1) + assert np.abs(desired - actual) < 1e-4 + + def test_interpolate_derivative_cubic_function2(self): + r"""Interpolate the derivative of some simple function.""" + param, obj, oned_grids = self.setUp(ss=0.08) + + # Function to interpolate. + def func(x, y, z): + return (x - 1.0) ** 2.0 * (y - 2.0) ** 2.0 * (z - 3.0) ** 2.0 + + def derivative(x, y, z): + return 8.0 * (x - 1.0) * (y - 2.0) * (z - 3.0) + + # Set up function values on transformed grid for interpolate function. + func_grid = [] + for pt in obj.points: + func_grid.append(func(pt[0], pt[1], pt[2])) + func_grid = np.array(func_grid) + + # Test over a grid. Pytest isn't used for effiency reasons. + # Had trouble interpolating points far away from the Gaussian 5 e^(-x(...)^2). + grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] + for real_pt in grid: + # Desired Point + desired = derivative(real_pt[0], real_pt[1], real_pt[2]) + + actual = obj.interpolate_function(real_pt, func_grid, oned_grids, nu=1) + assert np.abs(desired - actual) < 1e-4 + + def test_interpolate_derivative_cubic_function3(self): + r"""Interpolate the derivative of some simple function.""" + param, obj, oned_grids = self.setUp(ss=0.08) + + # Function to interpolate. + def func(x, y, z): + return (x - 1.0) ** 2.0 + (y - 2.0) ** 2.0 + (z - 3.0) ** 2.0 + + def derivative(x, y, z): + return 0.0 + + # Set up function values on transformed grid for interpolate function. + func_grid = [] + for pt in obj.points: + func_grid.append(func(pt[0], pt[1], pt[2])) + func_grid = np.array(func_grid) + + # Test over a grid. Pytest isn't used for effiency reasons. + # Had trouble interpolating points far away from the Gaussian 5 e^(-x(...)^2). + grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] + for real_pt in grid: + # Desired Point + desired = derivative(real_pt[0], real_pt[1], real_pt[2]) + + actual = obj.interpolate_function(real_pt, func_grid, oned_grids, nu=1) + assert np.abs(desired - actual) < 1e-4 + + class TestIntegration: r""" - Only one integration test as of this moment. Choose to make it, it's own seperate - class since many choices of oned grid is possible. + Only one integration test as of this moment. + + Choose to make it, it's own seperate class since many choices of oned grid is possible. + """ + def setUp_one_gaussian(self, ss=0.03): r"""Return a one Gaussian example and its UniformProTransform object.""" c = np.array([[5.0]]) From a6f0a96c007494ea973aebf9552c4b4d1e32ebd9 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 7 Jul 2020 10:47:48 -0400 Subject: [PATCH 24/43] Add documentation to Promolecular --- src/grid/protransform.py | 96 ++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index e27ce478c..a241927b0 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -35,7 +35,7 @@ class CubicProTransform(Grid): r""" - Promolecular Grid Transformation of a Cubic Grid. + Promolecular Grid Transformation of a Cubic Grid in :math:`[-1, 1]^3`. Grid is three dimensional and modeled as Tensor Product of Three, one dimensional grids. Theta space is defined to be :math:`[-1, 1]^3`. @@ -82,6 +82,7 @@ class CubicProTransform(Grid): >> from grid.onedgrid import GaussChebyshev >> numb_x = 50 + This is a grid in :math:`[-1, 1]`. >> oned = GaussChebyshev(numb_x) One dimensional grid is the same in all x, y, z directions. >> promol = CubicProTransform([oned, oned, oned], params.c_m, params.e_m, params.coords) @@ -99,8 +100,40 @@ class CubicProTransform(Grid): Notes ----- - TODO: Insert Info About Conditional Distribution Method. - TODO: Add Infor about how boundarys on theta-space are mapped to np.nan. + Let :math:`\rho^o(x, y, z) = \sum_{i=1}^M \sum_{j=1}^D e^{}` be the Promolecular density of a \ + linear combination of Gaussian functions. + + The conditional distribution transformation from :math:`\mathbb{R}^3` to :math:`[-1, 1]^3` + transfers the (x, y, z) coordinates in :math:`\mathbb{R}^3` to a set of coordinates, + denoted as :math:`(\theta_x, \theta_y, \theta_z)`, in :math:`[-1,1]^3` that are "bunched" + up where :math:`\rho^o` is large. + + Precisely it is, + + .. math:: + \begin{eqnarray} + \theta_x(x) :&= + -1 + 2 \frac{\int_{-\infty}^x \int \int \rho^o(x, y, z)dx dy dz } + {\int \int \int \rho^o(x, y, z)dxdydz}\\ + \theta_y(x, y) :&= + -1 + 2 \frac{\int_{-\infty}^y \int \rho^o(x, y, z)dy dz } + {\int \int \rho^o(x, y, z)dydz} \\ + \theta_z(x, y, z) :&= + -1 + 2 \frac{\int_{-\infty}^z \rho^o(x, y, z)dz } + {\int \rho^o(x, y, z)dz}\\ + \end{eqnarray} + + Integration of a integrable function :math:`f : \mathbb{R}^3 \rightarrow \mathbb{R}` can be + done as follows in theta space: + + .. math:: + \int \int \int f(x, y, z)dxdy dz \approx + \frac{1}{8} N \int_{-1}^1 \int_{-1}^1 \int_{-1}^1 \frac{f(\theta_x, \theta_y, \theta_z)} + {\rho^o(\theta_x, \theta_y, \theta_z)} d\theta_x d\theta_y d\theta_z, + + \text{where } N = \int \int \int \rho^o(x, y, z) dx dy dz. + + Note that this class always assumed the boundary of [-1, 1]^3 is always included. """ @@ -209,7 +242,7 @@ def inverse(self, theta_pt, bracket=(-10, 10)): def integrate(self, *value_arrays, trick=False, tol=1e-10): r""" - Integrate any function. + Integrate any real-valued function :math:`f: \mathbb{R}^3 \rightarrow \mathbb{R}`. Assumes integrand decays faster than the promolecular density. @@ -237,7 +270,16 @@ def integrate(self, *value_arrays, trick=False, tol=1e-10): Notes ----- - - TODO: Insert formula for integration. + - Formula for the integration of a integrable function + :math:`f : \mathbb{R}^3 \rightarrow \mathbb{R}` is done as follows: + + .. math:: + \int \int \int f(x, y, z)dxdy dz \approx + \frac{1}{8} N \int_{-1}^1 \int_{-1}^1 \int_{-1}^1 \frac{f(\theta_x, \theta_y, \theta_z)} + {\rho^o(\theta_x, \theta_y, \theta_z)} d\theta_x d\theta_y d\theta_z, + + \text{where } N = \int \int \int \rho^o(x, y, z) dx dy dz. + - This method assumes the integrand decays faster than the promolecular density. """ @@ -272,7 +314,7 @@ def derivative(self, real_pt, real_derivative): Parameters ---------- real_pt : np.ndarray(3) - Point in :math:`\mathbb{R}^3` + Point in :math:`\mathbb{R}^3`. real_derivative : np.ndarray(3) Derivative of a function in real space with respect to x, y, z coordinates. @@ -285,6 +327,10 @@ def derivative(self, real_pt, real_derivative): ----- This does not preserve the direction of steepest-ascent/gradient. + See Also + -------- + steepest_ascent_theta : Steepest-ascent direction. + """ jacobian = self.jacobian(real_pt) return solve_triangular(jacobian.T, real_derivative) @@ -316,7 +362,6 @@ def steepest_ascent_theta(self, real_pt, real_grad): def interpolate_function( self, real_pt, func_values, oned_grids, use_log=False, nu=0 ): - # TODO: Should oned_grids be stored as class attribute when only this method requires it. r""" Interpolate function at a point. @@ -339,7 +384,9 @@ def interpolate_function( If nu is 1: Returns the interpolated derivative of a function at a real point. """ + # TODO: Should oned_grids be stored as class attribute when only this method requires it. # TODO: Ask about use_log and derivative. + # TODO: Asser that nu can't be beyond 0 or one and integer. # Map to theta space. theta_pt = self.transform(real_pt) @@ -403,6 +450,7 @@ def jacobian(self, real_pt): Jacobian of the transformation from real space to theta space. Precisely, it is the lower-triangular matrix + .. math:: \begin{bmatrix} \frac{\partial \theta_x}{\partial X} & 0 & 0 \\ @@ -467,6 +515,13 @@ def hessian(self, real_pt): r""" Hessian of the transformation. + The Hessian :math:`H` is a three-dimensional array with (i, j, k)th entry: + + .. math:: + H_{i, j, k} = \frac{\partial^2 \theta_i(x_0, \cdots, x_{i-1}}{\partial x_i \partial x_j} + + \text{where } (x_0, x_1, x_2) := (x, y, z). + Parameters ---------- real_pt : np.ndarray(3,) @@ -691,33 +746,6 @@ def _get_bracket(self, indices, i_var): return self.points[index, i_var], self.points[index, i_var] + 10.0 - def _closest_point(self, point): - r""" - Return closest index of the grid point to a point. - - Imagine a point inside a small sub-cube. If `closest` is selected, it will - pick the corner in the sub-cube that is closest to that point. - if `origin` is selected, it will pick the corner that is the bottom, - left-most, down-most in the sub-cube. - - Parameters - ---------- - point : np.ndarray(3,) - Point in :math:`\mathbb{R}^3`. - which : str - If "closest", returns the closest index of the grid point. - If "origin", return the bottom, left-most, down-most closest index of the grid point. - - Returns - ------- - index : int - Index of the point in `points` closest to the grid point. - - """ - # O(n) operations, Index of closest point. - idx = np.nanargmin(np.sum((self.points - point) ** 2.0, axis=1)) - return int(idx) - def _index_to_indices(self, index): r""" Convert Index to Indices, ie integer m to (i, j, k) position of the Cubic Grid. From 4d56f356c479e212d0bd09ff4f07744bb21d6c17 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 14:12:45 -0400 Subject: [PATCH 25/43] Update setup.py to use dataclass in 3.6 dataclass is included in python3.7. Acknowledgement to Derrick --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 379c0d5c0..ba940cd77 100755 --- a/setup.py +++ b/setup.py @@ -67,5 +67,6 @@ def get_readme(): "scipy>=1.4", "importlib_resources", "sympy", + "dataclass; python_version < '3.7'", ], ) From 187c00e8d4157e34b25dec5f1ac14727f6234ef4 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 14:14:04 -0400 Subject: [PATCH 26/43] Update integrand doc in promol --- setup.py | 2 +- src/grid/protransform.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index ba940cd77..11bc6e9c4 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,6 @@ def get_readme(): "scipy>=1.4", "importlib_resources", "sympy", - "dataclass; python_version < '3.7'", + "dataclasses; python_version < '3.7'", ], ) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index a241927b0..e0b22c704 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -57,7 +57,7 @@ class CubicProTransform(Grid): Methods ------- integrate(trick=False) - Integral of a real-valued function over Euclidean space. + Integral of a real-valued function over Euclidean space. Can use promolecular trick. jacobian() Jacobian of the transformation from Real space to Theta space :math:`[-1, 1]^3`. hessian() @@ -65,11 +65,11 @@ class CubicProTransform(Grid): steepest_ascent_theta() Direction of steepest-ascent of a function in theta space from gradient in real space. transform(): - Transform Real point to theta point :math:`[-1, 1]^3`. + Transform Real point to Theta space :math:`[-1, 1]^3`. inverse(bracket=(-10, 10)) - Transform theta point to Real space :math:`\mathbb{R}^3`. + Transform Theta point to Real space :math:`\mathbb{R}^3`. interpolate_function(use_log=False, nu=0) - Interpolate a function (or its derivative) at a real point. + Interpolate a function (or its logarithm) at a real point. Can interpolate its derivative. Examples -------- @@ -242,9 +242,9 @@ def inverse(self, theta_pt, bracket=(-10, 10)): def integrate(self, *value_arrays, trick=False, tol=1e-10): r""" - Integrate any real-valued function :math:`f: \mathbb{R}^3 \rightarrow \mathbb{R}`. + Integrate any real-valued function on Euclidean space. - Assumes integrand decays faster than the promolecular density. + Assumes the function decays faster than the promolecular density. Parameters ---------- @@ -271,7 +271,7 @@ def integrate(self, *value_arrays, trick=False, tol=1e-10): Notes ----- - Formula for the integration of a integrable function - :math:`f : \mathbb{R}^3 \rightarrow \mathbb{R}` is done as follows: + :math:`f : \mathbb{R}^3 \rightarrow \mathbb{R}` is done as follows: .. math:: \int \int \int f(x, y, z)dxdy dz \approx @@ -280,7 +280,7 @@ def integrate(self, *value_arrays, trick=False, tol=1e-10): \text{where } N = \int \int \int \rho^o(x, y, z) dx dy dz. - - This method assumes the integrand decays faster than the promolecular density. + - This method assumes function f decays faster than the promolecular density. """ promolecular = self.promol.promolecular(self.points) From 7d420e4d58f8f29f17a53181f1e6eff2c0776f23 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Wed, 5 Aug 2020 14:29:10 -0400 Subject: [PATCH 27/43] Add parameter to doc of interpolation Also removed an if statement that wasnt really needed and to improve coverage --- src/grid/protransform.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index e0b22c704..96092cd93 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -376,6 +376,9 @@ def interpolate_function( use_log : bool If true, then logarithm is applied before interpolating to the function values, including `func_values`. + nu : int + If zero, then the function is interpolated. + If one, then the derivative is interpolated. Returns ------- @@ -386,7 +389,8 @@ def interpolate_function( """ # TODO: Should oned_grids be stored as class attribute when only this method requires it. # TODO: Ask about use_log and derivative. - # TODO: Asser that nu can't be beyond 0 or one and integer. + if nu not in (0, 1): + raise ValueError("The parameter nu %d is either zero or one " % nu) # Map to theta space. theta_pt = self.transform(real_pt) @@ -1062,8 +1066,6 @@ def is_same_sign(x, y): same_sign = is_same_sign(f_l_bnd, f_u_bnd) counter += 1 - if counter == maxiter: - raise RuntimeError("Dynamic Bracketing did not converge.") return tuple(bounds) # Set up Arguments for root_equation with dynamic bracketing. From fd6971441359cc969ce34393cd7df3f95a4992fe Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 15 May 2023 09:11:38 -0400 Subject: [PATCH 28/43] Rename interpolation --- src/grid/protransform.py | 6 +++--- src/grid/tests/test_protransform.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 96092cd93..1aaf7b436 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -37,7 +37,7 @@ class CubicProTransform(Grid): r""" Promolecular Grid Transformation of a Cubic Grid in :math:`[-1, 1]^3`. - Grid is three dimensional and modeled as Tensor Product of Three, one dimensional grids. + Grid is three-dimensional and modeled as Tensor Product of Three, one dimensional grids. Theta space is defined to be :math:`[-1, 1]^3`. Real space is defined to be :math:`\mathbb{R}^3.` @@ -68,7 +68,7 @@ class CubicProTransform(Grid): Transform Real point to Theta space :math:`[-1, 1]^3`. inverse(bracket=(-10, 10)) Transform Theta point to Real space :math:`\mathbb{R}^3`. - interpolate_function(use_log=False, nu=0) + interpolate(use_log=False, nu=0) Interpolate a function (or its logarithm) at a real point. Can interpolate its derivative. Examples @@ -359,7 +359,7 @@ def steepest_ascent_theta(self, real_pt, real_grad): jacobian = self.jacobian(real_pt) return jacobian.dot(real_grad) - def interpolate_function( + def interpolate( self, real_pt, func_values, oned_grids, use_log=False, nu=0 ): r""" diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 4f408e5c0..8890817ad 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -631,14 +631,14 @@ def func(x, y, z): desired = func(real_pt[0], real_pt[1], real_pt[2]) print(desired) - actual = obj.interpolate_function(real_pt, func_grid, oned_grids) + actual = obj.interpolate(real_pt, func_grid, oned_grids) print(actual) assert np.abs(desired - actual) < 1e-5 # Test on a exact point in the grid. real_pt = obj.points[5001] desired = func(real_pt[0], real_pt[1], real_pt[2]) - actual = obj.interpolate_function(real_pt, func_grid, oned_grids) + actual = obj.interpolate(real_pt, func_grid, oned_grids) assert np.abs(desired - actual) < 1e-10 def test_interpolate_derivative_cubic_function(self): @@ -665,7 +665,7 @@ def derivative(x, y, z): # Desired Point desired = derivative(real_pt[0], real_pt[1], real_pt[2]) - actual = obj.interpolate_function(real_pt, func_grid, oned_grids, nu=1) + actual = obj.interpolate(real_pt, func_grid, oned_grids, nu=1) assert np.abs(desired - actual) < 1e-4 def test_interpolate_derivative_cubic_function2(self): @@ -692,7 +692,7 @@ def derivative(x, y, z): # Desired Point desired = derivative(real_pt[0], real_pt[1], real_pt[2]) - actual = obj.interpolate_function(real_pt, func_grid, oned_grids, nu=1) + actual = obj.interpolate(real_pt, func_grid, oned_grids, nu=1) assert np.abs(desired - actual) < 1e-4 def test_interpolate_derivative_cubic_function3(self): @@ -719,7 +719,7 @@ def derivative(x, y, z): # Desired Point desired = derivative(real_pt[0], real_pt[1], real_pt[2]) - actual = obj.interpolate_function(real_pt, func_grid, oned_grids, nu=1) + actual = obj.interpolate(real_pt, func_grid, oned_grids, nu=1) assert np.abs(desired - actual) < 1e-4 From 012f52b02a716daebd925830a33ffb74901ce834 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 15 May 2023 09:31:53 -0400 Subject: [PATCH 29/43] Group pytest parameterize test in protrans - Makes it easier for viewing and generalizes some test to work over a wider range of points --- src/grid/tests/test_protransform.py | 521 ++++++++++++++-------------- 1 file changed, 258 insertions(+), 263 deletions(-) diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 8890817ad..ff1b02423 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -105,61 +105,60 @@ def test_promolecular_density(self): desired = params.promolecular(grid) assert np.all(np.abs(np.array(true_ans) - desired) < 1e-8) - @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) - def test_transforming_x_against_formula(self, pt): + @pytest.mark.parametrize("pts", [np.arange(-5.0, 5.0, 0.5)]) + def test_transforming_x_against_formula(self, pts): r"""Test transformming the X-transformation against analytic formula.""" - true_ans = _transform_coordinate([pt], 0, self.setUp()) + for pt in pts: + true_ans = _transform_coordinate([pt], 0, self.setUp()) - def formula_transforming_x(x): - r"""Return closed form formula for transforming x coordinate.""" - first_factor = (5.0 * np.pi ** 1.5 / (4 * 2 ** 0.5)) * ( - erf(2 ** 0.5 * (x - 1)) + 1.0 - ) + def formula_transforming_x(x): + r"""Return closed form formula for transforming x coordinate.""" + first_factor = (5.0 * np.pi ** 1.5 / (4 * 2 ** 0.5)) * ( + erf(2 ** 0.5 * (x - 1)) + 1.0 + ) - sec_fac = ((10.0 * np.pi ** 1.5) / (6.0 * 3 ** 0.5)) * ( - erf(3.0 ** 0.5 * (x - 2)) + 1.0 - ) + sec_fac = ((10.0 * np.pi ** 1.5) / (6.0 * 3 ** 0.5)) * ( + erf(3.0 ** 0.5 * (x - 2)) + 1.0 + ) - ans = (first_factor + sec_fac) / ( - 5.0 * (np.pi / 2) ** 1.5 + 10.0 * (np.pi / 3.0) ** 1.5 - ) - return -1.0 + 2.0 * ans + ans = (first_factor + sec_fac) / ( + 5.0 * (np.pi / 2) ** 1.5 + 10.0 * (np.pi / 3.0) ** 1.5 + ) + return -1.0 + 2.0 * ans - assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 + assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 - @pytest.mark.parametrize("x", [-10, -2, 0, 2.2, 1.23]) - @pytest.mark.parametrize("y", [-3, 2, -10.2321, 20.232109]) - def test_transforming_y_against_formula(self, x, y): + @pytest.mark.parametrize("pts_xy", [np.random.uniform(-10.0, 10.0, size=(100, 2))]) + def test_transforming_y_against_formula(self, pts_xy): r"""Test transforming the Y-transformation against analytic formula.""" - true_ans = _transform_coordinate([x, y], 1, self.setUp()) - - def formula_transforming_y(x, y): - r"""Return closed form formula for transforming y coordinate.""" - fac1 = 5.0 * np.sqrt(np.pi / 2.0) * np.exp(-2.0 * (x - 1) ** 2) - fac1 *= ( - np.sqrt(np.pi) - * (erf(2.0 ** 0.5 * (y - 2)) + 1.0) - / (2.0 * np.sqrt(2.0)) - ) - fac2 = 10.0 * np.sqrt(np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) - fac2 *= ( - np.sqrt(np.pi) - * (erf(3.0 ** 0.5 * (y - 2)) + 1.0) - / (2.0 * np.sqrt(3.0)) - ) - num = fac1 + fac2 - - dac1 = 5.0 * (np.pi / 2.0) * np.exp(-2.0 * (x - 1.0) ** 2.0) - dac2 = 10.0 * (np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) - den = dac1 + dac2 - return -1.0 + 2.0 * (num / den) - - assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 - - @pytest.mark.parametrize("x", [-10, -2, 0, 2.2]) - @pytest.mark.parametrize("y", [-3, 2, -10.2321]) - @pytest.mark.parametrize("z", [-10, 0, 2.343432]) - def test_transforming_z_against_formula(self, x, y, z): + for x, y in pts_xy: + true_ans = _transform_coordinate([x, y], 1, self.setUp()) + + def formula_transforming_y(x, y): + r"""Return closed form formula for transforming y coordinate.""" + fac1 = 5.0 * np.sqrt(np.pi / 2.0) * np.exp(-2.0 * (x - 1) ** 2) + fac1 *= ( + np.sqrt(np.pi) + * (erf(2.0 ** 0.5 * (y - 2)) + 1.0) + / (2.0 * np.sqrt(2.0)) + ) + fac2 = 10.0 * np.sqrt(np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) + fac2 *= ( + np.sqrt(np.pi) + * (erf(3.0 ** 0.5 * (y - 2)) + 1.0) + / (2.0 * np.sqrt(3.0)) + ) + num = fac1 + fac2 + + dac1 = 5.0 * (np.pi / 2.0) * np.exp(-2.0 * (x - 1.0) ** 2.0) + dac2 = 10.0 * (np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) + den = dac1 + dac2 + return -1.0 + 2.0 * (num / den) + + assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 + + @pytest.mark.parametrize("pts", [np.random.uniform(-10, 10, size=(100, 3))]) + def test_transforming_z_against_formula(self, pts): r"""Test transforming the Z-transformation against analytic formula.""" params, obj = self.setUp(ss=0.5, return_obj=True) @@ -187,14 +186,15 @@ def formula_transforming_z(x, y, z): den += 10.0 * (np.pi / 3.0) ** 0.5 * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) return -1.0 + 2.0 * (fac1 + fac2) / den - true_ans = formula_transforming_z(x, y, z) - # Test function - actual = _transform_coordinate([x, y, z], 2, params) - assert np.abs(true_ans - actual) < 1e-8 + for x, y, z in pts: + true_ans = formula_transforming_z(x, y, z) + # Test function + actual = _transform_coordinate([x, y, z], 2, params) + assert np.abs(true_ans - actual) < 1e-8 - # Test Method - actual = obj.transform(np.array([x, y, z]))[2] - assert np.abs(true_ans - actual) < 1e-8 + # Test Method + actual = obj.transform(np.array([x, y, z]))[2] + assert np.abs(true_ans - actual) < 1e-8 def test_transforming_simple_grid(self): r"""Test transforming a grid that only contains one non-boundary point.""" @@ -241,73 +241,71 @@ def test_integrating_itself(self): actual = obj.integrate(promol, trick=True) assert np.abs(actual - desired) < 1e-8 - @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.75)) - def test_derivative_tranformation_x_finite_difference(self, pt): + @pytest.mark.parametrize("pts", [np.arange(-5.0, 5.0, 0.75)]) + def test_derivative_tranformation_x_finite_difference(self, pts): r"""Test the derivative of X-transformation against finite-difference.""" params, obj = self.setUp(ss=0.2, return_obj=True) - pt = np.array([pt, 2.0, 3.0]) + for pt in pts: + pt = np.array([pt, 2.0, 3.0]) - actual = obj.jacobian(pt) + actual = obj.jacobian(pt) - def tranformation_x(pt): - return _transform_coordinate(pt, 0, params) + def tranformation_x(pt): + return _transform_coordinate(pt, 0, params) - grad = approx_fprime([pt[0]], tranformation_x, 1e-6) - assert np.abs(grad - actual[0, 0]) < 1e-4 + grad = approx_fprime([pt[0]], tranformation_x, 1e-6) + assert np.abs(grad - actual[0, 0]) < 1e-4 - @pytest.mark.parametrize("x", np.arange(-5.0, 5.0, 0.75)) - @pytest.mark.parametrize("y", [-2.5, -1.5, 0, 1.5]) - def test_derivative_tranformation_y_finite_difference(self, x, y): + @pytest.mark.parametrize("pts_xy", [np.random.uniform(-5.0, 5.0, size=(100, 2))]) + def test_derivative_tranformation_y_finite_difference(self, pts_xy): r"""Test the derivative of Y-transformation against finite-difference.""" params, obj = self.setUp(ss=0.2, return_obj=True) - actual = obj.jacobian(np.array([x, y, 3.0])) + for x, y in pts_xy: + actual = obj.jacobian(np.array([x, y, 3.0])) - def tranformation_y(pt): - return _transform_coordinate([x, pt[0]], 1, params) + def tranformation_y(pt): + return _transform_coordinate([x, pt[0]], 1, params) - grad = approx_fprime([y], tranformation_y, 1e-8) - assert np.abs(grad - actual[1, 1]) < 1e-5 + grad = approx_fprime([y], tranformation_y, 1e-8) + assert np.abs(grad - actual[1, 1]) < 1e-5 - def transformation_y_wrt_x(pt): - return _transform_coordinate([pt[0], y], 1, params) + def transformation_y_wrt_x(pt): + return _transform_coordinate([pt[0], y], 1, params) - h = 1e-8 - deriv = np.imag(transformation_y_wrt_x([complex(x, h)])) / h - assert np.abs(deriv - actual[1, 0]) < 1e-4 + h = 1e-8 + deriv = np.imag(transformation_y_wrt_x([complex(x, h)])) / h + assert np.abs(deriv - actual[1, 0]) < 1e-4 - @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) - @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) - @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) - def test_derivative_tranformation_z_finite_difference(self, x, y, z): + @pytest.mark.parametrize("pts", [np.random.uniform(-3.0, 3.0, size=(100, 3))]) + def test_derivative_tranformation_z_finite_difference(self, pts): r"""Test the derivative of Z-transformation against finite-difference.""" params, obj = self.setUp(ss=0.2, return_obj=True) - actual = obj.jacobian(np.array([x, y, z])) + for x, y, z in pts: + actual = obj.jacobian(np.array([x, y, z])) - def tranformation_z(pt): - return _transform_coordinate([x, y, pt[0]], 2, params) + def tranformation_z(pt): + return _transform_coordinate([x, y, pt[0]], 2, params) - grad = approx_fprime([z], tranformation_z, 1e-8) - assert np.abs(grad - actual[2, 2]) < 1e-5 + grad = approx_fprime([z], tranformation_z, 1e-8) + assert np.abs(grad - actual[2, 2]) < 1e-5 - def transformation_z_wrt_y(pt): - return _transform_coordinate([x, pt[0], z], 2, params) + def transformation_z_wrt_y(pt): + return _transform_coordinate([x, pt[0], z], 2, params) - deriv = approx_fprime([y], transformation_z_wrt_y, 1e-8) - assert np.abs(deriv - actual[2, 1]) < 1e-4 + deriv = approx_fprime([y], transformation_z_wrt_y, 1e-8) + assert np.abs(deriv - actual[2, 1]) < 1e-4 - def transformation_z_wrt_x(pt): - a = _transform_coordinate([pt[0], y, z], 2, params) - return a + def transformation_z_wrt_x(pt): + a = _transform_coordinate([pt[0], y, z], 2, params) + return a - h = 1e-8 - deriv = np.imag(transformation_z_wrt_x([complex(x, h)])) / h + h = 1e-8 + deriv = np.imag(transformation_z_wrt_x([complex(x, h)])) / h - assert np.abs(deriv - actual[2, 0]) < 1e-4 + assert np.abs(deriv - actual[2, 0]) < 1e-4 - @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) - @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) - @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) - def test_steepest_ascent_direction_with_numerics(self, x, y, z): + @pytest.mark.parametrize("pts", [np.random.uniform(-3.0, 3.0, size=(100, 3))]) + def test_steepest_ascent_direction_with_numerics(self, pts): r"""Test steepest-ascent direction match in real and theta space. The function to test is x^2 + y^2 + z^2. @@ -318,20 +316,21 @@ def grad(pt): # Gradient of x^2 + y^2 + z^2. return np.array([2.0 * pt[0], 2.0 * pt[1], 2.0 * pt[2]]) - # Take a step in real-space. - pt = np.array([x, y, z]) - grad_pt = grad(pt) - step = 1e-8 - pt_step = pt + grad_pt * step + for x, y, z in pts: + # Take a step in real-space. + pt = np.array([x, y, z]) + grad_pt = grad(pt) + step = 1e-8 + pt_step = pt + grad_pt * step - # Convert the steps in theta space and calculate finite-difference gradient. - transf = obj.transform(pt) - transf_step = obj.transform(pt_step) - grad_finite = (transf_step - transf) / step + # Convert the steps in theta space and calculate finite-difference gradient. + transf = obj.transform(pt) + transf_step = obj.transform(pt_step) + grad_finite = (transf_step - transf) / step - # Test the actual actual. - actual = obj.steepest_ascent_theta(pt, grad_pt) - assert np.all(np.abs(actual - grad_finite) < 1e-4) + # Test the actual actual. + actual = obj.steepest_ascent_theta(pt, grad_pt) + assert np.all(np.abs(actual - grad_finite) < 1e-4) def test_second_derivative_of_theta_x_dx(self): r"""Test the second derivative of d(theta_x)/d(x) .""" @@ -352,118 +351,116 @@ def tranformation_x(pt): if i != j: assert np.abs(actual[0, i, j]) < 1e-10 - @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) - @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) - @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) - def test_second_derivative_of_theta_y(self, x, y, z): + @pytest.mark.parametrize("pts", [np.random.uniform(-3.0, 3.0, size=(100, 3))]) + def test_second_derivative_of_theta_y(self, pts): r"""Test the second derivative of d(theta_y).""" params, obj = self.setUp(ss=0.2, return_obj=True) - actual = obj.hessian(np.array([x, y, z])) + for x, y, z in pts: + actual = obj.hessian(np.array([x, y, z])) - # test second derivative of theta_y wrt to dy dy. - def dtheta_y_dy(pt): - return obj.jacobian([x, pt[0], z])[1, 1] + # test second derivative of theta_y wrt to dy dy. + def dtheta_y_dy(pt): + return obj.jacobian([x, pt[0], z])[1, 1] - dthetay_dy_dy = approx_fprime([y], dtheta_y_dy, 1e-8) - assert np.abs(dthetay_dy_dy - actual[1, 1, 1]) < 1e-5 + dthetay_dy_dy = approx_fprime([y], dtheta_y_dy, 1e-8) + assert np.abs(dthetay_dy_dy - actual[1, 1, 1]) < 1e-5 - # test second derivative of d(theta_y)/d(y) wrt dx. - def dtheta_y_dy(pt): - return obj.jacobian([pt[0], y, z])[1, 1] + # test second derivative of d(theta_y)/d(y) wrt dx. + def dtheta_y_dy(pt): + return obj.jacobian([pt[0], y, z])[1, 1] - dthetay_dy_dx = approx_fprime([x], dtheta_y_dy, 1e-8) - assert np.abs(dthetay_dy_dx - actual[1, 1, 0]) < 1e-5 + dthetay_dy_dx = approx_fprime([x], dtheta_y_dy, 1e-8) + assert np.abs(dthetay_dy_dx - actual[1, 1, 0]) < 1e-5 - # test second derivative of d(theta_y)/(dx) wrt dy - def dtheta_y_dx(pt): - return obj.jacobian([x, pt[0], z])[1, 0] + # test second derivative of d(theta_y)/(dx) wrt dy + def dtheta_y_dx(pt): + return obj.jacobian([x, pt[0], z])[1, 0] - dtheta_y_dx_dy = approx_fprime([y], dtheta_y_dx, 1e-8) - assert np.abs(dtheta_y_dx_dy - actual[1, 0, 1]) < 1e-4 + dtheta_y_dx_dy = approx_fprime([y], dtheta_y_dx, 1e-8) + assert np.abs(dtheta_y_dx_dy - actual[1, 0, 1]) < 1e-4 - # test second derivative of d(theta_y)/(dx) wrt dx - def dtheta_y_dx(pt): - return obj.jacobian([pt[0], y, z])[1, 0] + # test second derivative of d(theta_y)/(dx) wrt dx + def dtheta_y_dx(pt): + return obj.jacobian([pt[0], y, z])[1, 0] - dtheta_y_dx_dx = approx_fprime([x], dtheta_y_dx, 1e-8) - assert np.abs(dtheta_y_dx_dx - actual[1, 0, 0]) < 1e-5 + dtheta_y_dx_dx = approx_fprime([x], dtheta_y_dx, 1e-8) + assert np.abs(dtheta_y_dx_dx - actual[1, 0, 0]) < 1e-5 - # Test other elements are all zeros. - for i in range(0, 3): - assert np.abs(actual[1, 2, i]) < 1e-10 - assert np.abs(actual[1, 0, 2]) < 1e-10 - assert np.abs(actual[1, 1, 2]) < 1e-10 - - @pytest.mark.parametrize("x", [-1.5, -0.5, 0, 2.5]) - @pytest.mark.parametrize("y", [-3.0, 2.0, -2.2321]) - @pytest.mark.parametrize("z", [-1.5, 0.0, 2.343432]) - def test_second_derivative_of_theta_z(self, x, y, z): + # Test other elements are all zeros. + for i in range(0, 3): + assert np.abs(actual[1, 2, i]) < 1e-10 + assert np.abs(actual[1, 0, 2]) < 1e-10 + assert np.abs(actual[1, 1, 2]) < 1e-10 + + @pytest.mark.parametrize("pts", [np.random.uniform(-3.0, 3.0, size=(100, 3))]) + def test_second_derivative_of_theta_z(self, pts): r"""Test the second derivative of d(theta_z).""" params, obj = self.setUp(ss=0.2, return_obj=True) - actual = obj.hessian(np.array([x, y, z])) + for x, y, z in pts: + actual = obj.hessian(np.array([x, y, z])) - # test second derivative of theta_z wrt to dz dz. - def dtheta_z_dz(pt): - return obj.jacobian([x, y, pt[0]])[2, 2] + # test second derivative of theta_z wrt to dz dz. + def dtheta_z_dz(pt): + return obj.jacobian([x, y, pt[0]])[2, 2] - dthetaz_dz_dz = approx_fprime([z], dtheta_z_dz, 1e-8) - assert np.abs(dthetaz_dz_dz - actual[2, 2, 2]) < 1e-5 + dthetaz_dz_dz = approx_fprime([z], dtheta_z_dz, 1e-8) + assert np.abs(dthetaz_dz_dz - actual[2, 2, 2]) < 1e-5 - # test second derivative of theta_z wrt to dz dy. - def dtheta_z_dz(pt): - return obj.jacobian([x, pt[0], z])[2, 2] + # test second derivative of theta_z wrt to dz dy. + def dtheta_z_dz(pt): + return obj.jacobian([x, pt[0], z])[2, 2] - dthetaz_dz_dy = approx_fprime([y], dtheta_z_dz, 1e-8) - assert np.abs(dthetaz_dz_dy - actual[2, 2, 1]) < 1e-5 + dthetaz_dz_dy = approx_fprime([y], dtheta_z_dz, 1e-8) + assert np.abs(dthetaz_dz_dy - actual[2, 2, 1]) < 1e-5 - # test second derivative of theta_z wrt to dz dx. - def dtheta_z_dz(pt): - return obj.jacobian([pt[0], y, z])[2, 2] + # test second derivative of theta_z wrt to dz dx. + def dtheta_z_dz(pt): + return obj.jacobian([pt[0], y, z])[2, 2] - dthetaz_dz_dx = approx_fprime([x], dtheta_z_dz, 1e-8) - assert np.abs(dthetaz_dz_dx - actual[2, 2, 0]) < 1e-5 + dthetaz_dz_dx = approx_fprime([x], dtheta_z_dz, 1e-8) + assert np.abs(dthetaz_dz_dx - actual[2, 2, 0]) < 1e-5 - # test second derivative of theta_z wrt to dy dz. - def dtheta_z_dy(pt): - return obj.jacobian([x, y, pt[0]])[2, 1] + # test second derivative of theta_z wrt to dy dz. + def dtheta_z_dy(pt): + return obj.jacobian([x, y, pt[0]])[2, 1] - dthetaz_dy_dz = approx_fprime([z], dtheta_z_dy, 1e-8) - assert np.abs(dthetaz_dy_dz - actual[2, 1, 2]) < 1e-5 + dthetaz_dy_dz = approx_fprime([z], dtheta_z_dy, 1e-8) + assert np.abs(dthetaz_dy_dz - actual[2, 1, 2]) < 1e-5 - # test second derivative of theta_z wrt to dy dy. - def dtheta_z_dy(pt): - return obj.jacobian([x, pt[0], z])[2, 1] + # test second derivative of theta_z wrt to dy dy. + def dtheta_z_dy(pt): + return obj.jacobian([x, pt[0], z])[2, 1] - dthetaz_dy_dy = approx_fprime([y], dtheta_z_dy, 1e-8) - assert np.abs(dthetaz_dy_dy - actual[2, 1, 1]) < 1e-5 + dthetaz_dy_dy = approx_fprime([y], dtheta_z_dy, 1e-8) + assert np.abs(dthetaz_dy_dy - actual[2, 1, 1]) < 1e-5 - # test second derivative of theta_z wrt to dy dx. - def dtheta_z_dy(pt): - return obj.jacobian([pt[0], y, z])[2, 1] + # test second derivative of theta_z wrt to dy dx. + def dtheta_z_dy(pt): + return obj.jacobian([pt[0], y, z])[2, 1] - dthetaz_dy_dx = approx_fprime([x], dtheta_z_dy, 1e-8) - assert np.abs(dthetaz_dy_dx - actual[2, 1, 0]) < 1e-5 + dthetaz_dy_dx = approx_fprime([x], dtheta_z_dy, 1e-8) + assert np.abs(dthetaz_dy_dx - actual[2, 1, 0]) < 1e-5 - # test second derivative of theta_z wrt to dx dz. - def dtheta_z_dx(pt): - return obj.jacobian([x, y, pt[0]])[2, 0] + # test second derivative of theta_z wrt to dx dz. + def dtheta_z_dx(pt): + return obj.jacobian([x, y, pt[0]])[2, 0] - dthetaz_dx_dz = approx_fprime([z], dtheta_z_dx, 1e-8) - assert np.abs(dthetaz_dx_dz - actual[2, 0, 2]) < 1e-5 + dthetaz_dx_dz = approx_fprime([z], dtheta_z_dx, 1e-8) + assert np.abs(dthetaz_dx_dz - actual[2, 0, 2]) < 1e-5 - # test second derivative of theta_z wrt to dx dy. - def dtheta_z_dx(pt): - return obj.jacobian([x, pt[0], z])[2, 0] + # test second derivative of theta_z wrt to dx dy. + def dtheta_z_dx(pt): + return obj.jacobian([x, pt[0], z])[2, 0] - dthetaz_dx_dy = approx_fprime([y], dtheta_z_dx, 1e-8) - assert np.abs(dthetaz_dx_dy - actual[2, 0, 1]) < 1e-5 + dthetaz_dx_dy = approx_fprime([y], dtheta_z_dx, 1e-8) + assert np.abs(dthetaz_dx_dy - actual[2, 0, 1]) < 1e-5 - # test second derivative of theta_z wrt to dx dx. - def dtheta_z_dx(pt): - return obj.jacobian([pt[0], y, z])[2, 0] + # test second derivative of theta_z wrt to dx dx. + def dtheta_z_dx(pt): + return obj.jacobian([pt[0], y, z])[2, 0] - dthetaz_dx_dx = approx_fprime([x], dtheta_z_dx, 1e-8) - assert np.abs(dthetaz_dx_dx - actual[2, 0, 0]) < 1e-5 + dthetaz_dx_dx = approx_fprime([x], dtheta_z_dx, 1e-8) + assert np.abs(dthetaz_dx_dx - actual[2, 0, 0]) < 1e-5 class TestOneGaussianAgainstNumerics: @@ -488,32 +485,31 @@ def setUp(self, ss=0.1, return_obj=False): return params, obj return params - @pytest.mark.parametrize("pt", np.arange(-5.0, 5.0, 0.5)) - def test_transforming_x_against_numerics(self, pt): + @pytest.mark.parametrize("pts", [np.arange(-5.0, 5.0, 0.5)]) + def test_transforming_x_against_numerics(self, pts): r"""Test transforming X against numerical algorithms.""" - true_ans = _transform_coordinate([pt], 0, self.setUp()) - - def promolecular_in_x(grid, every_grid): - r"""Construct the formula of promolecular for integration.""" - promol_x = 5.0 * np.exp(-2.0 * (grid - 1.0) ** 2.0) - promol_x_all = 5.0 * np.exp(-2.0 * (every_grid - 1.0) ** 2.0) - return promol_x, promol_x_all - - grid = np.arange(-4.0, pt, 0.000005) # Integration till a x point - every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration - promol_x, promol_x_all = promolecular_in_x(grid, every_grid) - - # Integration over y and z cancel out from numerator and denominator. - actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz( - promol_x_all, every_grid - ) - assert np.abs(true_ans - actual) < 1e-5 + for pt in pts: + true_ans = _transform_coordinate([pt], 0, self.setUp()) + + def promolecular_in_x(grid, every_grid): + r"""Construct the formula of promolecular for integration.""" + promol_x = 5.0 * np.exp(-2.0 * (grid - 1.0) ** 2.0) + promol_x_all = 5.0 * np.exp(-2.0 * (every_grid - 1.0) ** 2.0) + return promol_x, promol_x_all + + grid = np.arange(-4.0, pt, 0.000005) # Integration till a x point + every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration + promol_x, promol_x_all = promolecular_in_x(grid, every_grid) + + # Integration over y and z cancel out from numerator and denominator. + actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz( + promol_x_all, every_grid + ) + assert np.abs(true_ans - actual) < 1e-5 - @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2, 1.23]) - @pytest.mark.parametrize("y", [-3.0, 2.0, -10.2321, 20.232109]) - def test_transforming_y_against_numerics(self, x, y): + @pytest.mark.parametrize("pts_xy", [np.random.uniform(-10.0, 10.0, size=(100, 2))]) + def test_transforming_y_against_numerics(self, pts_xy): r"""Test transformation y against numerical algorithms.""" - true_ans = _transform_coordinate([x, y], 1, self.setUp()) def promolecular_in_y(grid, every_grid): r"""Construct the formula of promolecular for integration.""" @@ -521,23 +517,23 @@ def promolecular_in_y(grid, every_grid): promol_y_all = 5.0 * np.exp(-2.0 * (every_grid - 2.0) ** 2.0) return promol_y_all, promol_y - grid = np.arange(-5.0, y, 0.000001) # Integration till a x point - every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration - promol_y_all, promol_y = promolecular_in_y(grid, every_grid) + for x, y in pts_xy: + true_ans = _transform_coordinate([x, y], 1, self.setUp()) - # Integration over z cancel out from numerator and denominator. - # Further, gaussian at a point does too. - actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz( - promol_y_all, every_grid - ) - assert np.abs(true_ans - actual) < 1e-5 + grid = np.arange(-5.0, y, 0.000001) # Integration till a x point + every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration + promol_y_all, promol_y = promolecular_in_y(grid, every_grid) - @pytest.mark.parametrize("x", [-10.0, -2.0, 0.0, 2.2]) - @pytest.mark.parametrize("y", [-3.0, 2.0, -10.2321]) - @pytest.mark.parametrize("z", [-10.0, 0.0, 2.343432]) - def test_transforming_z_against_numerics(self, x, y, z): + # Integration over z cancel out from numerator and denominator. + # Further, gaussian at a point does too. + actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz( + promol_y_all, every_grid + ) + assert np.abs(true_ans - actual) < 1e-5 + + @pytest.mark.parametrize("pts", [np.random.uniform(-10.0, 10.0, size=(100, 3))]) + def test_transforming_z_against_numerics(self, pts): r"""Test transforming Z against numerical algorithms.""" - grid = np.arange(-5.0, z, 0.00001) # Integration till a x point def promolecular_in_z(grid, every_grid): r"""Construct the formula of promolecular for integration.""" @@ -545,48 +541,50 @@ def promolecular_in_z(grid, every_grid): promol_z_all = 5.0 * np.exp(-2.0 * (every_grid - 3.0) ** 2.0) return promol_z_all, promol_z - every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration - promol_z_all, promol_z = promolecular_in_z(grid, every_grid) + for x, y, z in pts: + grid = np.arange(-5.0, z, 0.00001) # Integration till a x point - actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz( - promol_z_all, every_grid - ) - true_ans = _transform_coordinate([x, y, z], 2, self.setUp()) - assert np.abs(true_ans - actual) < 1e-5 + every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration + promol_z_all, promol_z = promolecular_in_z(grid, every_grid) - @pytest.mark.parametrize("x", [0.0, 0.25, 1.1, 0.5, 1.5]) - @pytest.mark.parametrize("y", [0.0, 1.25, 2.2, 2.25, 2.5]) - @pytest.mark.parametrize("z", [0.0, 2.25, 3.2, 3.25, 4.5]) - def test_jacobian(self, x, y, z): + actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz( + promol_z_all, every_grid + ) + true_ans = _transform_coordinate([x, y, z], 2, self.setUp()) + assert np.abs(true_ans - actual) < 1e-4 + + @pytest.mark.parametrize("pts", [np.random.uniform(-3.0, 3.0, size=(100, 3))]) + def test_jacobian(self, pts): r"""Test that the jacobian of the transformation.""" params, obj = self.setUp(ss=0.2, return_obj=True) - actual = obj.jacobian(np.array([x, y, z])) + for x, y, z in pts: + actual = obj.jacobian(np.array([x, y, z])) - # assert lower-triangular component is zero. - assert np.abs(actual[1, 0]) < 1e-5 - assert np.abs(actual[2, 0]) < 1e-5 - assert np.abs(actual[2, 1]) < 1e-5 + # assert lower-triangular component is zero. + assert np.abs(actual[1, 0]) < 1e-5 + assert np.abs(actual[2, 0]) < 1e-5 + assert np.abs(actual[2, 1]) < 1e-5 - # test derivative wrt to x - def tranformation_x(pt): - return _transform_coordinate([pt[0], y, z], 0, params) + # test derivative wrt to x + def tranformation_x(pt): + return _transform_coordinate([pt[0], y, z], 0, params) - grad = approx_fprime([x], tranformation_x, 1e-8) - assert np.abs(grad - actual[0, 0]) < 1e-5 + grad = approx_fprime([x], tranformation_x, 1e-8) + assert np.abs(grad - actual[0, 0]) < 1e-5 - # test derivative wrt to y - def tranformation_y(pt): - return _transform_coordinate([x, pt[0]], 1, params) + # test derivative wrt to y + def tranformation_y(pt): + return _transform_coordinate([x, pt[0]], 1, params) - grad = approx_fprime([y], tranformation_y, 1e-8) - assert np.abs(grad - actual[1, 1]) < 1e-5 + grad = approx_fprime([y], tranformation_y, 1e-8) + assert np.abs(grad - actual[1, 1]) < 1e-5 - # Test derivative wrt to z - def tranformation_z(pt): - return _transform_coordinate([x, y, pt[0]], 2, params) + # Test derivative wrt to z + def tranformation_z(pt): + return _transform_coordinate([x, y, pt[0]], 2, params) - grad = approx_fprime([z], tranformation_z, 1e-8) - assert np.abs(grad - actual[2, 2]) < 1e-5 + grad = approx_fprime([z], tranformation_z, 1e-8) + assert np.abs(grad - actual[2, 2]) < 1e-5 class TestInterpolation: @@ -629,10 +627,7 @@ def func(x, y, z): for real_pt in grid: # Desired Point desired = func(real_pt[0], real_pt[1], real_pt[2]) - - print(desired) actual = obj.interpolate(real_pt, func_grid, oned_grids) - print(actual) assert np.abs(desired - actual) < 1e-5 # Test on a exact point in the grid. From 717649d53c232ccf05fdeca80d916aa19b153621 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 15 May 2023 10:02:42 -0400 Subject: [PATCH 30/43] Generalize Promol For HyperRectangle class - Makes it consistent for the cubic grid class --- src/grid/protransform.py | 128 +++++++++------------------- src/grid/tests/test_protransform.py | 10 ++- 2 files changed, 46 insertions(+), 92 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 1aaf7b436..8e2734968 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -22,6 +22,7 @@ from dataclasses import astuple, dataclass, field from grid.basegrid import Grid, OneDGrid +from grid.cubic import _HyperRectangleGrid import numpy as np @@ -33,7 +34,7 @@ __all__ = ["CubicProTransform"] -class CubicProTransform(Grid): +class CubicProTransform(_HyperRectangleGrid): r""" Promolecular Grid Transformation of a Cubic Grid in :math:`[-1, 1]^3`. @@ -43,7 +44,7 @@ class CubicProTransform(Grid): Attributes ---------- - num_pts : (int, int, int) + shape : (int, int, int) The number of points, including both of the end/boundary points, in x, y, and z direction. prointegral : float The integration value of the promolecular density over Euclidean space. @@ -149,28 +150,22 @@ def __init__(self, oned_grids, coeffs, exps, coords): "There should be three One-Dimensional grids in `oned_grids`." ) - self._num_pts = tuple([grid.size for grid in oned_grids]) - self._dim = len(oned_grids) + self._shape = tuple([grid.size for grid in oned_grids]) + dimension = len(oned_grids) # pad coefficients and exponents with zeros to have the same size, easier to use numpy. coeffs, exps = _pad_coeffs_exps_with_zeros(coeffs, exps) - self._promol = _PromolParams(coeffs, exps, coords, self._dim) + self._promol = _PromolParams(coeffs, exps, coords, dimension) self._prointegral = self._promol.integrate_all() - empty_pts = np.empty((np.prod(self._num_pts), self._dim), dtype=np.float64) weights = np.kron( np.kron(oned_grids[0].weights, oned_grids[1].weights), oned_grids[2].weights ) + # Transform Cubic Grid in Theta-Space to Real-space. + points = self._transform(oned_grids) # The prointegral is needed because of promolecular integration. # Divide by 8 needed because the grid is in [-1, 1] rather than [0, 1]. - super().__init__(empty_pts, weights * self._prointegral / 2.0 ** self._dim) - # Transform Cubic Grid in Theta-Space to Real-space. - self._transform(oned_grids) - - @property - def num_pts(self): - r"""Return number of points in each direction.""" - return self._num_pts + super().__init__(points, weights * self._prointegral / 2.0 ** dimension, self._shape) @property def prointegral(self): @@ -182,11 +177,6 @@ def promol(self): r"""Return `PromolParams` data class.""" return self._promol - @property - def dim(self): - r"""Return the dimension of the cubic grid.""" - return self._dim - def transform(self, real_pt): r""" Transform a real point in three-dimensional Reals to theta space. @@ -404,12 +394,12 @@ def z_spline(z, x_index, y_index): # x_index, y_index is assumed to be in the grid while z is not assumed. # Get smallest and largest index for selecting func vals on this specific z-slice. # The `1` and `self.num_puts[2] - 2` is needed because I don't want the boundary. - small_index = self._indices_to_index((x_index, y_index, 1)) - large_index = self._indices_to_index( - (x_index, y_index, self.num_pts[2] - 2) + small_index = self.coordinates_to_index((x_index, y_index, 1)) + large_index = self.coordinates_to_index( + (x_index, y_index, self.shape[2] - 2) ) val = CubicSpline( - oned_grids[2].points[1 : self.num_pts[2] - 2], + oned_grids[2].points[1 : self.shape[2] - 2], func_values[small_index:large_index], )(z, nu) @@ -423,10 +413,10 @@ def y_splines(y, x_index, z): # The `1` and `self.num_puts[1] - 2` is needed because I don't want the boundary. # Assumes x_index is in the grid while y, z may not be. val = CubicSpline( - oned_grids[1].points[1 : self.num_pts[2] - 2], + oned_grids[1].points[1 : self.shape[2] - 2], [ z_spline(z, x_index, y_index) - for y_index in range(1, self.num_pts[1] - 2) + for y_index in range(1, self.shape[1] - 2) ], )(y, nu) if nu == 1: @@ -438,8 +428,8 @@ def y_splines(y, x_index, z): def x_spline(x, y, z): # x, y, z may not be in the grid. val = CubicSpline( - oned_grids[0].points[1 : self.num_pts[2] - 2], - [y_splines(y, x_index, z) for x_index in range(1, self.num_pts[0] - 2)], + oned_grids[0].points[1 : self.shape[2] - 2], + [y_splines(y, x_index, z) for x_index in range(1, self.shape[0] - 2)], )(x, nu) if nu == 1: # Derivative in real-space with respect to x. @@ -539,7 +529,7 @@ def hessian(self, real_pt): (i, j, k) is zero unless j = k = 0. """ - hessian = np.zeros((self.dim, self.dim, self.dim), dtype=np.float64) + hessian = np.zeros((self.ndim, self.ndim, self.ndim), dtype=np.float64) c_m, e_m, coords, pi_over_exps, dim = astuple(self.promol) @@ -678,33 +668,35 @@ def _transform(self, oned_grids): # Transform the entire grid. # Indices (i, j, k) start from bottom, left-most corner of the [-1, 1]^3 cube. counter = 0 - for ix in range(self.num_pts[0]): + points = np.empty((np.prod(self.shape), len(oned_grids)), dtype=np.float64) + for ix in range(self.shape[0]): cart_pt = [None, None, None] theta_x = oned_grids[0].points[ix] - brack_x = self._get_bracket((ix,), 0) + brack_x = self._get_bracket((ix,), 0, points) cart_pt[0] = _inverse_coordinate(theta_x, 0, cart_pt, self.promol, brack_x) - for iy in range(self.num_pts[1]): + for iy in range(self.shape[1]): theta_y = oned_grids[1].points[iy] - brack_y = self._get_bracket((ix, iy), 1) + brack_y = self._get_bracket((ix, iy), 1, points) cart_pt[1] = _inverse_coordinate( theta_y, 1, cart_pt, self.promol, brack_y ) - for iz in range(self.num_pts[2]): + for iz in range(self.shape[2]): theta_z = oned_grids[2].points[iz] - brack_z = self._get_bracket((ix, iy, iz), 2) + brack_z = self._get_bracket((ix, iy, iz), 2, points) cart_pt[2] = _inverse_coordinate( theta_z, 2, cart_pt, self.promol, brack_z ) - self.points[counter] = cart_pt.copy() + points[counter] = cart_pt.copy() counter += 1 + return points - def _get_bracket(self, indices, i_var): + def _get_bracket(self, indices, i_var, prev_points): r""" Obtain brackets for root-finder based on the coordinate of the point. @@ -715,6 +707,10 @@ def _get_bracket(self, indices, i_var): of the cube. i_var : int Index of point being transformed. + prev_points: ndarray(M, 3) + Points that are transformed and empty points that haven't been transformed yet. + The points that are transformed is used to obtain brackets for this current + point that hasn't been transformed yet. Returns ------- @@ -725,7 +721,7 @@ def _get_bracket(self, indices, i_var): # If it is a boundary point, then return nan. Done by indices. if ( 0 in indices[: i_var + 1] - or (self.num_pts[i_var] - 1) in indices[: i_var + 1] + or (self.shape[i_var] - 1) in indices[: i_var + 1] ): return np.nan, np.nan # If it is a new point, with no nearby point, get a large initial guess. @@ -735,64 +731,20 @@ def _get_bracket(self, indices, i_var): return min, max # If the previous point has been converted, use that as a initial guess. if i_var == 0: - index = (indices[0] - 1) * self.num_pts[1] * self.num_pts[2] + index = (indices[0] - 1) * self.shape[1] * self.shape[2] elif i_var == 1: - index = indices[0] * self.num_pts[1] * self.num_pts[2] + self.num_pts[2] * ( + index = indices[0] * self.shape[1] * self.shape[2] + self.shape[2] * ( indices[1] - 1 ) elif i_var == 2: index = ( - indices[0] * self.num_pts[1] * self.num_pts[2] - + self.num_pts[2] * indices[1] - + indices[2] - - 1 + indices[0] * self.shape[1] * self.shape[2] + + self.shape[2] * indices[1] + + indices[2] + - 1 ) - return self.points[index, i_var], self.points[index, i_var] + 10.0 - - def _index_to_indices(self, index): - r""" - Convert Index to Indices, ie integer m to (i, j, k) position of the Cubic Grid. - - Cubic Grid has shape (N_x, N_y, N_z) where N_x is the number of points - in the x-direction, etc. Then 0 <= i <= N_x - 1, 0 <= j <= N_y - 1, etc. - - Parameters - ---------- - index : int - Index of the grid point. - - Returns - ------- - indices : (int, int, int) - The ith, jth, kth position of the grid point. - - """ - assert index >= 0, "Index should be positive. %r" % index - n_1d, n_2d = self.num_pts[2], self.num_pts[1] * self.num_pts[2] - i = index // n_2d - j = (index - n_2d * i) // n_1d - k = index - n_2d * i - n_1d * j - return i, j, k - - def _indices_to_index(self, indices): - r""" - Convert Indices to Index, ie (i, j, k) to a index/integer m. - - Parameters - ---------- - indices : (int, int, int) - The ith, jth, kth position of the grid point. - - Returns - ------- - index : int - Index of the grid point. - - """ - n_1d, n_2d = self.num_pts[2], self.num_pts[1] * self.num_pts[2] - index = n_2d * indices[0] + n_1d * indices[1] + indices[2] - return index + return prev_points[index, i_var], prev_points[index, i_var] + 10.0 @dataclass diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index ff1b02423..6305675ba 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -761,14 +761,16 @@ def gaussian(grid): def test_padding_arrays(): r"""Test different array sizes are correctly padded.""" - coeff = np.array([[1.0, 2.0], [1.0, 2.0, 3.0, 4.0], [5.0]]) - exps = np.array([[4.0, 5.0], [5.0, 6.0, 7.0, 8.0], [9.0]]) + coeff = np.array([[1.0, 2.0], [1.0, 2.0, 3.0, 4.0], [5.0]], dtype=object) + exps = np.array([[4.0, 5.0], [5.0, 6.0, 7.0, 8.0], [9.0]], dtype=object) coeff_pad, exps_pad = _pad_coeffs_exps_with_zeros(coeff, exps) coeff_desired = np.array( - [[1.0, 2.0, 0.0, 0.0], [1.0, 2.0, 3.0, 4.0], [5.0, 0.0, 0.0, 0.0]] + [[1.0, 2.0, 0.0, 0.0], [1.0, 2.0, 3.0, 4.0], [5.0, 0.0, 0.0, 0.0]], + dtype=object ) np.testing.assert_array_equal(coeff_desired, coeff_pad) exp_desired = np.array( - [[4.0, 5.0, 0.0, 0.0], [5.0, 6.0, 7.0, 8.0], [9.0, 0.0, 0.0, 0.0]] + [[4.0, 5.0, 0.0, 0.0], [5.0, 6.0, 7.0, 8.0], [9.0, 0.0, 0.0, 0.0]], + dtype=object ) np.testing.assert_array_equal(exp_desired, exps_pad) From 9fc1a13f4b30e6bac819499243deebf0cbeeb7a7 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 15 May 2023 10:04:04 -0400 Subject: [PATCH 31/43] Remove doc in protransform test --- src/grid/tests/test_protransform.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 6305675ba..da6d4235e 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -17,20 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -r""" -Tests for Cubic Promolecular transformation. - -Tests ------ -TestTwoGaussianDiffCenters : - Test Transformation of Two Gaussian promolecular against different methods both analytical - and numerics. -TestOneGaussianAgainstNumerics : - Test a single Gaussian against numerical integration/differentiation. - -""" - - from grid.basegrid import OneDGrid from grid.onedgrid import GaussChebyshevLobatto from grid.protransform import ( From a5c9f8ae31f2809224741ec63913367522d56bc0 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 15 May 2023 16:02:43 -0400 Subject: [PATCH 32/43] Change np.nan to infinity --- src/grid/protransform.py | 19 ++++++++----------- src/grid/tests/test_protransform.py | 8 ++++---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 8e2734968..87d04d615 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -276,7 +276,7 @@ def integrate(self, *value_arrays, trick=False, tol=1e-10): promolecular = self.promol.promolecular(self.points) # Integrand is set to zero when promolecular is less than certain value and, # When on the boundary (hence when promolecular is nan). - cond = (promolecular <= tol) | (np.isnan(promolecular)) + cond = (promolecular <= tol) | (np.isinf(promolecular)) promolecular = np.ma.masked_where(cond, promolecular, copy=False) integrands = [] @@ -719,11 +719,8 @@ def _get_bracket(self, indices, i_var, prev_points): """ # If it is a boundary point, then return nan. Done by indices. - if ( - 0 in indices[: i_var + 1] - or (self.shape[i_var] - 1) in indices[: i_var + 1] - ): - return np.nan, np.nan + if is_boundary: + return np.inf, np.inf # If it is a new point, with no nearby point, get a large initial guess. elif indices[i_var] == 1: min = (np.min(self.promol.coords[:, i_var]) - 3.0) * 20.0 @@ -982,16 +979,16 @@ def _inverse_coordinate(theta_pt, i_var, transformed, promol, bracket=(-10, 10)) # Check's if this is a boundary points which is mapped to np.nan # These two conditions are added for individual point transformation. if np.abs(theta_pt - -1.0) < 1e-10: - return np.nan + return np.inf if np.abs(theta_pt - 1.0) < 1e-10: - return np.nan + return np.inf # This condition is added for transformation of the entire grid. # The [:i_var] is needed because of the way I've set-up transforming points in _transform. # Likewise for the bracket, see the function `get_bracket`. - if np.nan in bracket or np.nan in transformed[:i_var]: - return np.nan + if np.inf in bracket or np.inf in transformed[:i_var]: + return np.inf - def _dynamic_bracketing(l_bnd, u_bnd, maxiter=50): + def _dynamic_bracketing(l_bnd, u_bnd, maxiter=500): r"""Dynamically changes the lower (or upper bound) to have different sign values.""" bounds = [l_bnd, u_bnd] diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index da6d4235e..3b2ca6fb0 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -191,16 +191,16 @@ def test_transforming_simple_grid(self): non_boundary_pt_index = num_pt ** 2 + num_pt + 1 real_pt = obj.points[non_boundary_pt_index] # Test that this point is not the boundary. - assert real_pt[0] != np.nan - assert real_pt[1] != np.nan - assert real_pt[2] != np.nan + assert real_pt[0] != np.inf + assert real_pt[1] != np.inf + assert real_pt[2] != np.inf # Test that converting the point back to unit cube gives [0.5, 0.5, 0.5]. for i_var in range(0, 3): transf = _transform_coordinate(real_pt, i_var, obj.promol) assert np.abs(transf) < 1e-5 # Test that all other points are indeed boundary points. all_nans = np.delete(obj.points, non_boundary_pt_index, axis=0) - assert np.all(np.any(np.isnan(all_nans), axis=1)) + assert np.all(np.any(np.isinf(all_nans), axis=1)) def test_transforming_with_inverse_transformation_is_identity(self): r"""Test transforming with inverse transformation is identity.""" From a6e60ba692d8a3c241e035a290b1d48c49a1100d Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Mon, 15 May 2023 16:15:55 -0400 Subject: [PATCH 33/43] Fix interpolate in cubic to not include boundary Useful for promolecular transformation --- src/grid/cubic.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/grid/cubic.py b/src/grid/cubic.py index d583aa8a6..6dec10f78 100644 --- a/src/grid/cubic.py +++ b/src/grid/cubic.py @@ -192,7 +192,12 @@ def y_splines(y, x_index, z, nu_y=nu_y): # The `1` and `self.num_puts[1] - 2` is needed because I don't want the boundary. # Assumes x_index is in the grid while y, z may not be. val = CubicSpline( - self.points[np.arange(1, self.shape[1] - 2) * self.shape[2], 1], + self.points[ + [ + self.coordinates_to_index((x_index, j, 0)) + for j in np.arange(1, self.shape[1] - 2)], + 1 + ], [ z_spline(z, x_index, y_index, nu_z) for y_index in range(1, self.shape[1] - 2) @@ -208,8 +213,11 @@ def y_splines(y, x_index, z, nu_y=nu_y): def x_spline(x, y, z, nu_x): val = CubicSpline( self.points[ - np.arange(1, self.shape[0] - 2) * self.shape[1] * self.shape[2], 0 - ], + [ + self.coordinates_to_index((i, 0, 0)) + for i in np.arange(1, self.shape[0] - 2)], + 0 + ], [ y_splines(y, x_index, z, nu_y) for x_index in range(1, self.shape[0] - 2) From 53fa6896769d4d0d4ea919c80b89653f598c86f6 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 07:55:16 -0400 Subject: [PATCH 34/43] Add grid_pts option to interpolate - Useful for promolecular transform where the grid to interpolate is different - Make inteprolate its own function due to problems with sub-classing in python --- src/grid/cubic.py | 49 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/grid/cubic.py b/src/grid/cubic.py index 6dec10f78..0b14fdf13 100644 --- a/src/grid/cubic.py +++ b/src/grid/cubic.py @@ -108,7 +108,18 @@ def get_points_along_axes(self): return x, y def interpolate( - self, points, values, use_log=False, nu_x=0, nu_y=0, nu_z=0, method="cubic" + self, points, values, use_log=False, nu_x=0, nu_y=0, nu_z=0, method="cubic", grid_pts=None + ): + r"""Core of the Interpolate Algorithm.""" + # Needed because CubicProTransform is a subclass of this method and has its own + # interpolate function. Since interpolate references itself, it chooses + # CubicProTransform rather than _HyperRectangleGrid class. + return self._interpolate( + points, values, use_log, nu_x, nu_y, nu_z, method, grid_pts + ) + + def _interpolate( + self, points, values, use_log=False, nu_x=0, nu_y=0, nu_z=0, method="cubic", grid_pts=None ): r"""Interpolate function value at a given point. @@ -137,10 +148,14 @@ def interpolate( If zero, then the function in z-direction is interpolated. If greater than zero, then the "nu_z"th-order derivative in the z-direction is interpolated. - method : str, optional + method: str, optional The method of interpolation to perform. Supported are "cubic" (most accurate but computationally expensive), "linear", or "nearest" (least accurate but cheap computationally). The last two methods use SciPy's RegularGridInterpolator function. + grid_pts: list[OneDGrids], optional + If provided, then uses `grid_pts` rather than the points of the HyperRectangle class + `self.points` to construct interpolation. Useful when doing a promolecular + transformation. Returns ------- @@ -161,6 +176,10 @@ def interpolate( f"Number of function values {values.shape[0]} does not match number of " f"grid points {np.prod(self.shape)}." ) + if grid_pts is not None and not isinstance(grid_pts, np.ndarray): + raise TypeError( + f"The grid points {type(grid_pts)} should have type None or numpy array." + ) if use_log: values = np.log(values) @@ -172,6 +191,10 @@ def interpolate( interpolate = RegularGridInterpolator((x, y, z), values, method=method) return interpolate(points) + # If grid_pts isn't specified, then use the grid stored as the class attribute. + if grid_pts is None: + grid_pts = self.points + # Interpolate the Z-Axis. def z_spline(z, x_index, y_index, nu_z=nu_z): # x_index, y_index is assumed to be in the grid while z is not assumed. @@ -182,7 +205,7 @@ def z_spline(z, x_index, y_index, nu_z=nu_z): (x_index, y_index, self.shape[2] - 2) ) val = CubicSpline( - self.points[small_index:large_index, 2], + grid_pts[small_index:large_index, 2], values[small_index:large_index], )(z, nu_z) return val @@ -192,7 +215,7 @@ def y_splines(y, x_index, z, nu_y=nu_y): # The `1` and `self.num_puts[1] - 2` is needed because I don't want the boundary. # Assumes x_index is in the grid while y, z may not be. val = CubicSpline( - self.points[ + grid_pts[ [ self.coordinates_to_index((x_index, j, 0)) for j in np.arange(1, self.shape[1] - 2)], @@ -212,7 +235,7 @@ def y_splines(y, x_index, z, nu_y=nu_y): # Interpolate the point (x, y, z) from a list of interpolated points on x,y-axis. def x_spline(x, y, z, nu_x): val = CubicSpline( - self.points[ + grid_pts[ [ self.coordinates_to_index((i, 0, 0)) for i in np.arange(1, self.shape[0] - 2)], @@ -232,7 +255,9 @@ def x_spline(x, y, z, nu_x): if use_log: # All derivatives require the interpolation of f at (x,y,z) interpolated = np.exp( - self.interpolate(points, values, use_log=False, nu_x=0, nu_y=0, nu_z=0) + self._interpolate( + points, values, use_log=False, nu_x=0, nu_y=0, nu_z=0, grid_pts=grid_pts + ) ) # Only consider taking the derivative in only one direction one_var_deriv = sum([nu_x == 0, nu_y == 0, nu_z == 0]) == 2 @@ -245,24 +270,24 @@ def x_spline(x, y, z, nu_x): # Interpolate d^k ln(f) d"deriv_var" for all k from 1 to "deriv_var" if nu_x > 0: derivs = [ - self.interpolate( - points, values, use_log=False, nu_x=i, nu_y=0, nu_z=0 + self._interpolate( + points, values, use_log=False, nu_x=i, nu_y=0, nu_z=0, grid_pts=grid_pts ) for i in range(1, nu_x + 1) ] deriv_var = nu_x elif nu_y > 0: derivs = [ - self.interpolate( - points, values, use_log=False, nu_x=0, nu_y=i, nu_z=0 + self._interpolate( + points, values, use_log=False, nu_x=0, nu_y=i, nu_z=0, grid_pts=grid_pts ) for i in range(1, nu_y + 1) ] deriv_var = nu_y else: derivs = [ - self.interpolate( - points, values, use_log=False, nu_x=0, nu_y=0, nu_z=i + self._interpolate( + points, values, use_log=False, nu_x=0, nu_y=0, nu_z=i, grid_pts=grid_pts ) for i in range(1, nu_z + 1) ] From fa43c9d0e8085b59102af92b71f29256b8c68178 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 07:57:23 -0400 Subject: [PATCH 35/43] Fix interpolate in cubic with log is vectorized --- src/grid/cubic.py | 30 ++++++++++++++++++------------ src/grid/tests/test_cubic.py | 26 +++++++++++++------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/grid/cubic.py b/src/grid/cubic.py index 0b14fdf13..a8648340e 100644 --- a/src/grid/cubic.py +++ b/src/grid/cubic.py @@ -268,6 +268,7 @@ def x_spline(x, y, z, nu_x): elif one_var_deriv: # Taking the k-th derivative wrt to only one variable (x, y, z) # Interpolate d^k ln(f) d"deriv_var" for all k from 1 to "deriv_var" + # Each entry of `derivs` is the interpolation of the derivative eval on points. if nu_x > 0: derivs = [ self._interpolate( @@ -292,19 +293,24 @@ def x_spline(x, y, z, nu_x): for i in range(1, nu_z + 1) ] deriv_var = nu_z - # Sympy symbols and dictionary of symbols pointing to the derivative values - sympy_symbols = symbols("x:" + str(deriv_var)) - symbol_values = { - "x" + str(i): float(derivs[i]) for i in range(0, deriv_var) - } - return interpolated * float( - sum( - [ - bell(deriv_var, i, sympy_symbols).evalf(subs=symbol_values) - for i in range(1, deriv_var + 1) - ] + + deriv_interpolated = [] + for i_pt in range(len(points)): + # Sympy symbols and dictionary of symbols pointing to the derivative values + sympy_symbols = symbols("x:" + str(deriv_var)) + symbol_values = { + "x" + str(i): float(derivs[i][i_pt]) for i in range(0, deriv_var) + } + value = interpolated[i_pt] * float( + sum( + [ + bell(deriv_var, i, sympy_symbols).evalf(subs=symbol_values) + for i in range(1, deriv_var + 1) + ] + ) ) - ) + deriv_interpolated.append(value) + return np.array(deriv_interpolated) else: raise NotImplementedError( "Taking mixed derivative while applying the logarithm is not supported." diff --git a/src/grid/tests/test_cubic.py b/src/grid/tests/test_cubic.py index 18770e0b3..80ce231f1 100644 --- a/src/grid/tests/test_cubic.py +++ b/src/grid/tests/test_cubic.py @@ -189,7 +189,7 @@ def test_point_and_weights_are_correct(self): assert_allclose(actual_weight, cubic.weights[index]) index += 1 - def test_interpolation_of_gaussian_vertorized(self): + def test_interpolation_of_gaussian_vectorized(self): r"""Test interpolation of a Gaussian function with vectorization.""" oned = MidPoint(50) cubic = Tensor1DGrids(oned, oned, oned) @@ -250,49 +250,49 @@ def test_interpolation_of_various_derivative_gaussian_using_logarithm(self): def gaussian(points): return np.exp(-3 * np.linalg.norm(points, axis=1) ** 2.0) - def derivative_wrt_one_var(point, i_var_deriv): + def derivative_wrt_one_var(points, i_var_deriv): return ( - np.exp(-3 * np.linalg.norm(point) ** 2.0) - * point[i_var_deriv] + np.exp(-3 * np.linalg.norm(points, axis=1) ** 2.0) + * points[:, i_var_deriv] * (-3 * 2.0) ) - def derivative_second_x(point): - return np.exp(-3 * np.linalg.norm(point) ** 2.0) * point[0] ** 2.0 * ( + def derivative_second_x(points): + return np.exp(-3 * np.linalg.norm(points, axis=1) ** 2.0) * points[:, 0] ** 2.0 * ( -3 * 2.0 - ) ** 2.0 + np.exp(-3 * np.linalg.norm(point) ** 2.0) * (-3 * 2.0) + ) ** 2.0 + np.exp(-3 * np.linalg.norm(points, axis=1) ** 2.0) * (-3 * 2.0) gaussian_pts = gaussian(cubic.points) - pt = np.random.uniform(-0.5, 0.5, (3,)) + pt = np.random.uniform(-0.5, 0.5, (100,3)) # Test taking derivative in x-direction interpolated = cubic.interpolate( - pt[np.newaxis, :], gaussian_pts, use_log=True, nu_x=1 + pt, gaussian_pts, use_log=True, nu_x=1 ) assert_allclose(interpolated, derivative_wrt_one_var(pt, 0), rtol=1e-4) # Test taking derivative in y-direction interpolated = cubic.interpolate( - pt[np.newaxis, :], gaussian_pts, use_log=True, nu_y=1 + pt, gaussian_pts, use_log=True, nu_y=1 ) assert_allclose(interpolated, derivative_wrt_one_var(pt, 1), rtol=1e-4) # Test taking derivative in z-direction interpolated = cubic.interpolate( - pt[np.newaxis, :], gaussian_pts, use_log=True, nu_z=1 + pt, gaussian_pts, use_log=True, nu_z=1 ) assert_allclose(interpolated, derivative_wrt_one_var(pt, 2), rtol=1e-4) # Test taking second-derivative in x-direction interpolated = cubic.interpolate( - pt[np.newaxis, :], gaussian_pts, use_log=True, nu_x=2, nu_y=0, nu_z=0 + pt, gaussian_pts, use_log=True, nu_x=2, nu_y=0, nu_z=0 ) assert_allclose(interpolated, derivative_second_x(pt), rtol=1e-4) # Test raises error with self.assertRaises(NotImplementedError): cubic.interpolate( - pt[np.newaxis, :], gaussian_pts, use_log=True, nu_x=2, nu_y=2 + pt, gaussian_pts, use_log=True, nu_x=2, nu_y=2 ) def test_integration_of_gaussian(self): From 0d754166fe1d7c321e37889b2c27f3bf48b5f5c0 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 08:43:26 -0400 Subject: [PATCH 36/43] Add cut-off for promolecular transform - Needed to say a point is on the boundary --- src/grid/protransform.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 87d04d615..062536d45 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -138,7 +138,25 @@ class CubicProTransform(_HyperRectangleGrid): """ - def __init__(self, oned_grids, coeffs, exps, coords): + def __init__(self, oned_grids, coeffs, exps, coords, cut_off=1e-8): + r""" + Construct CubicProTransform object. + + Parameters + ---------- + oned_grids: List[OneDGrid] + List of three one-dimensional grid representing the grids along x-axis. + coeffs: List[List[float]] + Coefficients of the promolecular transformation over :math:`M` centers. + exps: List[List[float]] + Exponents of the promolecular transformation over :math:`M` centers. + coords: ndarray(M, 3) + The coordinates of the promolecular expansion. + cut_off: float + If the distance between a point in theta-space to the boundary is less than the + cut_off, then the point is considered to be part of the boundary. + + """ if not isinstance(oned_grids, list): raise TypeError("oned_grid should be of type list.") if not np.all([isinstance(grid, OneDGrid) for grid in oned_grids]): @@ -162,7 +180,7 @@ def __init__(self, oned_grids, coeffs, exps, coords): np.kron(oned_grids[0].weights, oned_grids[1].weights), oned_grids[2].weights ) # Transform Cubic Grid in Theta-Space to Real-space. - points = self._transform(oned_grids) + points = self._transform(oned_grids, cut_off) # The prointegral is needed because of promolecular integration. # Divide by 8 needed because the grid is in [-1, 1] rather than [0, 1]. super().__init__(points, weights * self._prointegral / 2.0 ** dimension, self._shape) @@ -988,7 +1006,7 @@ def _inverse_coordinate(theta_pt, i_var, transformed, promol, bracket=(-10, 10)) if np.inf in bracket or np.inf in transformed[:i_var]: return np.inf - def _dynamic_bracketing(l_bnd, u_bnd, maxiter=500): + def _dynamic_bracketing(l_bnd, u_bnd, maxiter=5000): r"""Dynamically changes the lower (or upper bound) to have different sign values.""" bounds = [l_bnd, u_bnd] From 3a484324997facc5f0b51b102a132d19dd50f378 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 08:44:52 -0400 Subject: [PATCH 37/43] Add lower and upper bound to promolecular - Increases generality --- src/grid/protransform.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 062536d45..150de3d1c 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -167,7 +167,8 @@ def __init__(self, oned_grids, coeffs, exps, coords, cut_off=1e-8): raise ValueError( "There should be three One-Dimensional grids in `oned_grids`." ) - + self._l_bnd = -1.0 + self._u_bnd = 1.0 self._shape = tuple([grid.size for grid in oned_grids]) dimension = len(oned_grids) @@ -185,6 +186,18 @@ def __init__(self, oned_grids, coeffs, exps, coords, cut_off=1e-8): # Divide by 8 needed because the grid is in [-1, 1] rather than [0, 1]. super().__init__(points, weights * self._prointegral / 2.0 ** dimension, self._shape) + @property + def l_bnd(self): + r"""float: Lower bound in theta-space. Any point in theta-space that contains this point + gets mapped to infinity.""" + return self._l_bnd + + @property + def u_bnd(self): + r"""float: Upper bound in theta-space. Any point in theta-space that contains this point + gets mapped to infinity.""" + return self._u_bnd + @property def prointegral(self): r"""Return integration of Promolecular density.""" @@ -682,39 +695,43 @@ def hessian(self, real_pt): return hessian - def _transform(self, oned_grids): + def _transform(self, oned_grids, cut_off=1e-8): # Transform the entire grid. # Indices (i, j, k) start from bottom, left-most corner of the [-1, 1]^3 cube. + def _is_boundary_pt(theta_pt): + if np.abs(theta_pt - self.l_bnd) < cut_off or np.abs(theta_pt - self.u_bnd) < cut_off: + return True + return False + counter = 0 points = np.empty((np.prod(self.shape), len(oned_grids)), dtype=np.float64) for ix in range(self.shape[0]): cart_pt = [None, None, None] theta_x = oned_grids[0].points[ix] - - brack_x = self._get_bracket((ix,), 0, points) + is_boundary = _is_boundary_pt(theta_x) + brack_x = self._get_bracket((ix,), 0, points, is_boundary) cart_pt[0] = _inverse_coordinate(theta_x, 0, cart_pt, self.promol, brack_x) for iy in range(self.shape[1]): theta_y = oned_grids[1].points[iy] - - brack_y = self._get_bracket((ix, iy), 1, points) + is_boundary = _is_boundary_pt(theta_y) + brack_y = self._get_bracket((ix, iy), 1, points, is_boundary) cart_pt[1] = _inverse_coordinate( theta_y, 1, cart_pt, self.promol, brack_y ) for iz in range(self.shape[2]): theta_z = oned_grids[2].points[iz] - - brack_z = self._get_bracket((ix, iy, iz), 2, points) + is_boundary = _is_boundary_pt(theta_z) + brack_z = self._get_bracket((ix, iy, iz), 2, points, is_boundary) cart_pt[2] = _inverse_coordinate( theta_z, 2, cart_pt, self.promol, brack_z ) - points[counter] = cart_pt.copy() counter += 1 return points - def _get_bracket(self, indices, i_var, prev_points): + def _get_bracket(self, indices, i_var, prev_points, is_boundary): r""" Obtain brackets for root-finder based on the coordinate of the point. @@ -729,6 +746,8 @@ def _get_bracket(self, indices, i_var, prev_points): Points that are transformed and empty points that haven't been transformed yet. The points that are transformed is used to obtain brackets for this current point that hasn't been transformed yet. + is_boundary: bool + If the current indices is a boundary point, then returns inf, as it maps to infinity. Returns ------- From d52ec8b494acaf6f7ad0bad8b11f1e363630208a Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 08:45:30 -0400 Subject: [PATCH 38/43] Update interpolation in promolecular transform - Matches the HyperRectangle class --- src/grid/protransform.py | 101 ++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 60 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index 150de3d1c..bcdb71a52 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -381,7 +381,12 @@ def steepest_ascent_theta(self, real_pt, real_grad): return jacobian.dot(real_grad) def interpolate( - self, real_pt, func_values, oned_grids, use_log=False, nu=0 + self, + points, + values, + oned_grids, + use_log=False, + nu=0, ): r""" Interpolate function at a point. @@ -390,10 +395,10 @@ def interpolate( ---------- real_pt : np.ndarray(3,) Point in :math:`\mathbb{R}^3` that needs to be interpolated. - func_values : np.ndarray(N,) + values : np.ndarray(N,) Function values at each point of the grid `points`. - oned_grids = list(3,) - List Containing Three One-Dimensional grid corresponding to x, y, z direction. + oned_grids = list(OneDGrids) + List containing three one-dimensional grid corresponding to x, y, z direction use_log : bool If true, then logarithm is applied before interpolating to the function values, including `func_values`. @@ -412,63 +417,39 @@ def interpolate( # TODO: Ask about use_log and derivative. if nu not in (0, 1): raise ValueError("The parameter nu %d is either zero or one " % nu) - # Map to theta space. - theta_pt = self.transform(real_pt) - - if use_log: - func_values = np.log(func_values) - - jac = self.jacobian(real_pt).T - - # Interpolate the Z-Axis based on x, y coordinates in grid. - def z_spline(z, x_index, y_index): - # x_index, y_index is assumed to be in the grid while z is not assumed. - # Get smallest and largest index for selecting func vals on this specific z-slice. - # The `1` and `self.num_puts[2] - 2` is needed because I don't want the boundary. - small_index = self.coordinates_to_index((x_index, y_index, 1)) - large_index = self.coordinates_to_index( - (x_index, y_index, self.shape[2] - 2) + + grid_pts = ( + np.vstack( + np.meshgrid( + oned_grids[0].points, + oned_grids[1].points, + oned_grids[2].points, + indexing="ij", + ) + ) + .reshape(3, -1) + .T ) - val = CubicSpline( - oned_grids[2].points[1 : self.shape[2] - 2], - func_values[small_index:large_index], - )(z, nu) - - if nu == 1: - # Derivative in real-space with respect to z. - return (jac[:, 2] * val)[2] - return val - - # Interpolate the Y-Axis based on x coordinate in grid. - def y_splines(y, x_index, z): - # The `1` and `self.num_puts[1] - 2` is needed because I don't want the boundary. - # Assumes x_index is in the grid while y, z may not be. - val = CubicSpline( - oned_grids[1].points[1 : self.shape[2] - 2], - [ - z_spline(z, x_index, y_index) - for y_index in range(1, self.shape[1] - 2) - ], - )(y, nu) - if nu == 1: - # Derivative in real-space with respect to y. - return (jac[:, 1] * val)[1] - return val - - # Interpolate the X-Axis. - def x_spline(x, y, z): - # x, y, z may not be in the grid. - val = CubicSpline( - oned_grids[0].points[1 : self.shape[2] - 2], - [y_splines(y, x_index, z) for x_index in range(1, self.shape[0] - 2)], - )(x, nu) - if nu == 1: - # Derivative in real-space with respect to x. - return (jac[:, 0] * val)[0] - return val - - interpolated = x_spline(theta_pt[0], theta_pt[1], theta_pt[2]) - return interpolated + theta_points = np.array([self.transform(x) for x in points], dtype=float) + + if nu == 0: + interpolation = super()._interpolate(theta_points, values, use_log, 0, 0, 0, + grid_pts=grid_pts) + if nu == 1: + # Interpolate in the theta-space and take the derivatives + interpolate_x = super()._interpolate(theta_points, values, use_log, 1, 0, 0, + grid_pts=grid_pts) + interpolate_y = super()._interpolate(theta_points, values, use_log, 0, 1, 0, + grid_pts=grid_pts) + interpolate_z = super()._interpolate(theta_points, values, use_log, 0, 0, 1, + grid_pts=grid_pts) + interpolation = np.vstack((interpolate_x.T, interpolate_y.T, interpolate_z.T)).T + # Transform the derivatives back to real-space. + interpolation = np.array([ + self.jacobian(points[i]).dot(interpolation[i]) for i in range(len(interpolation)) + ]) + + return interpolation def jacobian(self, real_pt): r""" From adacc379a85f3b423beea141f08df254c5ab3ed3 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 08:45:43 -0400 Subject: [PATCH 39/43] Add error raise if couldn't solve for inverse - For promolecular transform --- src/grid/protransform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index bcdb71a52..f9be44e77 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -1033,6 +1033,9 @@ def is_same_sign(x, y): same_sign = is_same_sign(f_l_bnd, f_u_bnd) counter += 1 + if counter == maxiter: + raise RuntimeError(f"Couldn't find correct bounds {bracket} for the root-solver " + f"to solve for the inverse.") return tuple(bounds) # Set up Arguments for root_equation with dynamic bracketing. From 791fb4a4c1069de2f62d485ce747890477ad2f7d Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 08:46:16 -0400 Subject: [PATCH 40/43] Remove requirement for boundary in promoltransf --- src/grid/tests/test_protransform.py | 36 +++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 3b2ca6fb0..7e8dc4c2b 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -27,6 +27,7 @@ ) import numpy as np +from numpy.testing import assert_allclose import pytest @@ -37,7 +38,7 @@ class TestTwoGaussianDiffCenters: r"""Test a Sum of Two Gaussian function against analytic formulas and numerical procedures.""" - def setUp(self, ss=0.1, return_obj=False): + def setUp(self, ss=0.1, return_obj=False, add_boundary=True): r"""Set up a two parameter Gaussian function.""" c = np.array([[5.0], [10.0]]) e = np.array([[2.0], [3.0]]) @@ -46,8 +47,14 @@ def setUp(self, ss=0.1, return_obj=False): if return_obj: num_pts = int(2 / ss) + 1 weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + if add_boundary: + l_bnd = -1.0 + end_point = True + else: + l_bnd = -0.99 + end_point = False oned = OneDGrid( - np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1), + np.linspace(l_bnd, 1, num=num_pts, endpoint=end_point), weights, domain=(-1, 1), ) obj = CubicProTransform( @@ -91,11 +98,12 @@ def test_promolecular_density(self): desired = params.promolecular(grid) assert np.all(np.abs(np.array(true_ans) - desired) < 1e-8) - @pytest.mark.parametrize("pts", [np.arange(-5.0, 5.0, 0.5)]) - def test_transforming_x_against_formula(self, pts): + @pytest.mark.parametrize("pts, add_boundary", [[np.arange(-5.0, 5.0, 0.5), False], + [np.arange(-5.0, 5.0, 0.5), True]]) + def test_transforming_x_against_formula(self, pts, add_boundary): r"""Test transformming the X-transformation against analytic formula.""" for pt in pts: - true_ans = _transform_coordinate([pt], 0, self.setUp()) + true_ans = _transform_coordinate([pt], 0, self.setUp(add_boundary=add_boundary)) def formula_transforming_x(x): r"""Return closed form formula for transforming x coordinate.""" @@ -114,11 +122,13 @@ def formula_transforming_x(x): assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 - @pytest.mark.parametrize("pts_xy", [np.random.uniform(-10.0, 10.0, size=(100, 2))]) - def test_transforming_y_against_formula(self, pts_xy): + @pytest.mark.parametrize("pts_xy, add_boundary", + [[np.random.uniform(-10.0, 10.0, size=(100, 2)), False], + [np.random.uniform(-10.0, 10.0, size=(100, 2)), True]]) + def test_transforming_y_against_formula(self, pts_xy, add_boundary): r"""Test transforming the Y-transformation against analytic formula.""" for x, y in pts_xy: - true_ans = _transform_coordinate([x, y], 1, self.setUp()) + true_ans = _transform_coordinate([x, y], 1, self.setUp(add_boundary=add_boundary)) def formula_transforming_y(x, y): r"""Return closed form formula for transforming y coordinate.""" @@ -143,10 +153,12 @@ def formula_transforming_y(x, y): assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 - @pytest.mark.parametrize("pts", [np.random.uniform(-10, 10, size=(100, 3))]) - def test_transforming_z_against_formula(self, pts): + @pytest.mark.parametrize("pts, add_boundary", + [[np.random.uniform(-10.0, 10.0, size=(100, 3)), False], + [np.random.uniform(-10.0, 10.0, size=(100, 3)), True]]) + def test_transforming_z_against_formula(self, pts, add_boundary): r"""Test transforming the Z-transformation against analytic formula.""" - params, obj = self.setUp(ss=0.5, return_obj=True) + params, obj = self.setUp(ss=0.5, return_obj=True, add_boundary=add_boundary) def formula_transforming_z(x, y, z): r"""Return closed form formula for transforming z coordinate.""" @@ -586,7 +598,7 @@ def setUp(self, ss=0.1): num_pts = int(2 / ss) + 1 weights = np.array([(2.0 / (num_pts - 2))] * num_pts) oned_x = OneDGrid( - np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1) + np.linspace(-1.0, 1.0, num=num_pts, endpoint=True), weights, domain=(-1, 1) ) obj = CubicProTransform( From 7cf98c56b4bb1906d307947333c537192629a485 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 08:46:50 -0400 Subject: [PATCH 41/43] Update interpolation tests in promol transf --- src/grid/tests/test_protransform.py | 158 +++++++++++++--------------- 1 file changed, 75 insertions(+), 83 deletions(-) diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 7e8dc4c2b..3f7411bb0 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -611,109 +611,101 @@ def test_interpolate_cubic_function(self): param, obj, oned_grids = self.setUp(ss=0.08) # Function to interpolate. - def func(x, y, z): - return (x - 1.0) ** 3.0 + (y - 2.0) ** 3.0 + (z - 3.0) ** 3.0 - - # Set up function values on transformed grid for interpolate function. - func_grid = [] - for pt in obj.points: - func_grid.append(func(pt[0], pt[1], pt[2])) - func_grid = np.array(func_grid) - - # Test over a grid. Pytest isn't used for effiency reasons. - grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] - for real_pt in grid: - # Desired Point - desired = func(real_pt[0], real_pt[1], real_pt[2]) - actual = obj.interpolate(real_pt, func_grid, oned_grids) - assert np.abs(desired - actual) < 1e-5 - - # Test on a exact point in the grid. - real_pt = obj.points[5001] - desired = func(real_pt[0], real_pt[1], real_pt[2]) - actual = obj.interpolate(real_pt, func_grid, oned_grids) - assert np.abs(desired - actual) < 1e-10 + def func(pts): + return (pts[:, 0] - 1.0) ** 3.0 + (pts[:, 1] - 2.0) ** 3.0 + (pts[:, 2] - 3.0) ** 3.0 + + # Test over a grid that is very close to the nucleus. + grid = np.vstack( + ( + np.random.uniform(1.0, 1.5, (100,)).T, + np.random.uniform(1.5, 2.5, (100,)).T, + np.random.uniform(2.5, 3.5, (100,)).T + ) + ).T + actuals = obj.interpolate(grid, func(obj.points), oned_grids, use_log=False) + desired = func(grid) + assert_allclose(desired, actuals, atol=1e-3, rtol=1e-4) - def test_interpolate_derivative_cubic_function(self): - r"""Interpolate the derivative of some simple function.""" + def test_interpolate_gaussian(self): + r"""Interpolate a Gaussian function with use_log set to True.""" param, obj, oned_grids = self.setUp(ss=0.08) # Function to interpolate. - def func(x, y, z): - return (x - 1.0) * (y - 2.0) * (z - 3.0) - - def derivative(x, y, z): - return 1.0 - - # Set up function values on transformed grid for interpolate function. - func_grid = [] - for pt in obj.points: - func_grid.append(func(pt[0], pt[1], pt[2])) - func_grid = np.array(func_grid) + def func(pts, alpha=2.0): + return np.exp(-alpha * np.linalg.norm(pts - param.coords[0], axis=1)**2.0) # Test over a grid. Pytest isn't used for effiency reasons. - # Had trouble interpolating points far away from the Gaussian 5 e^(-x(...)^2). - grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] - for real_pt in grid: - # Desired Point - desired = derivative(real_pt[0], real_pt[1], real_pt[2]) - - actual = obj.interpolate(real_pt, func_grid, oned_grids, nu=1) - assert np.abs(desired - actual) < 1e-4 + # TODO: the grid points need to be close to center to achieve good accuracy. + real_grid = np.vstack( + ( + np.random.uniform(0.75, 1.25, (100,)).T, + np.random.uniform(0.75, 2.25, (100,)).T, + np.random.uniform(3.75, 3.25, (100,)).T + ) + ).T + actuals = obj.interpolate(real_grid, func(obj.points), oned_grids, use_log=True) + desired = func(real_grid) + assert_allclose(desired, actuals, atol=1e-1) - def test_interpolate_derivative_cubic_function2(self): + def test_interpolate_derivative_cubic_function(self): r"""Interpolate the derivative of some simple function.""" param, obj, oned_grids = self.setUp(ss=0.08) # Function to interpolate. - def func(x, y, z): - return (x - 1.0) ** 2.0 * (y - 2.0) ** 2.0 * (z - 3.0) ** 2.0 - - def derivative(x, y, z): - return 8.0 * (x - 1.0) * (y - 2.0) * (z - 3.0) - - # Set up function values on transformed grid for interpolate function. - func_grid = [] - for pt in obj.points: - func_grid.append(func(pt[0], pt[1], pt[2])) - func_grid = np.array(func_grid) + def func(pts): + return (pts[:, 0] - 1.0) * (pts[:, 1] - 2.0) * (pts[:, 2] - 3.0) + + def derivative(pts): + return np.vstack([ + (pts[:, 1] - 2.0) * (pts[:, 2] - 3.0), + (pts[:, 0] - 1.0) * (pts[:, 2] - 3.0), + (pts[:, 0] - 1.0) * (pts[:, 1] - 2.0), + ] + ).T # Test over a grid. Pytest isn't used for effiency reasons. - # Had trouble interpolating points far away from the Gaussian 5 e^(-x(...)^2). - grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] - for real_pt in grid: - # Desired Point - desired = derivative(real_pt[0], real_pt[1], real_pt[2]) - - actual = obj.interpolate(real_pt, func_grid, oned_grids, nu=1) - assert np.abs(desired - actual) < 1e-4 + grid = np.vstack( + ( + np.random.uniform(1.0, 1.5, (100,)).T, + np.random.uniform(1.5, 2.5, (100,)).T, + np.random.uniform(2.5, 3.5, (100,)).T + ) + ).T + actual = obj.interpolate(grid, func(obj.points), oned_grids, nu=1) + desired = derivative(grid) + assert_allclose(actual, desired, atol=1e-2) - def test_interpolate_derivative_cubic_function3(self): + def test_interpolate_derivative_cubic_function2(self): r"""Interpolate the derivative of some simple function.""" param, obj, oned_grids = self.setUp(ss=0.08) # Function to interpolate. - def func(x, y, z): - return (x - 1.0) ** 2.0 + (y - 2.0) ** 2.0 + (z - 3.0) ** 2.0 - - def derivative(x, y, z): - return 0.0 - - # Set up function values on transformed grid for interpolate function. - func_grid = [] - for pt in obj.points: - func_grid.append(func(pt[0], pt[1], pt[2])) - func_grid = np.array(func_grid) + def func(pts): + return (pts[:, 0] - 1.0) ** 2.0 * (pts[:, 1] - 2.0) ** 2.0 * (pts[:, 2] - 3.0) ** 2.0 + + def derivative(pts): + x, y, z = pts[:, 0], pts[:, 1], pts[:, 2] + return np.vstack([ + 2.0 * (x - 1.0) * (y - 2.0) ** 2.0 * (z - 3.0) ** 2.0, + 2.0 * (x - 1.0) ** 2.0 * (y - 2.0) * (z - 3.0) ** 2.0, + 2.0 * (x - 1.0) ** 2.0 * (y - 2.0) ** 2.0 * (z - 3.0), + ] + ).T # Test over a grid. Pytest isn't used for effiency reasons. - # Had trouble interpolating points far away from the Gaussian 5 e^(-x(...)^2). - grid = [[1.1, 2.1, 3.1], [1.0, 2.0, 3.0], [0.75, 1.75, 3.1]] - for real_pt in grid: - # Desired Point - desired = derivative(real_pt[0], real_pt[1], real_pt[2]) - - actual = obj.interpolate(real_pt, func_grid, oned_grids, nu=1) - assert np.abs(desired - actual) < 1e-4 + grid = np.vstack( + ( + np.random.uniform(1.0, 1.5, (100,)).T, + np.random.uniform(1.5, 2.5, (100,)).T, + np.random.uniform(2.5, 3.5, (100,)).T + ) + ).T + actual = obj.interpolate(grid, func(obj.points), oned_grids, nu=1, use_log=False) + desired = derivative(grid) + assert_allclose(actual, desired, atol=1e-2) + # Test with logarithm set to True + actual = obj.interpolate(grid, func(obj.points), oned_grids, nu=1, use_log=True) + assert_allclose(actual, desired, atol=1e-2) class TestIntegration: From cc4625e4f037d9a2bafd906b66c9efb8bc1a6f36 Mon Sep 17 00:00:00 2001 From: Ali-Tehrani Date: Tue, 16 May 2023 14:37:39 -0400 Subject: [PATCH 42/43] Remove TODO in promol transform --- src/grid/protransform.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index f9be44e77..b87d3c101 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -413,8 +413,6 @@ def interpolate( If nu is 1: Returns the interpolated derivative of a function at a real point. """ - # TODO: Should oned_grids be stored as class attribute when only this method requires it. - # TODO: Ask about use_log and derivative. if nu not in (0, 1): raise ValueError("The parameter nu %d is either zero or one " % nu) From e439e5f62b2938926e90d2c9612e0819ef6daff7 Mon Sep 17 00:00:00 2001 From: Farnaz Heidar-Zadeh Date: Thu, 5 Jun 2025 07:59:24 -0400 Subject: [PATCH 43/43] Fix _get_bracket of Transformed Cube & black linter issues --- src/grid/protransform.py | 221 +++++++++++----------------- src/grid/tests/test_protransform.py | 148 ++++++++----------- 2 files changed, 152 insertions(+), 217 deletions(-) diff --git a/src/grid/protransform.py b/src/grid/protransform.py index d16cd71a2..6ac11f8f8 100644 --- a/src/grid/protransform.py +++ b/src/grid/protransform.py @@ -164,9 +164,7 @@ def __init__(self, oned_grids, coeffs, exps, coords, cut_off=1e-8): if not np.all([grid.domain == (-1, 1) for grid in oned_grids]): raise ValueError("One Dimensional grid domain should be (-1, 1).") if not len(oned_grids) == 3: - raise ValueError( - "There should be three One-Dimensional grids in `oned_grids`." - ) + raise ValueError("There should be three One-Dimensional grids in `oned_grids`.") self._l_bnd = -1.0 self._u_bnd = 1.0 self._shape = tuple([grid.size for grid in oned_grids]) @@ -184,18 +182,18 @@ def __init__(self, oned_grids, coeffs, exps, coords, cut_off=1e-8): points = self._transform(oned_grids, cut_off) # The prointegral is needed because of promolecular integration. # Divide by 8 needed because the grid is in [-1, 1] rather than [0, 1]. - super().__init__(points, weights * self._prointegral / 2.0 ** dimension, self._shape) + super().__init__(points, weights * self._prointegral / 2.0**dimension, self._shape) @property def l_bnd(self): r"""float: Lower bound in theta-space. Any point in theta-space that contains this point - gets mapped to infinity.""" + gets mapped to infinity.""" return self._l_bnd @property def u_bnd(self): r"""float: Upper bound in theta-space. Any point in theta-space that contains this point - gets mapped to infinity.""" + gets mapped to infinity.""" return self._u_bnd @property @@ -224,10 +222,7 @@ def transform(self, real_pt): """ return np.array( - [ - _transform_coordinate(real_pt, i, self.promol) - for i in range(0, self.promol.dim) - ] + [_transform_coordinate(real_pt, i, self.promol) for i in range(0, self.promol.dim)] ) def inverse(self, theta_pt, bracket=(-10, 10)): @@ -255,9 +250,7 @@ def inverse(self, theta_pt, bracket=(-10, 10)): """ real_pt = [] for i in range(0, self.promol.dim): - scalar = _inverse_coordinate( - theta_pt[i], i, real_pt[:i], self.promol, bracket - ) + scalar = _inverse_coordinate(theta_pt[i], i, real_pt[:i], self.promol, bracket) real_pt.append(scalar) return np.array(real_pt) @@ -417,35 +410,39 @@ def interpolate( raise ValueError("The parameter nu %d is either zero or one " % nu) grid_pts = ( - np.vstack( - np.meshgrid( - oned_grids[0].points, - oned_grids[1].points, - oned_grids[2].points, - indexing="ij", - ) + np.vstack( + np.meshgrid( + oned_grids[0].points, + oned_grids[1].points, + oned_grids[2].points, + indexing="ij", ) - .reshape(3, -1) - .T ) + .reshape(3, -1) + .T + ) theta_points = np.array([self.transform(x) for x in points], dtype=float) if nu == 0: - interpolation = super()._interpolate(theta_points, values, use_log, 0, 0, 0, - grid_pts=grid_pts) + interpolation = super()._interpolate( + theta_points, values, use_log, 0, 0, 0, grid_pts=grid_pts + ) if nu == 1: # Interpolate in the theta-space and take the derivatives - interpolate_x = super()._interpolate(theta_points, values, use_log, 1, 0, 0, - grid_pts=grid_pts) - interpolate_y = super()._interpolate(theta_points, values, use_log, 0, 1, 0, - grid_pts=grid_pts) - interpolate_z = super()._interpolate(theta_points, values, use_log, 0, 0, 1, - grid_pts=grid_pts) + interpolate_x = super()._interpolate( + theta_points, values, use_log, 1, 0, 0, grid_pts=grid_pts + ) + interpolate_y = super()._interpolate( + theta_points, values, use_log, 0, 1, 0, grid_pts=grid_pts + ) + interpolate_z = super()._interpolate( + theta_points, values, use_log, 0, 0, 1, grid_pts=grid_pts + ) interpolation = np.vstack((interpolate_x.T, interpolate_y.T, interpolate_z.T)).T # Transform the derivatives back to real-space. - interpolation = np.array([ - self.jacobian(points[i]).dot(interpolation[i]) for i in range(len(interpolation)) - ]) + interpolation = np.array( + [self.jacobian(points[i]).dot(interpolation[i]) for i in range(len(interpolation))] + ) return interpolation @@ -480,7 +477,7 @@ def jacobian(self, real_pt): # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt - coords - diff_squared = diff_coords ** 2.0 + diff_squared = diff_coords**2.0 # If i_var is zero, then distance is just all zeros. for i_var in range(0, self.promol.dim): @@ -496,22 +493,16 @@ def jacobian(self, real_pt): for j_deriv in range(0, i_var + 1): if i_var == j_deriv: # Derivative eliminates `integrate_till_pt_x`, and adds a Gaussian. - inner_term = single_gauss * np.exp( - -e_m * diff_squared[:, i_var][:, np.newaxis] - ) + inner_term = single_gauss * np.exp(-e_m * diff_squared[:, i_var][:, np.newaxis]) jacobian[i_var, i_var] = np.sum(inner_term) / transf_den elif j_deriv < i_var: # Derivative of inside of Gaussian. deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) - deriv_num = np.sum( - single_gauss * integrate_till_pt_x * deriv_inside - ) + deriv_num = np.sum(single_gauss * integrate_till_pt_x * deriv_inside) deriv_den = np.sum(single_gauss * deriv_inside * pi_over_exps) # Quotient Rule - jacobian[i_var, j_deriv] = ( - deriv_num * transf_den - transf_num * deriv_den - ) - jacobian[i_var, j_deriv] /= transf_den ** 2.0 + jacobian[i_var, j_deriv] = deriv_num * transf_den - transf_num * deriv_den + jacobian[i_var, j_deriv] /= transf_den**2.0 return 2.0 * jacobian @@ -545,7 +536,7 @@ def hessian(self, real_pt): # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt - coords - diff_squared = diff_coords ** 2.0 + diff_squared = diff_coords**2.0 # i_var is the transformation to theta-space. # j_deriv is the first partial derivative wrt x, y, z. @@ -576,22 +567,18 @@ def hessian(self, real_pt): ) if j_deriv == k_deriv: # Diagonal derivative e.g. d(theta_X)(dx dx) - gauss_extra *= self.promol.derivative_gaussian( - diff_coords, j_deriv - ) + gauss_extra *= self.promol.derivative_gaussian(diff_coords, j_deriv) derivative = np.sum(gauss_extra) / transf_den else: # Partial derivative of diagonal derivative e.g. d^2(theta_y)(dy dx). - deriv_inside = self.promol.derivative_gaussian( - diff_coords, k_deriv - ) + deriv_inside = self.promol.derivative_gaussian(diff_coords, k_deriv) dnum_dkdj = np.sum(gauss_extra * deriv_inside) dden_dk = np.sum(single_gauss * deriv_inside * pi_over_exps) # Numerator is different from `transf_num` since Gaussian is added. dnum_dj = np.sum(gauss_extra) # Quotient Rule derivative = dnum_dkdj * transf_den - dnum_dj * dden_dk - derivative /= transf_den ** 2.0 + derivative /= transf_den**2.0 # Here, quotient rule all the way down. elif j_deriv < i_var: @@ -599,51 +586,35 @@ def hessian(self, real_pt): gauss_extra = single_gauss * np.exp( -e_m * diff_squared[:, k_deriv][:, np.newaxis] ) - deriv_inside = self.promol.derivative_gaussian( - diff_coords, j_deriv - ) + deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) ddnum_djdi = np.sum(gauss_extra * deriv_inside) dden_dj = np.sum(single_gauss * deriv_inside * pi_over_exps) # Quotient Rule dnum_dj = np.sum(gauss_extra) derivative = ddnum_djdi * transf_den - dnum_dj * dden_dj - derivative /= transf_den ** 2.0 + derivative /= transf_den**2.0 elif k_deriv == j_deriv: # Double Quotient Rule. # See wikipedia "Quotient Rules Higher order formulas". - deriv_inside = self.promol.derivative_gaussian( - diff_coords, k_deriv - ) - dnum_dj = np.sum( - single_gauss * integrate_till_pt_x * deriv_inside - ) + deriv_inside = self.promol.derivative_gaussian(diff_coords, k_deriv) + dnum_dj = np.sum(single_gauss * integrate_till_pt_x * deriv_inside) dden_dj = np.sum(single_gauss * pi_over_exps * deriv_inside) - prod_rule = deriv_inside ** 2.0 - 2.0 * e_m - sec_deriv_num = np.sum( - single_gauss * integrate_till_pt_x * prod_rule - ) - sec_deriv_den = np.sum( - single_gauss * pi_over_exps * prod_rule - ) + prod_rule = deriv_inside**2.0 - 2.0 * e_m + sec_deriv_num = np.sum(single_gauss * integrate_till_pt_x * prod_rule) + sec_deriv_den = np.sum(single_gauss * pi_over_exps * prod_rule) output = sec_deriv_num * transf_den - dnum_dj * dden_dj - output /= transf_den ** 2.0 - quot = transf_den * ( - dnum_dj * dden_dj + transf_num * sec_deriv_den - ) + output /= transf_den**2.0 + quot = transf_den * (dnum_dj * dden_dj + transf_num * sec_deriv_den) quot -= 2.0 * transf_num * dden_dj * dden_dj - derivative = output - quot / transf_den ** 3.0 + derivative = output - quot / transf_den**3.0 elif k_deriv != j_deriv: # K is i_Sec_diff and i is i_diff - deriv_inside = self.promol.derivative_gaussian( - diff_coords, j_deriv - ) - deriv_inside_sec = self.promol.derivative_gaussian( - diff_coords, k_deriv - ) + deriv_inside = self.promol.derivative_gaussian(diff_coords, j_deriv) + deriv_inside_sec = self.promol.derivative_gaussian(diff_coords, k_deriv) gauss_and_inte_x = single_gauss * integrate_till_pt_x gauss_and_inte = single_gauss * pi_over_exps @@ -653,21 +624,15 @@ def hessian(self, real_pt): dnum_dk = np.sum(gauss_and_inte_x * deriv_inside_sec) dden_dk = np.sum(gauss_and_inte * deriv_inside_sec) - ddnum_dkdk = np.sum( - gauss_and_inte_x * deriv_inside * deriv_inside_sec - ) - ddden_dkdk = np.sum( - gauss_and_inte * deriv_inside * deriv_inside_sec - ) + ddnum_dkdk = np.sum(gauss_and_inte_x * deriv_inside * deriv_inside_sec) + ddden_dkdk = np.sum(gauss_and_inte * deriv_inside * deriv_inside_sec) output = ddnum_dkdk / transf_den - output -= dnum_di * dden_dk / transf_den ** 2.0 + output -= dnum_di * dden_dk / transf_den**2.0 product = dnum_dk * dden_di + transf_num * ddden_dkdk derivative = output - derivative -= product / transf_den ** 2.0 - derivative += ( - 2.0 * transf_num * dden_di * dden_dk / transf_den ** 3.0 - ) + derivative -= product / transf_den**2.0 + derivative += 2.0 * transf_num * dden_di * dden_dk / transf_den**3.0 # The 2.0 is needed because we're in [-1, 1] rather than [0, 1]. hessian[i_var, j_deriv, k_deriv] = 2.0 * derivative @@ -695,17 +660,13 @@ def _is_boundary_pt(theta_pt): theta_y = oned_grids[1].points[iy] is_boundary = _is_boundary_pt(theta_y) brack_y = self._get_bracket((ix, iy), 1, points, is_boundary) - cart_pt[1] = _inverse_coordinate( - theta_y, 1, cart_pt, self.promol, brack_y - ) + cart_pt[1] = _inverse_coordinate(theta_y, 1, cart_pt, self.promol, brack_y) for iz in range(self.shape[2]): theta_z = oned_grids[2].points[iz] is_boundary = _is_boundary_pt(theta_z) brack_z = self._get_bracket((ix, iy, iz), 2, points, is_boundary) - cart_pt[2] = _inverse_coordinate( - theta_z, 2, cart_pt, self.promol, brack_z - ) + cart_pt[2] = _inverse_coordinate(theta_z, 2, cart_pt, self.promol, brack_z) points[counter] = cart_pt.copy() counter += 1 return points @@ -738,29 +699,29 @@ def _get_bracket(self, indices, i_var, prev_points, is_boundary): if is_boundary: return np.inf, np.inf # If it is a new point, with no nearby point, get a large initial guess. - elif indices[i_var] == 0 or indices[i_var] == 1: - # TODO: These boundary could be made more efficient, since if there is boundary pts - # in the Theta-Grid, then indices[i_var] == 1 should be used, if there isn't - # boundary points in theta-grid then indices[i_var] == 0 should be used. - min = (np.min(self.promol.coords[:, i_var]) - 3.0) * 20.0 - max = (np.max(self.promol.coords[:, i_var]) + 3.0) * 20.0 + else: + min = np.min(self.promol.coords[:, i_var]) - 20.0 + max = np.max(self.promol.coords[:, i_var]) + 20.0 return min, max # If the previous point has been converted, use that as a initial guess. - if i_var == 0: - index = (indices[0] - 1) * self.shape[1] * self.shape[2] - elif i_var == 1: - index = indices[0] * self.shape[1] * self.shape[2] + self.shape[2] * ( - indices[1] - 1 - ) - elif i_var == 2: - index = ( - indices[0] * self.shape[1] * self.shape[2] - + self.shape[2] * indices[1] - + indices[2] - - 1 - ) - - return prev_points[index, i_var], prev_points[index, i_var] + 10.0 + # if i_var == 0: + # print(" shape = %s" % str(self.shape)) + # index = (indices[0] - 1) * self.shape[1] * self.shape[2] + # print(" index = %d" % index) + # elif i_var == 1: + # index = indices[0] * self.shape[1] * self.shape[2] + self.shape[2] * (indices[1] - 1) + # elif i_var == 2: + # index = ( + # indices[0] * self.shape[1] * self.shape[2] + # + self.shape[2] * indices[1] + # + indices[2] + # - 1 + # ) + # print(" prev_points = %s" % str(prev_points)) + # print(" shape prev_points = %s" % str(prev_points.shape)) + # print(f" returned {prev_points[index, i_var]}") + + # return prev_points[index, i_var], prev_points[index, i_var] + 10.0 @dataclass @@ -790,7 +751,7 @@ def __post_init__(self): def integrate_all(self): r"""Integration of Gaussian over Entire Real space ie :math:`\mathbb{R}^D`.""" - return np.sum(self.c_m * self.pi_over_exponents ** self.dim) + return np.sum(self.c_m * self.pi_over_exponents**self.dim) def derivative_gaussian(self, diff_coords, j_deriv): r"""Return derivative of single Gaussian but without exponential.""" @@ -830,9 +791,7 @@ def promolecular(self, points): # K is maximum number of gaussian functions over all M atoms. cm, em, coords = self.c_m, self.e_m, self.coords # Shape (N, M, D), then Summing gives (N, M, 1) - distance = np.sum( - (points - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True - ) + distance = np.sum((points - coords[:, np.newaxis]) ** 2.0, axis=2, keepdims=True) # At each center, multiply Each Distance of a Coordinate, with its exponents. exponen = np.exp(-np.einsum("MND, MK-> MNK", distance, em)) # At each center, multiply the exponential with its coefficients. @@ -908,7 +867,7 @@ def _transform_coordinate(real_pt, i_var, promol): # Distance to centers/nuclei`s and Prefactors. diff_coords = real_pt[: i_var + 1] - coords[:, : i_var + 1] - diff_squared = diff_coords ** 2.0 + diff_squared = diff_coords**2.0 distance = np.sum(diff_squared[:, :i_var], axis=1)[:, np.newaxis] # If i_var is zero, then distance is just all zeros. @@ -917,9 +876,7 @@ def _transform_coordinate(real_pt, i_var, promol): # Get the integral of Gaussian till a point excluding a prefactor. # prefactor (pi / exponents) is included in `gaussian_integrals`. - cdf_gauss = promol.integration_gaussian_till_point( - diff_coords, i_var, with_factor=False - ) + cdf_gauss = promol.integration_gaussian_till_point(diff_coords, i_var, with_factor=False) # Final Result. transf_num = np.sum(single_gauss * cdf_gauss) @@ -1016,6 +973,7 @@ def is_same_sign(x, y): f_l_bnd = _root_equation(l_bnd, *args) f_u_bnd = _root_equation(u_bnd, *args) + # Get Index of the one that is closest to zero, the one that needs to change. f_bnds = np.abs([f_l_bnd, f_u_bnd]) idx = f_bnds.argmin() @@ -1035,8 +993,10 @@ def is_same_sign(x, y): counter += 1 if counter == maxiter: - raise RuntimeError(f"Couldn't find correct bounds {bracket} for the root-solver " - f"to solve for the inverse.") + raise RuntimeError( + f"Couldn't find correct bounds {bracket} for the root-solver " + f"to solve for the inverse." + ) return tuple(bounds) # Set up Arguments for root_equation with dynamic bracketing. @@ -1065,10 +1025,7 @@ def _pad_coeffs_exps_with_zeros(coeffs, exps): dtype=np.float64, ) exps = np.array( - [ - np.pad(a, (0, max_numb_of_gauss - len(a)), "constant", constant_values=0.0) - for a in exps - ], + [np.pad(a, (0, max_numb_of_gauss - len(a)), "constant", constant_values=0.0) for a in exps], dtype=np.float64, ) return coeffs, exps diff --git a/src/grid/tests/test_protransform.py b/src/grid/tests/test_protransform.py index 3f7411bb0..9a0037989 100644 --- a/src/grid/tests/test_protransform.py +++ b/src/grid/tests/test_protransform.py @@ -46,7 +46,7 @@ def setUp(self, ss=0.1, return_obj=False, add_boundary=True): params = _PromolParams(c, e, coord, dim=3) if return_obj: num_pts = int(2 / ss) + 1 - weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + weights = np.array([2.0 / (num_pts - 2)] * num_pts) if add_boundary: l_bnd = -1.0 end_point = True @@ -54,11 +54,16 @@ def setUp(self, ss=0.1, return_obj=False, add_boundary=True): l_bnd = -0.99 end_point = False oned = OneDGrid( - np.linspace(l_bnd, 1, num=num_pts, endpoint=end_point), weights, domain=(-1, 1), + np.linspace(l_bnd, 1, num=num_pts, endpoint=end_point), + weights, + domain=(-1, 1), ) obj = CubicProTransform( - [oned, oned, oned], params.c_m, params.e_m, params.coords, + [oned, oned, oned], + params.c_m, + params.e_m, + params.coords, ) return params, obj return params @@ -98,8 +103,9 @@ def test_promolecular_density(self): desired = params.promolecular(grid) assert np.all(np.abs(np.array(true_ans) - desired) < 1e-8) - @pytest.mark.parametrize("pts, add_boundary", [[np.arange(-5.0, 5.0, 0.5), False], - [np.arange(-5.0, 5.0, 0.5), True]]) + @pytest.mark.parametrize( + "pts, add_boundary", [[np.arange(-5.0, 5.0, 0.5), False], [np.arange(-5.0, 5.0, 0.5), True]] + ) def test_transforming_x_against_formula(self, pts, add_boundary): r"""Test transformming the X-transformation against analytic formula.""" for pt in pts: @@ -107,13 +113,9 @@ def test_transforming_x_against_formula(self, pts, add_boundary): def formula_transforming_x(x): r"""Return closed form formula for transforming x coordinate.""" - first_factor = (5.0 * np.pi ** 1.5 / (4 * 2 ** 0.5)) * ( - erf(2 ** 0.5 * (x - 1)) + 1.0 - ) + first_factor = (5.0 * np.pi**1.5 / (4 * 2**0.5)) * (erf(2**0.5 * (x - 1)) + 1.0) - sec_fac = ((10.0 * np.pi ** 1.5) / (6.0 * 3 ** 0.5)) * ( - erf(3.0 ** 0.5 * (x - 2)) + 1.0 - ) + sec_fac = ((10.0 * np.pi**1.5) / (6.0 * 3**0.5)) * (erf(3.0**0.5 * (x - 2)) + 1.0) ans = (first_factor + sec_fac) / ( 5.0 * (np.pi / 2) ** 1.5 + 10.0 * (np.pi / 3.0) ** 1.5 @@ -122,9 +124,13 @@ def formula_transforming_x(x): assert np.abs(true_ans - formula_transforming_x(pt)) < 1e-8 - @pytest.mark.parametrize("pts_xy, add_boundary", - [[np.random.uniform(-10.0, 10.0, size=(100, 2)), False], - [np.random.uniform(-10.0, 10.0, size=(100, 2)), True]]) + @pytest.mark.parametrize( + "pts_xy, add_boundary", + [ + [np.random.uniform(-10.0, 10.0, size=(100, 2)), False], + [np.random.uniform(-10.0, 10.0, size=(100, 2)), True], + ], + ) def test_transforming_y_against_formula(self, pts_xy, add_boundary): r"""Test transforming the Y-transformation against analytic formula.""" for x, y in pts_xy: @@ -133,17 +139,9 @@ def test_transforming_y_against_formula(self, pts_xy, add_boundary): def formula_transforming_y(x, y): r"""Return closed form formula for transforming y coordinate.""" fac1 = 5.0 * np.sqrt(np.pi / 2.0) * np.exp(-2.0 * (x - 1) ** 2) - fac1 *= ( - np.sqrt(np.pi) - * (erf(2.0 ** 0.5 * (y - 2)) + 1.0) - / (2.0 * np.sqrt(2.0)) - ) + fac1 *= np.sqrt(np.pi) * (erf(2.0**0.5 * (y - 2)) + 1.0) / (2.0 * np.sqrt(2.0)) fac2 = 10.0 * np.sqrt(np.pi / 3.0) * np.exp(-3.0 * (x - 2.0) ** 2.0) - fac2 *= ( - np.sqrt(np.pi) - * (erf(3.0 ** 0.5 * (y - 2)) + 1.0) - / (2.0 * np.sqrt(3.0)) - ) + fac2 *= np.sqrt(np.pi) * (erf(3.0**0.5 * (y - 2)) + 1.0) / (2.0 * np.sqrt(3.0)) num = fac1 + fac2 dac1 = 5.0 * (np.pi / 2.0) * np.exp(-2.0 * (x - 1.0) ** 2.0) @@ -153,9 +151,13 @@ def formula_transforming_y(x, y): assert np.abs(true_ans - formula_transforming_y(x, y)) < 1e-8 - @pytest.mark.parametrize("pts, add_boundary", - [[np.random.uniform(-10.0, 10.0, size=(100, 3)), False], - [np.random.uniform(-10.0, 10.0, size=(100, 3)), True]]) + @pytest.mark.parametrize( + "pts, add_boundary", + [ + [np.random.uniform(-10.0, 10.0, size=(100, 3)), False], + [np.random.uniform(-10.0, 10.0, size=(100, 3)), True], + ], + ) def test_transforming_z_against_formula(self, pts, add_boundary): r"""Test transforming the Z-transformation against analytic formula.""" params, obj = self.setUp(ss=0.5, return_obj=True, add_boundary=add_boundary) @@ -163,25 +165,13 @@ def test_transforming_z_against_formula(self, pts, add_boundary): def formula_transforming_z(x, y, z): r"""Return closed form formula for transforming z coordinate.""" a1, a2, a3 = (x - 1.0), (y - 2.0), (z - 3.0) - erfx = erf(2.0 ** 0.5 * a3) + 1.0 - fac1 = ( - 5.0 - * np.exp(-2.0 * (a1 ** 2.0 + a2 ** 2.0)) - * erfx - * np.pi ** 0.5 - / (2.0 * 2.0 ** 0.5) - ) + erfx = erf(2.0**0.5 * a3) + 1.0 + fac1 = 5.0 * np.exp(-2.0 * (a1**2.0 + a2**2.0)) * erfx * np.pi**0.5 / (2.0 * 2.0**0.5) b1, b2, b3 = (x - 2.0), (y - 2.0), (z - 2.0) - erfy = erf(3.0 ** 0.5 * b3) + 1.0 - fac2 = ( - 10.0 - * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) - * erfy - * np.pi ** 0.5 - / (2.0 * 3.0 ** 0.5) - ) - den = 5.0 * (np.pi / 2.0) ** 0.5 * np.exp(-2.0 * (a1 ** 2.0 + a2 ** 2.0)) - den += 10.0 * (np.pi / 3.0) ** 0.5 * np.exp(-3.0 * (b1 ** 2.0 + b2 ** 2.0)) + erfy = erf(3.0**0.5 * b3) + 1.0 + fac2 = 10.0 * np.exp(-3.0 * (b1**2.0 + b2**2.0)) * erfy * np.pi**0.5 / (2.0 * 3.0**0.5) + den = 5.0 * (np.pi / 2.0) ** 0.5 * np.exp(-2.0 * (a1**2.0 + a2**2.0)) + den += 10.0 * (np.pi / 3.0) ** 0.5 * np.exp(-3.0 * (b1**2.0 + b2**2.0)) return -1.0 + 2.0 * (fac1 + fac2) / den for x, y, z in pts: @@ -199,8 +189,8 @@ def test_transforming_simple_grid(self): ss = 1.0 params, obj = self.setUp(ss, return_obj=True) num_pt = int(2.0 / ss) + 1 # number of points in one-direction. - assert obj.points.shape == (num_pt ** 3, 3) - non_boundary_pt_index = num_pt ** 2 + num_pt + 1 + assert obj.points.shape == (num_pt**3, 3) + non_boundary_pt_index = num_pt**2 + num_pt + 1 real_pt = obj.points[non_boundary_pt_index] # Test that this point is not the boundary. assert real_pt[0] != np.inf @@ -472,14 +462,12 @@ def setUp(self, ss=0.1, return_obj=False): params = _PromolParams(c, e, coord, dim=3) if return_obj: num_pts = int(1 / ss) + 1 - weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + weights = np.array([2.0 / (num_pts - 2)] * num_pts) oned_x = OneDGrid( np.linspace(-1, 1, num=num_pts, endpoint=True), weights, domain=(-1, 1) ) - obj = CubicProTransform( - [oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords - ) + obj = CubicProTransform([oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords) return params, obj return params @@ -500,9 +488,7 @@ def promolecular_in_x(grid, every_grid): promol_x, promol_x_all = promolecular_in_x(grid, every_grid) # Integration over y and z cancel out from numerator and denominator. - actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz( - promol_x_all, every_grid - ) + actual = -1.0 + 2.0 * np.trapz(promol_x, grid) / np.trapz(promol_x_all, every_grid) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("pts_xy", [np.random.uniform(-10.0, 10.0, size=(100, 2))]) @@ -524,9 +510,7 @@ def promolecular_in_y(grid, every_grid): # Integration over z cancel out from numerator and denominator. # Further, gaussian at a point does too. - actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz( - promol_y_all, every_grid - ) + actual = -1.0 + 2.0 * np.trapz(promol_y, grid) / np.trapz(promol_y_all, every_grid) assert np.abs(true_ans - actual) < 1e-5 @pytest.mark.parametrize("pts", [np.random.uniform(-10.0, 10.0, size=(100, 3))]) @@ -545,9 +529,7 @@ def promolecular_in_z(grid, every_grid): every_grid = np.arange(-5.0, 10.0, 0.00001) # Full Integration promol_z_all, promol_z = promolecular_in_z(grid, every_grid) - actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz( - promol_z_all, every_grid - ) + actual = -1.0 + 2.0 * np.trapz(promol_z, grid) / np.trapz(promol_z_all, every_grid) true_ans = _transform_coordinate([x, y, z], 2, self.setUp()) assert np.abs(true_ans - actual) < 1e-4 @@ -596,14 +578,12 @@ def setUp(self, ss=0.1): params = _PromolParams(c, e, coord, dim=3) num_pts = int(2 / ss) + 1 - weights = np.array([(2.0 / (num_pts - 2))] * num_pts) + weights = np.array([2.0 / (num_pts - 2)] * num_pts) oned_x = OneDGrid( np.linspace(-1.0, 1.0, num=num_pts, endpoint=True), weights, domain=(-1, 1) ) - obj = CubicProTransform( - [oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords - ) + obj = CubicProTransform([oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords) return params, obj, [oned_x, oned_x, oned_x] def test_interpolate_cubic_function(self): @@ -619,7 +599,7 @@ def func(pts): ( np.random.uniform(1.0, 1.5, (100,)).T, np.random.uniform(1.5, 2.5, (100,)).T, - np.random.uniform(2.5, 3.5, (100,)).T + np.random.uniform(2.5, 3.5, (100,)).T, ) ).T actuals = obj.interpolate(grid, func(obj.points), oned_grids, use_log=False) @@ -632,7 +612,7 @@ def test_interpolate_gaussian(self): # Function to interpolate. def func(pts, alpha=2.0): - return np.exp(-alpha * np.linalg.norm(pts - param.coords[0], axis=1)**2.0) + return np.exp(-alpha * np.linalg.norm(pts - param.coords[0], axis=1) ** 2.0) # Test over a grid. Pytest isn't used for effiency reasons. # TODO: the grid points need to be close to center to achieve good accuracy. @@ -640,7 +620,7 @@ def func(pts, alpha=2.0): ( np.random.uniform(0.75, 1.25, (100,)).T, np.random.uniform(0.75, 2.25, (100,)).T, - np.random.uniform(3.75, 3.25, (100,)).T + np.random.uniform(3.75, 3.25, (100,)).T, ) ).T actuals = obj.interpolate(real_grid, func(obj.points), oned_grids, use_log=True) @@ -656,11 +636,12 @@ def func(pts): return (pts[:, 0] - 1.0) * (pts[:, 1] - 2.0) * (pts[:, 2] - 3.0) def derivative(pts): - return np.vstack([ - (pts[:, 1] - 2.0) * (pts[:, 2] - 3.0), - (pts[:, 0] - 1.0) * (pts[:, 2] - 3.0), - (pts[:, 0] - 1.0) * (pts[:, 1] - 2.0), - ] + return np.vstack( + [ + (pts[:, 1] - 2.0) * (pts[:, 2] - 3.0), + (pts[:, 0] - 1.0) * (pts[:, 2] - 3.0), + (pts[:, 0] - 1.0) * (pts[:, 1] - 2.0), + ] ).T # Test over a grid. Pytest isn't used for effiency reasons. @@ -668,7 +649,7 @@ def derivative(pts): ( np.random.uniform(1.0, 1.5, (100,)).T, np.random.uniform(1.5, 2.5, (100,)).T, - np.random.uniform(2.5, 3.5, (100,)).T + np.random.uniform(2.5, 3.5, (100,)).T, ) ).T actual = obj.interpolate(grid, func(obj.points), oned_grids, nu=1) @@ -685,10 +666,11 @@ def func(pts): def derivative(pts): x, y, z = pts[:, 0], pts[:, 1], pts[:, 2] - return np.vstack([ - 2.0 * (x - 1.0) * (y - 2.0) ** 2.0 * (z - 3.0) ** 2.0, - 2.0 * (x - 1.0) ** 2.0 * (y - 2.0) * (z - 3.0) ** 2.0, - 2.0 * (x - 1.0) ** 2.0 * (y - 2.0) ** 2.0 * (z - 3.0), + return np.vstack( + [ + 2.0 * (x - 1.0) * (y - 2.0) ** 2.0 * (z - 3.0) ** 2.0, + 2.0 * (x - 1.0) ** 2.0 * (y - 2.0) * (z - 3.0) ** 2.0, + 2.0 * (x - 1.0) ** 2.0 * (y - 2.0) ** 2.0 * (z - 3.0), ] ).T @@ -697,7 +679,7 @@ def derivative(pts): ( np.random.uniform(1.0, 1.5, (100,)).T, np.random.uniform(1.5, 2.5, (100,)).T, - np.random.uniform(2.5, 3.5, (100,)).T + np.random.uniform(2.5, 3.5, (100,)).T, ) ).T actual = obj.interpolate(grid, func(obj.points), oned_grids, nu=1, use_log=False) @@ -724,9 +706,7 @@ def setUp_one_gaussian(self, ss=0.03): params = _PromolParams(c, e, coord, dim=3) num_pts = int(1 / ss) + 1 oned_x = GaussChebyshevLobatto(num_pts) - obj = CubicProTransform( - [oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords - ) + obj = CubicProTransform([oned_x, oned_x, oned_x], params.c_m, params.e_m, params.coords) return params, obj def test_integration_perturbed_gaussian_with_promolecular_trick(self): @@ -755,12 +735,10 @@ def test_padding_arrays(): exps = np.array([[4.0, 5.0], [5.0, 6.0, 7.0, 8.0], [9.0]], dtype=object) coeff_pad, exps_pad = _pad_coeffs_exps_with_zeros(coeff, exps) coeff_desired = np.array( - [[1.0, 2.0, 0.0, 0.0], [1.0, 2.0, 3.0, 4.0], [5.0, 0.0, 0.0, 0.0]], - dtype=object + [[1.0, 2.0, 0.0, 0.0], [1.0, 2.0, 3.0, 4.0], [5.0, 0.0, 0.0, 0.0]], dtype=object ) np.testing.assert_array_equal(coeff_desired, coeff_pad) exp_desired = np.array( - [[4.0, 5.0, 0.0, 0.0], [5.0, 6.0, 7.0, 8.0], [9.0, 0.0, 0.0, 0.0]], - dtype=object + [[4.0, 5.0, 0.0, 0.0], [5.0, 6.0, 7.0, 8.0], [9.0, 0.0, 0.0, 0.0]], dtype=object ) np.testing.assert_array_equal(exp_desired, exps_pad)