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)
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'))
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)
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)
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)
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'))
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'))
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)
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)