Beispiel #1
0
    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)
Beispiel #2
0
    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)
Beispiel #3
0
    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)
Beispiel #4
0
    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
Beispiel #5
0
    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)
Beispiel #6
0
    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)
Beispiel #7
0
    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)
Beispiel #8
0
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
Beispiel #9
0
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