Пример #1
0
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
Пример #2
0
 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
Пример #3
0
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
Пример #4
0
    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
Пример #5
0
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')
Пример #6
0
    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