def test_get_composition(self): comp = self.transformed_entry.composition expected_comp = Composition({ DummySpecies("Xa"): 1, DummySpecies("Xb"): 1 }) self.assertEqual(comp, expected_comp, "Wrong composition!")
def test_init(self): self.specie1 = DummySpecies("X") self.assertRaises(ValueError, DummySpecies, "Xe") self.assertRaises(ValueError, DummySpecies, "Xec") self.assertRaises(ValueError, DummySpecies, "Vac") self.specie2 = DummySpecies("X", 2, {"spin": 3}) self.assertEqual(self.specie2.spin, 3)
def test_get_composition(self): comp = self.transformed_entry.composition expected_comp = Composition({ DummySpecies("Xf"): 14 / 30, DummySpecies("Xg"): 1.0, DummySpecies("Xh"): 2 / 30 }) self.assertEqual(comp, expected_comp, "Wrong composition!")
def test_from_string(self): sp = DummySpecies.from_string("X") self.assertEqual(sp.oxi_state, 0) sp = DummySpecies.from_string("X2+") self.assertEqual(sp.oxi_state, 2) sp = DummySpecies.from_string("X2+spin=5") self.assertEqual(sp.oxi_state, 2) self.assertEqual(sp.spin, 5)
def setUp(self): comp = Composition("LiFeO2") entry = PDEntry(comp, 53) self.transformed_entry = TransformedPDEntry( { DummySpecies("Xa"): 1, DummySpecies("Xb"): 1 }, entry)
def test_normalize(self): norm_entry = self.transformed_entry.normalize(mode="atom") expected_comp = Composition({ DummySpecies("Xf"): 7 / 23, DummySpecies("Xg"): 15 / 23, DummySpecies("Xh"): 1 / 23 }) self.assertEqual(norm_entry.composition, expected_comp, "Wrong composition!")
def test_from_string(self): sp = DummySpecies.from_string("X") self.assertEqual(sp.oxi_state, 0) sp = DummySpecies.from_string("X2+") self.assertEqual(sp.oxi_state, 2) self.assertEqual(sp.to_latex_string(), "X$^{2+}$") sp = DummySpecies.from_string("X2+spin=5") self.assertEqual(sp.oxi_state, 2) self.assertEqual(sp.spin, 5) self.assertEqual(sp.to_latex_string(), "X$^{2+}$") self.assertEqual(sp.to_html_string(), "X<sup>2+</sup>") self.assertEqual(sp.to_unicode_string(), "X²⁺")
def from_dict(cls, d, lattice=None): """ Create PeriodicSite from dict representation. Args: d (dict): dict representation of PeriodicSite lattice: Optional lattice to override lattice specified in d. Useful for ensuring all sites in a structure share the same lattice. Returns: PeriodicSite """ species = {} for sp_occu in d["species"]: if "oxidation_state" in sp_occu and Element.is_valid_symbol( sp_occu["element"]): sp = Species.from_dict(sp_occu) elif "oxidation_state" in sp_occu: sp = DummySpecies.from_dict(sp_occu) else: sp = Element(sp_occu["element"]) species[sp] = sp_occu["occu"] props = d.get("properties", None) if props is not None: for key in props.keys(): props[key] = json.loads(json.dumps(props[key], cls=MontyEncoder), cls=MontyDecoder) lattice = lattice if lattice else Lattice.from_dict(d["lattice"]) return cls(species, d["abc"], lattice, properties=props)
def test_get_el_sp(self): self.assertEqual(get_el_sp("Fe2+"), Species("Fe", 2)) self.assertEqual(get_el_sp("3"), Element.Li) self.assertEqual(get_el_sp("3.0"), Element.Li) self.assertEqual(get_el_sp("U"), Element.U) self.assertEqual(get_el_sp("X2+"), DummySpecies("X", 2)) self.assertEqual(get_el_sp("Mn3+"), Species("Mn", 3))
def test_specie_cifwriter(self): si4 = Species("Si", 4) si3 = Species("Si", 3) n = DummySpecies("X", -3) coords = list() coords.append(np.array([0.5, 0.5, 0.5])) coords.append(np.array([0.75, 0.5, 0.75])) coords.append(np.array([0, 0, 0])) lattice = Lattice( np.array([ [3.8401979337, 0.00, 0.00], [1.9200989668, 3.3257101909, 0.00], [0.00, -2.2171384943, 3.1355090603], ])) struct = Structure(lattice, [n, {si3: 0.5, n: 0.5}, si4], coords) writer = CifWriter(struct) ans = """# generated using pymatgen data_X1.5Si1.5 _symmetry_space_group_name_H-M 'P 1' _cell_length_a 3.84019793 _cell_length_b 3.84019899 _cell_length_c 3.84019793 _cell_angle_alpha 119.99999086 _cell_angle_beta 90.00000000 _cell_angle_gamma 60.00000914 _symmetry_Int_Tables_number 1 _chemical_formula_structural X1.5Si1.5 _chemical_formula_sum 'X1.5 Si1.5' _cell_volume 40.04479464 _cell_formula_units_Z 1 loop_ _symmetry_equiv_pos_site_id _symmetry_equiv_pos_as_xyz 1 'x, y, z' loop_ _atom_type_symbol _atom_type_oxidation_number X3- -3.0 Si3+ 3.0 Si4+ 4.0 loop_ _atom_site_type_symbol _atom_site_label _atom_site_symmetry_multiplicity _atom_site_fract_x _atom_site_fract_y _atom_site_fract_z _atom_site_occupancy X3- X0 1 0.50000000 0.50000000 0.50000000 1 X3- X1 1 0.75000000 0.50000000 0.75000000 0.5 Si3+ Si2 1 0.75000000 0.50000000 0.75000000 0.5 Si4+ Si3 1 0.00000000 0.00000000 0.00000000 1 """ for l1, l2 in zip(str(writer).split("\n"), ans.split("\n")): self.assertEqual(l1.strip(), l2.strip()) # test that mixed valence works properly s2 = Structure.from_str(ans, "cif") self.assertEqual(struct.composition, s2.composition)
def setUp(self): comp = Composition("LiFeO2") entry = PDEntry(comp, 53) terminal_compositions = ["Li2O", "FeO", "LiO8"] terminal_compositions = [Composition(c) for c in terminal_compositions] sp_mapping = OrderedDict() for i, comp in enumerate(terminal_compositions): sp_mapping[comp] = DummySpecies("X" + chr(102 + i)) self.transformed_entry = TransformedPDEntry(entry, sp_mapping)
def from_dict(cls, d: dict) -> "Site": """ Create Site from dict representation """ atoms_n_occu = {} for sp_occu in d["species"]: if "oxidation_state" in sp_occu and Element.is_valid_symbol(sp_occu["element"]): sp = Species.from_dict(sp_occu) elif "oxidation_state" in sp_occu: sp = DummySpecies.from_dict(sp_occu) else: sp = Element(sp_occu["element"]) # type: ignore atoms_n_occu[sp] = sp_occu["occu"] props = d.get("properties", None) if props is not None: for key in props.keys(): props[key] = json.loads(json.dumps(props[key], cls=MontyEncoder), cls=MontyDecoder) return cls(atoms_n_occu, d["xyz"], properties=props)
def _sanitize_symbol(symbol): if symbol == "vacancy": symbol = DummySpecies("X_vacancy", oxidation_state=None) elif symbol == "X": symbol = DummySpecies("X", oxidation_state=None) return symbol
def test_sort(self): r = sorted([Element.Fe, DummySpecies("X")]) self.assertEqual(r, [DummySpecies("X"), Element.Fe]) self.assertTrue(DummySpecies("X", 3) < DummySpecies("X", 4))
def test_pickle(self): el1 = DummySpecies("X", 3) o = pickle.dumps(el1) self.assertEqual(el1, pickle.loads(o))
def structure_graph(self, include_critical_points=("bond", "ring", "cage")): """ A StructureGraph object describing bonding information in the crystal. Args: include_critical_points: add DummySpecies for the critical points themselves, a list of "nucleus", "bond", "ring", "cage", set to None to disable Returns: a StructureGraph """ structure = self.structure.copy() point_idx_to_struct_idx = {} if include_critical_points: # atoms themselves don't have field information # so set to 0 for prop in ("ellipticity", "laplacian", "field"): structure.add_site_property(prop, [0] * len(structure)) for idx, node in self.nodes.items(): cp = self.critical_points[node["unique_idx"]] if cp.type.value in include_critical_points: specie = DummySpecies( "X{}cp".format(cp.type.value[0]), oxidation_state=None ) structure.append( specie, node["frac_coords"], properties={ "ellipticity": cp.ellipticity, "laplacian": cp.laplacian, "field": cp.field, }, ) point_idx_to_struct_idx[idx] = len(structure) - 1 edge_weight = "bond_length" edge_weight_units = "Å" sg = StructureGraph.with_empty_graph( structure, name="bonds", edge_weight_name=edge_weight, edge_weight_units=edge_weight_units, ) edges = self.edges.copy() idx_to_delete = [] # check for duplicate bonds for idx, edge in edges.items(): unique_idx = self.nodes[idx]["unique_idx"] # only check edges representing bonds, not rings if self.critical_points[unique_idx].type == CriticalPointType.bond: if idx not in idx_to_delete: for idx2, edge2 in edges.items(): if idx != idx2 and edge == edge2: idx_to_delete.append(idx2) warnings.warn( "Duplicate edge detected, try re-running " "critic2 with custom parameters to fix this. " "Mostly harmless unless user is also " "interested in rings/cages." ) logger.debug( "Duplicate edge between points {} (unique point {})" "and {} ({}).".format( idx, self.nodes[idx]["unique_idx"], idx2, self.nodes[idx2]["unique_idx"], ) ) # and remove any duplicate bonds present for idx in idx_to_delete: del edges[idx] for idx, edge in edges.items(): unique_idx = self.nodes[idx]["unique_idx"] # only add edges representing bonds, not rings if self.critical_points[unique_idx].type == CriticalPointType.bond: from_idx = edge["from_idx"] to_idx = edge["to_idx"] # have to also check bond is between nuclei if non-nuclear # attractors not in structure skip_bond = False if include_critical_points and "nnattr" not in include_critical_points: from_type = self.critical_points[ self.nodes[from_idx]["unique_idx"] ].type to_type = self.critical_points[ self.nodes[from_idx]["unique_idx"] ].type skip_bond = (from_type != CriticalPointType.nucleus) or ( to_type != CriticalPointType.nucleus ) if not skip_bond: from_lvec = edge["from_lvec"] to_lvec = edge["to_lvec"] relative_lvec = np.subtract(to_lvec, from_lvec) # for edge case of including nnattrs in bonding graph when other critical # points also included, indices may get mixed struct_from_idx = point_idx_to_struct_idx.get(from_idx, from_idx) struct_to_idx = point_idx_to_struct_idx.get(to_idx, to_idx) weight = self.structure.get_distance( struct_from_idx, struct_to_idx, jimage=relative_lvec ) crit_point = self.critical_points[unique_idx] edge_properties = { "field": crit_point.field, "laplacian": crit_point.laplacian, "ellipticity": crit_point.ellipticity, "frac_coords": self.nodes[idx]["frac_coords"], } sg.add_edge( struct_from_idx, struct_to_idx, from_jimage=from_lvec, to_jimage=to_lvec, weight=weight, edge_properties=edge_properties, ) return sg
def test_eq(self): self.assertFalse(DummySpecies("Xg") == DummySpecies("Xh")) self.assertFalse(DummySpecies("Xg") == DummySpecies("Xg", 3)) self.assertTrue(DummySpecies("Xg", 3) == DummySpecies("Xg", 3))
def _gen_input_file(self): """ Generate the necessary struct_enum.in file for enumlib. See enumlib documentation for details. """ coord_format = "{:.6f} {:.6f} {:.6f}" # Using symmetry finder, get the symmetrically distinct sites. fitter = SpacegroupAnalyzer(self.structure, self.symm_prec) symmetrized_structure = fitter.get_symmetrized_structure() logger.debug("Spacegroup {} ({}) with {} distinct sites".format( fitter.get_space_group_symbol(), fitter.get_space_group_number(), len(symmetrized_structure.equivalent_sites), )) """ Enumlib doesn"t work when the number of species get too large. To simplify matters, we generate the input file only with disordered sites and exclude the ordered sites from the enumeration. The fact that different disordered sites with the exact same species may belong to different equivalent sites is dealt with by having determined the spacegroup earlier and labelling the species differently. """ # index_species and index_amounts store mappings between the indices # used in the enum input file, and the actual species and amounts. index_species = [] index_amounts = [] # Stores the ordered sites, which are not enumerated. ordered_sites = [] disordered_sites = [] coord_str = [] for sites in symmetrized_structure.equivalent_sites: if sites[0].is_ordered: ordered_sites.append(sites) else: sp_label = [] species = dict(sites[0].species.items()) if sum(species.values()) < 1 - EnumlibAdaptor.amount_tol: # Let us first make add a dummy element for every single # site whose total occupancies don't sum to 1. species[DummySpecies("X")] = 1 - sum(species.values()) for sp in species.keys(): if sp not in index_species: index_species.append(sp) sp_label.append(len(index_species) - 1) index_amounts.append(species[sp] * len(sites)) else: ind = index_species.index(sp) sp_label.append(ind) index_amounts[ind] += species[sp] * len(sites) sp_label = "/".join(["{}".format(i) for i in sorted(sp_label)]) for site in sites: coord_str.append("{} {}".format( coord_format.format(*site.coords), sp_label)) disordered_sites.append(sites) def get_sg_info(ss): finder = SpacegroupAnalyzer(Structure.from_sites(ss), self.symm_prec) return finder.get_space_group_number() target_sgnum = get_sg_info(symmetrized_structure.sites) curr_sites = list(itertools.chain.from_iterable(disordered_sites)) sgnum = get_sg_info(curr_sites) ordered_sites = sorted(ordered_sites, key=lambda sites: len(sites)) logger.debug("Disordered sites has sg # %d" % (sgnum)) self.ordered_sites = [] # progressively add ordered sites to our disordered sites # until we match the symmetry of our input structure if self.check_ordered_symmetry: while sgnum != target_sgnum and len(ordered_sites) > 0: sites = ordered_sites.pop(0) temp_sites = list(curr_sites) + sites new_sgnum = get_sg_info(temp_sites) if sgnum != new_sgnum: logger.debug("Adding %s in enum. New sg # %d" % (sites[0].specie, new_sgnum)) index_species.append(sites[0].specie) index_amounts.append(len(sites)) sp_label = len(index_species) - 1 for site in sites: coord_str.append("{} {}".format( coord_format.format(*site.coords), sp_label)) disordered_sites.append(sites) curr_sites = temp_sites sgnum = new_sgnum else: self.ordered_sites.extend(sites) for sites in ordered_sites: self.ordered_sites.extend(sites) self.index_species = index_species lattice = self.structure.lattice output = [self.structure.formula, "bulk"] for vec in lattice.matrix: output.append(coord_format.format(*vec)) output.append("%d" % len(index_species)) output.append("%d" % len(coord_str)) output.extend(coord_str) output.append("{} {}".format(self.min_cell_size, self.max_cell_size)) output.append(str(self.enum_precision_parameter)) output.append("full") ndisordered = sum([len(s) for s in disordered_sites]) base = int(ndisordered * lcm(*[ f.limit_denominator(ndisordered * self.max_cell_size).denominator for f in map(fractions.Fraction, index_amounts) ])) # This multiplicative factor of 10 is to prevent having too small bases # which can lead to rounding issues in the next step. # An old bug was that a base was set to 8, with a conc of 0.4:0.6. That # resulted in a range that overlaps and a conc of 0.5 satisfying this # enumeration. See Cu7Te5.cif test file. base *= 10 # base = ndisordered #10 ** int(math.ceil(math.log10(ndisordered))) # To get a reasonable number of structures, we fix concentrations to the # range expected in the original structure. total_amounts = sum(index_amounts) for amt in index_amounts: conc = amt / total_amounts if abs(conc * base - round(conc * base)) < 1e-5: output.append("{} {} {}".format(int(round(conc * base)), int(round(conc * base)), base)) else: min_conc = int(math.floor(conc * base)) output.append("{} {} {}".format(min_conc - 1, min_conc + 1, base)) output.append("") logger.debug("Generated input file:\n{}".format("\n".join(output))) with open("struct_enum.in", "w") as f: f.write("\n".join(output))