Example #1
0
    def __init__(self, lf, occ_model, projector):
        '''Localize canonical HF orbitals.

           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model.

           Projector
                Projectors for atomic basis function. A list of TwoIndex
                instances.

           **Optional arguments:**

        '''
        self._lf = lf
        self._proj = projector
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        self._nvirt = (lf.default_nbasis - occ_model.noccs[0])
        self._cache = Cache()
        self._locblock = None
        self._popmatrix = None
Example #2
0
    def __init__(self, lf, nbasis, occ_model=None, norb=None):
        """
           **Arguments:**

           lf
                A LinalgFactory instance.

           nbasis
                The number of basis functions.

           **Optional arguments:**

           occ_model
                A model to assign new occupation numbers when the orbitals are
                updated by a diagonalization of a Fock matrix.

           norb
               the number of orbitals (occupied + virtual). When not given,
               it is set to nbasis.
        """
        self._lf = lf
        self._nbasis = nbasis
        self._occ_model = occ_model
        if norb is None:
            self._norb = nbasis
        else:
            self._norb = norb
        # The cache is used to store different representations of the
        # wavefunction, i.e. as expansion, as density matrix or both.
        self._cache = Cache()
        # Write some screen log
        self._log_init()
Example #3
0
    def __init__(self, system, grid, local, slow, lmax, moldens=None):
        '''
           **Arguments:**

           system
                The system to be partitioned.

           grid
                The integration grid

           local
                Whether or not to use local (non-periodic) grids.

           slow
                When ``True``, also the AIM properties are computed that use the
                AIM overlap operators.

           lmax
                The maximum angular momentum in multipole expansions.

           **Optional arguments:**

           moldens
                The all-electron density grid data.
        '''
        JustOnceClass.__init__(self)
        self._system = system
        self._grid = grid
        self._local = local
        self._slow = slow
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()
        # Caching of work arrays to avoid reallocation
        if moldens is not None:
            self._cache.dump('moldens', moldens)

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()
Example #4
0
    def __init__(self, lf, nbasis, occ_model=None, norb=None):
        """
           **Arguments:**

           lf
                A LinalgFactory instance.

           nbasis
                The number of basis functions.

           **Optional arguments:**

           occ_model
                A model to assign new occupation numbers when the orbitals are
                updated by a diagonalization of a Fock matrix.

           norb
               the number of orbitals (occupied + virtual). When not given,
               it is set to nbasis.
        """
        self._lf = lf
        self._nbasis = nbasis
        self._occ_model = occ_model
        if norb is None:
            self._norb = nbasis
        else:
            self._norb = norb
        # The cache is used to store different representations of the
        # wavefunction, i.e. as expansion, as density matrix or both.
        self._cache = Cache()
        # Write some screen log
        self._log_init()
Example #5
0
    def __init__(self, terms, external=None):
        '''
           **Arguments:**

           terms
                The terms in the Hamiltonian.

           **Optional arguments:**

           external
                A dictionary with external energy contributions that do not
                depend on the wavefunction, e.g. nuclear-nuclear interactions
                or QM/MM mechanical embedding terms. Use ``nn`` as key for the
                nuclear-nuclear term.
        '''
        # check arguments:
        if len(terms) == 0:
            raise ValueError(
                'At least one term must be present in the Hamiltonian.')

        # Assign attributes
        self.terms = list(terms)
        self.external = {} if external is None else external

        # Create a cache for shared intermediate results. This cache should only
        # be used for derived quantities that depend on the wavefunction and
        # need to be updated at each SCF cycle.
        self.cache = Cache()
Example #6
0
    def __init__(self, lf, occ_model, projector):
        '''Localize canonical HF orbitals.

           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model.

           Projector
                Projectors for atomic basis function. A list of TwoIndex
                instances.

           **Optional arguments:**

        '''
        self._lf = lf
        self._proj = projector
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        self._nvirt = (lf.default_nbasis-occ_model.noccs[0])
        self._cache = Cache()
        self._locblock = None
        self._popmatrix = None
Example #7
0
    def __init__(self, system, terms, grid=None, idiot_proof=True):
        '''
           **Arguments:**

           system
                The System object for which the energy must be computed.

           terms
                The terms in the Hamiltonian.

           **Optional arguments:**

           grid
                The integration grid, in case some terms need one.

           idiot_proof
                When set to False, the kinetic energy, external potential and
                Hartree terms are not added automatically and a error is raised
                when no exchange is present.
        '''
        # check arguments:
        if len(terms) == 0:
            raise ValueError('At least one term must be present in the Hamiltonian.')
        for term in terms:
            if term.require_grid and grid is None:
                raise TypeError('The term %s requires a grid, but not grid is given.' % term)

        # Assign attributes
        self.system = system
        self.terms = list(terms)
        self.grid = grid

        if idiot_proof:
            # Check if an exchange term is present
            if not any(term.exchange for term in self.terms):
                raise ValueError('No exchange term is given and idiot_proof option is set to True.')
            # Add standard terms if missing
            #  1) Kinetic energy
            if sum(isinstance(term, KineticEnergy) for term in terms) == 0:
                self.terms.append(KineticEnergy())
            #  2) Hartree (or HatreeFock, which is a subclass of Hartree)
            if sum(isinstance(term, Hartree) for term in terms) == 0:
                self.terms.append(Hartree())
            #  3) External Potential
            if sum(isinstance(term, ExternalPotential) for term in terms) == 0:
                self.terms.append(ExternalPotential())


        # Create a cache for shared intermediate results. This cache should only
        # be used for derived quantities that depend on the wavefunction and
        # need to be updated at each SCF cycle.
        self.cache = Cache()

        # bind the terms to this hamiltonian such that certain shared
        # intermediated results can be reused for the sake of efficiency.
        for term in self.terms:
            term.set_hamiltonian(self)
Example #8
0
    def __init__(self, lf, occ_model, npairs=None, nvirt=None):
        '''
           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model

           **Optional arguments:**

           npairs
                Number of electron pairs, if not specified,
                npairs = number of occupied orbitals

           nvirt
                Number of virtual orbitals, if not specified,
                nvirt = (nbasis-npairs)
        '''
        check_type('pairs', npairs, int, type(None))
        check_type('virtuals', nvirt, int, type(None))
        self._lf = lf
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        if npairs is None:
            npairs = occ_model.noccs[0]
        elif npairs >= lf.default_nbasis:
            raise ValueError(
                'Number of electron pairs (%i) larger than number of basis functions (%i)'
                % (npairs, self.nbasis))
        if nvirt is None:
            nvirt = (lf.default_nbasis - npairs)
        elif nvirt >= lf.default_nbasis:
            raise ValueError(
                'Number of virtuals (%i) larger than number of basis functions (%i)'
                % (nvirt, self.nbasis))
        self._npairs = npairs
        self._nvirt = nvirt
        self._cache = Cache()
        self._ecore = 0
        self._geminal = lf.create_two_index(npairs, nvirt)
        self._lagrange = lf.create_two_index(npairs, nvirt)
Example #9
0
    def __init__(self, system, grid, local, slow, lmax, moldens=None):
        '''
           **Arguments:**

           system
                The system to be partitioned.

           grid
                The integration grid

           local
                Whether or not to use local (non-periodic) grids.

           slow
                When ``True``, also the AIM properties are computed that use the
                AIM overlap operators.

           lmax
                The maximum angular momentum in multipole expansions.

           **Optional arguments:**

           moldens
                The all-electron density grid data.
        '''
        JustOnceClass.__init__(self)
        self._system = system
        self._grid = grid
        self._local = local
        self._slow = slow
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()
        # Caching of work arrays to avoid reallocation
        if moldens is not None:
            self._cache.dump('moldens', moldens)

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()
Example #10
0
    def __init__(self, lf, occ_model, npairs=None, nvirt=None):
        '''
           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model

           **Optional arguments:**

           npairs
                Number of electron pairs, if not specified,
                npairs = number of occupied orbitals

           nvirt
                Number of virtual orbitals, if not specified,
                nvirt = (nbasis-npairs)
        '''
        check_type('pairs', npairs, int, type(None))
        check_type('virtuals', nvirt, int, type(None))
        self._lf = lf
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        if npairs is None:
            npairs = occ_model.noccs[0]
        elif npairs >= lf.default_nbasis:
            raise ValueError('Number of electron pairs (%i) larger than number of basis functions (%i)' %(npairs, self.nbasis))
        if nvirt is None:
            nvirt = (lf.default_nbasis-npairs)
        elif nvirt >= lf.default_nbasis:
            raise ValueError('Number of virtuals (%i) larger than number of basis functions (%i)' %(nvirt, self.nbasis))
        self._npairs = npairs
        self._nvirt = nvirt
        self._cache = Cache()
        self._ecore = 0
        self._geminal = lf.create_two_index(npairs, nvirt)
        self._lagrange = lf.create_two_index(npairs, nvirt)
Example #11
0
class Geminal(object):
    '''A collection of geminals and optimization routines.

       This is just a base class that serves as a template for
       specific implementations.
    '''
    def __init__(self, lf, occ_model, npairs=None, nvirt=None):
        '''
           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model

           **Optional arguments:**

           npairs
                Number of electron pairs, if not specified,
                npairs = number of occupied orbitals

           nvirt
                Number of virtual orbitals, if not specified,
                nvirt = (nbasis-npairs)
        '''
        check_type('pairs', npairs, int, type(None))
        check_type('virtuals', nvirt, int, type(None))
        self._lf = lf
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        if npairs is None:
            npairs = occ_model.noccs[0]
        elif npairs >= lf.default_nbasis:
            raise ValueError('Number of electron pairs (%i) larger than number of basis functions (%i)' %(npairs, self.nbasis))
        if nvirt is None:
            nvirt = (lf.default_nbasis-npairs)
        elif nvirt >= lf.default_nbasis:
            raise ValueError('Number of virtuals (%i) larger than number of basis functions (%i)' %(nvirt, self.nbasis))
        self._npairs = npairs
        self._nvirt = nvirt
        self._cache = Cache()
        self._ecore = 0
        self._geminal = lf.create_two_index(npairs, nvirt)
        self._lagrange = lf.create_two_index(npairs, nvirt)

    def __call__(self, one, two, core, orb, olp, scf, **kwargs):
        '''Optimize geminal coefficients and---if required---find
           optimal set of orbitals.

           **Arguments:**

           one, two
                One- and two-body integrals (some Hamiltonian matrix elements).

           core
                The core energy (not included in 'one' and 'two').

           orb
                An expansion instance. It contains the MO coefficients
                (orbitals).

           olp
                The AO overlap matrix. A TwoIndex instance.

           scf
                A boolean. If True: Initializes orbital optimization.

           **Keywords:**
                See :py:meth:`RAp1rog.solve`
                and :py:meth:`RAp1rog.solve_scf`
        '''
        if scf:
            return self.solve_scf(one, two, core, orb, olp, **kwargs)
        else:
            return self.solve(one, two, core, orb, olp, **kwargs)

    def solve(self, one, two, core, orb, olp, **kwargs):
        raise NotImplementedError

    def solve_scf(self, one, two, core, orb, olp, **kwargs):
        raise NotImplementedError

    def _get_nbasis(self):
        '''The number of basis functions'''
        return self._nbasis

    nbasis = property(_get_nbasis)

    def _get_nocc(self):
        '''The number of occupied orbitals'''
        return self._nocc

    nocc = property(_get_nocc)

    def _get_nvirt(self):
        '''The number of virtual orbitals'''
        return self._nvirt

    nvirt = property(_get_nvirt)

    def _get_npairs(self):
        '''The number of electron pairs'''
        return self._npairs

    npairs = property(_get_npairs)

    def _get_lf(self):
        '''The LinalgFactory instance'''
        return self._lf

    lf = property(_get_lf)

    def _get_ecore(self):
        '''The core energy'''
        return self._ecore

    ecore = property(_get_ecore)

    def _get_dimension(self):
        '''The number of unknowns (i.e. the number of geminal coefficients)'''
        return self._npairs*self._nvirt

    dimension = property(_get_dimension)

    def _get_geminal(self):
        '''The geminal coefficients'''
        return self._geminal

    geminal = property(_get_geminal)

    def _get_lagrange(self):
        '''The Lagrange multipliers'''
        return self._lagrange

    lagrange = property(_get_lagrange)

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Clear all wavefunction information'''
        self._cache.clear()

    def clear_dm(self):
        '''Clear RDM information'''
        self._cache.clear(tags='d', dealloc=True)

    def clear_geminal(self):
        '''Clear geminal information'''
        self._geminal.clear()

    def clear_lagrange(self):
        '''Clear lagrange information'''
        self._lagrange.clear()

    def update_ecore(self, new):
        '''Update core energy'''
        self._ecore = new

    def update_geminal(self, geminal=None):
        '''Update geminal matrix

           **Optional arguments:**

           geminal
                When provided, this geminal matrix is stored.
        '''
        if geminal is None:
            raise NotImplementedError
        else:
            self._geminal.assign(geminal)

    def update_lagrange(self, lagrange=None, dim1=None, dim2=None):
        '''Update Lagragne multipliers

           **Optional arguments:**

           lagrange
                When provided, this set of Lagrange multipliers is stored.
        '''
        if lagrange is None:
            raise NotImplementedError
        else:
            self.lagrange.assign(lagrange)

    def update_auxmatrix(self, select, two_mo, one_mo=None):
        '''Update auxiliary matrices'''
        raise NotImplementedError

    def get_auxmatrix(self, select):
        '''Get auxiliary matrices'''
        raise NotImplementedError

    def init_one_dm(self, select):
        '''Initialize 1-RDM as OneIndex object

           The 1-RDM expressed in the natural orbital basis is diagonal and
           only the diagonal elements are stored.

           **Arguments**

           select
                'ps2' or 'response'.
        '''
        check_options('onedm', select, 'ps2', 'response')
        dm, new = self._cache.load('one_dm_%s' % select, alloc=(self._lf.create_one_index, self.nbasis), tags='d')
        if not new:
            raise RuntimeError('The density matrix one_dm_%s already exists. Call one_dm_%s.clear prior to updating the 1DM.' % select)
        return dm

    def init_two_dm(self, select):
        r'''Initialize 2-RDM as TwoIndex object

           Only the symmetry-unique elements of the (response) 2-RDM are
           stored. These are matrix elements of type

           .. math::

                Gamma_{p\bar{q}p\bar{q}}

           (spin-up and spin-down (bar-sign)) or

           .. math::
                Gamma_{p\bar{p}q\bar{q}}

           and are stored as elements :math:`{pq}` of two_dm_pqpq, and
           two_dm_ppqq.

           **Arguments**

           select
                '(r(esponse))ppqq', or '(r(esponse))pqpq'.
        '''
        check_options('twodm', select, 'ppqq', 'pqpq', 'rppqq', 'rpqpq')
        dm, new = self._cache.load('two_dm_%s' % select, alloc=(self._lf.create_two_index, self.nbasis), tags='d')
        if not new:
            raise RuntimeError('The density matrix two_dm_%s already exists. Call two_dm_%s.clear prior to updating the 2DM.' % select)
        return dm

    def init_three_dm(self, select):
        '''Initialize 3-RDM

           **Arguments**

           select
        '''
        raise NotImplementedError

    def init_four_dm(self, select):
        '''Initialize 4-RDM

           **Arguments**

           select
        '''
        raise NotImplementedError

    def get_one_dm(self, select):
        '''Get a density matrix (1-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
                'ps2', or 'response'.
        '''
        if not 'one_dm_%s' % select in self._cache:
            self.update_one_dm(select)
        return self._cache.load('one_dm_%s' % select)

    def get_two_dm(self, select):
        '''Get a density matrix (2-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
                '(r(esponse))ppqq', or '(r(esponse))pqpq'.
        '''
        if not 'two_dm_%s' % select in self._cache:
            self.update_two_dm(select)
        return self._cache.load('two_dm_%s' % select)

    def get_three_dm(self, select):
        '''Get a density matrix (3-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
        '''
        raise NotImplementedError

    def get_four_dm(self, select):
        '''Get a density matrix (4-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
        '''
        raise NotImplementedError

    one_dm_ps2 = PropertyHelper(get_one_dm, 'ps2', 'Alpha 1-RDM')
    one_dm_response = PropertyHelper(get_one_dm, 'response', 'Alpha 1-RDM')
    two_dm_ppqq = PropertyHelper(get_two_dm, 'ppqq', 'Alpha-beta PS2 (ppqq) 2-RDM')
    two_dm_pqpq = PropertyHelper(get_two_dm, 'pqpq', 'Alpha-beta PS2 (pqpq) 2-RDM')
    two_dm_rppqq = PropertyHelper(get_two_dm, 'rppqq', 'Alpha-beta (ppqq) 2-RDM')
    two_dm_rpqpq = PropertyHelper(get_two_dm, 'rpqpq', 'Alpha-beta (pqpq) 2-RDM')

    def update_one_dm(self, one_dm=None):
        '''Update 1-RDM

           **Optional arguments:**

           one_dm
                When provided, this 1-RDM is stored.
        '''
        raise NotImplementedError

    def update_two_dm(self, two_dm=None):
        '''Update 2-RDM

           **Optional arguments:**

           two_dm
                When provided, this 2-RDM is stored.
        '''
        raise NotImplementedError

    def update_three_dm(self, three_dm=None):
        '''Update 3-RDM

           **Optional arguments:**

           three_dm
                When provided, this 3-RDM is stored.
        '''
        raise NotImplementedError

    def update_four_dm(self, four_dm=None):
        '''Update 2-RDM

           **Optional arguments:**

           four_dm
                When provided, this 4-RDM is stored.
        '''
        raise NotImplementedError

    # Initial guess generators:
    def generate_guess(self, guess, dim=None):
        '''Generate a guess of type 'guess'.

           **Arguments:**

           guess
               A dictionary, containing the type of guess.

           **Optional arguments:**

           dim
               Length of guess.
        '''
        check_options('guess.type', guess['type'], 'random', 'const')
        check_type('guess.factor', guess['factor'], int, float)
        if guess['factor'] == 0:
            raise ValueError('Scaling factor must be different from 0.')
        if dim is None:
            dim = self.dimension
        if guess['type'] == 'random':
            return np.random.random(dim)*guess['factor']
        elif guess['type'] == 'const':
            return np.ones(dim)*guess['factor']

    def compute_rotation_matrix(self, coeff):
        '''Compute orbital rotation matrix'''
        raise NotImplementedError

    # Check convergence:
    def check_convergence(self, e0, e1, gradient, thresh):
        '''Check convergence.

           **Arguements:**

           e0, e1
                Used to calculate energy difference e0-e1

           gradient
                The gradient, a OneIndex instance

           thresh
                Dictionary containing threshold parameters ('energy', 'gradientmax',
                'gradientnorm')

           **Returns:**
                True if energy difference, norm of orbital gradient, largest
                element of orbital gradient are smaller than some threshold
                values.
        '''
        return abs(e0-e1) < thresh['energy'] and \
               gradient.get_max() < thresh['gradientmax'] and \
               gradient.norm() < thresh['gradientnorm']

    def check_stepsearch(self, linesearch):
        '''Check trustradius. Abort calculation if trustradius is smaller than
           1e-8
        '''
        return linesearch.method == 'trust-region' and \
               linesearch.trustradius < 1e-8

    def prod(self, lst):
        return reduce(mul, lst)

    def perm(self, a):
        '''Calculate the permament of a matrix

           **Arguements**

           a
                A np array
        '''
        check_type('matrix', a, np.ndarray)
        n = len(a)
        r = range(n)
        s = permutations(r)
        import math # FIXME: fsum really needed for accuracy?
        return math.fsum(self.prod(a[i][sigma[i]] for i in r) for sigma in s)
Example #12
0
class MeanFieldWFN(object):
    def __init__(self, lf, nbasis, occ_model=None, norb=None):
        """
           **Arguments:**

           lf
                A LinalgFactory instance.

           nbasis
                The number of basis functions.

           **Optional arguments:**

           occ_model
                A model to assign new occupation numbers when the orbitals are
                updated by a diagonalization of a Fock matrix.

           norb
               the number of orbitals (occupied + virtual). When not given,
               it is set to nbasis.
        """
        self._lf = lf
        self._nbasis = nbasis
        self._occ_model = occ_model
        if norb is None:
            self._norb = nbasis
        else:
            self._norb = norb
        # The cache is used to store different representations of the
        # wavefunction, i.e. as expansion, as density matrix or both.
        self._cache = Cache()
        # Write some screen log
        self._log_init()

    @classmethod
    def from_hdf5(cls, grp, lf):
        # make the wfn object
        from horton.checkpoint import load_hdf5_low
        occ_model = load_hdf5_low(grp['occ_model'], lf) if 'occ_model' in grp else None
        result = cls(lf, grp['nbasis'][()], occ_model, grp['norb'][()])
        # load stuff into cache
        for spin in 'alpha', 'beta':
            if 'exp_%s' % spin in grp:
                exp = result.init_exp(spin)
                exp.read_from_hdf5(grp['exp_%s' % spin])
            if 'dm_%s' % spin in grp:
                dm = result.init_dm(spin)
                dm.read_from_hdf5(grp['dm_%s' % spin])
        return result

    def to_hdf5(self, grp):
        grp.attrs['class'] = self.__class__.__name__
        grp['nbasis'] = self._nbasis
        grp['norb'] = self._norb
        if self.occ_model is not None:
            tmp = grp.create_group('occ_model')
            self.occ_model.to_hdf5(tmp)
        for spin in 'alpha', 'beta':
            if 'exp_%s' % spin in self._cache:
                tmp = grp.create_group('exp_%s' % spin)
                self._cache.load('exp_%s' % spin).to_hdf5(tmp)
            if 'dm_%s' % spin in self._cache:
                tmp = grp.create_group('dm_%s' % spin)
                self._cache.load('dm_%s' % spin).to_hdf5(tmp)

    def _get_nbasis(self):
        '''The number of basis functions.'''
        return self._nbasis

    nbasis = property(_get_nbasis)

    def _get_norb(self):
        '''The number of orbitals in the expansion(s)'''
        return self._norb

    norb = property(_get_norb)

    def _get_occ_model(self):
        '''The model for the orbital occupations'''
        return self._occ_model

    def _set_occ_model(self, occ_model):
        self._occ_model = occ_model

    occ_model = property(_get_occ_model, _set_occ_model)

    def _get_temperature(self):
        '''The electronic temperature used for the Fermi smearing'''
        if self._occ_model is None:
            return 0
        else:
            return self._occ_model.temperature

    temperature = property(_get_temperature)

    def _get_cache(self):
        '''The cache object in which the main attributes are stored'''
        return self._cache

    cache = property(_get_cache)

    def _log_init(self):
        '''Write a summary of the wavefunction to the screen logger'''
        if log.do_medium:
            log('Initialized: %s' % self)
            if self.occ_model is not None:
                self.occ_model.log()
            log.blank()

    def _iter_expansions(self):
        '''Iterate over all expansion in the cache'''
        for spin in 'alpha', 'beta':
            if 'exp_%s' % spin in self._cache:
                yield self._cache.load('exp_%s' % spin)

    def _iter_density_matrices(self):
        '''Iterate over all density matrices in the cache'''
        for select in 'alpha', 'beta', 'full', 'spin':
            if 'dm_%s' % select in self._cache:
                yield self._cache.load('dm_%s' % select)

    def _assign_dm_full(self, dm):
        raise NotImplementedError

    def _assign_dm_spin(self, dm):
        raise NotImplementedError

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Clear all wavefunction information'''
        self._cache.clear()

    def clear_exp(self):
        '''Clear the wavefunction expansions'''
        self._cache.clear(tags='e')

    def clear_dm(self):
        '''Clear the density matrices'''
        self._cache.clear(tags='d')

    def init_exp(self, spin, norb=None):
        if spin not in ['alpha', 'beta']:
            raise ValueError('The select argument must be alpha or beta')
        if norb is None:
            norb = self._norb
        exp, new = self._cache.load('exp_%s' % spin, alloc=(self._lf.create_expansion, self._nbasis, norb), tags='e')
        if not new:
            raise RuntimeError('The expansion exp_%s already exists. Call wfn.clear prior to updating the wfn.' % spin)
        return exp

    def init_dm(self, select):
        if select not in ['alpha', 'beta', 'full', 'spin']:
            raise ValueError('The select argument must be one of alpha, beta, full or spin.')
        dm, new = self._cache.load('dm_%s' % select, alloc=(self._lf.create_one_body, self.nbasis), tags='d')
        if not new:
            raise RuntimeError('The density matrix dm_%s already exists. Call wfn.clear prior to updating the wfn.' % select)
        return dm

    def update_dm(self, select, dm=None):
        """Derive the density matrix from the expansion(s) and store in cache

           **Arguments:**

           select
                'alpha', 'beta', 'full' or 'spin'.

           **Optional arguments:**

           dm
                When provided, this density matrix is stored instead of one
                derived from the orbitals.
        """
        cached_dm = self.init_dm(select)
        if dm is None:
            if select == 'alpha':
                self.exp_alpha.compute_density_matrix(cached_dm)
            elif select == 'beta':
                self.exp_beta.compute_density_matrix(cached_dm)
            elif select == 'full':
                self._assign_dm_full(cached_dm)
            elif select == 'spin':
                self._assign_dm_spin(cached_dm)
        else:
            cached_dm.assign(dm)
        return cached_dm

    def get_dm(self, select):
        '''Get a density matrix. If not available, it will be created (if possible)

           **Arguments:**

           select
                'alpha', 'beta', 'full' or 'spin'.
        '''
        if not 'dm_%s' % select in self._cache:
            self.update_dm(select)
        return self._cache.load('dm_%s' % select)

    def get_exp(self, spin):
        '''Return an expansion of the wavefunction, if available.

           **Arguments:**

           select
                the spin component: 'alpha' or 'beta'.
        '''
        return self._cache.load('exp_%s' % spin)

    def get_level_shift(self, spin, overlap):
        '''Return a level shift operator for the given spin component.

           **Arguments:**

           select
                the spin component: 'alpha' or 'beta'.
        '''
        level_shift, new = self._cache.load('level_shift_%s' % spin, alloc=(self._lf.create_one_body, self.nbasis))
        if not new:
            level_shift.assign(overlap)
            level_shift.idot(self.get_dm(spin))
            level_shift.idot(overlap)
        return level_shift

    dm_alpha = PropertyHelper(get_dm, 'alpha', 'Alpha density matrix')
    dm_beta = PropertyHelper(get_dm, 'beta', 'Beta density matrix')
    dm_full = PropertyHelper(get_dm, 'full', 'Full density matrix')
    dm_spin = PropertyHelper(get_dm, 'spin', 'Spin density matrix')
    exp_alpha = PropertyHelper(get_exp, 'alpha', 'Alpha orbital expansion')
    exp_beta = PropertyHelper(get_exp, 'beta', 'Beta orbital expansion')

    def apply_basis_permutation(self, permutation):
        """Reorder the expansion coefficients and the density matrices"""
        for exp in self._iter_expansions():
            exp.apply_basis_permutation(permutation)
        for dm in self._iter_density_matrices():
            dm.apply_basis_permutation(permutation)

    def apply_basis_signs(self, signs):
        """Fix the signs of the expansion coefficients and the density matrices"""
        for exp in self._iter_expansions():
            exp.apply_basis_signs(signs)
        for dm in self._iter_density_matrices():
            dm.apply_basis_signs(signs)

    def check_normalization(self, olp, eps=1e-4):
        '''Run an internal test to see if the orbitals are normalized

           **Arguments:**

           olp
                The overlap one_body operators

           **Optional arguments:**

           eps
                The allowed deviation from unity, very loose by default.
        '''
        for exp in self._iter_expansions():
            exp.check_normalization(olp, eps)
Example #13
0
    def __init__(self, coordinates, numbers, pseudo_numbers, grid, moldens, spindens, local, lmax):
        '''
           **Arguments:**

           coordinates
                An array (N, 3) with centers for the atom-centered grids.

           numbers
                An array (N,) with atomic numbers.

           pseudo_numbers
                An array (N,) with effective charges. When set to None, this
                defaults to``numbers.astype(float)``.

           grid
                The integration grid

           moldens
                The spin-summed electron density on the grid.

           spindens
                The spin difference density on the grid. (Can be None)

           local
                Whether or not to use local (non-periodic) subgrids for atomic
                integrals.

           lmax
                The maximum angular momentum in multipole expansions.
        '''

        # Init base class
        JustOnceClass.__init__(self)

        # Some type checking for first three arguments
        natom, coordinates, numbers, pseudo_numbers = typecheck_geo(coordinates, numbers, pseudo_numbers)
        self._natom = natom
        self._coordinates = coordinates
        self._numbers = numbers
        self._pseudo_numbers = pseudo_numbers

        # Assign remaining arguments as attributes
        self._grid = grid
        self._moldens = moldens
        self._spindens = spindens
        self._local = local
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()
Example #14
0
class Part(JustOnceClass):
    name = None
    linear = False # whether the populations are linear in the density matrix.

    def __init__(self, coordinates, numbers, pseudo_numbers, grid, moldens, spindens, local, lmax):
        '''
           **Arguments:**

           coordinates
                An array (N, 3) with centers for the atom-centered grids.

           numbers
                An array (N,) with atomic numbers.

           pseudo_numbers
                An array (N,) with effective charges. When set to None, this
                defaults to``numbers.astype(float)``.

           grid
                The integration grid

           moldens
                The spin-summed electron density on the grid.

           spindens
                The spin difference density on the grid. (Can be None)

           local
                Whether or not to use local (non-periodic) subgrids for atomic
                integrals.

           lmax
                The maximum angular momentum in multipole expansions.
        '''

        # Init base class
        JustOnceClass.__init__(self)

        # Some type checking for first three arguments
        natom, coordinates, numbers, pseudo_numbers = typecheck_geo(coordinates, numbers, pseudo_numbers)
        self._natom = natom
        self._coordinates = coordinates
        self._numbers = numbers
        self._pseudo_numbers = pseudo_numbers

        # Assign remaining arguments as attributes
        self._grid = grid
        self._moldens = moldens
        self._spindens = spindens
        self._local = local
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()

    def __getitem__(self, key):
        return self.cache.load(key)

    def _get_natom(self):
        return self._natom

    natom = property(_get_natom)

    def _get_coordinates(self):
        return self._coordinates

    coordinates = property(_get_coordinates)

    def _get_numbers(self):
        return self._numbers

    numbers = property(_get_numbers)

    def _get_pseudo_numbers(self):
        return self._pseudo_numbers

    pseudo_numbers = property(_get_pseudo_numbers)

    def _get_grid(self):
        return self.get_grid()

    grid = property(_get_grid)

    def _get_local(self):
        return self._local

    local = property(_get_local)

    def _get_lmax(self):
        return self._lmax

    lmax = property(_get_lmax)

    def _get_cache(self):
        return self._cache

    cache = property(_get_cache)

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Discard all cached results, e.g. because wfn changed'''
        JustOnceClass.clear(self)
        self.cache.clear()

    def get_grid(self, index=None):
        '''Return an integration grid

           **Optional arguments:**

           index
                The index of the atom. If not given, a grid for the entire
                system is returned. If self.local is False, a full system grid
                is always returned.
        '''
        if index is None or not self.local:
            return self._grid
        else:
            return self._subgrids[index]

    def get_moldens(self, index=None, output=None):
        result = self.to_atomic_grid(index, self._moldens)
        if output is not None:
            output[:] = result
        return result

    def get_spindens(self, index=None, output=None):
        result = self.to_atomic_grid(index, self._spindens)
        if output is not None:
            output[:] = result
        return result

    def get_wcor(self, index):
        '''Return the weight corrections on a grid

           See get_grid for the meaning of the optional arguments
        '''
        raise NotImplementedError

    def _init_subgrids(self):
        raise NotImplementedError

    def _init_log_base(self):
        raise NotImplementedError

    def _init_log_scheme(self):
        raise NotImplementedError

    def _init_log_memory(self):
        if log.do_medium:
            # precompute arrays sizes for certain grids
            nbyte_global = self.grid.size*8
            nbyte_locals = np.array([self.get_grid(i).size*8 for i in xrange(self.natom)])

            # compute and report usage
            estimates = self.get_memory_estimates()
            nbyte_total = 0
            log('Coarse estimate of memory usage for the partitioning:')
            log('                         Label  Memory[GB]')
            log.hline()
            for label, nlocals, nglobal in estimates:
                nbyte = np.dot(nlocals, nbyte_locals) + nglobal*nbyte_global
                log('%30s  %10.3f' % (label, nbyte/1024.0**3))
                nbyte_total += nbyte
            log('%30s  %10.3f' % ('Total', nbyte_total/1024.0**3))
            log.hline()
            log.blank()

    def get_memory_estimates(self):
        return [
            ('Atomic weights', np.ones(self.natom), 0),
            ('Promolecule', np.zeros(self.natom), 1),
            ('Working arrays', np.zeros(self.natom), 2),
        ]

    def to_atomic_grid(self, index, data):
        raise NotImplementedError

    def compute_pseudo_population(self, index):
        grid = self.get_grid(index)
        dens = self.get_moldens(index)
        at_weights = self.cache.load('at_weights', index)
        wcor = self.get_wcor(index)
        return grid.integrate(at_weights, dens, wcor)

    @just_once
    def do_partitioning(self):
        self.update_at_weights()
    do_partitioning.names = []

    def update_at_weights(self):
        '''Updates the at_weights arrays in the case (and all related arrays)'''
        raise NotImplementedError

    @just_once
    def do_populations(self):
        populations, new = self.cache.load('populations', alloc=self.natom, tags='o')
        if new:
            self.do_partitioning()
            pseudo_populations = self.cache.load('pseudo_populations', alloc=self.natom, tags='o')[0]
            if log.do_medium:
                log('Computing atomic populations.')
            for i in xrange(self.natom):
                pseudo_populations[i] = self.compute_pseudo_population(i)
            populations[:] = pseudo_populations
            populations += self.numbers - self.pseudo_numbers

    @just_once
    def do_charges(self):
        charges, new = self._cache.load('charges', alloc=self.natom, tags='o')
        if new:
            self.do_populations()
            populations = self._cache.load('populations')
            if log.do_medium:
                log('Computing atomic charges.')
            charges[:] = self.numbers - populations

    @just_once
    def do_spin_charges(self):
        if self._spindens is not None:
            spin_charges, new = self._cache.load('spin_charges', alloc=self.natom, tags='o')
            self.do_partitioning()
            if log.do_medium:
                log('Computing atomic spin charges.')
            for index in xrange(self.natom):
                grid = self.get_grid(index)
                spindens = self.get_spindens(index)
                at_weights = self.cache.load('at_weights', index)
                wcor = self.get_wcor(index)
                spin_charges[index] = grid.integrate(at_weights, spindens, wcor)

    @just_once
    def do_moments(self):
        ncart = get_ncart_cumul(self.lmax)
        cartesian_multipoles, new1 = self._cache.load('cartesian_multipoles', alloc=(self.natom, ncart), tags='o')

        npure = get_npure_cumul(self.lmax)
        pure_multipoles, new1 = self._cache.load('pure_multipoles', alloc=(self.natom, npure), tags='o')

        nrad = self.lmax+1
        radial_moments, new2 = self._cache.load('radial_moments', alloc=(self.natom, nrad), tags='o')

        if new1 or new2:
            self.do_partitioning()
            if log.do_medium:
                log('Computing cartesian and pure AIM multipoles and radial AIM moments.')

            for i in xrange(self.natom):
                # 1) Define a 'window' of the integration grid for this atom
                center = self.coordinates[i]
                grid = self.get_grid(i)

                # 2) Compute the AIM
                aim = self.get_moldens(i)*self.cache.load('at_weights', i)

                # 3) Compute weight corrections
                wcor = self.get_wcor(i)

                # 4) Compute Cartesian multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                cartesian_multipoles[i] = -grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=1)
                cartesian_multipoles[i, 0] += self.pseudo_numbers[i]

                # 5) Compute Pure multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                pure_multipoles[i] = -grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=2)
                pure_multipoles[i, 0] += self.pseudo_numbers[i]

                # 6) Compute Radial moments
                # For the radial moments, it is not common to put a minus sign
                # for the negative electron charge.
                radial_moments[i] = grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=3)

    def do_all(self):
        '''Computes all properties and return a list of their keys.'''
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if callable(attr) and attr_name.startswith('do_') and attr_name != 'do_all':
                attr()
        return list(self.cache.iterkeys(tags='o'))
Example #15
0
class Part(JustOnceClass):
    name = None
    linear = False  # whether the populations are linear in the density matrix.

    def __init__(self, system, grid, local, slow, lmax, moldens=None):
        '''
           **Arguments:**

           system
                The system to be partitioned.

           grid
                The integration grid

           local
                Whether or not to use local (non-periodic) grids.

           slow
                When ``True``, also the AIM properties are computed that use the
                AIM overlap operators.

           lmax
                The maximum angular momentum in multipole expansions.

           **Optional arguments:**

           moldens
                The all-electron density grid data.
        '''
        JustOnceClass.__init__(self)
        self._system = system
        self._grid = grid
        self._local = local
        self._slow = slow
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()
        # Caching of work arrays to avoid reallocation
        if moldens is not None:
            self._cache.dump('moldens', moldens)

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()

    def __getitem__(self, key):
        return self.cache.load(key)

    def _get_system(self):
        return self._system

    system = property(_get_system)

    def _get_grid(self):
        return self.get_grid()

    grid = property(_get_grid)

    def _get_local(self):
        return self._local

    local = property(_get_local)

    def _get_slow(self):
        return self._slow

    slow = property(_get_slow)

    def _get_lmax(self):
        return self._lmax

    lmax = property(_get_lmax)

    def _get_cache(self):
        return self._cache

    cache = property(_get_cache)

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Discard all cached results, e.g. because wfn changed'''
        JustOnceClass.clear(self)
        self.cache.clear()

    def update_grid(self, grid):
        '''Specify a new grid

           **Arguments:**

           grid
                The new grid

           When the new and old grid are the same, no action is taken. When
           a really new grid is provided, the subgrids are updated and the
           cache is cleared.
        '''
        if not (grid is self._grid):
            self._grid = grid
            if self.local:
                self._init_subgrids()
            self.clear()

    def get_grid(self, index=None):
        '''Return an integration grid

           **Optional arguments:**

           index
                The index of the atom. If not given, a grid for the entire
                system is returned. If self.local is False, a full system grid
                is always returned.
        '''
        if index is None or not self.local:
            return self._grid
        else:
            return self._subgrids[index]

    def get_moldens(self, index=None, output=None):
        self.do_moldens()
        moldens = self.cache.load('moldens')
        result = self.to_atomic_grid(index, moldens)
        if output is not None:
            output[:] = result
        return result

    def get_spindens(self, index=None, output=None):
        self.do_spindens()
        spindens = self.cache.load('spindens')
        result = self.to_atomic_grid(index, spindens)
        if output is not None:
            output[:] = result
        return result

    def get_wcor(self, index):
        '''Return the weight corrections on a grid

           See get_grid for the meaning of the optional arguments
        '''
        raise NotImplementedError

    def _init_subgrids(self):
        raise NotImplementedError

    def _init_log_base(self):
        raise NotImplementedError

    def _init_log_scheme(self):
        raise NotImplementedError

    def _init_log_memory(self):
        if log.do_medium:
            # precompute arrays sizes for certain grids
            nbyte_global = self.grid.size * 8
            nbyte_locals = np.array(
                [self.get_grid(i).size * 8 for i in xrange(self.system.natom)])

            # compute and report usage
            estimates = self.get_memory_estimates()
            nbyte_total = 0
            log('Coarse estimate of memory usage for the partitioning:')
            log('                         Label  Memory[GB]')
            log.hline()
            for label, nlocals, nglobal in estimates:
                nbyte = np.dot(nlocals, nbyte_locals) + nglobal * nbyte_global
                log('%30s  %10.3f' % (label, nbyte / 1024.0**3))
                nbyte_total += nbyte
            log('%30s  %10.3f' % ('Total', nbyte_total / 1024.0**3))
            log.hline()
            log.blank()

    def get_memory_estimates(self):
        return [
            ('Atomic weights', np.ones(self.system.natom), 0),
            ('Promolecule', np.zeros(self.system.natom), 1),
            ('Working arrays', np.zeros(self.system.natom), 2),
        ]

    def to_atomic_grid(self, index, data):
        raise NotImplementedError

    def compute_pseudo_population(self, index):
        grid = self.get_grid(index)
        dens = self.get_moldens(index)
        at_weights = self.cache.load('at_weights', index)
        wcor = self.get_wcor(index)
        return grid.integrate(at_weights, dens, wcor)

    @just_once
    def do_moldens(self):
        raise NotImplementedError

    @just_once
    def do_spindens(self):
        raise NotImplementedError

    @just_once
    def do_partitioning(self):
        self.update_at_weights()

    do_partitioning.names = []

    def update_at_weights(self):
        '''Updates the at_weights arrays in the case (and all related arrays)'''
        raise NotImplementedError

    @just_once
    def do_populations(self):
        populations, new = self.cache.load('populations',
                                           alloc=self.system.natom,
                                           tags='o')
        if new:
            self.do_partitioning()
            self.do_moldens()
            pseudo_populations = self.cache.load('pseudo_populations',
                                                 alloc=self.system.natom,
                                                 tags='o')[0]
            if log.do_medium:
                log('Computing atomic populations.')
            for i in xrange(self.system.natom):
                pseudo_populations[i] = self.compute_pseudo_population(i)
            populations[:] = pseudo_populations
            populations += self.system.numbers - self.system.pseudo_numbers

    @just_once
    def do_charges(self):
        charges, new = self._cache.load('charges',
                                        alloc=self.system.natom,
                                        tags='o')
        if new:
            self.do_populations()
            populations = self._cache.load('populations')
            if log.do_medium:
                log('Computing atomic charges.')
            charges[:] = self.system.numbers - populations

    @just_once
    def do_spin_charges(self):
        spin_charges, new = self._cache.load('spin_charges',
                                             alloc=self.system.natom,
                                             tags='o')
        if new:
            if isinstance(self.system.wfn, RestrictedWFN):
                spin_charges[:] = 0.0
            else:
                try:
                    self.do_spindens()
                except NotImplementedError:
                    self.cache.clear_item('spin_charges')
                    return
                self.do_partitioning()
                if log.do_medium:
                    log('Computing atomic spin charges.')
                for index in xrange(self.system.natom):
                    grid = self.get_grid(index)
                    spindens = self.get_spindens(index)
                    at_weights = self.cache.load('at_weights', index)
                    wcor = self.get_wcor(index)
                    spin_charges[index] = grid.integrate(
                        at_weights, spindens, wcor)

    @just_once
    def do_moments(self):
        if log.do_medium:
            log('Computing cartesian and pure AIM multipoles and radial AIM moments.'
                )

        ncart = get_ncart_cumul(self.lmax)
        cartesian_multipoles, new1 = self._cache.load(
            'cartesian_multipoles',
            alloc=(self._system.natom, ncart),
            tags='o')

        npure = get_npure_cumul(self.lmax)
        pure_multipoles, new1 = self._cache.load('pure_multipoles',
                                                 alloc=(self._system.natom,
                                                        npure),
                                                 tags='o')

        nrad = self.lmax + 1
        radial_moments, new2 = self._cache.load('radial_moments',
                                                alloc=(self._system.natom,
                                                       nrad),
                                                tags='o')

        if new1 or new2:
            self.do_partitioning()
            for i in xrange(self._system.natom):
                # 1) Define a 'window' of the integration grid for this atom
                center = self._system.coordinates[i]
                grid = self.get_grid(i)

                # 2) Compute the AIM
                aim = self.get_moldens(i) * self.cache.load('at_weights', i)

                # 3) Compute weight corrections (TODO: needs to be assessed!)
                wcor = self.get_wcor(i)

                # 4) Compute Cartesian multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                cartesian_multipoles[i] = -grid.integrate(
                    aim, wcor, center=center, lmax=self.lmax, mtype=1)
                cartesian_multipoles[i, 0] += self.system.pseudo_numbers[i]

                # 5) Compute Pure multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                pure_multipoles[i] = -grid.integrate(
                    aim, wcor, center=center, lmax=self.lmax, mtype=2)
                pure_multipoles[i, 0] += self.system.pseudo_numbers[i]

                # 6) Compute Radial moments
                # For the radial moments, it is not common to put a minus sign
                # for the negative electron charge.
                radial_moments[i] = grid.integrate(aim,
                                                   wcor,
                                                   center=center,
                                                   lmax=self.lmax,
                                                   mtype=3)

    def do_all(self):
        '''Computes all properties and return a list of their names.'''
        slow_methods = [
            'do_overlap_operators', 'do_bond_order',
            'do_noninteracting_response'
        ]
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if callable(attr) and attr_name.startswith(
                    'do_') and attr_name != 'do_all':
                if self._slow or (not attr_name in slow_methods):
                    attr()
        return list(self.cache.iterkeys(tags='o'))
Example #16
0
class Hamiltonian(object):
    def __init__(self, system, terms, grid=None, idiot_proof=True):
        '''
           **Arguments:**

           system
                The System object for which the energy must be computed.

           terms
                The terms in the Hamiltonian.

           **Optional arguments:**

           grid
                The integration grid, in case some terms need one.

           idiot_proof
                When set to False, the kinetic energy, external potential and
                Hartree terms are not added automatically and a error is raised
                when no exchange is present.
        '''
        # check arguments:
        if len(terms) == 0:
            raise ValueError('At least one term must be present in the Hamiltonian.')
        for term in terms:
            if term.require_grid and grid is None:
                raise TypeError('The term %s requires a grid, but not grid is given.' % term)

        # Assign attributes
        self.system = system
        self.terms = list(terms)
        self.grid = grid

        if idiot_proof:
            # Check if an exchange term is present
            if not any(term.exchange for term in self.terms):
                raise ValueError('No exchange term is given and idiot_proof option is set to True.')
            # Add standard terms if missing
            #  1) Kinetic energy
            if sum(isinstance(term, KineticEnergy) for term in terms) == 0:
                self.terms.append(KineticEnergy())
            #  2) Hartree (or HatreeFock, which is a subclass of Hartree)
            if sum(isinstance(term, Hartree) for term in terms) == 0:
                self.terms.append(Hartree())
            #  3) External Potential
            if sum(isinstance(term, ExternalPotential) for term in terms) == 0:
                self.terms.append(ExternalPotential())


        # Create a cache for shared intermediate results. This cache should only
        # be used for derived quantities that depend on the wavefunction and
        # need to be updated at each SCF cycle.
        self.cache = Cache()

        # bind the terms to this hamiltonian such that certain shared
        # intermediated results can be reused for the sake of efficiency.
        for term in self.terms:
            term.set_hamiltonian(self)

    def add_term(self, term):
        '''Add a new term to the hamiltonian'''
        self.terms.append(term)
        term.set_hamiltonian(self)

    def clear(self):
        '''Mark the properties derived from the wfn as outdated.

           This method does not recompute anything, but just marks operators
           as outdated. They are recomputed as they are needed.
        '''
        self.cache.clear()

    def compute(self):
        '''Compute the energy.

           **Returns:**

           The total energy, including nuclear-nuclear repulsion.
        '''
        total = 0.0
        for term in self.terms:
            energy = term.compute()
            self.system.extra['energy_%s' % term.label] = energy
            total += energy
        energy = self.system.compute_nucnuc()
        self.system.extra['energy_nn'] = energy
        total += energy
        self.system.extra['energy'] = total
        # Store result in chk file
        self.system.update_chk('extra')
        return total

    def log_energy(self):
        '''Write an overview of the last energy computation on screen'''
        log('Contributions to the energy:')
        log.hline()
        log('                                       Energy term                 Value')
        log.hline()
        for term in self.terms:
            energy = self.system.extra['energy_%s' % term.label]
            log('%50s  %20.12f' % (term.label, energy))
        log('%50s  %20.12f' % ('nn', self.system.extra['energy_nn']))
        log('%50s  %20.12f' % ('total', self.system.extra['energy']))
        log.hline()
        log.blank()

    def compute_fock(self, fock_alpha, fock_beta):
        '''Compute alpha (and beta) Fock matrix(es).

           **Arguments:**

           fock_alpha
                A One-Body operator output argument for the alpha fock matrix.

           fock_alpha
                A One-Body operator output argument for the beta fock matrix.

           In the case of a closed-shell computation, the argument fock_beta is
           ``None``.
        '''
        # Loop over all terms and add contributions to the Fock matrix. Some
        # terms will actually only evaluate potentials on grids and add these
        # results to the total potential on a grid.
        for term in self.terms:
            term.add_fock_matrix(fock_alpha, fock_beta, postpone_grid=True)
        # Collect all the total potentials and turn them into contributions
        # for the fock matrix/matrices.

        # Collect potentials for alpha electrons
        # d = density
        if 'dpot_total_alpha' in self.cache:
            dpot = self.cache.load('dpot_total_alpha')
            self.system.compute_grid_density_fock(self.grid.points, self.grid.weights, dpot, fock_alpha)
        # g = gradient
        if 'gpot_total_alpha' in self.cache:
            gpot = self.cache.load('gpot_total_alpha')
            self.system.compute_grid_gradient_fock(self.grid.points, self.grid.weights, gpot, fock_alpha)

        if isinstance(self.system.wfn, UnrestrictedWFN):
            # Colect potentials for beta electrons
            # d = density
            if 'dpot_total_beta' in self.cache:
                dpot = self.cache.load('dpot_total_beta')
                self.system.compute_grid_density_fock(self.grid.points, self.grid.weights, dpot, fock_beta)
            # g = gradient
            if 'gpot_total_beta' in self.cache:
                gpot = self.cache.load('gpot_total_beta')
                self.system.compute_grid_gradient_fock(self.grid.points, self.grid.weights, gpot, fock_beta)
Example #17
0
class Geminal(object):
    '''A collection of geminals and optimization routines.

       This is just a base class that serves as a template for
       specific implementations.
    '''
    def __init__(self, lf, occ_model, npairs=None, nvirt=None):
        '''
           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model

           **Optional arguments:**

           npairs
                Number of electron pairs, if not specified,
                npairs = number of occupied orbitals

           nvirt
                Number of virtual orbitals, if not specified,
                nvirt = (nbasis-npairs)
        '''
        check_type('pairs', npairs, int, type(None))
        check_type('virtuals', nvirt, int, type(None))
        self._lf = lf
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        if npairs is None:
            npairs = occ_model.noccs[0]
        elif npairs >= lf.default_nbasis:
            raise ValueError(
                'Number of electron pairs (%i) larger than number of basis functions (%i)'
                % (npairs, self.nbasis))
        if nvirt is None:
            nvirt = (lf.default_nbasis - npairs)
        elif nvirt >= lf.default_nbasis:
            raise ValueError(
                'Number of virtuals (%i) larger than number of basis functions (%i)'
                % (nvirt, self.nbasis))
        self._npairs = npairs
        self._nvirt = nvirt
        self._cache = Cache()
        self._ecore = 0
        self._geminal = lf.create_two_index(npairs, nvirt)
        self._lagrange = lf.create_two_index(npairs, nvirt)

    def __call__(self, one, two, core, orb, olp, scf, **kwargs):
        '''Optimize geminal coefficients and---if required---find
           optimal set of orbitals.

           **Arguments:**

           one, two
                One- and two-body integrals (some Hamiltonian matrix elements).

           core
                The core energy (not included in 'one' and 'two').

           orb
                An expansion instance. It contains the MO coefficients
                (orbitals).

           olp
                The AO overlap matrix. A TwoIndex instance.

           scf
                A boolean. If True: Initializes orbital optimization.

           **Keywords:**
                See :py:meth:`RAp1rog.solve`
                and :py:meth:`RAp1rog.solve_scf`
        '''
        if scf:
            return self.solve_scf(one, two, core, orb, olp, **kwargs)
        else:
            return self.solve(one, two, core, orb, olp, **kwargs)

    def solve(self, one, two, core, orb, olp, **kwargs):
        raise NotImplementedError

    def solve_scf(self, one, two, core, orb, olp, **kwargs):
        raise NotImplementedError

    def _get_nbasis(self):
        '''The number of basis functions'''
        return self._nbasis

    nbasis = property(_get_nbasis)

    def _get_nocc(self):
        '''The number of occupied orbitals'''
        return self._nocc

    nocc = property(_get_nocc)

    def _get_nvirt(self):
        '''The number of virtual orbitals'''
        return self._nvirt

    nvirt = property(_get_nvirt)

    def _get_npairs(self):
        '''The number of electron pairs'''
        return self._npairs

    npairs = property(_get_npairs)

    def _get_lf(self):
        '''The LinalgFactory instance'''
        return self._lf

    lf = property(_get_lf)

    def _get_ecore(self):
        '''The core energy'''
        return self._ecore

    ecore = property(_get_ecore)

    def _get_dimension(self):
        '''The number of unknowns (i.e. the number of geminal coefficients)'''
        return self._npairs * self._nvirt

    dimension = property(_get_dimension)

    def _get_geminal(self):
        '''The geminal coefficients'''
        return self._geminal

    geminal = property(_get_geminal)

    def _get_lagrange(self):
        '''The Lagrange multipliers'''
        return self._lagrange

    lagrange = property(_get_lagrange)

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Clear all wavefunction information'''
        self._cache.clear()

    def clear_dm(self):
        '''Clear RDM information'''
        self._cache.clear(tags='d', dealloc=True)

    def clear_geminal(self):
        '''Clear geminal information'''
        self._geminal.clear()

    def clear_lagrange(self):
        '''Clear lagrange information'''
        self._lagrange.clear()

    def update_ecore(self, new):
        '''Update core energy'''
        self._ecore = new

    def update_geminal(self, geminal=None):
        '''Update geminal matrix

           **Optional arguments:**

           geminal
                When provided, this geminal matrix is stored.
        '''
        if geminal is None:
            raise NotImplementedError
        else:
            self._geminal.assign(geminal)

    def update_lagrange(self, lagrange=None, dim1=None, dim2=None):
        '''Update Lagragne multipliers

           **Optional arguments:**

           lagrange
                When provided, this set of Lagrange multipliers is stored.
        '''
        if lagrange is None:
            raise NotImplementedError
        else:
            self.lagrange.assign(lagrange)

    def update_auxmatrix(self, select, two_mo, one_mo=None):
        '''Update auxiliary matrices'''
        raise NotImplementedError

    def get_auxmatrix(self, select):
        '''Get auxiliary matrices'''
        raise NotImplementedError

    def init_one_dm(self, select):
        '''Initialize 1-RDM as OneIndex object

           The 1-RDM expressed in the natural orbital basis is diagonal and
           only the diagonal elements are stored.

           **Arguments**

           select
                'ps2' or 'response'.
        '''
        check_options('onedm', select, 'ps2', 'response')
        dm, new = self._cache.load('one_dm_%s' % select,
                                   alloc=(self._lf.create_one_index,
                                          self.nbasis),
                                   tags='d')
        if not new:
            raise RuntimeError(
                'The density matrix one_dm_%s already exists. Call one_dm_%s.clear prior to updating the 1DM.'
                % select)
        return dm

    def init_two_dm(self, select):
        r'''Initialize 2-RDM as TwoIndex object

           Only the symmetry-unique elements of the (response) 2-RDM are
           stored. These are matrix elements of type

           .. math::

                Gamma_{p\bar{q}p\bar{q}}

           (spin-up and spin-down (bar-sign)) or

           .. math::
                Gamma_{p\bar{p}q\bar{q}}

           and are stored as elements :math:`{pq}` of two_dm_pqpq, and
           two_dm_ppqq.

           **Arguments**

           select
                '(r(esponse))ppqq', or '(r(esponse))pqpq'.
        '''
        check_options('twodm', select, 'ppqq', 'pqpq', 'rppqq', 'rpqpq')
        dm, new = self._cache.load('two_dm_%s' % select,
                                   alloc=(self._lf.create_two_index,
                                          self.nbasis),
                                   tags='d')
        if not new:
            raise RuntimeError(
                'The density matrix two_dm_%s already exists. Call two_dm_%s.clear prior to updating the 2DM.'
                % select)
        return dm

    def init_three_dm(self, select):
        '''Initialize 3-RDM

           **Arguments**

           select
        '''
        raise NotImplementedError

    def init_four_dm(self, select):
        '''Initialize 4-RDM

           **Arguments**

           select
        '''
        raise NotImplementedError

    def get_one_dm(self, select):
        '''Get a density matrix (1-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
                'ps2', or 'response'.
        '''
        if not 'one_dm_%s' % select in self._cache:
            self.update_one_dm(select)
        return self._cache.load('one_dm_%s' % select)

    def get_two_dm(self, select):
        '''Get a density matrix (2-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
                '(r(esponse))ppqq', or '(r(esponse))pqpq'.
        '''
        if not 'two_dm_%s' % select in self._cache:
            self.update_two_dm(select)
        return self._cache.load('two_dm_%s' % select)

    def get_three_dm(self, select):
        '''Get a density matrix (3-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
        '''
        raise NotImplementedError

    def get_four_dm(self, select):
        '''Get a density matrix (4-RDM). If not available, it will be created
           (if possible)

           **Arguments:**

           select
        '''
        raise NotImplementedError

    one_dm_ps2 = PropertyHelper(get_one_dm, 'ps2', 'Alpha 1-RDM')
    one_dm_response = PropertyHelper(get_one_dm, 'response', 'Alpha 1-RDM')
    two_dm_ppqq = PropertyHelper(get_two_dm, 'ppqq',
                                 'Alpha-beta PS2 (ppqq) 2-RDM')
    two_dm_pqpq = PropertyHelper(get_two_dm, 'pqpq',
                                 'Alpha-beta PS2 (pqpq) 2-RDM')
    two_dm_rppqq = PropertyHelper(get_two_dm, 'rppqq',
                                  'Alpha-beta (ppqq) 2-RDM')
    two_dm_rpqpq = PropertyHelper(get_two_dm, 'rpqpq',
                                  'Alpha-beta (pqpq) 2-RDM')

    def update_one_dm(self, one_dm=None):
        '''Update 1-RDM

           **Optional arguments:**

           one_dm
                When provided, this 1-RDM is stored.
        '''
        raise NotImplementedError

    def update_two_dm(self, two_dm=None):
        '''Update 2-RDM

           **Optional arguments:**

           two_dm
                When provided, this 2-RDM is stored.
        '''
        raise NotImplementedError

    def update_three_dm(self, three_dm=None):
        '''Update 3-RDM

           **Optional arguments:**

           three_dm
                When provided, this 3-RDM is stored.
        '''
        raise NotImplementedError

    def update_four_dm(self, four_dm=None):
        '''Update 2-RDM

           **Optional arguments:**

           four_dm
                When provided, this 4-RDM is stored.
        '''
        raise NotImplementedError

    # Initial guess generators:
    def generate_guess(self, guess, dim=None):
        '''Generate a guess of type 'guess'.

           **Arguments:**

           guess
               A dictionary, containing the type of guess.

           **Optional arguments:**

           dim
               Length of guess.
        '''
        check_options('guess.type', guess['type'], 'random', 'const')
        check_type('guess.factor', guess['factor'], int, float)
        if guess['factor'] == 0:
            raise ValueError('Scaling factor must be different from 0.')
        if dim is None:
            dim = self.dimension
        if guess['type'] == 'random':
            return np.random.random(dim) * guess['factor']
        elif guess['type'] == 'const':
            return np.ones(dim) * guess['factor']

    def compute_rotation_matrix(self, coeff):
        '''Compute orbital rotation matrix'''
        raise NotImplementedError

    # Check convergence:
    def check_convergence(self, e0, e1, gradient, thresh):
        '''Check convergence.

           **Arguements:**

           e0, e1
                Used to calculate energy difference e0-e1

           gradient
                The gradient, a OneIndex instance

           thresh
                Dictionary containing threshold parameters ('energy', 'gradientmax',
                'gradientnorm')

           **Returns:**
                True if energy difference, norm of orbital gradient, largest
                element of orbital gradient are smaller than some threshold
                values.
        '''
        return abs(e0-e1) < thresh['energy'] and \
               gradient.get_max() < thresh['gradientmax'] and \
               gradient.norm() < thresh['gradientnorm']

    def check_stepsearch(self, linesearch):
        '''Check trustradius. Abort calculation if trustradius is smaller than
           1e-8
        '''
        return linesearch.method == 'trust-region' and \
               linesearch.trustradius < 1e-8

    def prod(self, lst):
        return reduce(mul, lst)

    def perm(self, a):
        '''Calculate the permament of a matrix

           **Arguements**

           a
                A np array
        '''
        check_type('matrix', a, np.ndarray)
        n = len(a)
        r = range(n)
        s = permutations(r)
        import math  # FIXME: fsum really needed for accuracy?
        return math.fsum(self.prod(a[i][sigma[i]] for i in r) for sigma in s)
Example #18
0
    def __init__(self, system, terms, grid=None, idiot_proof=True):
        '''
           **Arguments:**

           system
                The System object for which the energy must be computed.

           terms
                The terms in the Hamiltonian.

           **Optional arguments:**

           grid
                The integration grid, in case some terms need one.

           idiot_proof
                When set to False, the kinetic energy, external potential and
                Hartree terms are not added automatically and a error is raised
                when no exchange is present.
        '''
        # check arguments:
        if len(terms) == 0:
            raise ValueError(
                'At least one term must be present in the Hamiltonian.')
        for term in terms:
            if term.require_grid and grid is None:
                raise TypeError(
                    'The term %s requires a grid, but not grid is given.' %
                    term)

        # Assign attributes
        self.system = system
        self.terms = list(terms)
        self.grid = grid

        if idiot_proof:
            # Check if an exchange term is present
            if not any(term.exchange for term in self.terms):
                raise ValueError(
                    'No exchange term is given and idiot_proof option is set to True.'
                )
            # Add standard terms if missing
            #  1) Kinetic energy
            if sum(isinstance(term, KineticEnergy) for term in terms) == 0:
                self.terms.append(KineticEnergy())
            #  2) Hartree (or HatreeFock, which is a subclass of Hartree)
            if sum(isinstance(term, Hartree) for term in terms) == 0:
                self.terms.append(Hartree())
            #  3) External Potential
            if sum(isinstance(term, ExternalPotential) for term in terms) == 0:
                self.terms.append(ExternalPotential())

        # Create a cache for shared intermediate results. This cache should only
        # be used for derived quantities that depend on the wavefunction and
        # need to be updated at each SCF cycle.
        self.cache = Cache()

        # bind the terms to this hamiltonian such that certain shared
        # intermediated results can be reused for the sake of efficiency.
        for term in self.terms:
            term.set_hamiltonian(self)
Example #19
0
class MeanFieldWFN(object):
    def __init__(self, lf, nbasis, occ_model=None, norb=None):
        """
           **Arguments:**

           lf
                A LinalgFactory instance.

           nbasis
                The number of basis functions.

           **Optional arguments:**

           occ_model
                A model to assign new occupation numbers when the orbitals are
                updated by a diagonalization of a Fock matrix.

           norb
               the number of orbitals (occupied + virtual). When not given,
               it is set to nbasis.
        """
        self._lf = lf
        self._nbasis = nbasis
        self._occ_model = occ_model
        if norb is None:
            self._norb = nbasis
        else:
            self._norb = norb
        # The cache is used to store different representations of the
        # wavefunction, i.e. as expansion, as density matrix or both.
        self._cache = Cache()
        # Write some screen log
        self._log_init()

    @classmethod
    def from_hdf5(cls, grp, lf):
        # make the wfn object
        from horton.checkpoint import load_hdf5_low
        occ_model = load_hdf5_low(grp['occ_model'],
                                  lf) if 'occ_model' in grp else None
        result = cls(lf, grp['nbasis'][()], occ_model, grp['norb'][()])
        # load stuff into cache
        for spin in 'alpha', 'beta':
            if 'exp_%s' % spin in grp:
                exp = result.init_exp(spin)
                exp.read_from_hdf5(grp['exp_%s' % spin])
            if 'dm_%s' % spin in grp:
                dm = result.init_dm(spin)
                dm.read_from_hdf5(grp['dm_%s' % spin])
        return result

    def to_hdf5(self, grp):
        grp.attrs['class'] = self.__class__.__name__
        grp['nbasis'] = self._nbasis
        grp['norb'] = self._norb
        if self.occ_model is not None:
            tmp = grp.create_group('occ_model')
            self.occ_model.to_hdf5(tmp)
        for spin in 'alpha', 'beta':
            if 'exp_%s' % spin in self._cache:
                tmp = grp.create_group('exp_%s' % spin)
                self._cache.load('exp_%s' % spin).to_hdf5(tmp)
            if 'dm_%s' % spin in self._cache:
                tmp = grp.create_group('dm_%s' % spin)
                self._cache.load('dm_%s' % spin).to_hdf5(tmp)

    def _get_nbasis(self):
        '''The number of basis functions.'''
        return self._nbasis

    nbasis = property(_get_nbasis)

    def _get_norb(self):
        '''The number of orbitals in the expansion(s)'''
        return self._norb

    norb = property(_get_norb)

    def _get_occ_model(self):
        '''The model for the orbital occupations'''
        return self._occ_model

    def _set_occ_model(self, occ_model):
        self._occ_model = occ_model

    occ_model = property(_get_occ_model, _set_occ_model)

    def _get_temperature(self):
        '''The electronic temperature used for the Fermi smearing'''
        if self._occ_model is None:
            return 0
        else:
            return self._occ_model.temperature

    temperature = property(_get_temperature)

    def _get_cache(self):
        '''The cache object in which the main attributes are stored'''
        return self._cache

    cache = property(_get_cache)

    def _log_init(self):
        '''Write a summary of the wavefunction to the screen logger'''
        if log.do_medium:
            log('Initialized: %s' % self)
            if self.occ_model is not None:
                self.occ_model.log()
            log.blank()

    def _iter_expansions(self):
        '''Iterate over all expansion in the cache'''
        for spin in 'alpha', 'beta':
            if 'exp_%s' % spin in self._cache:
                yield self._cache.load('exp_%s' % spin)

    def _iter_density_matrices(self):
        '''Iterate over all density matrices in the cache'''
        for select in 'alpha', 'beta', 'full', 'spin':
            if 'dm_%s' % select in self._cache:
                yield self._cache.load('dm_%s' % select)

    def _assign_dm_full(self, dm):
        raise NotImplementedError

    def _assign_dm_spin(self, dm):
        raise NotImplementedError

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Clear all wavefunction information'''
        self._cache.clear()

    def clear_exp(self):
        '''Clear the wavefunction expansions'''
        self._cache.clear(tags='e')

    def clear_dm(self):
        '''Clear the density matrices'''
        self._cache.clear(tags='d')

    def init_exp(self, spin, norb=None):
        if spin not in ['alpha', 'beta']:
            raise ValueError('The select argument must be alpha or beta')
        if norb is None:
            norb = self._norb
        exp, new = self._cache.load('exp_%s' % spin,
                                    alloc=(self._lf.create_expansion,
                                           self._nbasis, norb),
                                    tags='e')
        if not new:
            raise RuntimeError(
                'The expansion exp_%s already exists. Call wfn.clear prior to updating the wfn.'
                % spin)
        return exp

    def init_dm(self, select):
        if select not in ['alpha', 'beta', 'full', 'spin']:
            raise ValueError(
                'The select argument must be one of alpha, beta, full or spin.'
            )
        dm, new = self._cache.load('dm_%s' % select,
                                   alloc=(self._lf.create_one_body,
                                          self.nbasis),
                                   tags='d')
        if not new:
            raise RuntimeError(
                'The density matrix dm_%s already exists. Call wfn.clear prior to updating the wfn.'
                % select)
        return dm

    def update_dm(self, select, dm=None):
        """Derive the density matrix from the expansion(s) and store in cache

           **Arguments:**

           select
                'alpha', 'beta', 'full' or 'spin'.

           **Optional arguments:**

           dm
                When provided, this density matrix is stored instead of one
                derived from the orbitals.
        """
        cached_dm = self.init_dm(select)
        if dm is None:
            if select == 'alpha':
                self.exp_alpha.compute_density_matrix(cached_dm)
            elif select == 'beta':
                self.exp_beta.compute_density_matrix(cached_dm)
            elif select == 'full':
                self._assign_dm_full(cached_dm)
            elif select == 'spin':
                self._assign_dm_spin(cached_dm)
        else:
            cached_dm.assign(dm)
        return cached_dm

    def get_dm(self, select):
        '''Get a density matrix. If not available, it will be created (if possible)

           **Arguments:**

           select
                'alpha', 'beta', 'full' or 'spin'.
        '''
        if not 'dm_%s' % select in self._cache:
            self.update_dm(select)
        return self._cache.load('dm_%s' % select)

    def get_exp(self, spin):
        '''Return an expansion of the wavefunction, if available.

           **Arguments:**

           select
                the spin component: 'alpha' or 'beta'.
        '''
        return self._cache.load('exp_%s' % spin)

    def get_level_shift(self, spin, overlap):
        '''Return a level shift operator for the given spin component.

           **Arguments:**

           select
                the spin component: 'alpha' or 'beta'.
        '''
        level_shift, new = self._cache.load('level_shift_%s' % spin,
                                            alloc=(self._lf.create_one_body,
                                                   self.nbasis))
        if not new:
            level_shift.assign(overlap)
            level_shift.idot(self.get_dm(spin))
            level_shift.idot(overlap)
        return level_shift

    dm_alpha = PropertyHelper(get_dm, 'alpha', 'Alpha density matrix')
    dm_beta = PropertyHelper(get_dm, 'beta', 'Beta density matrix')
    dm_full = PropertyHelper(get_dm, 'full', 'Full density matrix')
    dm_spin = PropertyHelper(get_dm, 'spin', 'Spin density matrix')
    exp_alpha = PropertyHelper(get_exp, 'alpha', 'Alpha orbital expansion')
    exp_beta = PropertyHelper(get_exp, 'beta', 'Beta orbital expansion')

    def apply_basis_permutation(self, permutation):
        """Reorder the expansion coefficients and the density matrices"""
        for exp in self._iter_expansions():
            exp.apply_basis_permutation(permutation)
        for dm in self._iter_density_matrices():
            dm.apply_basis_permutation(permutation)

    def apply_basis_signs(self, signs):
        """Fix the signs of the expansion coefficients and the density matrices"""
        for exp in self._iter_expansions():
            exp.apply_basis_signs(signs)
        for dm in self._iter_density_matrices():
            dm.apply_basis_signs(signs)

    def check_normalization(self, olp, eps=1e-4):
        '''Run an internal test to see if the orbitals are normalized

           **Arguments:**

           olp
                The overlap one_body operators

           **Optional arguments:**

           eps
                The allowed deviation from unity, very loose by default.
        '''
        for exp in self._iter_expansions():
            exp.check_normalization(olp, eps)
Example #20
0
    def __init__(self, coordinates, numbers, pseudo_numbers, grid, moldens, spindens, local, lmax):
        '''
           **Arguments:**

           coordinates
                An array (N, 3) with centers for the atom-centered grids.

           numbers
                An array (N,) with atomic numbers.

           pseudo_numbers
                An array (N,) with effective charges. When set to None, this
                defaults to``numbers.astype(float)``.

           grid
                The integration grid

           moldens
                The spin-summed electron density on the grid.

           spindens
                The spin difference density on the grid. (Can be None)

           local
                Whether or not to use local (non-periodic) subgrids for atomic
                integrals.

           lmax
                The maximum angular momentum in multipole expansions.
        '''

        # Init base class
        JustOnceClass.__init__(self)

        # Some type checking for first three arguments
        natom, coordinates, numbers, pseudo_numbers = typecheck_geo(coordinates, numbers, pseudo_numbers)
        self._natom = natom
        self._coordinates = coordinates
        self._numbers = numbers
        self._pseudo_numbers = pseudo_numbers

        # Assign remaining arguments as attributes
        self._grid = grid
        self._moldens = moldens
        self._spindens = spindens
        self._local = local
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()
Example #21
0
class Part(JustOnceClass):
    name = None
    linear = False # whether the populations are linear in the density matrix.

    def __init__(self, coordinates, numbers, pseudo_numbers, grid, moldens, spindens, local, lmax):
        '''
           **Arguments:**

           coordinates
                An array (N, 3) with centers for the atom-centered grids.

           numbers
                An array (N,) with atomic numbers.

           pseudo_numbers
                An array (N,) with effective charges. When set to None, this
                defaults to``numbers.astype(float)``.

           grid
                The integration grid

           moldens
                The spin-summed electron density on the grid.

           spindens
                The spin difference density on the grid. (Can be None)

           local
                Whether or not to use local (non-periodic) subgrids for atomic
                integrals.

           lmax
                The maximum angular momentum in multipole expansions.
        '''

        # Init base class
        JustOnceClass.__init__(self)

        # Some type checking for first three arguments
        natom, coordinates, numbers, pseudo_numbers = typecheck_geo(coordinates, numbers, pseudo_numbers)
        self._natom = natom
        self._coordinates = coordinates
        self._numbers = numbers
        self._pseudo_numbers = pseudo_numbers

        # Assign remaining arguments as attributes
        self._grid = grid
        self._moldens = moldens
        self._spindens = spindens
        self._local = local
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()

    def __getitem__(self, key):
        return self.cache.load(key)

    def _get_natom(self):
        return self._natom

    natom = property(_get_natom)

    def _get_coordinates(self):
        return self._coordinates

    coordinates = property(_get_coordinates)

    def _get_numbers(self):
        return self._numbers

    numbers = property(_get_numbers)

    def _get_pseudo_numbers(self):
        return self._pseudo_numbers

    pseudo_numbers = property(_get_pseudo_numbers)

    def _get_grid(self):
        return self.get_grid()

    grid = property(_get_grid)

    def _get_local(self):
        return self._local

    local = property(_get_local)

    def _get_lmax(self):
        return self._lmax

    lmax = property(_get_lmax)

    def _get_cache(self):
        return self._cache

    cache = property(_get_cache)

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Discard all cached results, e.g. because wfn changed'''
        JustOnceClass.clear(self)
        self.cache.clear()

    def get_grid(self, index=None):
        '''Return an integration grid

           **Optional arguments:**

           index
                The index of the atom. If not given, a grid for the entire
                system is returned. If self.local is False, a full system grid
                is always returned.
        '''
        if index is None or not self.local:
            return self._grid
        else:
            return self._subgrids[index]

    def get_moldens(self, index=None, output=None):
        result = self.to_atomic_grid(index, self._moldens)
        if output is not None:
            output[:] = result
        return result

    def get_spindens(self, index=None, output=None):
        result = self.to_atomic_grid(index, self._spindens)
        if output is not None:
            output[:] = result
        return result

    def get_wcor(self, index):
        '''Return the weight corrections on a grid

           See get_grid for the meaning of the optional arguments
        '''
        raise NotImplementedError

    def _init_subgrids(self):
        raise NotImplementedError

    def _init_log_base(self):
        raise NotImplementedError

    def _init_log_scheme(self):
        raise NotImplementedError

    def _init_log_memory(self):
        if log.do_medium:
            # precompute arrays sizes for certain grids
            nbyte_global = self.grid.size*8
            nbyte_locals = np.array([self.get_grid(i).size*8 for i in xrange(self.natom)])

            # compute and report usage
            estimates = self.get_memory_estimates()
            nbyte_total = 0
            log('Coarse estimate of memory usage for the partitioning:')
            log('                         Label  Memory[GB]')
            log.hline()
            for label, nlocals, nglobal in estimates:
                nbyte = np.dot(nlocals, nbyte_locals) + nglobal*nbyte_global
                log('%30s  %10.3f' % (label, nbyte/1024.0**3))
                nbyte_total += nbyte
            log('%30s  %10.3f' % ('Total', nbyte_total/1024.0**3))
            log.hline()
            log.blank()

    def get_memory_estimates(self):
        return [
            ('Atomic weights', np.ones(self.natom), 0),
            ('Promolecule', np.zeros(self.natom), 1),
            ('Working arrays', np.zeros(self.natom), 2),
        ]

    def to_atomic_grid(self, index, data):
        raise NotImplementedError

    def compute_pseudo_population(self, index):
        grid = self.get_grid(index)
        dens = self.get_moldens(index)
        at_weights = self.cache.load('at_weights', index)
        wcor = self.get_wcor(index)
        return grid.integrate(at_weights, dens, wcor)

    @just_once
    def do_partitioning(self):
        self.update_at_weights()
    do_partitioning.names = []

    def update_at_weights(self):
        '''Updates the at_weights arrays in the case (and all related arrays)'''
        raise NotImplementedError

    @just_once
    def do_populations(self):
        populations, new = self.cache.load('populations', alloc=self.natom, tags='o')
        if new:
            self.do_partitioning()
            pseudo_populations = self.cache.load('pseudo_populations', alloc=self.natom, tags='o')[0]
            if log.do_medium:
                log('Computing atomic populations.')
            for i in xrange(self.natom):
                pseudo_populations[i] = self.compute_pseudo_population(i)
            populations[:] = pseudo_populations
            populations += self.numbers - self.pseudo_numbers

    @just_once
    def do_charges(self):
        charges, new = self._cache.load('charges', alloc=self.natom, tags='o')
        if new:
            self.do_populations()
            populations = self._cache.load('populations')
            if log.do_medium:
                log('Computing atomic charges.')
            charges[:] = self.numbers - populations

    @just_once
    def do_spin_charges(self):
        if self._spindens is not None:
            spin_charges, new = self._cache.load('spin_charges', alloc=self.natom, tags='o')
            self.do_partitioning()
            if log.do_medium:
                log('Computing atomic spin charges.')
            for index in xrange(self.natom):
                grid = self.get_grid(index)
                spindens = self.get_spindens(index)
                at_weights = self.cache.load('at_weights', index)
                wcor = self.get_wcor(index)
                spin_charges[index] = grid.integrate(at_weights, spindens, wcor)

    @just_once
    def do_moments(self):
        ncart = get_ncart_cumul(self.lmax)
        cartesian_multipoles, new1 = self._cache.load('cartesian_multipoles', alloc=(self.natom, ncart), tags='o')

        npure = get_npure_cumul(self.lmax)
        pure_multipoles, new1 = self._cache.load('pure_multipoles', alloc=(self.natom, npure), tags='o')

        nrad = self.lmax+1
        radial_moments, new2 = self._cache.load('radial_moments', alloc=(self.natom, nrad), tags='o')

        if new1 or new2:
            self.do_partitioning()
            if log.do_medium:
                log('Computing cartesian and pure AIM multipoles and radial AIM moments.')

            for i in xrange(self.natom):
                # 1) Define a 'window' of the integration grid for this atom
                center = self.coordinates[i]
                grid = self.get_grid(i)

                # 2) Compute the AIM
                aim = self.get_moldens(i)*self.cache.load('at_weights', i)

                # 3) Compute weight corrections
                wcor = self.get_wcor(i)

                # 4) Compute Cartesian multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                cartesian_multipoles[i] = -grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=1)
                cartesian_multipoles[i, 0] += self.pseudo_numbers[i]

                # 5) Compute Pure multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                pure_multipoles[i] = -grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=2)
                pure_multipoles[i, 0] += self.pseudo_numbers[i]

                # 6) Compute Radial moments
                # For the radial moments, it is not common to put a minus sign
                # for the negative electron charge.
                radial_moments[i] = grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=3)

    def do_all(self):
        '''Computes all properties and return a list of their keys.'''
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if callable(attr) and attr_name.startswith('do_') and attr_name != 'do_all':
                attr()
        return list(self.cache.iterkeys(tags='o'))
Example #22
0
class Part(JustOnceClass):
    name = None
    linear = False # whether the populations are linear in the density matrix.

    def __init__(self, system, grid, local, slow, lmax, moldens=None):
        '''
           **Arguments:**

           system
                The system to be partitioned.

           grid
                The integration grid

           local
                Whether or not to use local (non-periodic) grids.

           slow
                When ``True``, also the AIM properties are computed that use the
                AIM overlap operators.

           lmax
                The maximum angular momentum in multipole expansions.

           **Optional arguments:**

           moldens
                The all-electron density grid data.
        '''
        JustOnceClass.__init__(self)
        self._system = system
        self._grid = grid
        self._local = local
        self._slow = slow
        self._lmax = lmax

        # Caching stuff, to avoid recomputation of earlier results
        self._cache = Cache()
        # Caching of work arrays to avoid reallocation
        if moldens is not None:
            self._cache.dump('moldens', moldens)

        # Initialize the subgrids
        if local:
            self._init_subgrids()

        # Some screen logging
        self._init_log_base()
        self._init_log_scheme()
        self._init_log_memory()
        if log.do_medium:
            log.blank()

    def __getitem__(self, key):
        return self.cache.load(key)

    def _get_system(self):
        return self._system

    system = property(_get_system)

    def _get_grid(self):
        return self.get_grid()

    grid = property(_get_grid)

    def _get_local(self):
        return self._local

    local = property(_get_local)

    def _get_slow(self):
        return self._slow

    slow = property(_get_slow)

    def _get_lmax(self):
        return self._lmax

    lmax = property(_get_lmax)

    def _get_cache(self):
        return self._cache

    cache = property(_get_cache)

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Discard all cached results, e.g. because wfn changed'''
        JustOnceClass.clear(self)
        self.cache.clear()

    def update_grid(self, grid):
        '''Specify a new grid

           **Arguments:**

           grid
                The new grid

           When the new and old grid are the same, no action is taken. When
           a really new grid is provided, the subgrids are updated and the
           cache is cleared.
        '''
        if not (grid is self._grid):
            self._grid = grid
            if self.local:
                self._init_subgrids()
            self.clear()

    def get_grid(self, index=None):
        '''Return an integration grid

           **Optional arguments:**

           index
                The index of the atom. If not given, a grid for the entire
                system is returned. If self.local is False, a full system grid
                is always returned.
        '''
        if index is None or not self.local:
            return self._grid
        else:
            return self._subgrids[index]

    def get_moldens(self, index=None, output=None):
        self.do_moldens()
        moldens = self.cache.load('moldens')
        result = self.to_atomic_grid(index, moldens)
        if output is not None:
            output[:] = result
        return result

    def get_spindens(self, index=None, output=None):
        self.do_spindens()
        spindens = self.cache.load('spindens')
        result = self.to_atomic_grid(index, spindens)
        if output is not None:
            output[:] = result
        return result

    def get_wcor(self, index):
        '''Return the weight corrections on a grid

           See get_grid for the meaning of the optional arguments
        '''
        raise NotImplementedError

    def _init_subgrids(self):
        raise NotImplementedError

    def _init_log_base(self):
        raise NotImplementedError

    def _init_log_scheme(self):
        raise NotImplementedError

    def _init_log_memory(self):
        if log.do_medium:
            # precompute arrays sizes for certain grids
            nbyte_global = self.grid.size*8
            nbyte_locals = np.array([self.get_grid(i).size*8 for i in xrange(self.system.natom)])

            # compute and report usage
            estimates = self.get_memory_estimates()
            nbyte_total = 0
            log('Coarse estimate of memory usage for the partitioning:')
            log('                         Label  Memory[GB]')
            log.hline()
            for label, nlocals, nglobal in estimates:
                nbyte = np.dot(nlocals, nbyte_locals) + nglobal*nbyte_global
                log('%30s  %10.3f' % (label, nbyte/1024.0**3))
                nbyte_total += nbyte
            log('%30s  %10.3f' % ('Total', nbyte_total/1024.0**3))
            log.hline()
            log.blank()

    def get_memory_estimates(self):
        return [
            ('Atomic weights', np.ones(self.system.natom), 0),
            ('Promolecule', np.zeros(self.system.natom), 1),
            ('Working arrays', np.zeros(self.system.natom), 2),
        ]

    def to_atomic_grid(self, index, data):
        raise NotImplementedError

    def compute_pseudo_population(self, index):
        grid = self.get_grid(index)
        dens = self.get_moldens(index)
        at_weights = self.cache.load('at_weights', index)
        wcor = self.get_wcor(index)
        return grid.integrate(at_weights, dens, wcor)

    @just_once
    def do_moldens(self):
        raise NotImplementedError

    @just_once
    def do_spindens(self):
        raise NotImplementedError

    @just_once
    def do_partitioning(self):
        self.update_at_weights()
    do_partitioning.names = []

    def update_at_weights(self):
        '''Updates the at_weights arrays in the case (and all related arrays)'''
        raise NotImplementedError

    @just_once
    def do_populations(self):
        populations, new = self.cache.load('populations', alloc=self.system.natom, tags='o')
        if new:
            self.do_partitioning()
            self.do_moldens()
            pseudo_populations = self.cache.load('pseudo_populations', alloc=self.system.natom, tags='o')[0]
            if log.do_medium:
                log('Computing atomic populations.')
            for i in xrange(self.system.natom):
                pseudo_populations[i] = self.compute_pseudo_population(i)
            populations[:] = pseudo_populations
            populations += self.system.numbers - self.system.pseudo_numbers

    @just_once
    def do_charges(self):
        charges, new = self._cache.load('charges', alloc=self.system.natom, tags='o')
        if new:
            self.do_populations()
            populations = self._cache.load('populations')
            if log.do_medium:
                log('Computing atomic charges.')
            charges[:] = self.system.numbers - populations

    @just_once
    def do_spin_charges(self):
        spin_charges, new = self._cache.load('spin_charges', alloc=self.system.natom, tags='o')
        if new:
            if isinstance(self.system.wfn, RestrictedWFN):
                spin_charges[:] = 0.0
            else:
                try:
                    self.do_spindens()
                except NotImplementedError:
                    self.cache.clear_item('spin_charges')
                    return
                self.do_partitioning()
                if log.do_medium:
                    log('Computing atomic spin charges.')
                for index in xrange(self.system.natom):
                    grid = self.get_grid(index)
                    spindens = self.get_spindens(index)
                    at_weights = self.cache.load('at_weights', index)
                    wcor = self.get_wcor(index)
                    spin_charges[index] = grid.integrate(at_weights, spindens, wcor)

    @just_once
    def do_moments(self):
        if log.do_medium:
            log('Computing cartesian and pure AIM multipoles and radial AIM moments.')

        ncart = get_ncart_cumul(self.lmax)
        cartesian_multipoles, new1 = self._cache.load('cartesian_multipoles', alloc=(self._system.natom, ncart), tags='o')

        npure = get_npure_cumul(self.lmax)
        pure_multipoles, new1 = self._cache.load('pure_multipoles', alloc=(self._system.natom, npure), tags='o')

        nrad = self.lmax+1
        radial_moments, new2 = self._cache.load('radial_moments', alloc=(self._system.natom, nrad), tags='o')

        if new1 or new2:
            self.do_partitioning()
            for i in xrange(self._system.natom):
                # 1) Define a 'window' of the integration grid for this atom
                center = self._system.coordinates[i]
                grid = self.get_grid(i)

                # 2) Compute the AIM
                aim = self.get_moldens(i)*self.cache.load('at_weights', i)

                # 3) Compute weight corrections (TODO: needs to be assessed!)
                wcor = self.get_wcor(i)

                # 4) Compute Cartesian multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                cartesian_multipoles[i] = -grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=1)
                cartesian_multipoles[i, 0] += self.system.pseudo_numbers[i]

                # 5) Compute Pure multipole moments
                # The minus sign is present to account for the negative electron
                # charge.
                pure_multipoles[i] = -grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=2)
                pure_multipoles[i, 0] += self.system.pseudo_numbers[i]

                # 6) Compute Radial moments
                # For the radial moments, it is not common to put a minus sign
                # for the negative electron charge.
                radial_moments[i] = grid.integrate(aim, wcor, center=center, lmax=self.lmax, mtype=3)

    def do_all(self):
        '''Computes all properties and return a list of their names.'''
        slow_methods = ['do_overlap_operators', 'do_bond_order', 'do_noninteracting_response']
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if callable(attr) and attr_name.startswith('do_') and attr_name != 'do_all':
                if self._slow or (not attr_name in slow_methods):
                    attr()
        return list(self.cache.iterkeys(tags='o'))
Example #23
0
class Localization(object):
    '''Base class for all localization methods'''
    def __init__(self, lf, occ_model, projector):
        '''Localize canonical HF orbitals.

           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model.

           Projector
                Projectors for atomic basis function. A list of TwoIndex
                instances.

           **Optional arguments:**

        '''
        self._lf = lf
        self._proj = projector
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        self._nvirt = (lf.default_nbasis-occ_model.noccs[0])
        self._cache = Cache()
        self._locblock = None
        self._popmatrix = None

    @timer.with_section('Localization')
    def __call__(self, orb, select, **kwargs):
        '''Localizes the orbitals using a unitary transformation to rotate the
           AO/MO coefficient matrix. The orbitals are optimized by minimizing
           an objective function.

           This works only for restricted orbitals.

           **Arguments:**

           orb
                The AO/MO coefficients. An Expansion instance.

           select
                The orbital block to be localised (str). Any of ``occ`` (occupied
                orbitals), ``virt`` (virtual orbitals)

           **Keywords:**

           :maxiter: (int) maximum number of iterations for localization
                     (default 2000)
           :threshold: (float) localization threshold for objective function
                       (default 1e-6)
           :levelshift: level shift of Hessian (float) (default 1e-8)
           :stepsearch: step search options (dictionary) containing:

                        * method: step search method used (str). One of
                          ``trust-region`` (default), ``None``, ``backtracking``
                        * alpha: scaling factor for Newton step (float), used in
                          ``backtracking`` and ``None`` method (default 0.75)
                        * c1: parameter used in ``backtracking`` (float)
                          (default 1e-4)
                        * minalpha: minimum step length used in ``backracking``
                          (float) (default 1e-6)
                        * maxiterouter: maximum number of search steps (int)
                          (default 10)
                        * maxiterinner: maximum number of optimization
                          steps in each search step (int) (used only in ``pcg``,
                          default 500)
                        * maxeta: upper bound for estimated vs actual change in
                          ``trust-region`` (float) (default 0.75)
                        * mineta: lower bound for estimated vs actual change in
                          ``trust-region`` (float) (default 0.25)
                        * upscale: scaling factor to increase trustradius in
                          ``trust-region`` (float) (default 2.0)
                        * downscale: scaling factor to decrease trustradius in
                          ``trust-region`` (float) and scaling factor in
                          ``backtracking`` (default 0.25)
                        * trustradius: initial trustradius (float) (default
                          0.75)
                        * maxtrustradius: maximum trustradius (float) (default
                          0.75)
                        * threshold: trust-region optimization threshold, only
                          used in ``pcg`` (float) (default 1e-8)
                        * optimizer: optimizes step to boundary of trustradius
                          (str). One of ``pcg``, ``dogleg``, ``ddl`` (default
                          ddl)
        '''
        if log.do_medium:
            log('Performing localization of %s block' %(select))
        log.cite('pipek1989', 'the Pipek-Mezey localization scheme')
        #
        # Assign default keyword arguements
        #
        names = []
        def _helper(x,y):
            names.append(x)
            return kwargs.get(x,y)
        maxiter = _helper('maxiter', 2000)
        thresh = _helper('threshold', 1e-6)
        lshift = _helper('levelshift', 1e-8)
        stepsearch = _helper('stepsearch', dict({}))
        stepsearch.setdefault('method', 'trust-region')
        stepsearch.setdefault('minalpha', 1e-6)
        stepsearch.setdefault('alpha', 1.0)
        stepsearch.setdefault('c1', 0.0001)
        stepsearch.setdefault('maxiterouter', 10)
        stepsearch.setdefault('maxiterinner', 500)
        stepsearch.setdefault('maxeta', 0.75)
        stepsearch.setdefault('mineta', 0.25)
        stepsearch.setdefault('upscale', 2.0)
        stepsearch.setdefault('downscale', 0.25)
        stepsearch.setdefault('trustradius', 0.75)
        stepsearch.setdefault('maxtrustradius', 0.75)
        stepsearch.setdefault('threshold', 1e-8)
        stepsearch.setdefault('optimizer', 'ddl')

        for name, value in kwargs.items():
            if name not in names:
                raise ValueError("Unknown keyword argument %s" % name)
            if value < 0:
                raise ValueError('Illegal value for %s: %s' %(name, value))

        #
        # Update information about localization block
        #
        self.update_locblock(select)

        if log.do_medium:
            log('%3s  %12s  %10s' %('Iter', 'D(ObjectiveFunction)', 'Steplength'))
        #
        # Initialize step search
        #
        stepsearch_ = RStepSearch(self.lf, **stepsearch)
        #
        # Calculate initial objective function
        #
        self.solve_model(orb)
        objfct_ref = self.compute_objective_function()

        maxThresh = True
        maxIter = True
        it = 0
        while maxThresh and maxIter:
            #
            # Update population matrix for new orbitals
            #
            self.compute_population_matrix(orb)
            #
            # Calculate orbital gradient and diagonal approximation to the Hessian
            #
            kappa, gradient, hessian = self.orbital_rotation_step(lshift)
            #
            # Apply steps search to orbital rotation step 'kappa' and perform
            # orbital rotation
            #
            stepsearch_(self, None, None, orb,
                       **{'kappa': kappa, 'gradient': gradient, 'hessian': hessian
                         })
            #
            # update objective function
            #
            objfct = self.compute_objective_function()
            it += 1
            #
            # Print localization progress
            #
            if log.do_medium:
                log('%4i   %14.8f' %(it, abs(objfct-objfct_ref)))
            #
            # Check convergence
            #
            maxThresh = abs(objfct-objfct_ref)>thresh
            maxIter = it<maxiter
            #
            # Prepare for new iteration
            #
            objfct_ref = objfct
        if maxThresh and not maxIter:
            if log.do_medium:
                log(' ')
                log('Warning: Orbital localization not converged in %i iteration' %(it-1))
                log(' ')
        else:
            if log.do_medium:
                log(' ')
                log('Orbital localization converged in %i iteration' %(it-1))
                log(' ')

    def _get_nbasis(self):
        '''The number of basis functions'''
        return self._nbasis

    nbasis = property(_get_nbasis)

    def _get_nocc(self):
        '''The number of occupied orbitals'''
        return self._nocc

    nocc = property(_get_nocc)

    def _get_nvirt(self):
        '''The number of virtual orbitals'''
        return self._nvirt

    nvirt = property(_get_nvirt)

    def _get_lf(self):
        '''The LinalgFactory'''
        return self._lf

    lf = property(_get_lf)

    def _get_proj(self):
        '''The Projectors. A list of TwoIndex instances'''
        return self._proj

    proj = property(_get_proj)

    def _get_locblock(self):
        '''The orbital block to be localized'''
        return self._locblock

    locblock = property(_get_locblock)

    def _get_popmatrix(self):
        '''The population matrix. A list of TwoIndex instances'''
        return self._popmatrix

    popmatrix = property(_get_popmatrix)

    def update_locblock(self, new):
        '''Update localization block'''
        self._locblock = new

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Clear all wavefunction information'''
        self._cache.clear()

    def compute_rotation_matrix(self, coeff):
        '''Determine orbital rotation matrix

           **Arguments:**

           coeff
                The non-reduntant orbital rotations, we need only values for
                p<q
        '''
        indl = np.tril_indices(self.nbasis, -1)
        kappa = self.lf.create_two_index(self.nbasis, self.nbasis)
        #
        # k_pq = -k_qp
        #
        kappa.assign(coeff, indl)
        kappa.iadd_t(kappa, -1.0)

        out = compute_unitary_matrix(kappa)
        return out

    def compute_population_matrix(self, exp):
        '''Determine population matrix

           **Arguments:**

           exp
                The current AO/MO coefficients. An Expansion instance
        '''
        #
        # Get orbital block to be localized, a OneIndex instance
        #
        block = self.assign_locblock()
        #
        # Calculate population matrices for orbital block
        #
        popmat = []
        for op in self.proj:
            pop = self.lf.create_two_index()
            expblock = exp.copy()
            expblock.imul(block)
            expblock.itranspose()
            pop.assign_dot(expblock, op)
            expblock.itranspose()
            pop.idot(expblock)

            popmat.append(pop)
        self._popmatrix = popmat
Example #24
0
class Localization(object):
    '''Base class for all localization methods'''
    def __init__(self, lf, occ_model, projector):
        '''Localize canonical HF orbitals.

           **Arguments:**

           lf
                A LinalgFactory instance.

           occ_model
                Occupation model.

           Projector
                Projectors for atomic basis function. A list of TwoIndex
                instances.

           **Optional arguments:**

        '''
        self._lf = lf
        self._proj = projector
        self._nocc = occ_model.noccs[0]
        self._nbasis = lf.default_nbasis
        self._nvirt = (lf.default_nbasis - occ_model.noccs[0])
        self._cache = Cache()
        self._locblock = None
        self._popmatrix = None

    @timer.with_section('Localization')
    def __call__(self, orb, select, **kwargs):
        '''Localizes the orbitals using a unitary transformation to rotate the
           AO/MO coefficient matrix. The orbitals are optimized by minimizing
           an objective function.

           This works only for restricted orbitals.

           **Arguments:**

           orb
                The AO/MO coefficients. An Expansion instance.

           select
                The orbital block to be localised (str). Any of ``occ`` (occupied
                orbitals), ``virt`` (virtual orbitals)

           **Keywords:**

           :maxiter: (int) maximum number of iterations for localization
                     (default 2000)
           :threshold: (float) localization threshold for objective function
                       (default 1e-6)
           :levelshift: level shift of Hessian (float) (default 1e-8)
           :stepsearch: step search options (dictionary) containing:

                        * method: step search method used (str). One of
                          ``trust-region`` (default), ``None``, ``backtracking``
                        * alpha: scaling factor for Newton step (float), used in
                          ``backtracking`` and ``None`` method (default 0.75)
                        * c1: parameter used in ``backtracking`` (float)
                          (default 1e-4)
                        * minalpha: minimum step length used in ``backracking``
                          (float) (default 1e-6)
                        * maxiterouter: maximum number of search steps (int)
                          (default 10)
                        * maxiterinner: maximum number of optimization
                          steps in each search step (int) (used only in ``pcg``,
                          default 500)
                        * maxeta: upper bound for estimated vs actual change in
                          ``trust-region`` (float) (default 0.75)
                        * mineta: lower bound for estimated vs actual change in
                          ``trust-region`` (float) (default 0.25)
                        * upscale: scaling factor to increase trustradius in
                          ``trust-region`` (float) (default 2.0)
                        * downscale: scaling factor to decrease trustradius in
                          ``trust-region`` (float) and scaling factor in
                          ``backtracking`` (default 0.25)
                        * trustradius: initial trustradius (float) (default
                          0.75)
                        * maxtrustradius: maximum trustradius (float) (default
                          0.75)
                        * threshold: trust-region optimization threshold, only
                          used in ``pcg`` (float) (default 1e-8)
                        * optimizer: optimizes step to boundary of trustradius
                          (str). One of ``pcg``, ``dogleg``, ``ddl`` (default
                          ddl)
        '''
        if log.do_medium:
            log('Performing localization of %s block' % (select))
        log.cite('pipek1989', 'the Pipek-Mezey localization scheme')
        #
        # Assign default keyword arguements
        #
        names = []

        def _helper(x, y):
            names.append(x)
            return kwargs.get(x, y)

        maxiter = _helper('maxiter', 2000)
        thresh = _helper('threshold', 1e-6)
        lshift = _helper('levelshift', 1e-8)
        stepsearch = _helper('stepsearch', dict({}))
        stepsearch.setdefault('method', 'trust-region')
        stepsearch.setdefault('minalpha', 1e-6)
        stepsearch.setdefault('alpha', 1.0)
        stepsearch.setdefault('c1', 0.0001)
        stepsearch.setdefault('maxiterouter', 10)
        stepsearch.setdefault('maxiterinner', 500)
        stepsearch.setdefault('maxeta', 0.75)
        stepsearch.setdefault('mineta', 0.25)
        stepsearch.setdefault('upscale', 2.0)
        stepsearch.setdefault('downscale', 0.25)
        stepsearch.setdefault('trustradius', 0.75)
        stepsearch.setdefault('maxtrustradius', 0.75)
        stepsearch.setdefault('threshold', 1e-8)
        stepsearch.setdefault('optimizer', 'ddl')

        for name, value in kwargs.items():
            if name not in names:
                raise ValueError("Unknown keyword argument %s" % name)
            if value < 0:
                raise ValueError('Illegal value for %s: %s' % (name, value))

        #
        # Update information about localization block
        #
        self.update_locblock(select)

        if log.do_medium:
            log('%3s  %12s  %10s' %
                ('Iter', 'D(ObjectiveFunction)', 'Steplength'))
        #
        # Initialize step search
        #
        stepsearch_ = RStepSearch(self.lf, **stepsearch)
        #
        # Calculate initial objective function
        #
        self.solve_model(orb)
        objfct_ref = self.compute_objective_function()

        maxThresh = True
        maxIter = True
        it = 0
        while maxThresh and maxIter:
            #
            # Update population matrix for new orbitals
            #
            self.compute_population_matrix(orb)
            #
            # Calculate orbital gradient and diagonal approximation to the Hessian
            #
            kappa, gradient, hessian = self.orbital_rotation_step(lshift)
            #
            # Apply steps search to orbital rotation step 'kappa' and perform
            # orbital rotation
            #
            stepsearch_(
                self, None, None, orb, **{
                    'kappa': kappa,
                    'gradient': gradient,
                    'hessian': hessian
                })
            #
            # update objective function
            #
            objfct = self.compute_objective_function()
            it += 1
            #
            # Print localization progress
            #
            if log.do_medium:
                log('%4i   %14.8f' % (it, abs(objfct - objfct_ref)))
            #
            # Check convergence
            #
            maxThresh = abs(objfct - objfct_ref) > thresh
            maxIter = it < maxiter
            #
            # Prepare for new iteration
            #
            objfct_ref = objfct
        if maxThresh and not maxIter:
            if log.do_medium:
                log(' ')
                log('Warning: Orbital localization not converged in %i iteration'
                    % (it - 1))
                log(' ')
        else:
            if log.do_medium:
                log(' ')
                log('Orbital localization converged in %i iteration' %
                    (it - 1))
                log(' ')

    def _get_nbasis(self):
        '''The number of basis functions'''
        return self._nbasis

    nbasis = property(_get_nbasis)

    def _get_nocc(self):
        '''The number of occupied orbitals'''
        return self._nocc

    nocc = property(_get_nocc)

    def _get_nvirt(self):
        '''The number of virtual orbitals'''
        return self._nvirt

    nvirt = property(_get_nvirt)

    def _get_lf(self):
        '''The LinalgFactory'''
        return self._lf

    lf = property(_get_lf)

    def _get_proj(self):
        '''The Projectors. A list of TwoIndex instances'''
        return self._proj

    proj = property(_get_proj)

    def _get_locblock(self):
        '''The orbital block to be localized'''
        return self._locblock

    locblock = property(_get_locblock)

    def _get_popmatrix(self):
        '''The population matrix. A list of TwoIndex instances'''
        return self._popmatrix

    popmatrix = property(_get_popmatrix)

    def update_locblock(self, new):
        '''Update localization block'''
        self._locblock = new

    def __clear__(self):
        self.clear()

    def clear(self):
        '''Clear all wavefunction information'''
        self._cache.clear()

    def compute_rotation_matrix(self, coeff):
        '''Determine orbital rotation matrix

           **Arguments:**

           coeff
                The non-reduntant orbital rotations, we need only values for
                p<q
        '''
        indl = np.tril_indices(self.nbasis, -1)
        kappa = self.lf.create_two_index(self.nbasis, self.nbasis)
        #
        # k_pq = -k_qp
        #
        kappa.assign(coeff, indl)
        kappa.iadd_t(kappa, -1.0)

        out = compute_unitary_matrix(kappa)
        return out

    def compute_population_matrix(self, exp):
        '''Determine population matrix

           **Arguments:**

           exp
                The current AO/MO coefficients. An Expansion instance
        '''
        #
        # Get orbital block to be localized, a OneIndex instance
        #
        block = self.assign_locblock()
        #
        # Calculate population matrices for orbital block
        #
        popmat = []
        for op in self.proj:
            pop = self.lf.create_two_index()
            expblock = exp.copy()
            expblock.imul(block)
            expblock.itranspose()
            pop.assign_dot(expblock, op)
            expblock.itranspose()
            pop.idot(expblock)

            popmat.append(pop)
        self._popmatrix = popmat
Example #25
0
class System(object):
    def __init__(self,
                 coordinates,
                 numbers,
                 obasis=None,
                 grid=None,
                 wfn=None,
                 lf=None,
                 cache=None,
                 extra=None,
                 cell=None,
                 pseudo_numbers=None,
                 chk=None):
        """
           **Arguments:**

           coordinates
                A (N, 3) float numpy array with Cartesian coordinates of the
                atoms.

           numbers
                A (N,) int numpy vector with the atomic numbers.

           **Optional arguments:**

           obasis
                A string or an instance of either the basis set or basis set
                description classes, e.g. 'STO-3G', GOBasisDesc('STO-3G'), ...
                for the orbitals.

           grid
                A grid object used for molecular integration.

           wfn
                A wavefunction object.

           lf
                A LinalgFactory instance. When not given, a DenseLinalgFactory
                is used by default.

           cache
                A cache object with computed results that depend on other
                attributes of the system class. Cached items should be tagged
                according to the attributes they depend on:

                    - ``o``: obasis
                    - ``c``: coordinates
                    - ``g``: grid

                When given as a dictionary, each value must consist of two
                items: the object to be cached and the tags.

           extra
                A dictionary with additional information about the system. The
                keys must be strings.

           cell
                A Cell object that describes the (generally triclinic) periodic
                boundary conditions. So far, this is nearly nowhere supported in
                Horton, so don't get too excited.

           pseudo_numbers
                The core charges of the pseudo potential, if applicable

           chk
                A filename for the checkpoint file or an open h5.File object.
                If the file does not exist yet, it will be created. If the file
                already exists, it must be an HDF5 file that is structured
                such that it adheres to the format that Horton creates itself.
                If chk is an open h5.File object, it will not be closed when the
                System instance is deleted.
        """

        # A) Assign all attributes
        self._coordinates = np.array(coordinates, dtype=float, copy=False)
        self._numbers = np.array(numbers, dtype=int, copy=False)
        # some checks
        if len(self._coordinates.shape
               ) != 2 or self._coordinates.shape[1] != 3:
            raise TypeError(
                'coordinates argument must be a 2D array with three columns')
        if len(self._numbers.shape) != 1:
            raise TypeError('numbers must a vector of integers.')
        if self._numbers.shape[0] != self._coordinates.shape[0]:
            raise TypeError(
                'numbers and coordinates must have compatible array shapes.')
        #
        self._grid = grid
        #
        self._wfn = wfn
        #
        if cache is None:
            self._cache = Cache()
        elif isinstance(cache, Cache):
            self._cache = cache
        elif isinstance(cache, dict):
            self._cache = Cache()
            for key, (value, tags) in cache.iteritems():
                self._cache.dump(key, value, tags=tags)
        else:
            raise TypeError('Could not interpret the cache argument.')
        #
        if lf is None:
            self._lf = DenseLinalgFactory()
        else:
            self._lf = lf
        #
        if extra is None:
            self._extra = {}
        else:
            self._extra = extra
        #
        self._obasis = None
        self._obasis_desc = None
        if obasis is not None:
            self.update_obasis(obasis)

        self._cell = cell
        self._pseudo_numbers = pseudo_numbers

        # The checkpoint file
        self._chk = None
        self._close_chk = False
        self.assign_chk(chk)

        self._log_init()

    def __del__(self):
        # Close the HD5 checkpoint file. This must be done carefully to avoid
        # spurious error messages when an unrelated exception occurs.
        if hasattr(self, '_chk') and self.chk is not None and self._close_chk:
            self.chk.close()

    def _get_natom(self):
        '''The number of atoms'''
        return len(self.numbers)

    natom = property(_get_natom)

    def _get_coordinates(self):
        '''The positions of the nuclei'''
        return self._coordinates.view()

    coordinates = property(_get_coordinates)

    def _get_numbers(self):
        '''An array with the atomic numbers'''
        return self._numbers.view()

    numbers = property(_get_numbers)

    def _get_obasis(self):
        '''The orbital basis'''
        return self._obasis

    obasis = property(_get_obasis)

    def _get_obasis_desc(self):
        '''The orbital basis description'''
        return self._obasis_desc

    obasis_desc = property(_get_obasis_desc)

    def _get_grid(self):
        '''The integration grid'''
        return self._grid

    grid = property(_get_grid)

    def _get_wfn(self):
        '''The wavefunction'''
        return self._wfn

    wfn = property(_get_wfn)

    def _get_lf(self):
        '''The LinalgFactory for this system'''
        return self._lf

    lf = property(_get_lf)

    def _get_cache(self):
        '''A cache of intermediate results that depend on the coordinates'''
        return self._cache

    cache = property(_get_cache)

    def _get_extra(self):
        '''A dictionary with extra properties of the system.'''
        return self._extra

    extra = property(_get_extra)

    def _get_cell(self):
        '''A Cell object describing the periodic boundary conditions.'''
        return self._cell

    cell = property(_get_cell)

    def _get_pseudo_numbers(self):
        result = self._pseudo_numbers
        if result is None:
            result = self._numbers
        return result

    pseudo_numbers = property(_get_pseudo_numbers)

    def _get_chk(self):
        '''A ``h5.File`` instance used as checkpoint file or ``None``'''
        return self._chk

    chk = property(_get_chk)

    @classmethod
    def from_file(cls, *args, **kwargs):
        """Create a System object from a file.

           A list of filenames may be provided, which will be loaded in that
           order. Each file complements or overrides the information loaded
           from a previous file in the list. Furthermore, keyword arguments
           may be used to specify additional constructor arguments.

           The ``lf`` optional argument is picked up from the kwargs list to
           contstruct (when needed) arrays to store the results loaded from
           file. When ``lf`` is not given, a DenseLinalgFactory is created by
           default.

           The filenames may also contain checkpoint files and open h5.File
           objects of checkpoint files. The last such checkpoint file will
           automatically be used as a checkpoint file for this class. If you
           want to override this behavior, provide the ``chk`` keyword argument
           (may be None).
        """
        constructor_args = {}
        lf = kwargs.get('lf')
        if lf is None:
            lf = DenseLinalgFactory()
        for fn in args:
            fn_args = load_system_args(fn, lf)
            constructor_args.update(fn_args)
        constructor_args.update(kwargs)

        # If the basis comes from an external code and some operators are
        # loaded, rows and columns may need to be reordered. Similar for the
        # orbital coefficients and the density matrices.
        permutation = constructor_args.get('permutation')
        if permutation is not None:
            cache = constructor_args.get('cache')
            if cache is not None:
                for value, tags in cache.itervalues():
                    if isinstance(value, LinalgObject):
                        value.apply_basis_permutation(permutation)
            wfn = constructor_args.get('wfn')
            if wfn is not None:
                wfn.apply_basis_permutation(permutation)
            del constructor_args['permutation']

        # After the permutation, correct for different sign conventions of the
        # orbitals
        signs = constructor_args.get('signs')
        if signs is not None:
            cache = constructor_args.get('cache')
            if cache is not None:
                for value, tags in cache.itervalues():
                    if isinstance(value, LinalgObject):
                        value.apply_basis_signs(signs)
            wfn = constructor_args.get('wfn')
            if wfn is not None:
                wfn.apply_basis_signs(signs)
            del constructor_args['signs']

        return cls(**constructor_args)

    def _log_init(self):
        '''Write some basic information about the system to the screen logger.'''
        if log.do_medium:
            log('Initialized: %s' % self)
            log.deflist([('Number of atoms', self.natom)] +
                        [('Number of %s' % periodic[n].symbol,
                          (self.numbers == n).sum())
                         for n in sorted(np.unique(self.numbers))] + [
                             ('Linalg Factory', self._lf),
                             ('Orbital basis', self._obasis),
                             ('Wavefunction', self._wfn),
                             ('Checkpoint file', self._chk),
                         ])
            if len(self._cache) > 0:
                log('The following cached items are present: %s' %
                    (', '.join(self._cache.iterkeys())))
            if len(self._extra) > 0:
                log('The following extra attributes are present: %s' %
                    (', '.join(self._extra.iterkeys())))
            log.blank()

    def assign_chk(self, chk):
        if self.chk is not None and self._close_chk:
            self.chk.close()

        if isinstance(chk, basestring):
            # Suppose a filename is given. Create or open an HDF5 file.
            self._chk = h5.File(chk)
            self._close_chk = True
        elif isinstance(chk, h5.Group) or chk is None:
            self._chk = chk
            self._close_chk = False
        else:
            raise TypeError(
                'The chk argument, when not None, must be a filename or an open h5.Group object.'
            )
        self.update_chk()

    def update_chk(self, field_name=None):
        """Write (a part of) the system to the checkpoint file.

           **Optional Argument:**

           field
                A field string that specifies which part must be written to the
                checkpoint file. When not given, all possible fields are
                written. The latter is only useful in specific cases, e.g. upon
                initialization of the system. The available field names are
                specified in the attribute register dictionary in the
                module ``horton.checkpoint``.
        """
        if self._chk is not None:
            from horton.checkpoint import attribute_register
            if field_name is None:
                for field_name, field in attribute_register.iteritems():
                    field.write(self._chk, self)
            else:
                field = attribute_register[field_name]
                field.write(self._chk, self)

    def to_file(self, filename):
        '''Write the system to a file

           **Arguments:**

           filename
                The name of the file to write to. The extension of the file
                is used to determine the file format.
        '''
        dump_system(filename, self)

    def _get_charge(self):
        return self.pseudo_numbers.sum() - self.wfn.nel

    charge = property(_get_charge)

    def update_coordinates(self, coordinates=None):
        '''Update all attributes that depend on coodinates and clear related parts of cache

           **Optional arguments:**

           coordinates
                The new atomic coordinates

           When one wants to set new coordintes, one may also edit the
           system.coordinates array in-place and then call this method without
           any arguments.
        '''
        if coordinates is not None:
            self._coordinates[:] = coordinates
        if self._obasis is not None:
            self._obasis.centers[:] = self._coordinates
        if self._grid is not None:
            self._grid.update_centers(self)
        self.cache.clear(tags='cog')
        self._extra = {}

    def update_grid(self, grid=None):
        '''Define a new integration grid and clear related parts of the cache

           **Optional arguments:**

           grid
                The new integration grid. When not given, it is assumed that
                the grid was modified in-place and that only derived results in
                the cache need to be pruned.
        '''
        if grid is not None:
            self._grid = grid
        self.cache.clear(tags='g')

    def update_obasis(self, obasis=None):
        '''Regenerate the orbital basis and clear all attributes that depend on it.

           **Optional arguments:**

           obasis
                The new basis. This may be a string or an instance of GOBasis or
                GOBasisDesc. When not given, the orbital basis description
                stored in the system object (_obasis_desc attribute) will be
                used.
        '''
        # Get the orbital basis and if possible the orbital basis description.
        from horton.gbasis import GOBasisDesc, GOBasis
        if isinstance(obasis, str):
            obasis_desc = GOBasisDesc(obasis)
        elif isinstance(obasis, GOBasisDesc):
            obasis_desc = obasis
        elif isinstance(obasis, GOBasis):
            obasis_desc = None
        elif obasis is None:
            if self.obasis_desc is None:
                raise TypeError(
                    'No orbital basis description (obasis_desc) available to update obasis.'
                )
            obasis_desc = self.obasis_desc
        else:
            raise TypeError('Could not interpret the obasis argument.')
        if obasis_desc is not None:
            obasis = obasis_desc.apply_to(self)

        # Discard or reset results that depend on orbital basis
        if self.obasis is not None:
            self._cache.clear(tags='o')
            # Ideally, the user of the system object does some sort of
            # projection of the wavefunction on the new basis. This should be
            # done outside the system class as their are too many different ways
            # to handle this. Here, we set the wfn to None, just to force the
            # user to do something.
            self._wfn = None
            self._extra = {}

        # Assign new obasis
        self._lf.set_default_nbasis(obasis.nbasis)
        self._obasis = obasis
        self._obasis_desc = obasis_desc

        # Some consistency checks. These are needed when the initial value of
        # obasis was None. This may occur when the system object is initialized.
        if self._wfn is not None and self._obasis.nbasis != self._wfn.nbasis:
            raise TypeError(
                'The nbasis attribute of obasis and wfn are inconsistent.')
        for key, value in self._cache.iteritems():
            if isinstance(
                    value,
                    LinalgObject) and value.nbasis != self._obasis.nbasis:
                raise TypeError(
                    'The nbasis attribute of the cached object \'%s\' and obasis are inconsistent.'
                    % key)

    @timer.with_section('OLP integrals')
    def get_overlap(self):
        overlap, new = self.cache.load('olp',
                                       alloc=self.lf.create_one_body,
                                       tags='o')
        if new:
            self.obasis.compute_overlap(overlap)
            self.update_chk('cache.olp')
        return overlap

    @timer.with_section('KIN integrals')
    def get_kinetic(self):
        kinetic, new = self.cache.load('kin',
                                       alloc=self.lf.create_one_body,
                                       tags='o')
        if new:
            self.obasis.compute_kinetic(kinetic)
            self.update_chk('cache.kin')
        return kinetic

    @timer.with_section('NAI integrals')
    def get_nuclear_attraction(self):
        nuclear_attraction, new = self.cache.load(
            'na', alloc=self.lf.create_one_body, tags='o')
        if new:
            # TODO: ghost atoms and extra charges
            self.obasis.compute_nuclear_attraction(self.numbers.astype(float),
                                                   self.coordinates,
                                                   nuclear_attraction)
            self.update_chk('cache.na')
        return nuclear_attraction

    @timer.with_section('ER integrals')
    def get_electron_repulsion(self):
        electron_repulsion, new = self.cache.load(
            'er', alloc=self.lf.create_two_body, tags='o')
        if new:
            self.obasis.compute_electron_repulsion(electron_repulsion)
            # ER integrals are not checkpointed by default because they are too heavy.
            # Can be done manually by user if needed: ``system.update_chk('cache.er')``
            #self.update_chk('cache.er')
        return electron_repulsion

    @timer.with_section('Orbitals grid')
    def compute_grid_orbitals(self,
                              points,
                              iorbs=None,
                              orbs=None,
                              select='alpha'):
        '''Compute the electron density on a grid using self.wfn as input

           **Arguments:**

           points
                A Numpy array with grid points, shape (npoint,3)

           **Optional arguments:**

           iorbs
                The indexes of the orbitals to be computed. If not given, the
                orbitals with a non-zero occupation number are computed

           orbs
                An output array, shape (npoint, len(iorbs)). The results are
                added to this array.

           select
                'alpha', 'beta'

           **Returns:**

           orbs
                The array with the result. This is the same as the output
                argument, in case it was provided.
        '''
        exp = self.wfn.get_exp(select)
        if iorbs is None:
            iorbs = (exp.occupations > 0).nonzero()[0]
        shape = (len(points), len(iorbs))
        if orbs is None:
            orbs = np.zeros(shape, float)
        elif orbs.shape != shape:
            raise TypeError('The shape of the output array is wrong')
        self.obasis.compute_grid_orbitals_exp(exp, points, iorbs, orbs)
        return orbs

    @timer.with_section('Density grid')
    def compute_grid_density(self,
                             points,
                             rhos=None,
                             select='full',
                             epsilon=0):
        '''Compute the electron density on a grid using self.wfn as input

           **Arguments:**

           points
                A Numpy array with grid points, shape (npoint,3)

           **Optional arguments:**

           rhos
                An output array, shape (npoint,). The results are added to this
                array.

           select
                'alpha', 'beta', 'full' or 'spin'. ('full' is the default.)

           epsilon
                Allow errors on the density of this magnitude for the sake of
                efficiency.

           **Returns:**

           rhos
                The array with the result. This is the same as the output
                argument, in case it was provided.
        '''
        if rhos is None:
            rhos = np.zeros(len(points), float)
        elif rhos.shape != (points.shape[0], ):
            raise TypeError('The shape of the output array is wrong')
        dm = self.wfn.get_dm(select)
        self.obasis.compute_grid_density_dm(dm, points, rhos, epsilon)
        return rhos

    @timer.with_section('Gradient grid')
    def compute_grid_gradient(self, points, gradrhos=None, select='full'):
        '''Compute the electron density on a grid using self.wfn as input

           **Arguments:**

           points
                A Numpy array with grid points, shape (npoint,3)

           **Optional arguments:**

           gradrhos
                An output array, shape (npoint, 3). The results are added to
                this array.

           select
                'alpha', 'beta', 'full' or 'spin'. ('full' is the default.)

           **Returns:**

           gradrhos
                The array with the result. This is the same as the output
                argument, in case it was provided.
        '''
        if gradrhos is None:
            gradrhos = np.zeros((len(points), 3), float)
        elif gradrhos.shape != (points.shape[0], 3):
            raise TypeError('The shape of the output array is wrong')
        dm = self.wfn.get_dm(select)
        self.obasis.compute_grid_gradient_dm(dm, points, gradrhos)
        return gradrhos

    @timer.with_section('Hartree grid')
    def compute_grid_hartree(self, points, hartree=None, select='full'):
        '''Compute the hartree potential on a grid using self.wfn as input

           **Arguments:**

           points
                A Numpy array with grid points, shape (npoint,3)

           **Optional arguments:**

           hartree
                An output array, shape (npoint,). The results are added to this
                array.

           select
                'alpha', 'beta', 'full' or 'spin'. ('full' is the default.)

           **Returns:**

           hartree
                The array with the result. This is the same as the output
                argument, in case it was provided.
        '''
        if hartree is None:
            hartree = np.zeros(len(points), float)
        elif hartree.shape != (points.shape[0], ):
            raise TypeError('The shape of the output array is wrong')
        dm = self.wfn.get_dm(select)
        self.obasis.compute_grid_hartree_dm(dm, points, hartree)
        return hartree

    @timer.with_section('ESP grid')
    def compute_grid_esp(self, points, esp=None, select='full'):
        '''Compute the esp on a grid using self.wfn as input

           **Arguments:**

           points
                A Numpy array with grid points, shape (npoint,3)

           **Optional arguments:**

           esp
                An output array, shape (npoint,). The results are added to this
                array.

           select
                'alpha', 'beta', 'full' or 'spin'. ('full' is the default.)

           **Returns:**

           esp
                The array with the result. This is the same as the output
                argument, in case it was provided.
        '''
        if esp is None:
            esp = np.zeros(len(points), float)
        elif esp.shape != (points.shape[0], ):
            raise TypeError('The shape of the output array is wrong')
        dm = self.wfn.get_dm(select)
        self.obasis.compute_grid_hartree_dm(dm, points, esp)
        esp *= -1
        compute_grid_nucpot(self.numbers, self.coordinates, points, esp)
        return esp

    @timer.with_section('Fock grid dens')
    def compute_grid_density_fock(self, points, weights, pots, fock):
        '''See documentation self.obasis.compute_grid_density_fock'''
        self.obasis.compute_grid_density_fock(points, weights, pots, fock)

    @timer.with_section('Fock grid grad')
    def compute_grid_gradient_fock(self, points, weights, pots, fock):
        '''See documentation self.obasis.compute_grid_gradient_fock'''
        self.obasis.compute_grid_gradient_fock(points, weights, pots, fock)

    def compute_nucnuc(self):
        '''Compute interaction energy of the nuclei'''
        # TODO: move this to low-level code one day.
        result = 0.0
        for i in xrange(self.natom):
            for j in xrange(i):
                distance = np.linalg.norm(self.coordinates[i] -
                                          self.coordinates[j])
                result += self.numbers[i] * self.numbers[j] / distance
        self._extra['energy_nn'] = result
        return result
Example #26
0
class Hamiltonian(object):
    def __init__(self, system, terms, grid=None, idiot_proof=True):
        '''
           **Arguments:**

           system
                The System object for which the energy must be computed.

           terms
                The terms in the Hamiltonian.

           **Optional arguments:**

           grid
                The integration grid, in case some terms need one.

           idiot_proof
                When set to False, the kinetic energy, external potential and
                Hartree terms are not added automatically and a error is raised
                when no exchange is present.
        '''
        # check arguments:
        if len(terms) == 0:
            raise ValueError(
                'At least one term must be present in the Hamiltonian.')
        for term in terms:
            if term.require_grid and grid is None:
                raise TypeError(
                    'The term %s requires a grid, but not grid is given.' %
                    term)

        # Assign attributes
        self.system = system
        self.terms = list(terms)
        self.grid = grid

        if idiot_proof:
            # Check if an exchange term is present
            if not any(term.exchange for term in self.terms):
                raise ValueError(
                    'No exchange term is given and idiot_proof option is set to True.'
                )
            # Add standard terms if missing
            #  1) Kinetic energy
            if sum(isinstance(term, KineticEnergy) for term in terms) == 0:
                self.terms.append(KineticEnergy())
            #  2) Hartree (or HatreeFock, which is a subclass of Hartree)
            if sum(isinstance(term, Hartree) for term in terms) == 0:
                self.terms.append(Hartree())
            #  3) External Potential
            if sum(isinstance(term, ExternalPotential) for term in terms) == 0:
                self.terms.append(ExternalPotential())

        # Create a cache for shared intermediate results. This cache should only
        # be used for derived quantities that depend on the wavefunction and
        # need to be updated at each SCF cycle.
        self.cache = Cache()

        # bind the terms to this hamiltonian such that certain shared
        # intermediated results can be reused for the sake of efficiency.
        for term in self.terms:
            term.set_hamiltonian(self)

    def add_term(self, term):
        '''Add a new term to the hamiltonian'''
        self.terms.append(term)
        term.set_hamiltonian(self)

    def clear(self):
        '''Mark the properties derived from the wfn as outdated.

           This method does not recompute anything, but just marks operators
           as outdated. They are recomputed as they are needed.
        '''
        self.cache.clear()

    def compute(self):
        '''Compute the energy.

           **Returns:**

           The total energy, including nuclear-nuclear repulsion.
        '''
        total = 0.0
        for term in self.terms:
            energy = term.compute()
            self.system.extra['energy_%s' % term.label] = energy
            total += energy
        energy = self.system.compute_nucnuc()
        self.system.extra['energy_nn'] = energy
        total += energy
        self.system.extra['energy'] = total
        # Store result in chk file
        self.system.update_chk('extra')
        return total

    def log_energy(self):
        '''Write an overview of the last energy computation on screen'''
        log('Contributions to the energy:')
        log.hline()
        log('                                       Energy term                 Value'
            )
        log.hline()
        for term in self.terms:
            energy = self.system.extra['energy_%s' % term.label]
            log('%50s  %20.12f' % (term.label, energy))
        log('%50s  %20.12f' % ('nn', self.system.extra['energy_nn']))
        log('%50s  %20.12f' % ('total', self.system.extra['energy']))
        log.hline()
        log.blank()

    def compute_fock(self, fock_alpha, fock_beta):
        '''Compute alpha (and beta) Fock matrix(es).

           **Arguments:**

           fock_alpha
                A One-Body operator output argument for the alpha fock matrix.

           fock_alpha
                A One-Body operator output argument for the beta fock matrix.

           In the case of a closed-shell computation, the argument fock_beta is
           ``None``.
        '''
        # Loop over all terms and add contributions to the Fock matrix. Some
        # terms will actually only evaluate potentials on grids and add these
        # results to the total potential on a grid.
        for term in self.terms:
            term.add_fock_matrix(fock_alpha, fock_beta, postpone_grid=True)
        # Collect all the total potentials and turn them into contributions
        # for the fock matrix/matrices.

        # Collect potentials for alpha electrons
        # d = density
        if 'dpot_total_alpha' in self.cache:
            dpot = self.cache.load('dpot_total_alpha')
            self.system.compute_grid_density_fock(self.grid.points,
                                                  self.grid.weights, dpot,
                                                  fock_alpha)
        # g = gradient
        if 'gpot_total_alpha' in self.cache:
            gpot = self.cache.load('gpot_total_alpha')
            self.system.compute_grid_gradient_fock(self.grid.points,
                                                   self.grid.weights, gpot,
                                                   fock_alpha)

        if isinstance(self.system.wfn, UnrestrictedWFN):
            # Colect potentials for beta electrons
            # d = density
            if 'dpot_total_beta' in self.cache:
                dpot = self.cache.load('dpot_total_beta')
                self.system.compute_grid_density_fock(self.grid.points,
                                                      self.grid.weights, dpot,
                                                      fock_beta)
            # g = gradient
            if 'gpot_total_beta' in self.cache:
                gpot = self.cache.load('gpot_total_beta')
                self.system.compute_grid_gradient_fock(self.grid.points,
                                                       self.grid.weights, gpot,
                                                       fock_beta)
Example #27
0
    def __init__(self,
                 coordinates,
                 numbers,
                 obasis=None,
                 grid=None,
                 wfn=None,
                 lf=None,
                 cache=None,
                 extra=None,
                 cell=None,
                 pseudo_numbers=None,
                 chk=None):
        """
           **Arguments:**

           coordinates
                A (N, 3) float numpy array with Cartesian coordinates of the
                atoms.

           numbers
                A (N,) int numpy vector with the atomic numbers.

           **Optional arguments:**

           obasis
                A string or an instance of either the basis set or basis set
                description classes, e.g. 'STO-3G', GOBasisDesc('STO-3G'), ...
                for the orbitals.

           grid
                A grid object used for molecular integration.

           wfn
                A wavefunction object.

           lf
                A LinalgFactory instance. When not given, a DenseLinalgFactory
                is used by default.

           cache
                A cache object with computed results that depend on other
                attributes of the system class. Cached items should be tagged
                according to the attributes they depend on:

                    - ``o``: obasis
                    - ``c``: coordinates
                    - ``g``: grid

                When given as a dictionary, each value must consist of two
                items: the object to be cached and the tags.

           extra
                A dictionary with additional information about the system. The
                keys must be strings.

           cell
                A Cell object that describes the (generally triclinic) periodic
                boundary conditions. So far, this is nearly nowhere supported in
                Horton, so don't get too excited.

           pseudo_numbers
                The core charges of the pseudo potential, if applicable

           chk
                A filename for the checkpoint file or an open h5.File object.
                If the file does not exist yet, it will be created. If the file
                already exists, it must be an HDF5 file that is structured
                such that it adheres to the format that Horton creates itself.
                If chk is an open h5.File object, it will not be closed when the
                System instance is deleted.
        """

        # A) Assign all attributes
        self._coordinates = np.array(coordinates, dtype=float, copy=False)
        self._numbers = np.array(numbers, dtype=int, copy=False)
        # some checks
        if len(self._coordinates.shape
               ) != 2 or self._coordinates.shape[1] != 3:
            raise TypeError(
                'coordinates argument must be a 2D array with three columns')
        if len(self._numbers.shape) != 1:
            raise TypeError('numbers must a vector of integers.')
        if self._numbers.shape[0] != self._coordinates.shape[0]:
            raise TypeError(
                'numbers and coordinates must have compatible array shapes.')
        #
        self._grid = grid
        #
        self._wfn = wfn
        #
        if cache is None:
            self._cache = Cache()
        elif isinstance(cache, Cache):
            self._cache = cache
        elif isinstance(cache, dict):
            self._cache = Cache()
            for key, (value, tags) in cache.iteritems():
                self._cache.dump(key, value, tags=tags)
        else:
            raise TypeError('Could not interpret the cache argument.')
        #
        if lf is None:
            self._lf = DenseLinalgFactory()
        else:
            self._lf = lf
        #
        if extra is None:
            self._extra = {}
        else:
            self._extra = extra
        #
        self._obasis = None
        self._obasis_desc = None
        if obasis is not None:
            self.update_obasis(obasis)

        self._cell = cell
        self._pseudo_numbers = pseudo_numbers

        # The checkpoint file
        self._chk = None
        self._close_chk = False
        self.assign_chk(chk)

        self._log_init()