def is_valid(self, indices, tag): """Checks that the given atom specifiers are correctly given. Does not yet check that they exist or don't overlap with other subssystems. """ # Determine how the atoms are specified: indices, tag or special set if isinstance(indices, str): if indices != "remaining": error("Use \"remaining\" if you want to assign the yet unassigned atoms to a subsystem") elif (indices is None) and (tag is None): error("Provide system as indices or tag",)
def is_valid(self, indices, tag): """Checks that the given atom specifiers are correctly given. Does not yet check that they exist or don't overlap with other subssystems. """ # Determine how the atoms are specified: indices, tag or special set if isinstance(indices, str): if indices != "remaining": error( "Use \"remaining\" if you want to assign the yet unassigned atoms to a subsystem" ) elif (indices is None) and (tag is None): error("Provide system as indices or tag", )
def enable_charge_calculation(self, division="Bader", source="all-electron", gridrefinement=4): """Enable the dynamic calculation of atom-centered charges with the specified algorithm and from the specified electron density. These charges are only used for the interaction between other subsystems. Parameters: division: string Indicates the division algorithm that is used. Available options are: - "Bader": Bader algorithm - "van Der Waals": Spheres with van Der Waals radius source: string Indicates what type of electron density is used. Available options are: - "pseudo": Use the pseudo electron density provided by all ASE DFT calculators - "all-electron": Use the all-electron density provided by at least GPAW gridrefinement: int Indicates the subdivision that is used for the all-electron density. Can be other than unity only for Bader algorithm with all-electron density. """ divisions = ["Bader", "van Der Waals"] charge_sources = ["pseudo", "all-electron"] if division not in divisions: error("Invalid division algorithm: " + division) if source not in charge_sources: error("Invalid source for electron density: " + source) if gridrefinement != 1: if (division != "Bader") or (division == "Bader" and source != "all-electron"): warn( "The gridrefinement is available only for the Bader algorithm with all-electron density, it is ignored.", 3) self.charge_calculation_enabled = True self.division = division self.charge_source = source self.gridrefinement = gridrefinement
def enable_charge_calculation(self, division="Bader", source="all-electron", gridrefinement=4): """Enable the dynamic calculation of atom-centered charges with the specified algorithm and from the specified electron density. These charges are only used for the interaction between other subsystems. Parameters: division: string Indicates the division algorithm that is used. Available options are: - "Bader": Bader algorithm - "van Der Waals": Spheres with van Der Waals radius source: string Indicates what type of electron density is used. Available options are: - "pseudo": Use the pseudo electron density provided by all ASE DFT calculators - "all-electron": Use the all-electron density provided by at least GPAW gridrefinement: int Indicates the subdivision that is used for the all-electron density. Can be other than unity only for Bader algorithm with all-electron density. """ divisions = ["Bader", "van Der Waals"] charge_sources = ["pseudo", "all-electron"] if division not in divisions: error("Invalid division algorithm: " + division) if source not in charge_sources: error("Invalid source for electron density: " + source) if gridrefinement != 1: if (division != "Bader") or (division == "Bader" and source != "all-electron"): warn("The gridrefinement is available only for the Bader algorithm with all-electron density, it is ignored.", 3) self.charge_calculation_enabled = True self.division = division self.charge_source = source self.gridrefinement = gridrefinement
def get_bader_charges(atoms, calc, charge_source="all-electron", gridrefinement=4): """This function uses an external Bader charge calculator from http://theory.cm.utexas.edu/henkelman/code/bader/. This tool is provided also in pysic/tools. Before using this function the bader executable directory has to be added to PATH. Parameters: atoms: ASE Atoms The structure from which we want to calculate the charges from. calc: ASE calculator charge_source: string Indicates the electron density that is used in charge calculation. Can be "pseudo" or "all-electron". gridrefinement: int The factor by which the calculation grid is densified in charge calculation. Returns: numpy array of the atomic charges """ # First check that the bader executable is in PATH if spawn.find_executable("bader") is None: error(( "Cannot find the \"bader\" executable in PATH. The bader " "executable is provided in the pysic/tools folder, or it can be " "downloaded from http://theory.cm.utexas.edu/henkelman/code/bader/. " "Ensure that the executable is named \"bader\", place it in any " "directory you want and then add that directory to your system" "PATH.")) atoms_copy = atoms.copy() calc.set_atoms(atoms_copy) if charge_source == "pseudo": try: density = np.array(calc.get_pseudo_density()) except AttributeError: error("The calculator doesn't provide pseudo density.") if charge_source == "all-electron": try: density = np.array(calc.get_all_electron_density(gridrefinement=gridrefinement)) except AttributeError: error("The calculator doesn't provide all electron density.") wrk_dir = os.getcwd()+"/.BADERTEMP" dir_created = False # Write the density in bader supported units and format if rank == 0: # Create temporary folder for calculations if not os.path.exists(wrk_dir): os.makedirs(wrk_dir) dir_created = True else: error("Tried to create a temporary folder in " + wrk_dir + ", but the folder already existed. Please remove it manually first.") rho = density * Bohr**3 write(wrk_dir + '/electron_density.cube', atoms, data=rho) # Run the bader executable in terminal. The bader executable included # int pysic/tools has to be in the PATH/PYTHONPATH command = "cd " + wrk_dir + "; bader electron_density.cube" subprocess.check_output(command, shell=True) #os.system("gnome-terminal --disable-factory -e '"+command+"'") # Wait for the main process to write the file barrier() # ASE provides an existing function for attaching the charges to the # atoms (safe because using a copy). Although we don't want to actually # attach the charges to anything, we use this function and extract the # charges later. bader.attach_charges(atoms_copy, wrk_dir + "/ACF.dat") # The call for charges was changed between # ASE 3.6 and 3.7 try: bader_charges = np.array(atoms_copy.get_initial_charges()) except: bader_charges = np.array(atoms_copy.get_charges()) # Remove the temporary files if rank == 0: if dir_created: shutil.rmtree(wrk_dir) return bader_charges
def get_bader_charges(atoms, calc, charge_source="all-electron", gridrefinement=4): """This function uses an external Bader charge calculator from http://theory.cm.utexas.edu/henkelman/code/bader/. This tool is provided also in pysic/tools. Before using this function the bader executable directory has to be added to PATH. Parameters: atoms: ASE Atoms The structure from which we want to calculate the charges from. calc: ASE calculator charge_source: string Indicates the electron density that is used in charge calculation. Can be "pseudo" or "all-electron". gridrefinement: int The factor by which the calculation grid is densified in charge calculation. Returns: numpy array of the atomic charges """ # First check that the bader executable is in PATH if spawn.find_executable("bader") is None: error(( "Cannot find the \"bader\" executable in PATH. The bader " "executable is provided in the pysic/tools folder, or it can be " "downloaded from http://theory.cm.utexas.edu/henkelman/code/bader/. " "Ensure that the executable is named \"bader\", place it in any " "directory you want and then add that directory to your system" "PATH.")) atoms_copy = atoms.copy() calc.set_atoms(atoms_copy) if charge_source == "pseudo": try: density = np.array(calc.get_pseudo_density()) except AttributeError: error("The calculator doesn't provide pseudo density.") if charge_source == "all-electron": try: density = np.array( calc.get_all_electron_density(gridrefinement=gridrefinement)) except AttributeError: error("The calculator doesn't provide all electron density.") wrk_dir = os.getcwd() + "/.BADERTEMP" dir_created = False # Write the density in bader supported units and format if rank == 0: # Create temporary folder for calculations if not os.path.exists(wrk_dir): os.makedirs(wrk_dir) dir_created = True else: error( "Tried to create a temporary folder in " + wrk_dir + ", but the folder already existed. Please remove it manually first." ) rho = density * Bohr**3 write(wrk_dir + '/electron_density.cube', atoms, data=rho) # Run the bader executable in terminal. The bader executable included # int pysic/tools has to be in the PATH/PYTHONPATH command = "cd " + wrk_dir + "; bader electron_density.cube" subprocess.check_output(command, shell=True) #os.system("gnome-terminal --disable-factory -e '"+command+"'") # Wait for the main process to write the file barrier() # ASE provides an existing function for attaching the charges to the # atoms (safe because using a copy). Although we don't want to actually # attach the charges to anything, we use this function and extract the # charges later. bader.attach_charges(atoms_copy, wrk_dir + "/ACF.dat") # The call for charges was changed between # ASE 3.6 and 3.7 try: bader_charges = np.array(atoms_copy.get_initial_charges()) except: bader_charges = np.array(atoms_copy.get_charges()) # Remove the temporary files if rank == 0: if dir_created: shutil.rmtree(wrk_dir) return bader_charges
def update_charges_van_der_waals(self): """Updates the atomic charges by using the electron density within a sphere of van Der Waals radius. The charge for each atom in the system is integrated from the electron density inside the van Der Waals radius of the atom in hand. The link atoms will affect the distribution of the electron density. """ self.timer.start("van Der Waals charge calculation") # Turn debugging on or off here debugging = False atoms_with_links = self.atoms_for_subsystem calc = self.calculator # The electron density is calculated from the system with link atoms. # This way the link atoms can modify the charge distribution calc.set_atoms(atoms_with_links) if self.charge_source == "pseudo": try: density = np.array(calc.get_pseudo_density()) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide pseudo density.") if self.charge_source == "all-electron": try: density = np.array( calc.get_all_electron_density(gridrefinement=1)) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide all electron density.") # Write the charge density as .cube file for VMD if debugging: write('nacl.cube', atoms_with_links, data=density) grid = self.density_grid if debugging: debug_list = [] # The link atoms are at the end of the list n_atoms = len(atoms_with_links) projected_charges = np.zeros((1, n_atoms)) for i_atom, atom in enumerate(atoms_with_links): r_atom = atom.position z = atom.number # Get the van Der Waals radius R = ase.data.vdw.vdw_radii[z] # Create a 3 x 3 x 3 x 3 array that can be used for vectorized # operations with the density grid r_atom_array = np.tile( r_atom, (grid.shape[0], grid.shape[1], grid.shape[2], 1)) diff = grid - r_atom_array # Numpy < 1.8 doesn't recoxnize axis argument on norm. This is a # workaround for diff = np.linalg.norm(diff, axis=3) diff = np.apply_along_axis(np.linalg.norm, 3, diff) indices = np.where(diff <= R) densities = density[indices] atom_charge = np.sum(densities) projected_charges[0, i_atom] = atom_charge if debugging: debug_list.append((atom, indices, densities)) #DEBUG: Visualize the grid and contributing grid points as atoms if debugging: d = Atoms() d.set_cell(atoms_with_links.get_cell()) # Visualize the integration spheres with atoms for point in debug_list: atom = point[0] indices = point[1] densities = point[2] d.append(atom) print "Atom: " + str(atom.symbol) + ", Density sum: " + str( np.sum(densities)) print "Density points included: " + str(len(densities)) for i in range(len(indices[0])): x = indices[0][i] y = indices[1][i] z = indices[2][i] a = Atom('H') a.position = grid[x, y, z, :] d.append(a) view(d) # Normalize the projected charges according to the electronic charge in # the whole system excluding the link atom to preserve charge neutrality atomic_numbers = np.array(atoms_with_links.get_atomic_numbers()) total_electron_charge = -np.sum(atomic_numbers) total_charge = np.sum(np.array(projected_charges)) projected_charges *= total_electron_charge / total_charge # Add the nuclear charges and initial charges projected_charges += atomic_numbers # Set the calculated charges to the atoms. The call for charges was # changed between ASE 3.6 and 3.7 try: self.atoms_for_interaction.set_initial_charges( projected_charges[0, :].tolist()) except: self.atoms_for_interaction.set_charges( projected_charges[0, :].tolist()) self.pseudo_density = density self.timer.stop()
def __init__(self, atoms, info, index_map, reverse_index_map, n_atoms): """ Parameters: atoms: ASE Atoms The subsystem atoms. info: SubSystem object Contains all the information about the subsystem index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. n_atoms: int Number of atoms in the full system. """ # Extract data from info self.name = info.name self.calculator = copy.copy(info.calculator) self.cell_size_optimization_enabled = info.cell_size_optimization_enabled self.cell_padding = info.cell_padding self.charge_calculation_enabled = info.charge_calculation_enabled self.charge_source = info.charge_source self.division = info.division self.gridrefinement = info.gridrefinement self.n_atoms = n_atoms self.atoms_for_interaction = atoms.copy() self.atoms_for_subsystem = atoms.copy() self.index_map = index_map self.reverse_index_map = reverse_index_map self.potential_energy = None self.forces = None self.density_grid = None self.pseudo_density = None self.link_atom_indices = [] self.timer = Timer([ "Bader charge calculation", "van Der Waals charge calculation", "Energy", "Forces", "Density grid update", "Cell minimization" ]) # The older ASE versions do not support get_initial_charges() try: charges = np.array(atoms.get_initial_charges()) except: charges = np.array(atoms.get_charges()) self.initial_charges = charges ## Can't enable charge calculation on non-DFT calculator self.dft_system = hasattr(self.calculator, "get_pseudo_density") if self.charge_calculation_enabled is True and not self.dft_system: error("Can't enable charge calculation on non-DFT calculator!") # If the cell size minimization flag has been enabled, then try to reduce the # cell size if self.cell_size_optimization_enabled: pbc = atoms.get_pbc() if pbc[0] or pbc[1] or pbc[2]: warn(("Cannot optimize cell size when periodic boundary" "condition have been enabled, disabling optimization."), 2) self.cell_size_optimization_enabled = False else: self.optimize_cell()
def update_charges_van_der_waals(self): """Updates the atomic charges by using the electron density within a sphere of van Der Waals radius. The charge for each atom in the system is integrated from the electron density inside the van Der Waals radius of the atom in hand. The link atoms will affect the distribution of the electron density. """ self.timer.start("van Der Waals charge calculation") # Turn debugging on or off here debugging = False atoms_with_links = self.atoms_for_subsystem calc = self.calculator # The electron density is calculated from the system with link atoms. # This way the link atoms can modify the charge distribution calc.set_atoms(atoms_with_links) if self.charge_source == "pseudo": try: density = np.array(calc.get_pseudo_density()) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide pseudo density.") if self.charge_source == "all-electron": try: density = np.array(calc.get_all_electron_density(gridrefinement=1)) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide all electron density.") # Write the charge density as .cube file for VMD if debugging: write('nacl.cube', atoms_with_links, data=density) grid = self.density_grid if debugging: debug_list = [] # The link atoms are at the end of the list n_atoms = len(atoms_with_links) projected_charges = np.zeros((1, n_atoms)) for i_atom, atom in enumerate(atoms_with_links): r_atom = atom.position z = atom.number # Get the van Der Waals radius R = ase.data.vdw.vdw_radii[z] # Create a 3 x 3 x 3 x 3 array that can be used for vectorized # operations with the density grid r_atom_array = np.tile(r_atom, (grid.shape[0], grid.shape[1], grid.shape[2], 1)) diff = grid - r_atom_array # Numpy < 1.8 doesn't recoxnize axis argument on norm. This is a # workaround for diff = np.linalg.norm(diff, axis=3) diff = np.apply_along_axis(np.linalg.norm, 3, diff) indices = np.where(diff <= R) densities = density[indices] atom_charge = np.sum(densities) projected_charges[0, i_atom] = atom_charge if debugging: debug_list.append((atom, indices, densities)) #DEBUG: Visualize the grid and contributing grid points as atoms if debugging: d = Atoms() d.set_cell(atoms_with_links.get_cell()) # Visualize the integration spheres with atoms for point in debug_list: atom = point[0] indices = point[1] densities = point[2] d.append(atom) print "Atom: " + str(atom.symbol) + ", Density sum: " + str(np.sum(densities)) print "Density points included: " + str(len(densities)) for i in range(len(indices[0])): x = indices[0][i] y = indices[1][i] z = indices[2][i] a = Atom('H') a.position = grid[x, y, z, :] d.append(a) view(d) # Normalize the projected charges according to the electronic charge in # the whole system excluding the link atom to preserve charge neutrality atomic_numbers = np.array(atoms_with_links.get_atomic_numbers()) total_electron_charge = -np.sum(atomic_numbers) total_charge = np.sum(np.array(projected_charges)) projected_charges *= total_electron_charge/total_charge # Add the nuclear charges and initial charges projected_charges += atomic_numbers # Set the calculated charges to the atoms. The call for charges was # changed between ASE 3.6 and 3.7 try: self.atoms_for_interaction.set_initial_charges(projected_charges[0, :].tolist()) except: self.atoms_for_interaction.set_charges(projected_charges[0, :].tolist()) self.pseudo_density = density self.timer.stop()
def __init__(self, atoms, info, index_map, reverse_index_map, n_atoms): """ Parameters: atoms: ASE Atoms The subsystem atoms. info: SubSystem object Contains all the information about the subsystem index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. n_atoms: int Number of atoms in the full system. """ # Extract data from info self.name = info.name self.calculator = copy.copy(info.calculator) self.cell_size_optimization_enabled = info.cell_size_optimization_enabled self.cell_padding = info.cell_padding self.charge_calculation_enabled = info.charge_calculation_enabled self.charge_source = info.charge_source self.division = info.division self.gridrefinement = info.gridrefinement self.n_atoms = n_atoms self.atoms_for_interaction = atoms.copy() self.atoms_for_subsystem = atoms.copy() self.index_map = index_map self.reverse_index_map = reverse_index_map self.potential_energy = None self.forces = None self.density_grid = None self.pseudo_density = None self.link_atom_indices = [] self.timer = Timer([ "Bader charge calculation", "van Der Waals charge calculation", "Energy", "Forces", "Density grid update", "Cell minimization"]) # The older ASE versions do not support get_initial_charges() try: charges = np.array(atoms.get_initial_charges()) except: charges = np.array(atoms.get_charges()) self.initial_charges = charges ## Can't enable charge calculation on non-DFT calculator self.dft_system = hasattr(self.calculator, "get_pseudo_density") if self.charge_calculation_enabled is True and not self.dft_system: error("Can't enable charge calculation on non-DFT calculator!") # If the cell size minimization flag has been enabled, then try to reduce the # cell size if self.cell_size_optimization_enabled: pbc = atoms.get_pbc() if pbc[0] or pbc[1] or pbc[2]: warn(("Cannot optimize cell size when periodic boundary" "condition have been enabled, disabling optimization."), 2) self.cell_size_optimization_enabled = False else: self.optimize_cell()