def calculate(self, indices=None, fupdate=0.05): """ Charge density on a Cartesian grid is a common routine required for Stockholder-type and related methods. This abstract class prepares the grid if input Volume object is empty. """ # Obtain charge densities on the grid if it does not contain one. if not numpy.any(self.volume.data): self.logger.info( "Calculating charge densities on the provided empty grid.") if len(self.data.mocoeffs) == 1: self.charge_density = electrondensity_spin( self.data, self.volume, [self.data.mocoeffs[0][:self.data.homos[0] + 1]]) self.charge_density.data *= 2 else: self.charge_density = electrondensity_spin( self.data, self.volume, [ self.data.mocoeffs[0][:self.data.homos[0] + 1], self.data.mocoeffs[1][:self.data.homos[1] + 1], ], ) # If charge densities are provided beforehand, log this information # `Volume` object does not contain (nor rely on) information about the constituent atoms. else: self.logger.info( "Using charge densities from the provided Volume object.") self.charge_density = self.volume
def calculate(self, indices=None, fupdate=0.05): """ Calculate DDEC6 charges based on doi: 10.1039/c6ra04656h paper. Cartesian, uniformly spaced grids are assumed for this function. """ # Obtain charge densities on the grid if it does not contain one. if not numpy.any(self.volume.data): self.logger.info( "Calculating charge densities on the provided empty grid.") if len(self.data.mocoeffs) == 1: self.chgdensity = electrondensity_spin( self.data, self.volume, [self.data.mocoeffs[0][:self.data.homos[0]]]) self.chgdensity.data *= 2 else: self.chgdensity = electrondensity_spin( self.data, self.volume, [ self.data.mocoeffs[0][:self.data.homos[0]], self.data.mocoeffs[1][:self.data.homos[1]], ], ) # If charge densities are provided beforehand, log this information # `Volume` object does not contain (nor rely on) information about the constituent atoms. else: self.logger.info( "Using charge densities from the provided Volume object.") self.chgdensity = self.volume # STEP 1 # Carry out step 1 of DDEC6 algorithm [Determining ion charge value] # Refer to equations 49-57 in doi: 10.1039/c6ra04656h self.logger.info("Creating first reference charges.") ref, loc, stock = self.calculate_refcharges() self.refcharges = [ref] self._localizedcharges = [loc] self._stockholdercharges = [stock] # STEP 2 # Load new proatom densities. self.logger.info("Creating second reference charges.") self.proatom_density = [] self.radial_grid_r = [] for i, atom_number in enumerate(self.data.atomnos): density, r = self._read_proatom(self.proatom_path, atom_number, float(self.refcharges[0][i])) self.proatom_density.append(density) self.radial_grid_r.append(r) # Carry out step 2 of DDEC6 algorithm [Determining ion charge value again] ref, loc, stock = self.calculate_refcharges() self.refcharges.append(ref) self._localizedcharges.append(loc) self._stockholdercharges.append(stock) # STEP 3 # Load new proatom densities. self.proatom_density = [] self.radial_grid_r = [] for i, atom_number in enumerate(self.data.atomnos): density, r = self._read_proatom(self.proatom_path, atom_number, float(self.refcharges[1][i])) self.proatom_density.append(density) self.radial_grid_r.append(r) # Carry out step 3 of DDEC6 algorithm [Determine conditioned charge density and tau] self.logger.info("Conditioning charge densities.") self.condition_densities()
def calculate(self, indices=None, fupdate=0.05): """Calculate Bader's QTAIM charges using on-grid algorithm proposed by Henkelman group in doi:10.1016/j.commatsci.2005.04.010 Cartesian, uniformly spaced grids are assumed for this function. """ # First obtain charge densities on the grid if len(self.data.mocoeffs) == 1: self.chgdensity = electrondensity_spin( self.data, self.volume, [self.data.mocoeffs[0][:self.data.homos[0]]]) self.chgdensity.data *= 2 else: self.chgdensity = electrondensity_spin( self.data, self.volume, [ self.data.mocoeffs[0][:self.data.homos[0]], self.data.mocoeffs[1][:self.data.homos[1]], ], ) # Assign each grid point to Bader areas self.fragresults = numpy.zeros(self.chgdensity.data.shape, "d") next_index = 1 self.logger.info("Partitioning space into Bader areas.") # Generator to iterate over the elements excluding the outermost positions xshape, yshape, zshape = self.chgdensity.data.shape indices = ((x, y, z) for x in range(1, xshape - 1) for y in range(1, yshape - 1) for z in range(1, zshape - 1)) for xindex, yindex, zindex in indices: if self.fragresults[xindex, yindex, zindex] != 0: # index has already been assigned for this grid point continue else: listcoord = [] local_max_reached = False while not local_max_reached: # Here, `delta_rho` corresponds to equation 2, # and `grad_rho_dot_r` corresponds to equation 1 in the aforementioned # paper (doi:10.1016/j.commatsci.2005.04.010) delta_rho = ( self.chgdensity.data[xindex - 1:xindex + 2, yindex - 1:yindex + 2, zindex - 1:zindex + 2, ] - self.chgdensity.data[xindex, yindex, zindex]) grad_rho_dot_r = delta_rho / _griddist maxat = numpy.where( grad_rho_dot_r == numpy.amax(grad_rho_dot_r)) directions = list(zip(maxat[0], maxat[1], maxat[2])) next_direction = [ind - 1 for ind in directions[0]] if len(directions) > 1: # when one or more directions indicate max grad (of 0), prioritize # to include all points in the Bader space if directions[0] == [1, 1, 1]: next_direction = [ind - 1 for ind in direction[1]] listcoord.append((xindex, yindex, zindex)) bader_candidate_index = self.fragresults[ xindex + next_direction[0], yindex + next_direction[1], zindex + next_direction[2], ] if bader_candidate_index != 0: # Path arrived at a point that has already been assigned with an index bader_index = bader_candidate_index listcoord = tuple(numpy.array(listcoord).T) self.fragresults[listcoord] = bader_index local_max_reached = True elif (next_direction == [0, 0, 0] or xindex + next_direction[0] == 0 or xindex + next_direction[0] == (len(self.chgdensity.data) - 1) or yindex + next_direction[1] == 0 or yindex + next_direction[1] == (len(self.chgdensity.data[0]) - 1) or zindex + next_direction[2] == 0 or zindex + next_direction[2] == (len(self.chgdensity.data[0][0]) - 1)): # When next_direction is [0, 0, 0] -- local maximum # Other conditions indicate that the path is heading out to edge of # the grid. Here, assign new Bader space to avoid exiting the grid. bader_index = next_index next_index += 1 listcoord = tuple(numpy.array(listcoord).T) self.fragresults[listcoord] = bader_index local_max_reached = True else: # Advance to the next point according to the direction of # maximum gradient xindex += next_direction[0] yindex += next_direction[1] zindex += next_direction[2] # Now try to identify each Bader region to individual atom. # Try to find an area that captures enough representation self.matches = numpy.zeros_like(self.data.atomnos) for pos in range(len(self.data.atomcoords[-1])): gridpt = numpy.round( (self.data.atomcoords[-1][pos] - self.volume.origin) / self.volume.spacing) xgrid = int(gridpt[0]) ygrid = int(gridpt[1]) zgrid = int(gridpt[2]) self.matches[pos] = self.fragresults[xgrid, ygrid, zgrid] assert ( 0 not in self.matches ), "Failed to assign Bader regions to atoms. Try with a finer grid. Content of Bader area matches: {}".format( self.matches) assert len( numpy.unique(self.matches) != len(self.data.atomnos) ), "Failed to assign unique Bader regions to each atom. Try with a finer grid." # Finally integrate the assigned Bader areas self.logger.info("Creating fragcharges: array[1]") self.fragcharges = numpy.zeros(len(self.data.atomcoords[-1]), "d") for atom_index, baderarea_index in enumerate(self.matches): # turn off all other grid points chargedensity = copy.deepcopy(self.chgdensity) mask = self.fragresults == baderarea_index chargedensity.data *= mask self.fragcharges[atom_index] = chargedensity.integrate() return True