def is_equivalent(structure : Structure, atoms_1 : tuple, atoms_2 : tuple , eps=0.05): """ Find Vacancy Strucutres for diffusion into and out of the specified atom_i site. :param structure: Structure Structure to calculate diffusion pathways :param atom_i: int Atom to get diffion path from :return: [ Structure ] """ # To Find Pathway, look for voronoi edges structure = structure.copy() # type: Structure coords = get_midpoint(structure, atoms_1[0], atoms_1[1]) structure.append('H', coords) coords = get_midpoint(structure, atoms_2[0], atoms_2[1]) structure.append('H', coords) dist_1 = structure.get_neighbors(structure[-2], 3) dist_2 = structure.get_neighbors(structure[-1], 3) dist_1.sort(key=lambda x: x[1]) dist_2.sort(key=lambda x: x[1]) for (site_a, site_b) in zip(dist_1, dist_2): if abs(site_a[1] - site_b[1]) > eps: return False elif site_a[0].specie != site_b[0].specie: return False return True
def append_interstitial(supercell_info: SupercellInfo, unitcell_structure: Structure, frac_coords: List[float]) -> SupercellInfo: """ inv_trans_mat must be multiplied with coords from the right as the trans_mat is multiplied to the unitcell lattice vector from the left. see __mul__ of IStructure in pymatgen. x_u, x_s means the frac coordinates in unitcell and supercell, while a, b, c are the unitcell lattice vector. (a_u, b_u, c_u) . (a, b, c) = (a_s, b_s, c_s) . trans_mat . (a, b, c) (a_u, b_u, c_u) = (a_s, b_s, c_s) . trans_mat so, (a_s, b_s, c_s) = (a_u, b_u, c_u) . inv_trans_mat """ if supercell_info.unitcell_structure and \ supercell_info.unitcell_structure != unitcell_structure: raise NotPrimitiveError unitcell_structure.append(species=Element.H, coords=frac_coords) symmetrizer = StructureSymmetrizer(unitcell_structure) wyckoff_letter = (symmetrizer.spglib_sym_data["wyckoffs"][-1]) site_symmetry = (symmetrizer.spglib_sym_data["site_symmetry_symbols"][-1]) inv_matrix = inv(np.array(supercell_info.transformation_matrix)) new_coords = np.dot(frac_coords, inv_matrix).tolist() supercell_info.interstitials.append( Interstitial(frac_coords=new_coords, wyckoff_letter=wyckoff_letter, site_symmetry=site_symmetry)) return supercell_info
def output_clusters(self, fmt, periodic=None): """ Outputs the unique unit cell clusters from the graph Args: fmt (str): output format for pymatgen structures set up from clusters periodic (Boolean): Whether to output only periodic clusters Outputs: CLUS_*."fmt": A cluster structure file for each cluster in the graph """ if fmt == "poscar": tail = "vasp" else: tail = fmt site_sets = [] for cluster in self.clusters: if periodic: if cluster.periodic > 0: site_sets.append( frozenset([ int(node.labels["UC_index"]) for node in cluster.nodes ])) else: site_sets.append( frozenset([ int(node.labels["UC_index"]) for node in cluster.nodes ])) site_sets = set(site_sets) for index, site_list in enumerate(site_sets): cluster_structure = Structure(lattice=self.structure.lattice, species=[], coords=[]) symbols = [species for species in self.structure.symbol_set] if "X" in set(symbols): symbols.remove("X") symbols.append("X0+") for symbol in symbols: for site in site_list: site = self.structure.sites[site] if site.species_string == symbol: cluster_structure.append(symbol, site.coords, coords_are_cartesian=True) cluster_structure.to(fmt=fmt, filename="CLUS_" + str(index) + "." + tail)
def base_structure(lat, dict_str): st = Structure(lat, [], []) if dict_str[0] == {}: return st for tc in dict_str: sp = tc["type"] coords = read_coords(tc["coords"]) if len(coords.shape) == 1: coords = np.reshape(coords, (1, 3)) n = coords.shape[0] for i in range(n): st.append(sp, coords[i, :]) return st
def return_periodic_structure(self, fmt): """ Gathers all periodic clusters in the graph as a single pymatgen Structure Args: fmt (str): output format for pymatgen structure set up from cluster Returns: #CLUS_PER."fmt": A structure file containing periodic sites in the graph. cluster_structure (pymatgen Structure): Structure object containing all periodic clusters """ sites = [] for cluster in self.clusters: if cluster.periodic > 0: sites.append( frozenset([ int(node.labels["UC_index"]) for node in cluster.nodes ])) sites = set(sites) cluster_structure = Structure(lattice=self.structure.lattice, species=[], coords=[]) for index, site_list in enumerate(sites): symbols = [species for species in self.structure.symbol_set] if "X" in set(symbols): symbols.remove("X") symbols.append("X0+") for symbol in symbols: for site in site_list: site = self.structure.sites[site] if site.species_string == symbol: cluster_structure.append(symbol, site.coords, coords_are_cartesian=True) return cluster_structure
def sort_structure(structure, order): """ Given a pymatgen structure object sort the species so that their indices sit side by side in the structure, in given order - allows for POSCAR file to be written in a readable way after doping Args: - structure (Structure): pymatgen structure object - order ([str,str..]): list of species str in order to sort Returns: - structure (Structure): ordered pymatgen Structure object """ symbols = [species for species in structure.symbol_set] if "X" in set(symbols): symbols.remove("X") symbols.append("X0+") if set(symbols) == set(order): structure_sorted = Structure(lattice=structure.lattice, species=[], coords=[]) for symbol in symbols: for i, site in enumerate(structure.sites): if site.species_string == symbol: structure_sorted.append( symbol, site.coords, coords_are_cartesian=True, properties=site.properties, ) else: error_msg = "Error: sort structure elements in list passed in order does not match that found in POSCAR\n" error_msg += "Passed: {}\n".format(order) error_msg += "POSCAR: {}\n".format(symbols) raise ValueError(error_msg) return structure_sorted
def add_hydrogen( structure: Structure, cutoff: float, dist: float, ): all_bonds = calculate_bond_list(structure=structure, cutoff=cutoff) lattice = np.copy(structure.lattice.matrix) coords = np.copy(structure.cart_coords) for bonds in all_bonds: # only add hydrogen to atoms with two neighbors if len(bonds) != 2: continue bond_pos = bonds_to_positions(bonds, lattice, coords) a, b = bond_pos[0] _, c = bond_pos[1] structure.append( species=Element.H, coords=calc_hydrogen_loc(a, b - a, c - a, dist), coords_are_cartesian=True, )
def adsorb_both_surfaces(self, molecule, repeat=None, min_lw=5.0, reorient=True, find_args={}, ltol=0.1, stol=0.1, angle_tol=0.01): """ Function that generates all adsorption structures for a given molecular adsorbate on both surfaces of a slab. Args: molecule (Molecule): molecule corresponding to adsorbate repeat (3-tuple or list): repeat argument for supercell generation min_lw (float): minimum length and width of the slab, only used if repeat is None reorient (bool): flag on whether or not to reorient adsorbate along the miller index find_args (dict): dictionary of arguments to be passed to the call to self.find_adsorption_sites, e.g. {"distance":2.0} ltol (float): Fractional length tolerance. Default is 0.2. stol (float): Site tolerance. Defined as the fraction of the average free length per atom := ( V / Nsites ) ** (1/3) Default is 0.3. angle_tol (float): Angle tolerance in degrees. Default is 5 degrees. """ # First get all possible adsorption configurations for this surface adslabs = self.generate_adsorption_structures(molecule, repeat=repeat, min_lw=min_lw, reorient=reorient, find_args=find_args) # Now we need to sort the sites by their position along # c as well as whether or not they are adsorbate single_ads = [] for i, slab in enumerate(adslabs): sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) ads_indices = [site_index for site_index, site in enumerate(sorted_sites) \ if site.surface_properties == "adsorbate"] non_ads_indices = [site_index for site_index, site in enumerate(sorted_sites) \ if site.surface_properties != "adsorbate"] species, fcoords, props = [], [], {"surface_properties": []} for site in sorted_sites: species.append(site.specie) fcoords.append(site.frac_coords) props["surface_properties"].append(site.surface_properties) slab_other_side = Structure(slab.lattice, species, fcoords, site_properties=props) # For each adsorbate, get its distance from the surface and move it # to the other side with the same distance from the other surface for ads_index in ads_indices: props["surface_properties"].append("adsorbate") adsite = sorted_sites[ads_index] diff = abs(adsite.frac_coords[2] - \ sorted_sites[non_ads_indices[-1]].frac_coords[2]) slab_other_side.append(adsite.specie, [adsite.frac_coords[0], adsite.frac_coords[1], sorted_sites[0].frac_coords[2] - diff], properties={"surface_properties": "adsorbate"}) # slab_other_side[-1].add # Remove the adsorbates on the original side of the slab # to create a slab with one adsorbate on the other side slab_other_side.remove_sites(ads_indices) # Put both slabs with adsorption on one side # and the other in a list of slabs for grouping single_ads.extend([slab, slab_other_side]) # Now group the slabs. matcher = StructureMatcher(ltol=ltol, stol=stol, angle_tol=angle_tol) groups = matcher.group_structures(single_ads) # Each group should be a pair with adsorbate on one side and the other. # If a slab has no equivalent adsorbed slab on the other side, skip. adsorb_both_sides = [] for i, group in enumerate(groups): if len(group) != 2: continue ads1 = [site for site in group[0] if \ site.surface_properties == "adsorbate"][0] group[1].append(ads1.specie, ads1.frac_coords, properties={"surface_properties": "adsorbate"}) # Build the slab object species, fcoords, props = [], [], {"surface_properties": []} for site in group[1]: species.append(site.specie) fcoords.append(site.frac_coords) props["surface_properties"].append(site.surface_properties) s = Structure(group[1].lattice, species, fcoords, site_properties=props) adsorb_both_sides.append(s) return adsorb_both_sides
class MofStructure(Structure): """Extend the pymatgen Structure class to add MOF specific features""" def __init__(self, lattice, species, coords, charge=None, validate_proximity=False, to_unit_cell=False, coords_are_cartesian=False, site_properties=None, name="N/A"): """Create a MOf structure. The arguments are the same as in the pymatgen Structure class with the addition of the name argument. The super constructor is called and additional MOF specific properties are initialized. :param name: MOF name, used to identify the structure. """ super().__init__(lattice, species, coords, charge=charge, validate_proximity=validate_proximity, to_unit_cell=to_unit_cell, coords_are_cartesian=coords_are_cartesian, site_properties=site_properties) self._all_coord_spheres_indices = None self._all_distances = None self._metal_coord_spheres = [] self._name = name self.metal = None self.metal_indices = [] self.organic = None self.species_str = [str(s) for s in self.species] metal_set = set([s for s in self.species_str if Atom(s).is_metal]) non_metal_set = set( [s for s in self.species_str if not Atom(s).is_metal]) todays_date = datetime.datetime.now().isoformat() self.summary = { 'cif_okay': 'N/A', 'problematic': 'N/A', 'has_oms': 'N/A', 'metal_sites': [], 'oms_density': 'N/A', 'checksum': 'N/A', 'metal_species': list(metal_set), 'non_metal_species': list(non_metal_set), 'name': name, 'uc_volume': self.volume, 'density': self.density, 'date_created': str(todays_date) } self._tolerance = None self._split_structure_to_organic_and_metal() @classmethod def from_file(cls, filename, primitive=False, sort=False, merge_tol=0.0): """Create a MofStructure from a CIF file. This makes use of the from_file function of the Structure class and catches the exception in case a CIF file cannot be read. If the CIF is read successfully then the MofStructure is marked as okay, and the file checksum is added to the summary. If the CIF file cannot be read then it is marked as not okay and all the other properties are set to None and because there cannot be an empty Structure a carbon atom is added as placeholder at 0,0,0. :param filename: (str) The filename to read from. :param primitive: (bool) Whether to convert to a primitive cell Only available for cifs. Defaults to False. :param sort: (bool) Whether to sort sites. Default to False. :param merge_tol: (float) If this is some positive number, sites that are within merge_tol from each other will be merged. Usually 0.01 should be enough to deal with common numerical issues. :return: Return the created MofStructure """ mof_name = os.path.splitext(os.path.basename(filename))[0] try: s = Structure.from_file(filename, primitive=primitive, sort=sort, merge_tol=merge_tol) s_mof = cls(s.lattice, s.species, s.frac_coords, name=mof_name) s_mof.summary['cif_okay'] = True s_mof.summary['checksum'] = Helper.get_checksum(filename) except Exception as e: print('\nAn Exception occurred: {}'.format(e)) print('Cannot load {}\n'.format(filename)) # Make a placeholder MOF object, set all its summary entries # to None and set cif_okay to False s_mof = cls([[10, 0, 0], [0, 10, 0], [0, 0, 10]], ["C"], [[0, 0, 0]], name=mof_name) s_mof._mark_failed_to_read() s_mof.summary['cif_okay'] = False return s_mof def analyze_metals(self, output_folder, verbose='normal'): """Run analysis to detect all open metal sites in a MofStructure. In addition the metal sites are marked as unique. :param output_folder: Folder where OMS analysis results will be stored. :param verbose: Verbosity level for the output of the analysis. """ Helper.make_folder(output_folder) running_indicator = output_folder + "/analysis_running" open(running_indicator, 'w').close() self.summary['problematic'] = False ms_cs_list = {True: [], False: []} for m, omc in enumerate(self.metal_coord_spheres): m_index = self.metal_indices[m] omc.check_if_open() if not self.summary['problematic']: self.summary['problematic'] = omc.is_problematic cs = self._find_coordination_sequence(m_index) cs = [self.species_str[m_index]] + cs omc.is_unique = self._check_if_new_site(ms_cs_list[omc.is_open], cs) if omc.is_unique: ms_cs_list[omc.is_open].append(cs) self.summary['metal_sites'].append(omc.metal_summary) unique_sites = [s['unique'] for s in self.summary['metal_sites']] open_sites = [s['is_open'] for s in self.summary['metal_sites']] self.summary['oms_density'] = sum(unique_sites) / self.volume self.summary['has_oms'] = any(open_sites) self.write_results(output_folder, verbose) os.remove(running_indicator) def write_results(self, output_folder, verbose='normal'): """Store summary dictionary holding all MOF and OMS information to a JSON file, store CIF files for the metal and non-metal parts of the MOF as well as all the identified coordination spheres. :param output_folder: Location to be used to store :param verbose: Verbosity level (default: 'normal') """ Helper.make_folder(output_folder) for index, mcs in enumerate(self.metal_coord_spheres): mcs.write_cif_file(output_folder, index) if self.metal: output_fname = "{}/{}_metal.cif".format(output_folder, self.summary['name']) self.metal.to(filename=output_fname) output_fname = "{}/{}_organic.cif".format(output_folder, self.summary['name']) self.organic.to(filename=output_fname) json_file_out = "{}/{}.json".format(output_folder, self.summary['name']) summary = copy.deepcopy(self.summary) if verbose == 'normal': for ms in summary["metal_sites"]: ms.pop('all_dihedrals', None) ms.pop('min_dihedral', None) with open(json_file_out, 'w') as outfile: json.dump(summary, outfile, indent=3) @property def tolerance(self): """Tolerance values for dihedral checks. If not set, defaults are given. """ if self._tolerance is None: self._tolerance = {'on_plane': 15} return self._tolerance @property def name(self): """Name of the MofStructure.""" return self._name @name.setter def name(self, name): """Setter for the name of the MofStructure.""" self._name = name self.summary['name'] = name @property def all_distances(self): """Distances between all atoms in the MofStructure""" if self._all_distances is None: self._all_distances = self.lattice.get_all_distances( self.frac_coords, self.frac_coords) return self._all_distances @property def all_coord_spheres_indices(self): """Compute the indices of the atoms in the first coordination shell for all atoms in the MofStructure """ if self._all_coord_spheres_indices: return self._all_coord_spheres_indices self._all_coord_spheres_indices = [ self._find_cs_indices(i) for i in range(len(self)) ] return self._all_coord_spheres_indices @property def metal_coord_spheres(self): """For all metal atoms in a MofStructure compute the first coordination sphere as a MetalSite object. """ if not self._metal_coord_spheres: self._metal_coord_spheres = [ self._find_metal_coord_sphere(c) for c in self.metal_indices ] return self._metal_coord_spheres def _mark_failed_to_read(self): """If a CIF cannot be read set certain properties to None""" self.summary['metal_species'] = None self.summary['non_metal_species'] = None self.summary['uc_volume'] = None self.summary['density'] = None def _split_structure_to_organic_and_metal(self): """Split a MOF to two pymatgen Structures, one containing only metal atoms and one containing only non-metal atoms.""" self.metal = Structure(self.lattice, [], []) self.organic = Structure(self.lattice, [], []) i = 0 for s, fc in zip(self.species, self.frac_coords): if Atom(str(s)).is_metal: self.metal.append(s, fc) self.metal_indices.append(i) else: self.organic.append(s, fc) i += 1 def _find_cs_indices(self, center): """Find the indices of the atoms in the coordination sphere. :param center: Central atom of coordination sphere. :return: c_sphere_indices: Return in the coordination sphere of center. """ dist = list(self.all_distances[center]) if dist[center] > 0.0000001: sys.exit('The self distance appears to be non-zero') a = Atom(self.species_str[center]) c_sphere_indices = [ i for i, dis in enumerate(dist) if i != center and a.check_bond(self.species_str[i], dis) ] c_sphere_indices.insert(0, center) return c_sphere_indices def _find_metal_coord_sphere(self, center): """Identify the atoms in the first coordination sphere of a metal atom. Obtain all atoms connecting to the metal using the all_coord_spheres_indices values and keeping only valid bonds as well as center the atoms around the metal center for visualization purposes. :param center: :return: """ dist = self.all_distances[center] if dist[center] > 0.0000001: sys.exit('The self distance appears to be non-zero') c_sphere = MetalSite(self.lattice, [self.species[center]], [self.frac_coords[center]], tolerance=self.tolerance) cs_i = self.all_coord_spheres_indices[center] for i in cs_i[1:]: c_sphere.append(self.species_str[i], self.frac_coords[i]) c_sphere.keep_valid_bonds() c_sphere.center_around_metal() return c_sphere @staticmethod def _check_if_new_site(cs_list, cs): """Check if a given site is unique based on its coordination sequence""" for cs_i in cs_list: if Helper.compare_lists(cs_i, cs): return False return True # len(cs_list), def _find_coordination_sequence(self, center): """Compute the coordination sequence up to the 6th coordination shell. :param center: Atom to compute coordination sequence for :return cs: Coordination sequence for center """ shell_list = {(center, (0, 0, 0))} shell_list_prev = set([]) all_shells = set(shell_list) n_shells = 6 cs = [] count_total = 0 for n in range(0, n_shells): c_set = set([]) for a_uc in shell_list: a = a_uc[0] lattice = a_uc[1] coord_sphere = self.all_coord_spheres_indices[a] count_total += 1 coord_sphere_with_uc = [] for c in coord_sphere: diff = self.frac_coords[a] - self.frac_coords[c] new_lat_i = [round(d, 0) for d in diff] uc = tuple(l - nl for l, nl in zip(lattice, new_lat_i)) coord_sphere_with_uc.append((c, uc)) coord_sphere_with_uc = tuple(coord_sphere_with_uc) c_set = c_set.union(set(coord_sphere_with_uc)) for a in shell_list_prev: c_set.discard(a) for a in shell_list: c_set.discard(a) cs.append(len(c_set)) all_shells = all_shells.union(c_set) shell_list_prev = shell_list shell_list = c_set return cs
def finite_size_scale(standard, ssize, primordial, fsize, psize=[1,1,1]): """Function to perform finite size scaling for defect structure relaxation Inputs: standard = POSCAR file of structure containing defect ssize = Supercell size of structure with defect (list of size 3) primordial = POSCAR file of structure for basic unit of perfect cell psize = Supercell size of structure for padding. Default = [1,1,1] (list of size 3) fsize = Desired supercell size of final structure (list of size 3) Outputs: POSCAR file of structure containing defect and padding""" # Check if the input sizes work out with the desired final size padding = [0,0,0] for i in range(3): diff = fsize[i] - ssize[i] if diff < 0: raise RuntimeError('Desired final size of the structure must be larger than \ existing defect structure size. Defect Size = '+repr(ssize)+' Final Size = '+repr(fsize)) elif diff >= 0: if math.fmod(diff,psize[i]): raise RuntimeError('Primordial structure and defect structure sizes cannot \ be used to form desired final size. Reduce size of primordial structure. Defect Size = '+ repr(ssize)+' Final Size = '+repr(fsize)+' Primordial size = '+repr(psize)) else: padding[i] = diff/psize[i] # Load the defect structure and primordial structure try: defst = read_structure(standard) except: raise RuntimeError('Error: Unable to read standard structure. Please check file. Filename: '+\ standard) try: pst = read_structure(primordial) except: raise RuntimeError('Error: Unable to read primordial structure. Please check file. Filename: '+\ primordial) # Pad the structure positions = [site.coords for site in pst] syms = [str(site.specie.symbol) for site in pst] lv = [one/ssize for one in defst.lattice.matrix] vect = [] for m0 in range(padding[0]): for m1 in numpy.arange(0,fsize[1],psize[1]): for m2 in numpy.arange(0,fsize[2],psize[2]): vect.append([ssize[0]+m0*psize[0],m1,m2]) for m1 in range(padding[1]): for m0 in numpy.arange(0,ssize[0],psize[0]): for m2 in numpy.arange(0,fsize[2],psize[2]): vect.append([m0,ssize[1]+m1*psize[1],m2]) for m2 in range(padding[2]): for m0 in numpy.arange(0,ssize[0],psize[0]): for m1 in numpy.arange(0,ssize[1],psize[1]): vect.append([m0,m1,ssize[2]+m2*psize[2]]) #Construct a new structure with desired size new_lat = Lattice(numpy.array([fsize[c] * lv[c] for c in range(3)])) final = Structure(new_lat, defst.species_and_occu,defst.cart_coords, coords_are_cartesian=True) for m0,m1,m2 in vect: npos = positions + numpy.dot((m0, m1, m2), lv) for i in range(len(npos)): final.append(syms[i],npos[i],coords_are_cartesian=True) #Check for periodic issues in final structure final = check_periodic(final,defst) # Write output as POSCAR write_structure(final, 'POSCAR_Final') return final
def finite_size_scale(standard, ssize, primordial, fsize, psize=[1, 1, 1]): """Function to perform finite size scaling for defect structure relaxation Inputs: standard = POSCAR file of structure containing defect ssize = Supercell size of structure with defect (list of size 3) primordial = POSCAR file of structure for basic unit of perfect cell psize = Supercell size of structure for padding. Default = [1,1,1] (list of size 3) fsize = Desired supercell size of final structure (list of size 3) Outputs: POSCAR file of structure containing defect and padding""" # Check if the input sizes work out with the desired final size padding = [0, 0, 0] for i in range(3): diff = fsize[i] - ssize[i] if diff < 0: raise RuntimeError( 'Desired final size of the structure must be larger than \ existing defect structure size. Defect Size = ' + repr(ssize) + ' Final Size = ' + repr(fsize)) elif diff >= 0: if math.fmod(diff, psize[i]): raise RuntimeError( 'Primordial structure and defect structure sizes cannot \ be used to form desired final size. Reduce size of primordial structure. Defect Size = ' + repr(ssize) + ' Final Size = ' + repr(fsize) + ' Primordial size = ' + repr(psize)) else: padding[i] = diff / psize[i] # Load the defect structure and primordial structure try: defst = read_structure(standard) except: raise RuntimeError('Error: Unable to read standard structure. Please check file. Filename: '+\ standard) try: pst = read_structure(primordial) except: raise RuntimeError('Error: Unable to read primordial structure. Please check file. Filename: '+\ primordial) # Pad the structure positions = [site.coords for site in pst] syms = [str(site.specie.symbol) for site in pst] lv = [one / ssize for one in defst.lattice.matrix] vect = [] for m0 in range(padding[0]): for m1 in numpy.arange(0, fsize[1], psize[1]): for m2 in numpy.arange(0, fsize[2], psize[2]): vect.append([ssize[0] + m0 * psize[0], m1, m2]) for m1 in range(padding[1]): for m0 in numpy.arange(0, ssize[0], psize[0]): for m2 in numpy.arange(0, fsize[2], psize[2]): vect.append([m0, ssize[1] + m1 * psize[1], m2]) for m2 in range(padding[2]): for m0 in numpy.arange(0, ssize[0], psize[0]): for m1 in numpy.arange(0, ssize[1], psize[1]): vect.append([m0, m1, ssize[2] + m2 * psize[2]]) #Construct a new structure with desired size new_lat = Lattice(numpy.array([fsize[c] * lv[c] for c in range(3)])) final = Structure(new_lat, defst.species_and_occu, defst.cart_coords, coords_are_cartesian=True) for m0, m1, m2 in vect: npos = positions + numpy.dot((m0, m1, m2), lv) for i in range(len(npos)): final.append(syms[i], npos[i], coords_are_cartesian=True) #Check for periodic issues in final structure final = check_periodic(final, defst) # Write output as POSCAR write_structure(final, 'POSCAR_Final') return final
class Crossover: ''' crossover # ---------- args atype (list): atom type, e.g. ['Si', 'O'] for Si4O8 nat (list): number of atom, e.g. [4, 8] for Si4O8 mindist (2d list): constraint on minimum interatomic distance, mindist must be a symmetric matrix e.g. [[1.8, 1.2], [1.2, 1.5] Si - Si: 1.8 angstrom Si - O: 1.2 O - O: 1.5 crs_lat ('equal' or 'random') how to mix lattice vectors crs_func ('OP' or 'TP'): one point or two point crossover nat_diff_tole (int): tolerance for difference in number of atoms in crossover maxcnt_ea (int): maximum number of trial in crossover # ---------- instance methods self.gen_child(struc_A, struc_B) if success, return self.child if fail, return None ''' def __init__(self, atype, nat, mindist, crs_lat='equal', crs_func='OP', nat_diff_tole=4, maxcnt_ea=100): # ---------- check args # ------ atype, nat, mindist for x in [atype, nat, mindist]: if type(x) is not list: raise ValueError('atype, nat, and mindist must be list') if not len(atype) == len(nat) == len(mindist): raise ValueError('not len(atype) == len(nat) == len(mindist)') # -- check symmetric for i in range(len(mindist)): for j in range(i): if not mindist[i][j] == mindist[j][i]: raise ValueError( 'mindist is not symmetric. ' + '({}, {}): {}, ({}, {}): {}'.format( i, j, mindist[i][j], j, i, mindist[j][i])) self.atype = atype self.nat = nat self.mindist = mindist # ------ crs_lat if crs_lat == 'equal': self.w_lat = np.array([1.0, 1.0]) elif crs_lat == 'random': self.w_lat = np.random.choice([0.0, 1.0], size=2, replace=False) else: raise ValueError('crs_lat must be equal or random') # ------ crs_func if crs_func not in ['OP', 'TP']: raise ValueError('crs_func must be OP or TP') else: self.crs_func = crs_func # ------ nat_diff_tole, maxcnt_ea for x in [nat_diff_tole, maxcnt_ea]: if type(x) is int and x > 0: pass else: raise ValueError('nat_diff_tole and maxcnt_ea' ' must be positive int') self.nat_diff_tole = nat_diff_tole self.maxcnt_ea = maxcnt_ea def gen_child(self, struc_A, struc_B): ''' generate child struture # ---------- return (if success) self.child: (if fail) None: ''' # ---------- initialize self.parent_A = origin_shift(struc_A) self.parent_B = origin_shift(struc_B) count = 0 # ---------- lattice crossover self._lattice_crossover() # ---------- generate children while True: count += 1 # ------ coordinate crossover if self.crs_func == 'OP': self._one_point_crossover() elif self.crs_func == 'TP': self._two_point_crossover() self.child = Structure(lattice=self.lattice, species=self.species, coords=self.coords) # ------ check nat_diff self._check_nat() # get self._nat_diff if any([abs(n) > self.nat_diff_tole for n in self._nat_diff]): if count > self.maxcnt_ea: # fail self.child = None return self.child continue # slice again # ------ check mindist dist_list = check_distance(self.child, self.atype, self.mindist, check_all=True) # ------ something smaller than mindist if dist_list: # -- remove atoms within mindist if any([n > 0 for n in self._nat_diff]): self._remove_within_mindist() if self.child is None: # fail --> slice again if count > self.maxcnt_ea: return None continue else: # nothing to remove, nat_diff = [0, 0] if count > self.maxcnt_ea: return None continue # fail --> slice again # ------ recheck nat_diff self._check_nat() # ------ nothing smaller than mindist # -- remove atoms near the border line if any([n > 0 for n in self._nat_diff]): self._remove_border_line() # -- add atoms near border line if any([n < 0 for n in self._nat_diff]): self._add_border_line() # -- success --> break while loop if self.child is not None: break # -- fail --> slice again else: if count > self.maxcnt_ea: return None continue # ---------- final check for nat self._check_nat() if not all([n == 0 for n in self._nat_diff]): raise ValueError('There is a bug: final check for nat') # ---------- sort by atype self.child = sort_by_atype(self.child, self.atype) # ---------- return return self.child def _lattice_crossover(self): # ---------- component --> self.w_lat matrix = ((self.w_lat[0] * self.parent_A.lattice.matrix + self.w_lat[1] * self.parent_B.lattice.matrix) / self.w_lat.sum()) mat_len = np.sqrt((matrix**2).sum(axis=1)) # ---------- absolute value of vector lat_len = ((np.array(self.parent_A.lattice.abc) * self.w_lat[0] + np.array(self.parent_B.lattice.abc) * self.w_lat[1]) / self.w_lat.sum()) # ---------- correction of vector length lat_array = np.empty([3, 3]) for i in range(3): lat_array[i] = matrix[i] * lat_len[i] / mat_len[i] # ---------- Lattice for pymatgen self.lattice = Lattice(lat_array) def _one_point_crossover(self): # ---------- slice point while True: self._slice_point = np.random.normal(loc=0.5, scale=0.1) if 0.3 <= self._slice_point <= 0.7: break self._axis = np.random.choice([0, 1, 2]) # ---------- crossover species_A = [] species_B = [] coords_A = [] coords_B = [] for i in range(self.parent_A.num_sites): if self.parent_A.frac_coords[i, self._axis] <= self._slice_point: species_A.append(self.parent_A[i].species_string) coords_A.append(self.parent_A[i].frac_coords) else: species_B.append(self.parent_A[i].species_string) coords_B.append(self.parent_A[i].frac_coords) if self.parent_B.frac_coords[i, self._axis] >= self._slice_point: species_A.append(self.parent_B[i].species_string) coords_A.append(self.parent_B[i].frac_coords) else: species_B.append(self.parent_B[i].species_string) coords_B.append(self.parent_B[i].frac_coords) # ---------- adopt a structure with more atoms if len(species_A) > len(species_B): species = species_A coords = coords_A elif len(species_A) < len(species_B): species = species_B coords = coords_B else: if np.random.choice([0, 1]): species = species_A coords = coords_A else: species = species_B coords = coords_B # ---------- set instance variables self.species, self.coords = species, coords def _two_point_crossover(self): # ---------- slice point while True: self._slice_point = np.random.normal(loc=0.25, scale=0.1) if 0.1 <= self._slice_point <= 0.4: break sp0 = self._slice_point sp1 = self._slice_point + 0.5 self._axis = np.random.choice([0, 1, 2]) # ---------- crossover species_A = [] species_B = [] coords_A = [] coords_B = [] for i in range(self.parent_A.num_sites): if ((self.parent_A.frac_coords[i, self._axis] <= sp0) or (sp1 <= self.parent_A.frac_coords[i, self._axis])): species_A.append(self.parent_A[i].species_string) coords_A.append(self.parent_A[i].frac_coords) else: species_B.append(self.parent_A[i].species_string) coords_B.append(self.parent_A[i].frac_coords) if sp0 <= self.parent_B.frac_coords[i, self._axis] <= sp1: species_A.append(self.parent_B[i].species_string) coords_A.append(self.parent_B[i].frac_coords) else: species_B.append(self.parent_B[i].species_string) coords_B.append(self.parent_B[i].frac_coords) # ---------- adopt a structure with more atoms if len(species_A) > len(species_B): species = species_A coords = coords_A elif len(species_A) < len(species_B): species = species_B coords = coords_B else: if np.random.choice([0, 1]): species = species_A coords = coords_A else: species = species_B coords = coords_B # ---------- set instance variables self.species, self.coords = species, coords def _check_nat(self): self._nat_diff = [] species_list = [a.species_string for a in self.child] for i in range(len(self.atype)): self._nat_diff.append( species_list.count(self.atype[i]) - self.nat[i]) def _remove_within_mindist(self): ''' if success: self.child <-- child structure data if fail: self.child <-- None ''' for itype in range(len(self.atype)): while self._nat_diff[itype] > 0: # ---------- check dist dist_list = check_distance(self.child, self.atype, self.mindist, check_all=True) if not dist_list: # nothing within mindist return # ---------- appearance frequency ij_within_dist = [isite[0] for isite in dist_list ] + [jsite[1] for jsite in dist_list] site_counter = Counter(ij_within_dist) # ---------- get index for removing rm_index = None # ---- site[0]: index, site[1]: count for site in site_counter.most_common(): if self.child[site[0]].species_string == self.atype[itype]: rm_index = site[0] break # break for loop # ---------- remove atom if rm_index is None: self.child = None return else: self.child.remove_sites([rm_index]) self._nat_diff[itype] -= 1 # ---------- final check dist_list = check_distance(self.child, self.atype, self.mindist, check_all=True) if dist_list: # still something within mindist self.child = None def _remove_border_line(self): # ---------- rank atoms from border line coords_axis = self.child.frac_coords[:, self._axis] if self.crs_func == 'OP': # ------ one point crossover: boundary --> 0.0, slice_point, 1.0 near_sp = (self._slice_point/2.0 < coords_axis) & \ (coords_axis < (self._slice_point + 1.0)/2.0) near_one = (self._slice_point + 1.0) / 2.0 <= coords_axis # -- distance from nearest boundary coords_diff = np.where(near_sp, abs(coords_axis - self._slice_point), coords_axis) coords_diff = np.where(near_one, 1.0 - coords_diff, coords_diff) elif self.crs_func == 'TP': # ------ two point crossover: # boundary --> slice_point, slice_point + 0.5 # -- distance from nearst boundary coords_diff = abs(self.child.frac_coords[:, self._axis] - self._slice_point) coords_diff = np.where(0.5 < coords_diff, coords_diff - 0.5, coords_diff) coords_diff = np.where(0.25 < coords_diff, 0.5 - coords_diff, coords_diff) else: raise ValueError('crs_func should be OP or TP') atom_border_indx = np.argsort(coords_diff) # ---------- remove list rm_list = [] for itype, nrm in enumerate(self._nat_diff): rm_list.append([]) if nrm > 0: for ab_indx in atom_border_indx: if self.child[ab_indx].species_string == self.atype[itype]: rm_list[itype].append(ab_indx) if len(rm_list[itype]) == nrm: break # ---------- remove for each_type in rm_list: if each_type: self.child.remove_sites(each_type) def _add_border_line(self): for i in range(len(self.atype)): # ---------- counter cnt = 0 # ---------- add atoms while self._nat_diff[i] < 0: cnt += 1 coords = np.random.rand(3) self._mean_choice() coords[self._axis] = np.random.normal(loc=self._mean, scale=0.08) self.child.append(species=self.atype[i], coords=coords) if check_distance(self.child, self.atype, self.mindist): cnt = 0 # reset self._nat_diff[i] += 1 else: self.child.pop() # cancel # ------ fail if cnt == self.maxcnt_ea: self.child = None return def _mean_choice(self): '''which boundary possesses more atoms''' if self.crs_func == 'OP': n_zero = np.sum( np.abs(self.child.frac_coords[:, self._axis] - 0.0) < 0.1) n_slice = np.sum( np.abs(self.child.frac_coords[:, self._axis] - self._slice_point) < 0.1) if n_zero < n_slice: self._mean = 0.0 elif n_zero > n_slice: self._mean = self._slice_point else: self._mean = np.random.choice([0.0, self._slice_point]) elif self.crs_func == 'TP': n_sp0 = np.sum( np.abs(self.child.frac_coords[:, self._axis] - self._slice_point) < 0.1) n_sp1 = np.sum( np.abs(self.child.frac_coords[:, self._axis] - self._slice_point - 0.5) < 0.1) if n_sp0 < n_sp1: self._mean = self._slice_point elif n_sp0 > n_sp1: self._mean = self._slice_point + 0.5 else: self._mean = np.random.choice( [self._slice_point, self._slice_point + 0.5]) else: raise ValueError('crs_func must be OP or TP')
def get_vacancy_diffusion_pathways_from_cell(structure : Structure, atom_i : int, vis=False, get_midpoints=False): """ Find Vacancy Strucutres for diffusion into and out of the specified atom_i site. :param structure: Structure Structure to calculate diffusion pathways :param atom_i: int Atom to get diffion path from :return: [ Structure ] """ # To Find Pathway, look for voronoi edges orig_structure = structure.copy() structure = structure.copy() # type: Structure target_atom = structure[atom_i].specie vnn = VoronoiNN(targets=[target_atom]) edges = vnn.get_nn_info(structure, atom_i) base_coords = structure[atom_i].coords # Add H in middle of the discovered pathways. Use symmetry analysis to elminate equivlent H and therfore # equivalent pathways site_dir = {} for edge in edges: coords = np.round((base_coords + edge['site'].coords)/2,3) structure.append('H', coords, True) # site_dir[tuple(np.round(coords))] = structure.index(edge['site']) # Use Tuple for indexing dict, need to round site_dir[tuple(np.round(coords))] = [list(x) for x in np.round(structure.frac_coords % 1,2) ].index(list(np.round(edge['site'].frac_coords % 1, 2))) # Use Tuple for indexing dict, need to round # Add H for all other diffusion atoms, so symmetry is preserved for i in get_atom_i(orig_structure, target_atom): sym_edges = vnn.get_nn_info(orig_structure, i) base_coords = structure[i].coords for edge in sym_edges: coords = (base_coords + edge['site'].coords) / 2 try: structure.append('H', coords, True, True) except: pass # Remove symmetrically equivalent pathways: sga = SpacegroupAnalyzer(structure, 0.5, angle_tolerance=20) ss = sga.get_symmetrized_structure() final_structure = structure.copy() indices = [] for i in range(len(orig_structure), len(orig_structure)+len(edges)): # get all 'original' edge sites sites = ss.find_equivalent_sites(ss[i]) new_indices = [ss.index(site) for site in sites if ss.index(site) < len(orig_structure) + len(edges)] # Check if symmetrically equivalent to other original edge sites new_indices.remove(i) if i not in indices: # Don't duplicate effort indices = indices + new_indices indices.sort() indices = indices + list(range(len(orig_structure)+len(edges), len(final_structure))) final_structure.remove_sites(indices) diffusion_elements = [ site_dir[tuple(np.round(h.coords))] for h in final_structure[len(orig_structure):] ] if vis: view(final_structure, 'VESTA') print(diffusion_elements) if get_midpoints: centers = [h.frac_coords for h in final_structure[len(orig_structure):]] return (diffusion_elements, centers) return diffusion_elements
def adsorb_both_surfaces(self, molecule, repeat=None, min_lw=5.0, reorient=True, find_args={}, ltol=0.1, stol=0.1, angle_tol=0.01): """ Function that generates all adsorption structures for a given molecular adsorbate on both surfaces of a slab. Args: molecule (Molecule): molecule corresponding to adsorbate repeat (3-tuple or list): repeat argument for supercell generation min_lw (float): minimum length and width of the slab, only used if repeat is None reorient (bool): flag on whether or not to reorient adsorbate along the miller index find_args (dict): dictionary of arguments to be passed to the call to self.find_adsorption_sites, e.g. {"distance":2.0} ltol (float): Fractional length tolerance. Default is 0.2. stol (float): Site tolerance. Defined as the fraction of the average free length per atom := ( V / Nsites ) ** (1/3) Default is 0.3. angle_tol (float): Angle tolerance in degrees. Default is 5 degrees. """ # First get all possible adsorption configurations for this surface adslabs = self.generate_adsorption_structures(molecule, repeat=repeat, min_lw=min_lw, reorient=reorient, find_args=find_args) # Now we need to sort the sites by their position along # c as well as whether or not they are adsorbate single_ads = [] for i, slab in enumerate(adslabs): sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2]) ads_indices = [site_index for site_index, site in enumerate(sorted_sites) \ if site.surface_properties == "adsorbate"] non_ads_indices = [site_index for site_index, site in enumerate(sorted_sites) \ if site.surface_properties != "adsorbate"] species, fcoords, props = [], [], {"surface_properties": []} for site in sorted_sites: species.append(site.specie) fcoords.append(site.frac_coords) props["surface_properties"].append(site.surface_properties) slab_other_side = Structure(slab.lattice, species, fcoords, site_properties=props) # For each adsorbate, get its distance from the surface and move it # to the other side with the same distance from the other surface for ads_index in ads_indices: props["surface_properties"].append("adsorbate") adsite = sorted_sites[ads_index] diff = abs(adsite.frac_coords[2] - \ sorted_sites[non_ads_indices[-1]].frac_coords[2]) slab_other_side.append( adsite.specie, [ adsite.frac_coords[0], adsite.frac_coords[1], sorted_sites[0].frac_coords[2] - diff ], properties={"surface_properties": "adsorbate"}) # slab_other_side[-1].add # Remove the adsorbates on the original side of the slab # to create a slab with one adsorbate on the other side slab_other_side.remove_sites(ads_indices) # Put both slabs with adsorption on one side # and the other in a list of slabs for grouping single_ads.extend([slab, slab_other_side]) # Now group the slabs. matcher = StructureMatcher(ltol=ltol, stol=stol, angle_tol=angle_tol) groups = matcher.group_structures(single_ads) # Each group should be a pair with adsorbate on one side and the other. # If a slab has no equivalent adsorbed slab on the other side, skip. adsorb_both_sides = [] for i, group in enumerate(groups): if len(group) != 2: continue ads1 = [site for site in group[0] if \ site.surface_properties == "adsorbate"][0] group[1].append(ads1.specie, ads1.frac_coords, properties={"surface_properties": "adsorbate"}) # Build the slab object species, fcoords, props = [], [], {"surface_properties": []} for site in group[1]: species.append(site.specie) fcoords.append(site.frac_coords) props["surface_properties"].append(site.surface_properties) s = Structure(group[1].lattice, species, fcoords, site_properties=props) adsorb_both_sides.append(s) return adsorb_both_sides