def set_up_test_arrays(self, length=100, rank=3): ''' Helper for set up arrays to test math''' MA1 = MatrixArray(length=length, rank=rank) MA1['A', 'A'] = np.ones(length) * 2.0 MA1['B', 'B'] = np.ones(length) MA1['C', 'C'] = np.ones(length) * 3.0 MA1['B', 'C'] = np.ones(length) * 3.0 MA2 = MatrixArray(length=length, rank=rank) MA2['A', 'A'] = np.arange(length) MA2['B', 'B'] = np.ones(length) * 2.0 MA2['C', 'C'] = np.ones(length) * -3.0 array1 = np.zeros((length, rank, rank)) array1[:, 0, 0] = np.ones(length) * 2.0 array1[:, 1, 1] = np.ones(length) array1[:, 2, 2] = np.ones(length) * 3.0 array1[:, 1, 2] = np.ones(length) * 3.0 array1[:, 2, 1] = np.ones(length) * 3.0 array2 = np.zeros((length, rank, rank)) array2[:, 0, 0] = np.arange(length) array2[:, 1, 1] = np.ones(length) * 2.0 array2[:, 2, 2] = np.ones(length) * -3.0 return (MA1, MA2), (array1, array2)
def __init__(self, types): r'''Constructor Arguments --------- types: list List of types of sites Attributes ---------- density: :class:`pyPRISM.core.ValueTable` Table of site number density values total: float Total number density site: :class:`pyPRISM.core.MatrixArray` Site density for each pair. pair: :class:`pyPRISM.core.MatrixArray` Pair site density for each pair. ''' self.types = types self.density = ValueTable(types=types, name='density') self.total = 0. self.pair = MatrixArray(length=1, rank=len(types), types=types, space=Space.NonSpatial) self.site = MatrixArray(length=1, rank=len(types), types=types, space=Space.NonSpatial)
def test_MatrixArray_export(self): types = ['A', 'B', 'C'] length = 1024 rank = len(types) values1 = np.ones(length) values2 = np.ones(length) * 5.0 values3 = np.ones(length) * 2.1234 MA1 = MatrixArray(length=length, rank=rank, space=Space.Fourier) MA1['A', 'A'] = values2 MA1['A', 'B'] = values1 MA1['A', 'C'] = values1 MA1['B', 'B'] = values3 MA1['B', 'C'] = values3 MA1['C', 'C'] = values3 ntypes = len(types) PT = PairTable(types, 'density') PT[['A'], ['B', 'C']] = values1 PT[['A'], ['A']] = values2 PT.setUnset(values3) MA2 = PT.exportToMatrixArray(space=Space.Fourier) np.testing.assert_array_almost_equal(MA1.data, MA2.data) self.assertEqual(MA1.space, MA2.space)
def exportToMatrixArray(self, space=Space.Real): '''Convenience function for converting a table of arrays to a MatrixArray .. warning:: This only works if the PairTable contains numerical data that is all of the same shape that can be cast into a np.ndarray like object. ''' lengths = [] for i, t, val in self.iterpairs(): if val is None: raise ValueError( 'Can\'t export not-fully specified Table {}'.format( self.name)) lengths.append(len(val)) if not len(set(lengths)) <= 1: raise ValueError( 'Arrays in Table are not all the same length. Aborting export.' ) length = lengths[0] rank = len(self.types) MA = MatrixArray(length=length, rank=rank, space=space, types=self.types) for i, (t1, t2), val in self.iterpairs(): MA[t1, t2] = val return MA
def test_assign(self): '''Can we create and assign values?''' length = 100 rank = 8 MA = MatrixArray(length=length, rank=rank) # Make sure the array starts as all zeros array = np.zeros((length, rank, rank)) np.testing.assert_array_almost_equal(MA.data, array) # Test assignment (especially off diagonal) array = np.zeros((length, rank, rank)) MA['B', 'B'] = np.ones(length) MA['B', 'C'] = np.ones(length) * 3.0 array[:, 1, 1] = np.ones(length) array[:, 1, 2] = np.ones(length) * 3.0 array[:, 2, 1] = np.ones(length) * 3.0 np.testing.assert_array_almost_equal(MA.data, array)
def test_MatrixArray_loop(self): '''Can we transform an entire MatrixArray?''' length = 1024 rank = 3 dr = 0.1 d = Domain(length=length, dr=dr) array1 = np.sin(np.arange(0, 10 * np.pi, 0.01))[:length] array2 = 5 * np.sin(np.arange(0, 10 * np.pi, 0.01))[:length] array3 = np.cos(np.arange(0, 10 * np.pi, 0.01))[:length] array4 = np.cos(np.arange(0, 10 * np.pi, 0.01))[:length] + np.sin( np.arange(0, 10 * np.pi, 0.01))[:length] MA = MatrixArray(length=length, rank=rank) MA['A', 'A'] = array1 MA['B', 'B'] = array2 MA['B', 'C'] = array3 MA['C', 'C'] = array4 d.MatrixArray_to_fourier(MA) np.testing.assert_array_almost_equal(MA['A', 'A'], d.to_fourier(array1)) np.testing.assert_array_almost_equal(MA['B', 'B'], d.to_fourier(array2)) np.testing.assert_array_almost_equal(MA['B', 'C'], d.to_fourier(array3)) np.testing.assert_array_almost_equal(MA['C', 'B'], d.to_fourier(array3)) np.testing.assert_array_almost_equal(MA['C', 'C'], d.to_fourier(array4)) # This should recover the original values of the MatrixArray d.MatrixArray_to_real(MA) np.testing.assert_array_almost_equal(MA['A', 'A'], array1) np.testing.assert_array_almost_equal(MA['B', 'B'], array2) np.testing.assert_array_almost_equal(MA['B', 'C'], array3) np.testing.assert_array_almost_equal(MA['C', 'B'], array3) np.testing.assert_array_almost_equal(MA['C', 'C'], array4)
def __init__(self, sys): self.sys = deepcopy(sys) # Need to set the potential for each closure object for (i, j), (t1, t2), U in self.sys.potential.iterpairs(): if isinstance(self.sys.closure[t1, t2], AtomicClosure): #only set sigma if not set directly in potential if U.sigma is None: U.sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].potential = U.calculate( self.sys.domain.r) / self.sys.kT elif isinstance(self.sys.closure[t1, t2], MolecularClosure): raise NotImplementedError( 'Molecular closures are not fully implemented in this release.' ) #only set sigma if not set directly in potential if U.sigma is None: U.sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].potential = U.calculate_attractive( self.sys.domain.r) / self.sys.kT #cost function input and output self.x = np.zeros(sys.rank * sys.rank * sys.domain.length) self.y = np.zeros(sys.rank * sys.rank * sys.domain.length) # The omega objects must be converted to a MatrixArray of the actual correlation # function values rather than a table of OmegaObjects. applyFunc = lambda x: x.calculate(sys.domain.k) self.omega = self.sys.omega.apply( applyFunc, inplace=False).exportToMatrixArray(space=Space.Fourier) self.omega *= sys.density.site #omega should always be scaled by site density # Spaces are set based on when they are used in self.cost(...). In some cases, # this is redundant because these array's will be overwritten with copies and # then their space will be inferred from their parent MatrixArrays self.directCorr = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Real, types=sys.types) self.totalCorr = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Fourier, types=sys.types) self.GammaIn = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Real, types=sys.types) self.GammaOut = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Real, types=sys.types) self.OC = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Fourier, types=sys.types) self.I = IdentityMatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Fourier, types=sys.types)
class PRISM: r'''Primary container for a storing a PRISM calculation Each pyPRISM.PRISM object serves as an encapsulation of a fully specified PRISM problem including all inputs needed for the calculation and the function to be numerically minimized. Attributes ---------- domain: pyPRISM.Domain The Domain object fully specifies the Real- and Fourier- space solution grids. directCorr: pyPRISM.MatrixArray The direct correlation function for all pairs of site types omega: pyPRISM.MatrixArray The intra-molecular correlation function for all pairs of site types closure: pyPRISM.core.PairTable of pyPRISM.closure.Closure Table of closure objects used to generate the direct correlation functions (directCorr) pairCorr: pyPRISM.MatrixArray The *inter*-molecular pair correlation functions for all pairs of site types. Also commonly refered to as the radial distribution functions. totalCorr: pyPRISM.MatrixArray The *inter*-molecular total correlation function is simply the pair correlation function y-shifted by 1.0 i.e. totalCorr = pairCorr - 1.0 potential: pyPRISM.MatrixArray Interaction potentials for all pairs of sites GammaIn,GammaOut: pyPRISM.MatrixArray Primary inputs and outputs of the PRISM cost function. Gamma is defined as "totalCorr - directCorr" (in Fourier space) and results from a change of variables used to remove divergences in the closure relations. OC,IOC,I,etc: pyPRISM.MatrixArray Various MatrixArrays used as intermediates in the PRISM functional. These arrays are pre-allocated and stored for efficiency. x,y: float np.ndarray Current inputs and outputs of the cost function pairDensityMatrix: float np.ndarray Rank by rank array of pair densities between sites. See :class:`pyPRISM.core.Density` siteDensityMatrix: float np.ndarray Rank by rank array of site densities. See :class:`pyPRISM.core.Density` Methods ------- cost: Primary cost function used to define the criteria of a "converged" PRISM solution. The numerical solver will be given this function and will attempt to find the inputs (self.x) that make the outputs (self.y) as close to zero as possible. ''' def __init__(self, sys): self.sys = deepcopy(sys) # Need to set the potential for each closure object for (i, j), (t1, t2), U in self.sys.potential.iterpairs(): if isinstance(self.sys.closure[t1, t2], AtomicClosure): #only set sigma if not set directly in potential if U.sigma is None: U.sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].potential = U.calculate( self.sys.domain.r) / self.sys.kT elif isinstance(self.sys.closure[t1, t2], MolecularClosure): raise NotImplementedError( 'Molecular closures are not fully implemented in this release.' ) #only set sigma if not set directly in potential if U.sigma is None: U.sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].sigma = self.sys.diameter[t1, t2] self.sys.closure[t1, t2].potential = U.calculate_attractive( self.sys.domain.r) / self.sys.kT #cost function input and output self.x = np.zeros(sys.rank * sys.rank * sys.domain.length) self.y = np.zeros(sys.rank * sys.rank * sys.domain.length) # The omega objects must be converted to a MatrixArray of the actual correlation # function values rather than a table of OmegaObjects. applyFunc = lambda x: x.calculate(sys.domain.k) self.omega = self.sys.omega.apply( applyFunc, inplace=False).exportToMatrixArray(space=Space.Fourier) self.omega *= sys.density.site #omega should always be scaled by site density # Spaces are set based on when they are used in self.cost(...). In some cases, # this is redundant because these array's will be overwritten with copies and # then their space will be inferred from their parent MatrixArrays self.directCorr = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Real, types=sys.types) self.totalCorr = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Fourier, types=sys.types) self.GammaIn = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Real, types=sys.types) self.GammaOut = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Real, types=sys.types) self.OC = MatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Fourier, types=sys.types) self.I = IdentityMatrixArray(length=sys.domain.length, rank=sys.rank, space=Space.Fourier, types=sys.types) def __repr__(self): return '<PRISM length:{} rank:{}>'.format(self.sys.domain.length, self.sys.rank) def cost(self, x): r'''Cost function There are likely several cost functions that could be imagined using the PRISM equations. In this case we formulate a self-consistent formulation where we expect the input of the PRISM equations to be identical to the output. .. image:: ../../img/numerical_method.svg :width: 300px The goal of the solve method is to numerically optimize the input (:math:`r \gamma_{in}`) so that the output (:math:`r(\gamma_{in}-\gamma_{out})`) is minimized to zero. ''' self.x = x #store input # The np.copy is important otherwise x saves state between calls to # this function. self.GammaIn.data = np.copy( x.reshape((-1, self.sys.rank, self.sys.rank))) self.GammaIn /= self.sys.domain.long_r # directCorr is calculated directly in Real space but immediately # inverted to Fourier space. We must reset this from the last call. self.directCorr.space = Space.Real for (i, j), (t1, t2), closure in self.sys.closure.iterpairs(): if isinstance(closure, AtomicClosure): self.directCorr[t1, t2] = closure.calculate( self.sys.domain.r, self.GammaIn[t1, t2]) elif isinstance(closure, MolecularClosure): raise NotImplementedError( 'Molecular closures are untested and not fully implemented.' ) self.directCorr[t1, t2] = closure.calculate( self.GammaIn[t1, t2], self.omega[t1, t1], self.omega[t2, t2]) else: raise ValueError('Closure type not recognized') self.sys.domain.MatrixArray_to_fourier(self.directCorr) self.OC = self.omega.dot(self.directCorr) self.IOC = self.I - self.OC self.IOC.invert(inplace=True) self.totalCorr = self.IOC.dot(self.OC).dot(self.omega) self.totalCorr /= self.sys.density.pair self.GammaOut = self.totalCorr - self.directCorr self.sys.domain.MatrixArray_to_real(self.GammaOut) self.y = self.sys.domain.long_r * (self.GammaOut.data - self.GammaIn.data) return self.y.reshape((-1, )) def solve(self, guess=None, method='krylov', options=None): '''Attempt to numerically solve the PRISM equations Using the supplied inputs (in the constructor), we attempt to numerically solve the PRISM equations using the scheme laid out in :func:`cost`. If the numerical solution process is successful, the attributes of this class will contain the solved values for a given input i.e. self.totalCorr will contain the numerically optimized (solved) total correlation functions. This function also does basic checks to ensure that the results are physical. At this point, this consists of checking to make sure that the pair correlation functions are not negative. If this isn't true a warning is issued to the user. Parameters ---------- guess: np.ndarray, size (rank*rank*length) The initial guess of :math:`\gamma` to the numerical solution process. The numpy array should be of size rank x rank x length corresponding to the a full flattened MatrixArray. If not specified, an initial guess of all zeros is used. method: string Set the type of optimization scheme to use. The scipy documentation for `scipy.optimize.root <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root.html>`__ details the possible values for this parameter. options: dict Dictionary of options specific to the chosen solver method. The scipy documentation for `scipy.optimize.root <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root.html>`__ details the possible values for this parameter. ''' if guess is None: guess = np.zeros(self.sys.rank * self.sys.rank * self.sys.domain.length) if options is None: options = {'disp': True} self.minimize_result = root(self.cost, guess, method=method, options=options) if self.totalCorr.space == Space.Fourier: self.sys.domain.MatrixArray_to_real(self.totalCorr) tol = 1e-5 warnstr = 'Pair correlations are negative (value = {:3.2e}) for {}-{} pair!' for i, (t1, t2), H in self.totalCorr.iterpairs(): if np.any(H < -(1.0 + tol)): val = np.min(H) warnings.warn(warnstr.format(val, t1, t2)) return self.minimize_result
def pmf(PRISM): r'''Calculate the potentials of mean force Parameters ---------- PRISM: pyPRISM.core.PRISM A **solved** PRISM object. Returns ------- pmf: pyPRISM.core.MatrixArray The full MatrixArray of potentials of mean force **Mathematical Definition** .. math:: w_{\alpha,\beta}(r) = -k_{B} T \ln(h_{\alpha,\beta}(r)+1.0) **Variable Definitions** - :math:`w_{\alpha,\beta}(r)` Potential of mean force between site types :math:`\alpha` and :math:`\beta` at a distance :math:`r` - :math:`g_{\alpha,\beta}(r)` Pair correlation function between site types :math:`\alpha` and :math:`\beta` at a distance :math:`r` - :math:`h_{\alpha,\beta}(r)` Total correlation function between site types :math:`\alpha` and :math:`\beta` at a distance :math:`r` **Description** A potential of mean force (PMF) between site types :math:`\alpha` and :math:`\beta`, :math:`w_{\alpha,\beta}` represents the the ensemble averaged free energy change needed to bring these two sites from infinite separation to a distance :math:`r`. It can also be thought of as a potential that would be needed to reproduce the underlying :math:`g_{\alpha,\beta}(r)`. .. warning:: Passing an unsolved PRISM object to this function will still produce output based on the default values of the attributes of the PRISM object. Example ------- .. code-block:: python import pyPRISM sys = pyPRISM.System(['A','B']) # ** populate system variables ** PRISM = sys.createPRISM() PRISM.solve() pmf = pyPRISM.calculate.pmf(PRISM) pmf_BB = pmf['B','B'] ''' rdf = pair_correlation(PRISM) #let's ignore any warnings about negative values in the log function with np.errstate(invalid='ignore'): rdf = -1.0 * PRISM.sys.kT * np.log(rdf.data) #length and rank will be inferred from data pmf = MatrixArray(data=rdf, space=Space.Real, length=None, rank=None, types=PRISM.sys.types) return pmf