def test_class(self): # Tests entire class as single working unit sm = StructureMatcher() # Test group_structures and find_indices out = sm.group_structures(self.struct_list) self.assertEqual(list(map(len, out)), [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]) self.assertEqual(sum(map(len, out)), len(self.struct_list)) for s in self.struct_list[::2]: s.replace_species({'Ti': 'Zr', 'O': 'Ti'}) out = sm.group_structures(self.struct_list, anonymous=True) self.assertEqual(list(map(len, out)), [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1])
def test_class(self): # Tests entire class as single working unit sm = StructureMatcher() # Test group_structures and find_indices out = sm.group_structures(self.struct_list) self.assertEqual(list(map(len, out)), [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]) self.assertEqual(sum(map(len, out)), len(self.struct_list)) for s in self.struct_list[::2]: s.replace_species({'Ti': 'Zr', 'O':'Ti'}) out = sm.group_structures(self.struct_list, anonymous=True) self.assertEqual(list(map(len, out)), [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1])
def compare_structures(strt_list_to_compare): matcher = StructureMatcher() strt_holder = [] for grp in matcher.group_structures(strt_list_to_compare): strt_holder.append(grp[0]) return strt_holder
def test_structure(self): pep = self.peptide structure = pep.structure self.assertEqual(type(structure).__name__, 'Structure') self.assertEqual(len(structure.composition.elements), 21) self.assertEqual(structure.num_sites, 2004) self.assertEqual(structure.formula, "H1320 C30 S1 N6 O647") structure.remove_oxidation_states() self.assertEqual(len(structure.composition.elements), 5) ethane = self.ethane structure = ethane.structure self.assertEqual(len(structure.composition.elements), 2) self.assertEqual(structure.num_sites, 8) self.assertEqual(structure.formula, "H6 C2") quartz = self.quartz structure = quartz.structure self.assertEqual(len(structure.composition.elements), 2) self.assertEqual(structure.num_sites, 9) self.assertEqual(structure.formula, "Si3 O6") # test tilt structure tilt_str = Structure.from_file(os.path.join(test_dir, "POSCAR_tilt")) lmp_tilt_data = structure_2_lmpdata(tilt_str) s = StructureMatcher() groups = s.group_structures([lmp_tilt_data.structure, tilt_str]) self.assertEqual(len(groups), 1)
def test_previous_reconstructions(self): # Test to see if we generated all reconstruction # types correctly and nothing changes m = StructureMatcher() for n in self.rec_archive.keys(): if "base_reconstruction" in self.rec_archive[n].keys(): arch = self.rec_archive[ self.rec_archive[n]["base_reconstruction"]] sg = arch["spacegroup"]["symbol"] else: sg = self.rec_archive[n]["spacegroup"]["symbol"] if sg == "Fm-3m": rec = ReconstructionGenerator(self.Ni, 20, 20, n) el = self.Ni[0].species_string elif sg == "Im-3m": rec = ReconstructionGenerator(self.Fe, 20, 20, n) el = self.Fe[0].species_string elif sg == "Fd-3m": rec = ReconstructionGenerator(self.Si, 20, 20, n) el = self.Si[0].species_string slabs = rec.build_slabs() s = Structure.from_file(get_path(os.path.join("reconstructions", el + "_" + n + ".cif"))) self.assertTrue(any( [len(m.group_structures([s, slab])) == 1 for slab in slabs]))
def compare_structures(options): """Inspired to a similar function in pmg_structure.""" paths = options.paths if len(paths) < 2: print("You need more than one structure to compare!") return 1 try: structures = [abilab.Structure.from_file(p) for p in paths] except Exception as ex: print("Error reading structures from files. Are they in the right format?") print(str(ex)) return 1 from pymatgen.analysis.structure_matcher import StructureMatcher, ElementComparator compareby = "species" if options.anonymous else "element" m = StructureMatcher() if compareby == "species" else StructureMatcher(comparator=ElementComparator()) print("Grouping %s structures by `%s` with `anonymous: %s`" % (len(structures), compareby, options.anonymous)) for i, grp in enumerate(m.group_structures(structures, anonymous=options.anonymous)): print("Group {}: ".format(i)) for s in grp: spg_symbol, international_number = s.get_space_group_info() print("\t- {} ({}), vol: {:.2f} A^3, {} ({})".format( paths[structures.index(s)], s.formula, s.volume, spg_symbol, international_number)) print() if options.verbose: pprint(m.as_dict())
def test_class(self): # Tests entire class as single working unit sm = StructureMatcher() # Test group_structures and find_indices out = sm.group_structures(self.struct_list) self.assertEqual(map(len, out), [4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1]) self.assertEqual(sum(map(len, out)), len(self.struct_list))
def test_previous_reconstructions(self): # Test to see if we generated all reconstruction # types correctly and nothing changes m = StructureMatcher() for n in self.rec_archive.keys(): if "base_reconstruction" in self.rec_archive[n].keys(): arch = self.rec_archive[self.rec_archive[n] ["base_reconstruction"]] sg = arch["spacegroup"]["symbol"] else: sg = self.rec_archive[n]["spacegroup"]["symbol"] if sg == "Fm-3m": rec = ReconstructionGenerator(self.Ni, 20, 20, n) el = self.Ni[0].species_string elif sg == "Im-3m": rec = ReconstructionGenerator(self.Fe, 20, 20, n) el = self.Fe[0].species_string elif sg == "Fd-3m": rec = ReconstructionGenerator(self.Si, 20, 20, n) el = self.Si[0].species_string slabs = rec.build_slabs() s = Structure.from_file( get_path(os.path.join("reconstructions", el + "_" + n + ".cif"))) self.assertTrue( any([ len(m.group_structures([s, slab])) == 1 for slab in slabs ]))
def test_class(self): # Tests entire class as single working unit sm = StructureMatcher() # Test group_structures and find_indices out = sm.group_structures(self.struct_list) self.assertEqual(sm.find_indexes(self.struct_list, out), [0, 0, 0, 1, 2, 3, 4, 0, 5, 6, 7, 8, 8, 9, 9, 10])
def test_ignore_species(self): s1 = Structure.from_file(os.path.join(test_dir, "LiFePO4.cif")) s2 = Structure.from_file(os.path.join(test_dir, "POSCAR")) m = StructureMatcher(ignored_species=["Li"], primitive_cell=False, attempt_supercell=True) self.assertTrue(m.fit(s1, s2)) self.assertTrue(m.fit_anonymous(s1, s2)) groups = m.group_structures([s1, s2]) self.assertEqual(len(groups), 1) s2.make_supercell((2, 1, 1)) ss1 = m.get_s2_like_s1(s2, s1, include_ignored_species=True) self.assertAlmostEqual(ss1.lattice.a, 20.820740000000001) self.assertEqual(ss1.composition.reduced_formula, "LiFePO4") self.assertEqual( { k.symbol: v.symbol for k, v in m.get_best_electronegativity_anonymous_mapping( s1, s2).items() }, { "Fe": "Fe", "P": "P", "O": "O" })
def compare_structures(args): """ Compare structures in files for similarity using structure matcher. Args: args (dict): Args from argparse. """ filenames = args.filenames if len(filenames) < 2: print("You need more than one structure to compare!") sys.exit(-1) try: structures = [Structure.from_file(fn) for fn in filenames] except Exception as ex: print("Error converting file. Are they in the right format?") print(str(ex)) sys.exit(-1) m = StructureMatcher() if args.group == "species" else StructureMatcher( comparator=ElementComparator()) for idx, grp in enumerate(m.group_structures(structures)): print(f"Group {idx}: ") for s in grp: print(f"- {filenames[structures.index(s)]} ({s.formula})") print()
def filter_and_group_tasks(self, tasks): """ Groups tasks by structure matching """ filtered_tasks = [t for t in tasks if task_type( t['input']['incar']) in self.allowed_tasks] structures = [Structure.from_dict( t["output"]['structure']) for t in filtered_tasks] if self.separate_mag_orderings: for structure in structures: if has(structure.site_properties,"magmom"): structure.add_spin_by_site(structure.site_properties['magmom']) structure.remove_site_property('magmom') for idx, s in enumerate(structures): s.index = idx sm = StructureMatcher(ltol=self.ltol, stol=self.stol, angle_tol=self.angle_tol, primitive_cell=True, scale=True, attempt_supercell=False, allow_subset=False, comparator=ElementComparator()) grouped_structures = sm.group_structures(structures) grouped_tasks = [[filtered_tasks[struc.index] for struc in group] for group in grouped_structures] return grouped_tasks
def site_weighted_spectrum(xas_list: list[XAS], num_samples: int = 500) -> XAS: """ Obtain site-weighted XAS object based on site multiplicity for each absorbing index and its corresponding site-wise spectrum. Args: xas_list([XAS]): List of XAS object to be weighted num_samples(int): Number of samples for interpolation Returns: XAS object: The site-weighted spectrum """ m = StructureMatcher() groups = m.group_structures([i.structure for i in xas_list]) if len(groups) > 1: raise ValueError("The input structures mismatch") if not len({i.absorbing_element for i in xas_list}) == len({i.edge for i in xas_list}) == 1: raise ValueError( "Can only perform site-weighting for spectra with same absorbing element and same absorbing edge." ) if len({i.absorbing_index for i in xas_list}) == 1 or None in {i.absorbing_index for i in xas_list}: raise ValueError("Need at least two site-wise spectra to perform site-weighting") sa = SpacegroupAnalyzer(groups[0][0]) ss = sa.get_symmetrized_structure() maxes, mines = [], [] fs = [] multiplicities = [] for xas in xas_list: multiplicity = len(ss.find_equivalent_sites(ss[xas.absorbing_index])) multiplicities.append(multiplicity) maxes.append(max(xas.x)) mines.append(min(xas.x)) # use 3rd-order spline interpolation for mu (idx 3) vs energy (idx 0). f = interp1d( np.asarray(xas.x), np.asarray(xas.y), bounds_error=False, fill_value=0, kind="cubic", ) fs.append(f) # Interpolation within the intersection of x-axis ranges. x_axis = np.linspace(max(mines), min(maxes), num=num_samples) weighted_spectrum = np.zeros(num_samples) sum_multiplicities = sum(multiplicities) for i, j in enumerate(multiplicities): weighted_spectrum += (j * fs[i](x_axis)) / sum_multiplicities return XAS( x_axis, weighted_spectrum, ss, xas.absorbing_element, xas.edge, xas.spectrum_type, )
def apply_transformation(self, structure, return_ranked_list=False): #Make a mutable structure first mods = Structure.from_sites(structure) for sp, spin in self.mag_species_spin.items(): sp = get_el_sp(sp) oxi_state = getattr(sp, "oxi_state", 0) if spin: up = Specie(sp.symbol, oxi_state, {"spin": abs(spin)}) down = Specie(sp.symbol, oxi_state, {"spin": -abs(spin)}) mods.replace_species( {sp: Composition({up: self.order_parameter, down: 1 - self.order_parameter})}) else: mods.replace_species( {sp: Specie(sp.symbol, oxi_state, {"spin": spin})}) if mods.is_ordered: return [mods] if return_ranked_list > 1 else mods enum_args = self.enum_kwargs enum_args["min_cell_size"] = max(int( MagOrderingTransformation.determine_min_cell( structure, self.mag_species_spin, self.order_parameter)), enum_args.get("min_cell_size")) max_cell = self.enum_kwargs.get('max_cell_size') if max_cell: if enum_args["min_cell_size"] > max_cell: raise ValueError('Specified max cell size is smaller' ' than the minimum enumerable cell size') else: enum_args["max_cell_size"] = enum_args["min_cell_size"] t = EnumerateStructureTransformation(**enum_args) alls = t.apply_transformation(mods, return_ranked_list=return_ranked_list) try: num_to_return = int(return_ranked_list) except ValueError: num_to_return = 1 if num_to_return == 1 or not return_ranked_list: return alls[0]["structure"] if num_to_return else alls m = StructureMatcher(comparator=SpinComparator()) grouped = m.group_structures([d["structure"] for d in alls]) alls = [{"structure": g[0], "energy": self.emodel.get_energy(g[0])} for g in grouped] self._all_structures = sorted(alls, key=lambda d: d["energy"]) return self._all_structures[0:num_to_return]
def group_structures( structures, ltol=LTOL, stol=STOL, angle_tol=ANGLE_TOL, symprec=SYMPREC, separate_mag_orderings=False, ): """ Groups structures according to space group and structure matching Args: structures ([Structure]): list of structures to group ltol (float): StructureMatcher tuning parameter for matching tasks to materials stol (float): StructureMatcher tuning parameter for matching tasks to materials angle_tol (float): StructureMatcher tuning parameter for matching tasks to materials symprec (float): symmetry tolerance for space group finding separate_mag_orderings (bool): Separate magnetic orderings into different materials """ sm = StructureMatcher( ltol=ltol, stol=stol, angle_tol=angle_tol, primitive_cell=True, scale=True, attempt_supercell=False, allow_subset=False, comparator=ElementComparator(), ) def get_sg(struc): # helper function to get spacegroup with a loose tolerance try: sg = struc.get_space_group_info(symprec=SYMPREC)[1] except: sg = -1 return sg def get_mag_ordering(struc): # helperd function to get a label of the magnetic ordering type return np.around(np.abs(struc.total_magnetization) / struc.volume, decimals=1) # First group by spacegroup number then by structure matching for sg, pregroup in groupby(sorted(structures, key=get_sg), key=get_sg): for group in sm.group_structures(list(pregroup)): # Match magnetic orderings here if separate_mag_orderings: for _, mag_group in groupby(sorted(group, key=get_mag_ordering), key=get_mag_ordering): yield list(mag_group) else: yield group
def group_structures(s_l): """group_structures Applies a structure grouping algorithm to a list of structures. :param s_l: List of Structure objects. :return: List of lists of grouped structures. """ sm = StructureMatcher(scale=True, attempt_supercell=True, comparator=FrameworkComparator()) return sm.group_structures(s_l)
def test_coloring_with_fixed_species(): lattice = Lattice(3.945 * np.eye(3)) species = ["Sr", "Ti", "O", "O", "O"] frac_coords = np.array([[0, 0, 0], [0.5, 0.5, 0.5], [0.0, 0.5, 0.5], [0.5, 0.0, 0.5], [0.5, 0.5, 0.0]]) aristo = Structure(lattice, species, frac_coords) base_structure = aristo.copy() base_structure.remove_species(["Sr", "Ti"]) additional_species = species[:2] additional_frac_coords = frac_coords[:2] mapping_color_species = [DummySpecie("X"), "O"] num_types = len(mapping_color_species) index = 2 se = StructureEnumerator( base_structure, index, num_types, mapping_color_species=mapping_color_species, color_exchange=False, leave_superperiodic=False, use_all_colors=False, ) list_dstructs = se.generate(additional_species=additional_species, additional_frac_coords=additional_frac_coords) # with base_site_constraints mapping_color_species2 = [DummySpecie("X"), "O", "Sr", "Ti"] num_types2 = len(mapping_color_species2) base_site_constraints = [ [2], # Sr site [3], # Cu site [0, 1], # O or V [0, 1], # O or V [0, 1], # O or V ] se2 = StructureEnumerator( aristo, index, num_types2, mapping_color_species=mapping_color_species2, base_site_constraints=base_site_constraints, color_exchange=False, leave_superperiodic=False, use_all_colors=False, ) list_dstructs2 = se2.generate() # check uniqueness by StructureMatcher stm = StructureMatcher(ltol=1e-4, stol=1e-4) grouped = stm.group_structures(list_dstructs + list_dstructs2) assert len(grouped) == len(list_dstructs) assert all([(len(matched) == 2) for matched in grouped])
def test_mix(self): structures = [self.get_structure("Li2O"), self.get_structure("Li2O2"), self.get_structure("LiFePO4")] for fname in ["POSCAR.Li2O", "POSCAR.LiFePO4"]: structures.append(Structure.from_file(os.path.join(test_dir, fname))) sm = StructureMatcher(comparator=ElementComparator()) groups = sm.group_structures(structures) for g in groups: formula = g[0].composition.reduced_formula if formula in ["Li2O", "LiFePO4"]: self.assertEqual(len(g), 2) else: self.assertEqual(len(g), 1)
def test_mix(self): structures = [] for fname in ["POSCAR.Li2O", "Li2O.cif", "Li2O2.cif", "LiFePO4.cif", "POSCAR.LiFePO4"]: structures.append(read_structure(os.path.join(test_dir, fname))) sm = StructureMatcher(comparator=ElementComparator()) groups = sm.group_structures(structures) for g in groups: formula = g[0].composition.reduced_formula if formula in ["Li2O", "LiFePO4"]: self.assertEqual(len(g), 2) else: self.assertEqual(len(g), 1)
def test_coloring_with_fixed_species(sto_perovskite: Structure): base_structure = sto_perovskite.copy() base_structure.remove_species(["Sr", "Ti"]) additional_species = sto_perovskite.species[:2] additional_frac_coords = sto_perovskite.frac_coords[:2] mapping_color_species = [DummySpecie("X"), "O"] num_types = len(mapping_color_species) index = 2 se = StructureEnumerator( base_structure, index, num_types, mapping_color_species=mapping_color_species, color_exchange=False, remove_superperiodic=True, remove_incomplete=False, ) list_dstructs = se.generate(additional_species=additional_species, additional_frac_coords=additional_frac_coords) # with base_site_constraints mapping_color_species2 = [DummySpecie("X"), "O", "Sr", "Ti"] num_types2 = len(mapping_color_species2) base_site_constraints = [ [2], # Sr site [3], # Cu site [0, 1], # O or V [0, 1], # O or V [0, 1], # O or V ] se2 = StructureEnumerator( sto_perovskite, index, num_types2, mapping_color_species=mapping_color_species2, base_site_constraints=base_site_constraints, color_exchange=False, remove_superperiodic=True, remove_incomplete=False, ) list_dstructs2 = se2.generate() # check uniqueness by StructureMatcher stm = StructureMatcher(ltol=1e-4, stol=1e-4) grouped = stm.group_structures(list_dstructs + list_dstructs2) # type: ignore assert len(grouped) == len(list_dstructs) assert all([(len(matched) == 2) for matched in grouped])
def check(base_structure, num_type, indices, species, name): for index in indices: se = StructureEnumerator( base_structure, index, num_type, species, color_exchange=True, leave_superperiodic=False, ) list_ds = se.generate() stm = StructureMatcher(ltol=1e-4, stol=1e-4) grouped = stm.group_structures(list_ds) assert len(grouped) == len(list_ds)
def group_structures(structures, ltol=0.2, stol=0.3, angle_tol=5, symprec=0.1, separate_mag_orderings=False): """ Groups structures according to space group and structure matching Args: structures ([Structure]): list of structures to group ltol (float): StructureMatcher tuning parameter for matching tasks to materials stol (float): StructureMatcher tuning parameter for matching tasks to materials angle_tol (float): StructureMatcher tuning parameter for matching tasks to materials symprec (float): symmetry tolerance for space group finding separate_mag_orderings (bool): Separate magnetic orderings into different materials """ sm = StructureMatcher(ltol=ltol, stol=stol, angle_tol=angle_tol, primitive_cell=True, scale=True, attempt_supercell=False, allow_subset=False, comparator=ElementComparator()) def get_sg(struc): # helper function to get spacegroup with a loose tolerance return struc.get_space_group_info(symprec=symprec)[1] def get_mag_ordering(struc): # helperd function to get a label of the magnetic ordering type return CollinearMagneticStructureAnalyzer(struc).ordering.value # First group by spacegroup number then by structure matching for sg, pregroup in groupby(sorted(structures, key=get_sg), key=get_sg): for group in sm.group_structures(list(pregroup)): # Match magnetic orderings here if separate_mag_orderings: for _, mag_group in groupby(sorted(group, key=get_mag_ordering), key=get_mag_ordering): yield list(mag_group) else: yield group
def get_slabs(self, bonds=None, tol=0.1, max_broken_bonds=0): """ This method returns a list of slabs that are generated using the list of shift values from the method, _calculate_possible_shifts(). Before the shifts are used to create the slabs however, if the user decides to take into account whether or not a termination will break any polyhedral structure (bonds != None), this method will filter out any shift values that do so. Args: bonds ({(specie1, specie2): max_bond_dist}: bonds are specified as a dict of tuples: float of specie1, specie2 and the max bonding distance. For example, PO4 groups may be defined as {("P", "O"): 3}. tol (float): Threshold parameter in fcluster in order to check if two atoms are lying on the same plane. Default thresh set to 0.1 Angstrom in the direction of the surface normal. max_broken_bonds (int): Maximum number of allowable broken bonds for the slab. Use this to limit # of slabs (some structures may have a lot of slabs). Defaults to zero, which means no defined bonds must be broken. Returns: ([Slab]) List of all possible terminations of a particular surface. Slabs are sorted by the # of bonds broken. """ c_ranges = set() if bonds is None else self._get_c_ranges(bonds) slabs = [] for shift in self._calculate_possible_shifts(tol=tol): bonds_broken = 0 for r in c_ranges: if r[0] <= shift <= r[1]: bonds_broken += 1 if bonds_broken <= max_broken_bonds: # For now, set the energy to be equal to no. of broken bonds # per unit cell. slab = self.get_slab(shift, tol=tol, energy=bonds_broken) slabs.append(slab) # Further filters out any surfaces made that might be the same m = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) slabs = [g[0] for g in m.group_structures(slabs)] return sorted(slabs, key=lambda s: s.energy)
def test_ignore_species(self): s1 = Structure.from_file(os.path.join(test_dir, "LiFePO4.cif")) s2 = Structure.from_file(os.path.join(test_dir, "POSCAR")) m = StructureMatcher(ignored_species=["Li"], primitive_cell=False, attempt_supercell=True) self.assertTrue(m.fit(s1, s2)) self.assertTrue(m.fit_anonymous(s1, s2)) groups = m.group_structures([s1, s2]) self.assertEqual(len(groups), 1) s2.make_supercell((2, 1, 1)) ss1 = m.get_s2_like_s1(s2, s1, include_ignored_species=True) self.assertAlmostEqual(ss1.lattice.a, 20.820740000000001) self.assertEqual(ss1.composition.reduced_formula, "LiFePO4") self.assertEqual( {k.symbol: v.symbol for k, v in m.get_best_electronegativity_anonymous_mapping(s1, s2).items()}, {"Fe": "Fe", "P": "P", "O": "O"}, )
def test_basic_structure(kind, index): base_structure = get_lattice(kind) num_type = 2 species = ["Cu", "Au"] se = StructureEnumerator( base_structure, index, num_type, species, color_exchange=True, remove_superperiodic=True, ) list_ds = se.generate() stm = StructureMatcher(ltol=1e-4, stol=1e-4) grouped = stm.group_structures(list_ds) assert len(grouped) == len(list_ds)
class RemoveDuplicatesFilterTest(unittest.TestCase): def setUp(self): with open(os.path.join(test_dir, "TiO2_entries.json"), 'r') as fp: entries = json.load(fp, cls=PMGJSONDecoder) self._struct_list = [e.structure for e in entries] self._sm = StructureMatcher() def test_filter(self): transmuter = StandardTransmuter.from_structures(self._struct_list) fil = RemoveDuplicatesFilter() transmuter.apply_filter(fil) out = self._sm.group_structures(transmuter.transformed_structures) self.assertEqual(len(transmuter.transformed_structures), 11) def test_to_from_dict(self): fil = RemoveDuplicatesFilter() d = fil.to_dict self.assertIsInstance(RemoveDuplicatesFilter().from_dict(d), RemoveDuplicatesFilter)
def compare_structures(args): filenames = args.filenames if len(filenames) < 2: print "You need more than one structure to compare!" sys.exit(-1) try: structures = map(read_structure, filenames) except Exception as ex: print "Error converting file. Are they in the right format?" print str(ex) sys.exit(-1) from pymatgen.analysis.structure_matcher import StructureMatcher, ElementComparator m = StructureMatcher() if args.oxi else StructureMatcher(comparator=ElementComparator()) for i, grp in enumerate(m.group_structures(structures)): print "Group {}: ".format(i) for s in grp: print "- {} ({})".format(filenames[structures.index(s)], s.formula) print
def compare_structures(args): filenames = args.filenames if len(filenames) < 2: print("You need more than one structure to compare!") sys.exit(-1) try: structures = [Structure.from_file(fn) for fn in filenames] except Exception as ex: print("Error converting file. Are they in the right format?") print(str(ex)) sys.exit(-1) m = StructureMatcher() if args.group == "species" \ else StructureMatcher(comparator=ElementComparator()) for i, grp in enumerate(m.group_structures(structures)): print("Group {}: ".format(i)) for s in grp: print("- {} ({})".format(filenames[structures.index(s)], s.formula)) print()
def test_enumerated_structures(kind, num_types, composition_constraints, site_constraints): structure = get_lattice(kind) for index in range(1, 5): # TODO: when remove_superperiodic = remove_incomplete = False, this test is failed. zse = ZddStructureEnumerator( structure, index, num_types, composition_constraints=composition_constraints, base_site_constraints=site_constraints, remove_superperiodic=True, remove_incomplete=True, ) list_dstructs = zse.generate() stm = StructureMatcher(ltol=1e-4, stol=1e-4) grouped = stm.group_structures(list_dstructs) assert len(grouped) == len(list_dstructs)
class RemoveDuplicatesFilterTest(unittest.TestCase): def setUp(self): with open(os.path.join(test_dir, "TiO2_entries.json"), "rb") as fp: entries = json.load(fp, cls=PMGJSONDecoder) self._struct_list = [e.structure for e in entries] self._sm = StructureMatcher() def test_filter(self): transmuter = StandardTransmuter.from_structures(self._struct_list) fil = RemoveDuplicatesFilter() transmuter.apply_filter(fil) out = self._sm.group_structures(transmuter.transformed_structures) self.assertEqual( self._sm.find_indexes(transmuter.transformed_structures, out), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ) def test_to_from_dict(self): fil = RemoveDuplicatesFilter() d = fil.to_dict self.assertIsInstance(RemoveDuplicatesFilter().from_dict(d), RemoveDuplicatesFilter)
def test_structure(self): structure = self.lammps_data.structure self.assertEqual(len(structure.composition.elements), 2) self.assertEqual(structure.num_sites, 3) self.assertEqual(structure.formula, "Li2 O1") lammps_data = LammpsData.from_file(os.path.join(test_dir, "nvt.data"), 'full') self.assertEqual(type(lammps_data.structure).__name__, 'Structure') self.assertEqual(len(lammps_data.structure.composition.elements), 2) self.assertEqual(lammps_data.structure.num_sites, 648) self.assertEqual(lammps_data.structure.symbol_set[0], 'O') self.assertEqual(lammps_data.structure.symbol_set[1], 'H') self.assertEqual(lammps_data.structure.volume, 27000) # test tilt structure tilt_str = mg.Structure.from_file(os.path.join(test_dir, "POSCAR")) lmp_tilt_data = LammpsData.from_structure(tilt_str) s = StructureMatcher() groups = s.group_structures([lmp_tilt_data.structure, tilt_str]) self.assertEqual(len(groups), 1)
def compare_structures(args): filenames = args.filenames if len(filenames) < 2: print "You need more than one structure to compare!" sys.exit(-1) try: structures = map(read_structure, filenames) except Exception as ex: print "Error converting file. Are they in the right format?" print str(ex) sys.exit(-1) from pymatgen.analysis.structure_matcher import StructureMatcher, \ ElementComparator m = StructureMatcher() if args.oxi \ else StructureMatcher(comparator=ElementComparator()) for i, grp in enumerate(m.group_structures(structures)): print "Group {}: ".format(i) for s in grp: print "- {} ({})".format(filenames[structures.index(s)], s.formula) print
def group_structures(structures, ltol=0.2, stol=0.3, angle_tol=5, separate_mag_orderings=False): """ Groups structures according to space group and structure matching Args: structures ([Structure]): list of structures to group ltol (float): StructureMatcher tuning parameter for matching tasks to materials stol (float): StructureMatcher tuning parameter for matching tasks to materials angle_tol (float): StructureMatcher tuning parameter for matching tasks to materials separate_mag_orderings (bool): Separate magnetic orderings into different materials """ if separate_mag_orderings: for structure in structures: if has(structure.site_properties, "magmom"): structure.add_spin_by_site(structure.site_properties['magmom']) structure.remove_site_property('magmom') sm = StructureMatcher(ltol=ltol, stol=stol, angle_tol=angle_tol, primitive_cell=True, scale=True, attempt_supercell=False, allow_subset=False, comparator=ElementComparator()) def get_sg(struc): return struc.get_space_group_info(symprec=0.1)[1] # First group by spacegroup number then by structure matching for _, pregroup in groupby(sorted(structures, key=get_sg), key=get_sg): for group in sm.group_structures(sorted(pregroup, key=get_sg)): yield group
def group_structures( structures: List[Structure], ltol: float = SETTINGS.LTOL, stol: float = SETTINGS.STOL, angle_tol: float = SETTINGS.ANGLE_TOL, symprec: float = SETTINGS.SYMPREC, ) -> Iterator[List[Structure]]: """ Groups structures according to space group and structure matching Args: structures ([Structure]): list of structures to group ltol (float): StructureMatcher tuning parameter for matching tasks to materials stol (float): StructureMatcher tuning parameter for matching tasks to materials angle_tol (float): StructureMatcher tuning parameter for matching tasks to materials symprec (float): symmetry tolerance for space group finding """ sm = StructureMatcher( ltol=ltol, stol=stol, angle_tol=angle_tol, primitive_cell=True, scale=True, attempt_supercell=False, allow_subset=False, comparator=ElementComparator(), ) def _get_sg(struc): return get_sg(struc, symprec=symprec) # First group by spacegroup number then by structure matching for sg, pregroup in groupby(sorted(structures, key=_get_sg), key=_get_sg): for group in sm.group_structures(list(pregroup)): yield group
def apply_transformation(self, structure, return_ranked_list=False): """ Apply MagOrderTransformation to an input structure. :param structure: Any ordered structure. :param return_ranked_list: As in other Transformations. :return: """ if not structure.is_ordered: raise ValueError("Create an ordered approximation of " "your input structure first.") # retrieve order parameters order_parameters = [MagOrderParameterConstraint.from_dict(op_dict) for op_dict in self.order_parameter] # add dummy species on which to perform enumeration structure = self._add_dummy_species(structure, order_parameters) # trivial case if structure.is_ordered: structure = self._remove_dummy_species(structure) return [structure] if return_ranked_list > 1 else structure enum_kwargs = self.enum_kwargs.copy() enum_kwargs["min_cell_size"] = max( int(self.determine_min_cell(structure)), enum_kwargs.get("min_cell_size", 1) ) if enum_kwargs.get("max_cell_size", None): if enum_kwargs["min_cell_size"] > enum_kwargs["max_cell_size"]: warnings.warn("Specified max cell size ({}) is smaller " "than the minimum enumerable cell size ({}), " "changing max cell size to {}".format(enum_kwargs["max_cell_size"], enum_kwargs["min_cell_size"], enum_kwargs["min_cell_size"])) enum_kwargs["max_cell_size"] = enum_kwargs["min_cell_size"] else: enum_kwargs["max_cell_size"] = enum_kwargs["min_cell_size"] t = EnumerateStructureTransformation(**enum_kwargs) alls = t.apply_transformation(structure, return_ranked_list=return_ranked_list) # handle the fact that EnumerateStructureTransformation can either # return a single Structure or a list if isinstance(alls, Structure): # remove dummy species and replace Spin.up or Spin.down # with spin magnitudes given in mag_species_spin arg alls = self._remove_dummy_species(alls) alls = self._add_spin_magnitudes(alls) else: for idx, _ in enumerate(alls): alls[idx]["structure"] = self._remove_dummy_species(alls[idx]["structure"]) alls[idx]["structure"] = self._add_spin_magnitudes(alls[idx]["structure"]) try: num_to_return = int(return_ranked_list) except ValueError: num_to_return = 1 if num_to_return == 1 or not return_ranked_list: return alls[0]["structure"] if num_to_return else alls # remove duplicate structures and group according to energy model m = StructureMatcher(comparator=SpinComparator()) key = lambda x: SpacegroupAnalyzer(x, 0.1).get_space_group_number() out = [] for _, g in groupby(sorted([d["structure"] for d in alls], key=key), key): g = list(g) grouped = m.group_structures(g) out.extend([{"structure": g[0], "energy": self.energy_model.get_energy(g[0])} for g in grouped]) self._all_structures = sorted(out, key=lambda d: d["energy"]) return self._all_structures[0:num_to_return]
def get_slabs(self, bonds=None, tol=0.1, max_broken_bonds=0): """ This method returns a list of slabs that are generated using the list of shift values from the method, _calculate_possible_shifts(). Before the shifts are used to create the slabs however, if the user decides to take into account whether or not a termination will break any polyhedral structure (bonds != None), this method will filter out any shift values that do so. Args: bonds ({(specie1, specie2): max_bond_dist}: bonds are specified as a dict of tuples: float of specie1, specie2 and the max bonding distance. For example, PO4 groups may be defined as {("P", "O"): 3}. tol (float): Threshold parameter in fcluster in order to check if two atoms are lying on the same plane. Default thresh set to 0.1 Angstrom in the direction of the surface normal. max_broken_bonds (int): Maximum number of allowable broken bonds for the slab. Use this to limit # of slabs (some structures may have a lot of slabs). Defaults to zero, which means no defined bonds must be broken. Returns: ([Slab]) List of all possible terminations of a particular surface. Slabs are sorted by the # of bonds broken. """ def get_c_ranges(site1, site2, bond_dist): lattice = site1.lattice f1 = site1.frac_coords c_ranges = [] for dist, image in lattice.get_all_distance_and_image( f1, site2.frac_coords): if dist < bond_dist: f2 = site2.frac_coords + image # Checks if the distance between the two species # is less then the user input bond distance min_c = f1[2] max_c = f2[2] c_range = sorted([min_c, max_c]) if c_range[1] > 1: # Takes care of PBC when c coordinate of site # goes beyond the upper boundary of the cell c_ranges.append((c_range[0], 1)) c_ranges.append((0, c_range[1] - 1)) elif c_range[0] < 0: # Takes care of PBC when c coordinate of site # is below the lower boundary of the unit cell c_ranges.append((0, c_range[1])) c_ranges.append((c_range[0] + 1, 1)) else: c_ranges.append(c_range) return c_ranges bond_c_ranges = [] if bonds is not None: #Convert to species first bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()} for s1, s2 in itertools.combinations(self.oriented_unit_cell, 2): # Iterates through every possible pair of species in the # oriented unit cell all_sp = set(s1.species_and_occu.keys()) all_sp.update(s2.species_and_occu.keys()) for species, bond_dist in bonds.items(): # Checks if elements in species is in all_sp if all_sp.issuperset(species): bond_c_ranges.extend(get_c_ranges(s1, s2, bond_dist)) slabs = [] for shift in self._calculate_possible_shifts(tol=tol): bonds_broken = 0 for r in bond_c_ranges: if r[0] <= shift <= r[1]: bonds_broken += 1 if bonds_broken <= max_broken_bonds: # For now, set the energy to be equal to no. of broken bonds # per unit cell. slab = self.get_slab(shift, tol=tol, energy=bonds_broken) slabs.append(slab) # Further filters out any surfaces made that might be the same m = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False) slabs = [g[0] for g in m.group_structures(slabs)] return sorted(slabs, key=lambda s: s.energy)
def generate_substitution_structures(self, atom, target_species=[], sub_both_sides=False, range_tol=1e-2, dist_from_surf=0): """ Function that performs substitution-type doping on the surface and returns all possible configurations where one dopant is substituted per surface. Can substitute one surface or both. Args: atom (str): atom corresponding to substitutional dopant sub_both_sides (bool): If true, substitute an equivalent site on the other surface target_species (list): List of specific species to substitute range_tol (float): Find viable substitution sites at a specific distance from the surface +- this tolerance dist_from_surf (float): Distance from the surface to find viable substitution sites, defaults to 0 to substitute at the surface """ # Get symmetrized structure in case we want to substitue both sides sym_slab = SpacegroupAnalyzer(self.slab).get_symmetrized_structure() # Define a function for substituting a site def substitute(site, i): slab = self.slab.copy() props = self.slab.site_properties if sub_both_sides: # Find an equivalent site on the other surface eq_indices = [ indices for indices in sym_slab.equivalent_indices if i in indices ][0] for ii in eq_indices: if "%.6f" % (sym_slab[ii].frac_coords[2]) != \ "%.6f" % (site.frac_coords[2]): props["surface_properties"][ii] = "substitute" slab.replace(ii, atom) break props["surface_properties"][i] = "substitute" slab.replace(i, atom) slab.add_site_property("surface_properties", props["surface_properties"]) return slab # Get all possible substitution sites substituted_slabs = [] # Sort sites so that we can define a range relative to the position of the # surface atoms, i.e. search for sites above (below) the bottom (top) surface sorted_sites = sorted(sym_slab, key=lambda site: site.frac_coords[2]) if sorted_sites[0].surface_properties == "surface": d = sorted_sites[0].frac_coords[2] + dist_from_surf else: d = sorted_sites[-1].frac_coords[2] - dist_from_surf for i, site in enumerate(sym_slab): if d - range_tol < site.frac_coords[2] < d + range_tol: if target_species and site.species_string in target_species: substituted_slabs.append(substitute(site, i)) elif not target_species: substituted_slabs.append(substitute(site, i)) matcher = StructureMatcher() return [s[0] for s in matcher.group_structures(substituted_slabs)]
def get_tasker2_slabs(self, tol=0.01, same_species_only=True): """ Get a list of slabs that have been Tasker 2 corrected. Args: tol (float): Tolerance to determine if atoms are within same plane. This is a fractional tolerance, not an absolute one. same_species_only (bool): If True, only that are of the exact same species as the atom at the outermost surface are considered for moving. Otherwise, all atoms regardless of species that is within tol are considered for moving. Default is True (usually the desired behavior). Returns: ([Slab]) List of tasker 2 corrected slabs. """ sites = list(self.sites) slabs = [] sortedcsites = sorted(sites, key=lambda site: site.c) # Determine what fraction the slab is of the total cell size in the # c direction. Round to nearest rational number. nlayers_total = int(round(self.lattice.c / self.oriented_unit_cell.lattice.c)) nlayers_slab = int(round((sortedcsites[-1].c - sortedcsites[0].c) * nlayers_total)) slab_ratio = nlayers_slab / nlayers_total a = SpacegroupAnalyzer(self) symm_structure = a.get_symmetrized_structure() def equi_index(site): for i, equi_sites in enumerate(symm_structure.equivalent_sites): if site in equi_sites: return i raise ValueError("Cannot determine equi index!") for surface_site, shift in [(sortedcsites[0], slab_ratio), (sortedcsites[-1], -slab_ratio)]: tomove = [] fixed = [] for site in sites: if abs(site.c - surface_site.c) < tol and ( (not same_species_only) or site.species_and_occu == surface_site.species_and_occu): tomove.append(site) else: fixed.append(site) # Sort and group the sites by the species and symmetry equivalence tomove = sorted(tomove, key=lambda s: equi_index(s)) grouped = [list(sites) for k, sites in itertools.groupby( tomove, key=lambda s: equi_index(s))] if len(tomove) == 0 or any([len(g) % 2 != 0 for g in grouped]): warnings.warn("Odd number of sites to divide! Try changing " "the tolerance to ensure even division of " "sites or create supercells in a or b directions " "to allow for atoms to be moved!") continue combinations = [] for g in grouped: combinations.append( [c for c in itertools.combinations(g, int(len(g) / 2))]) for selection in itertools.product(*combinations): species = [site.species_and_occu for site in fixed] fcoords = [site.frac_coords for site in fixed] for s in tomove: species.append(s.species_and_occu) for group in selection: if s in group: fcoords.append(s.frac_coords) break else: # Move unselected atom to the opposite surface. fcoords.append(s.frac_coords + [0, 0, shift]) # sort by species to put all similar species together. sp_fcoord = sorted(zip(species, fcoords), key=lambda x: x[0]) species = [x[0] for x in sp_fcoord] fcoords = [x[1] for x in sp_fcoord] slab = Slab(self.lattice, species, fcoords, self.miller_index, self.oriented_unit_cell, self.shift, self.scale_factor, energy=self.energy) slabs.append(slab) s = StructureMatcher() unique = [ss[0] for ss in s.group_structures(slabs)] return unique
def apply_transformation(self, structure, return_ranked_list=False): """ Apply MagOrderTransformation to an input structure. :param structure: Any ordered structure. :param return_ranked_list: As in other Transformations. :return: """ if not structure.is_ordered: raise ValueError("Create an ordered approximation of " "your input structure first.") # retrieve order parameters order_parameters = [ MagOrderParameterConstraint.from_dict(op_dict) for op_dict in self.order_parameter ] # add dummy species on which to perform enumeration structure = self._add_dummy_species(structure, order_parameters) # trivial case if structure.is_ordered: structure = self._remove_dummy_species(structure) return [structure] if return_ranked_list > 1 else structure enum_kwargs = self.enum_kwargs.copy() enum_kwargs["min_cell_size"] = max( int(self.determine_min_cell(structure)), enum_kwargs.get("min_cell_size", 1)) max_cell = enum_kwargs.get('max_cell_size') if max_cell: if enum_kwargs["min_cell_size"] > max_cell: raise ValueError('Specified max cell size is smaller' ' than the minimum enumerable cell size') else: enum_kwargs["max_cell_size"] = enum_kwargs["min_cell_size"] t = EnumerateStructureTransformation(**enum_kwargs) alls = t.apply_transformation(structure, return_ranked_list=return_ranked_list) # handle the fact that EnumerateStructureTransformation can either # return a single Structure or a list if isinstance(alls, Structure): # remove dummy species and replace Spin.up or Spin.down # with spin magnitudes given in mag_species_spin arg alls = self._remove_dummy_species(alls) alls = self._add_spin_magnitudes(alls) else: for idx, _ in enumerate(alls): alls[idx]["structure"] = self._remove_dummy_species( alls[idx]["structure"]) alls[idx]["structure"] = self._add_spin_magnitudes( alls[idx]["structure"]) try: num_to_return = int(return_ranked_list) except ValueError: num_to_return = 1 if num_to_return == 1 or not return_ranked_list: return alls[0]["structure"] if num_to_return else alls # remove duplicate structures and group according to energy model m = StructureMatcher(comparator=SpinComparator()) key = lambda x: SpacegroupAnalyzer(x, 0.1).get_space_group_number() out = [] for _, g in groupby(sorted([d["structure"] for d in alls], key=key), key): g = list(g) grouped = m.group_structures(g) out.extend([{ "structure": g[0], "energy": self.energy_model.get_energy(g[0]) } for g in grouped]) self._all_structures = sorted(out, key=lambda d: d["energy"]) return self._all_structures[0:num_to_return]
def analyze(self, structures=None, structure_ids=None, against_icsd=False, energies=None): """ One encounter of a given structure will be labeled as True, its remaining matching structures as False. Args: structures (list): a list of structures to be compared. structure_ids (list): uids of structures, optional. against_icsd (bool): whether a comparison to icsd is also made. energies (list): list of energies (per atom) corresponding to structures. If given, the lowest energy instance of a given structure will be return as the unique one. Otherwise, there is no such guarantee. (optional) Returns: ([bool]) list of bools corresponding to the given list of structures corresponding to uniqueness """ self.structures = structures self.structure_ids = structure_ids self.against_icsd = against_icsd self.energies = energies smatch = StructureMatcher() self.groups = smatch.group_structures(structures) self.structure_is_unique = [] if self.energies: for i in range(len(self.groups)): self.groups[i] = [ x for _, x in sorted( zip( [ self.energies[self.structures.index(s)] for s in self.groups[i] ], self.groups[i], )) ] self._unique_structures = [i[0] for i in self.groups] for s in structures: if s in self._unique_structures: self.structure_is_unique.append(True) else: self.structure_is_unique.append(False) self._not_duplicate = self.structure_is_unique if self.against_icsd: structure_file = "oqmd1.2_exp_based_entries_structures.json" cache_matrio_data(structure_file) with open(os.path.join(CAMD_CACHE, structure_file), "r") as f: icsd_structures = json.load(f) chemsys = set() for s in self._unique_structures: chemsys = chemsys.union(set(s.composition.as_dict().keys())) self.icsd_structs_inchemsys = [] for k, v in icsd_structures.items(): try: s = Structure.from_dict(v) elems = set(s.composition.as_dict().keys()) if elems == chemsys: self.icsd_structs_inchemsys.append(s) # TODO: can we make this exception more specific, # do we have an example where this fails? except Exception as e: warnings.warn("Unable to process structure {}".format(k)) warnings.warn("Error: {}".format(e)) self.matching_icsd_strs = [] for i in range(len(structures)): if self.structure_is_unique[i]: match = None for s2 in self.icsd_structs_inchemsys: if smatch.fit(self.structures[i], s2): match = s2 break self.matching_icsd_strs.append( match) # store the matching ICSD structures. else: self.matching_icsd_strs.append(None) # Flip matching bools, and create a filter self._icsd_filter = [not i for i in self.matching_icsd_strs] self.structure_is_unique = (np.array(self.structure_is_unique) * np.array(self._icsd_filter)).tolist() self.unique_structures = list( itertools.compress(self.structures, self.structure_is_unique)) else: self.unique_structures = self._unique_structures # We store the final list of unique structures as unique_structures. # We return a corresponding list of bool to the initial structure # list provided. return self.structure_is_unique
def generate_substitution_structures(self, atom, target_species=[], sub_both_sides=False, range_tol=1e-2, dist_from_surf=0): """ Function that performs substitution-type doping on the surface and returns all possible configurations where one dopant is substituted per surface. Can substitute one surface or both. Args: atom (str): atom corresponding to substitutional dopant sub_both_sides (bool): If true, substitute an equivalent site on the other surface target_species (list): List of specific species to substitute range_tol (float): Find viable substitution sites at a specific distance from the surface +- this tolerance dist_from_surf (float): Distance from the surface to find viable substitution sites, defaults to 0 to substitute at the surface """ # Get symmetrized structure in case we want to substitue both sides sym_slab = SpacegroupAnalyzer(self.slab).get_symmetrized_structure() # Define a function for substituting a site def substitute(site, i): slab = self.slab.copy() props = self.slab.site_properties if sub_both_sides: # Find an equivalent site on the other surface eq_indices = [indices for indices in sym_slab.equivalent_indices if i in indices][0] for ii in eq_indices: if "%.6f" % (sym_slab[ii].frac_coords[2]) != \ "%.6f" % (site.frac_coords[2]): props["surface_properties"][ii] = "substitute" slab.replace(ii, atom) break props["surface_properties"][i] = "substitute" slab.replace(i, atom) slab.add_site_property("surface_properties", props["surface_properties"]) return slab # Get all possible substitution sites substituted_slabs = [] # Sort sites so that we can define a range relative to the position of the # surface atoms, i.e. search for sites above (below) the bottom (top) surface sorted_sites = sorted(sym_slab, key=lambda site: site.frac_coords[2]) if sorted_sites[0].surface_properties == "surface": d = sorted_sites[0].frac_coords[2] + dist_from_surf else: d = sorted_sites[-1].frac_coords[2] - dist_from_surf for i, site in enumerate(sym_slab): if d - range_tol < site.frac_coords[2] < d + range_tol: if target_species and site.species_string in target_species: substituted_slabs.append(substitute(site, i)) elif not target_species: substituted_slabs.append(substitute(site, i)) matcher = StructureMatcher() return [s[0] for s in matcher.group_structures(substituted_slabs)]
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
def adsorb_both_surfaces(slab, molecule, selective_dynamics=False, height=0.9, mi_vec=None, repeat=None, min_lw=5.0, reorient=True, find_args={}): """ Function that generates all adsorption structures for a given molecular adsorbate on both surfaces of a slab. Args: slab (Slab): slab object for which to find adsorbate sites selective_dynamics (bool): flag for whether to assign non-surface sites as fixed for selective dynamics molecule (Molecule): molecule corresponding to adsorbate selective_dynamics (bool): flag for whether to assign non-surface sites as fixed for selective dynamics height (float): height criteria for selection of surface sites mi_vec (3-D array-like): vector corresponding to the vector concurrent with the miller index, this enables use with slabs that have been reoriented, but the miller vector must be supplied manually 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} """ matcher = StructureMatcher() # Get adsorption on top adsgen_top = AdsorbateSiteFinder(slab, selective_dynamics=selective_dynamics, height=height, mi_vec=mi_vec, top_surface=True) structs = adsgen_top.generate_adsorption_structures(molecule, repeat=repeat, min_lw=min_lw, reorient=reorient, find_args=find_args) adslabs = [g[0] for g in matcher.group_structures(structs)] # Get adsorption on bottom adsgen_bottom = AdsorbateSiteFinder(slab, selective_dynamics=selective_dynamics, height=height, mi_vec=mi_vec, top_surface=False) structs = adsgen_bottom.generate_adsorption_structures(molecule, repeat=repeat, min_lw=min_lw, reorient=reorient, find_args=find_args) adslabs.extend([g[0] for g in matcher.group_structures(structs)]) # Group symmetrically similar slabs adsorbed_slabs = [] for group in matcher.group_structures(adslabs): # Further group each group by which surface adsorbed top_ads, bottom_ads = [], [] for s in group: sites = sorted(s, key=lambda site: site.frac_coords[2]) if sites[0].surface_properties == "adsorbate": bottom_ads.append(s) else: top_ads.append(s) if not top_ads or not bottom_ads: warnings.warn("There are not enough sites at the bottom or " "top to generate a symmetric adsorbed slab") continue # Combine the adsorbates of both top and bottom slabs # into one slab with one adsorbate on each side coords = list(top_ads[0].frac_coords) species = top_ads[0].species lattice = top_ads[0].lattice coords.extend([site.frac_coords for site in bottom_ads[0] if site.surface_properties == "adsorbate"]) species.extend([site.specie for site in bottom_ads[0] if site.surface_properties == "adsorbate"]) slab = Slab(lattice, species, coords, slab.miller_index, slab.oriented_unit_cell, slab.shift, slab.scale_factor) adsorbed_slabs.append(slab) return adsorbed_slabs