def remove_random_carbon(structure: Structure) -> Structure: structure = structure.copy() carbon_indices = [ idx for idx, s in enumerate(structure.species) if s == Element.C ] to_remove = np.random.randint(len(carbon_indices)) structure.remove_sites([to_remove]) return structure
def process_structure(s: Structure): # kill off any lone carbons or those with only 1 bond from pyputil.structure.bonds import calculate_bond_list n_bonds = np.array([len(b) for b in calculate_bond_list(s)]) s.remove_sites(np.where(n_bonds <= 1)[0]) # add hydrogen atoms on the edges add_hydrogen(s, cutoff=1.05, dist=DEFAULT_CH_DIST / DEFAULT_CC_DIST) # scale coordinates to bond distance s.lattice = Lattice(matrix=s.lattice.matrix * bond_dist) return s
def remove_unstable_interstitials(structure: Structure, relaxed_interstitials: list, dist=0.2, site_indices=None): """ :param structure: Structure decorated with all interstitials :param relaxed_interstitials: list of structures with interstitial as last index :param dist: tolerance for determining if site belongs to another site :return: """ to_keep = list(range(len(relaxed_interstitials[0])-1)) try: sga = SpacegroupAnalyzer(structure, symprec=0.1) structure = sga.get_symmetrized_structure() except TypeError: sga = SpacegroupAnalyzer(structure, symprec=0.01) structure = sga.get_symmetrized_structure() for ri in relaxed_interstitials: #type: Structure sites=structure.get_sites_in_sphere(ri.cart_coords[-1], dist, include_index=True) for indices in structure.equivalent_indices: #look at all sets of equivalent indices index = sites[0][2] if index in to_keep: # Already keeping this index continue if index in indices: to_keep = to_keep + indices #keep equivalent indices break if len(sites) != 1: # make sure only one site is found okay = False if len(sites) > 1: if all([ x[2] in indices for x in sites]): okay = True if not okay: if site_indices: raise Exception('Found {} sites for {}'.format(len(sites), site_indices[relaxed_interstitials.index(ri)])) raise Exception('Found {} sites'.format(len(sites))) to_remove = [i for i in range(len(structure)) if i not in to_keep] structure.remove_sites(to_remove) return structure
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 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 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