def parse_tok(t): if re.match("\w+-\d+", t): return {"task_id": t} elif "-" in t: elements = t.split("-") elements = [[Element(el).symbol] if el != "*" else ALL_ELEMENT_SYMBOLS for el in elements] chemsyss = [] for cs in itertools.product(*elements): if len(set(cs)) == len(cs): chemsyss.append("-".join(sorted(set(cs)))) return {"chemsys": {"$in": chemsyss}} else: all_formulas = set() syms = re.findall("[A-Z][a-z]*", t) to_permute = ALL_ELEMENT_SYMBOLS.difference(syms) parts = t.split("*") for syms in itertools.permutations(to_permute, len(parts) - 1): f = [] for p in zip(parts, syms): f.extend(p) f.append(parts[-1]) c = Composition("".join(f)) #Check for valid Elements in keys. map(lambda e: Element(e.symbol), c.keys()) all_formulas.add(c.reduced_formula) return {"pretty_formula": {"$in": list(all_formulas)}}
def parse_tok(t): if re.match("\w+-\d+", t): return {"task_id": t} elif "-" in t: elements = [parse_sym(sym) for sym in t.split("-")] chemsyss = [] for cs in itertools.product(*elements): if len(set(cs)) == len(cs): # Check for valid symbols cs = [Element(s).symbol for s in cs] chemsyss.append("-".join(sorted(cs))) return {"chemsys": {"$in": chemsyss}} else: all_formulas = set() explicit_els = [] wild_card_els = [] for sym in re.findall( r"(\*[\.\d]*|\{.*\}[\.\d]*|[A-Z][a-z]*)[\.\d]*", t): if ("*" in sym) or ("{" in sym): wild_card_els.append(sym) else: m = re.match("([A-Z][a-z]*)[\.\d]*", sym) explicit_els.append(m.group(1)) nelements = len(wild_card_els) + len(set(explicit_els)) parts = re.split(r"(\*|\{.*\})", t) parts = [parse_sym(s) for s in parts if s != ""] for f in itertools.product(*parts): c = Composition("".join(f)) if len(c) == nelements: # Check for valid Elements in keys. for e in c.keys(): Element(e.symbol) all_formulas.add(c.reduced_formula) return {"pretty_formula": {"$in": list(all_formulas)}}
def test_negative_compositions(self): self.assertEqual(Composition('Li-1(PO-1)4', allow_negative=True).formula, 'Li-1 P4 O-4') self.assertEqual(Composition('Li-1(PO-1)4', allow_negative=True).reduced_formula, 'Li-1(PO-1)4') self.assertEqual(Composition('Li-2Mg4', allow_negative=True).reduced_composition, Composition('Li-1Mg2', allow_negative=True)) self.assertEqual(Composition('Li-2.5Mg4', allow_negative=True).reduced_composition, Composition('Li-2.5Mg4', allow_negative=True)) #test math c1 = Composition('LiCl', allow_negative=True) c2 = Composition('Li') self.assertEqual(c1 - 2 * c2, Composition({'Li': -1, 'Cl': 1}, allow_negative=True)) self.assertEqual((c1 + c2).allow_negative, True) self.assertEqual(c1 / -1, Composition('Li-1Cl-1', allow_negative=True)) #test num_atoms c1 = Composition('Mg-1Li', allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.get_atomic_fraction('Mg'), 0.5) self.assertEqual(c1.get_atomic_fraction('Li'), 0.5) self.assertEqual(c1.fractional_composition, Composition('Mg-0.5Li0.5', allow_negative=True)) #test copy self.assertEqual(c1.copy(), c1) #test species c1 = Composition({'Mg':1, 'Mg2+':-1}, allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.element_composition, Composition()) self.assertEqual(c1.average_electroneg, 1.31)
def __init__(self, atoms_n_occu, coords, properties=None): """ Create a *non-periodic* site. Args: atoms_n_occu: Species on the site. Can be: i. A sequence of element / specie specified either as string symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers, e.g., (3, 56, ...) or actual Element or Specie objects. ii. List of dict of elements/species and occupancies, e.g., [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of disordered structures. coords: Cartesian coordinates of site. properties: Properties associated with the site as a dict, e.g. {"magmom": 5}. Defaults to None. """ if isinstance(atoms_n_occu, collections.Mapping): self._species = Composition(atoms_n_occu) totaloccu = self._species.num_atoms if totaloccu > 1 + Composition.amount_tolerance: raise ValueError("Species occupancies sum to more than 1!") self._is_ordered = totaloccu == 1 and len(self._species) == 1 else: self._species = Composition({get_el_sp(atoms_n_occu): 1}) self._is_ordered = True self._coords = coords self._properties = properties if properties else {}
def test_negative_compositions(self): self.assertEqual(Composition("Li-1(PO-1)4", allow_negative=True).formula, "Li-1 P4 O-4") self.assertEqual(Composition("Li-1(PO-1)4", allow_negative=True).reduced_formula, "Li-1(PO-1)4") self.assertEqual( Composition("Li-2Mg4", allow_negative=True).reduced_composition, Composition("Li-1Mg2", allow_negative=True) ) self.assertEqual( Composition("Li-2.5Mg4", allow_negative=True).reduced_composition, Composition("Li-2.5Mg4", allow_negative=True), ) # test math c1 = Composition("LiCl", allow_negative=True) c2 = Composition("Li") self.assertEqual(c1 - 2 * c2, Composition({"Li": -1, "Cl": 1}, allow_negative=True)) self.assertEqual((c1 + c2).allow_negative, True) self.assertEqual(c1 / -1, Composition("Li-1Cl-1", allow_negative=True)) # test num_atoms c1 = Composition("Mg-1Li", allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.get_atomic_fraction("Mg"), 0.5) self.assertEqual(c1.get_atomic_fraction("Li"), 0.5) self.assertEqual(c1.fractional_composition, Composition("Mg-0.5Li0.5", allow_negative=True)) # test copy self.assertEqual(c1.copy(), c1) # test species c1 = Composition({"Mg": 1, "Mg2+": -1}, allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.element_composition, Composition()) self.assertEqual(c1.average_electroneg, 1.31)
def test_from_dict(self): sym_dict = {"Fe": 6, "O": 8} self.assertEqual( Composition.from_dict(sym_dict).reduced_formula, "Fe3O4", "Creation form sym_amount dictionary failed!" ) comp = Composition({"Fe2+": 2, "Fe3+": 4, "O2-": 8}) comp2 = Composition.from_dict(comp.as_dict()) self.assertEqual(comp, comp2)
def test_almost_equals(self): c1 = Composition({'Fe': 2.0, 'O': 3.0, 'Mn': 0}) c2 = Composition({'O': 3.2, 'Fe': 1.9, 'Zn': 0}) c3 = Composition({'Ag': 2.0, 'O': 3.0}) c4 = Composition({'Fe': 2.0, 'O': 3.0, 'Ag': 2.0}) self.assertTrue(c1.almost_equals(c2, rtol=0.1)) self.assertFalse(c1.almost_equals(c2, rtol=0.01)) self.assertFalse(c1.almost_equals(c3, rtol=0.1)) self.assertFalse(c1.almost_equals(c4, rtol=0.1))
def test_almost_equals(self): c1 = Composition({"Fe": 2.0, "O": 3.0, "Mn": 0}) c2 = Composition({"O": 3.2, "Fe": 1.9, "Zn": 0}) c3 = Composition({"Ag": 2.0, "O": 3.0}) c4 = Composition({"Fe": 2.0, "O": 3.0, "Ag": 2.0}) self.assertTrue(c1.almost_equals(c2, rtol=0.1)) self.assertFalse(c1.almost_equals(c2, rtol=0.01)) self.assertFalse(c1.almost_equals(c3, rtol=0.1)) self.assertFalse(c1.almost_equals(c4, rtol=0.1))
def to_reduced_dict(self): """ Returns: dict with element symbol and reduced amount e.g., {"Fe": 2.0, "O":3.0}. """ reduced_formula = self._composition.reduced_formula c = Composition(reduced_formula) d = c.as_dict() d['charge'] = self._charge return d
def test_Metallofullerene(self): # Test: Parse Metallofullerene formula (e.g. Y3N@C80) formula = "Y3N@C80" sym_dict = {"Y": 3, "N": 1, "C": 80} cmp = Composition(formula) cmp2 = Composition.from_dict(sym_dict) self.assertEqual(cmp, cmp2)
def test_sub(self): self.assertEqual((self.comp[0] - Composition.from_formula("Li2O")).formula, "Li1 Fe2 P3 O11", "Incorrect composition after addition!") self.assertEqual((self.comp[0] - {"Fe": 2, "O": 3}).formula, "Li3 P3 O9")
def __init__(self, composition, energy, correction=0.0, parameters=None, data=None, entry_id=None, attribute=None): """ Initializes a ComputedEntry. Args: composition (Composition): Composition of the entry. For flexibility, this can take the form of all the typical input taken by a Composition, including a {symbol: amt} dict, a string formula, and others. energy (float): Energy of the entry. Usually the final calculated energy from VASP or other electronic structure codes. correction (float): A correction to be applied to the energy. This is used to modify the energy for certain analyses. Defaults to 0.0. parameters (dict): An optional dict of parameters associated with the entry. Defaults to None. data (dict): An optional dict of any additional data associated with the entry. Defaults to None. entry_id (obj): An optional id to uniquely identify the entry. attribute: Optional attribute of the entry. This can be used to specify that the entry is a newly found compound, or to specify a particular label for the entry, or else ... Used for further analysis and plotting purposes. An attribute can be anything but must be MSONable. """ self.uncorrected_energy = energy self.composition = Composition(composition) self.correction = correction self.parameters = parameters if parameters else {} self.data = data if data else {} self.entry_id = entry_id self.name = self.composition.reduced_formula self.attribute = attribute
def __init__(self, formula): ''' Args: chemical formula as a string. formula must have integer subscripts Ex: 'SrTiO3' Attributes: composition: the composition as a dictionary. Ex: {'Sr': 1, 'Ti': 1, 'O', 3} elements: the dictionary keys for the composition elec_neg: the maximum pairwise electronegetivity difference aos: the consituant atomic orbitals for each element as a dictionary band_edges: dictionary containing the highest occupied molecular orbital (H**O), lowest unocupied molecular orbital (LUMO), and whether the material is predicted to be a metal ''' self.composition = Composition(formula).as_dict() self.elements = self.composition.keys() for subscript in self.composition.values(): if not float(subscript).is_integer(): raise ValueError('composition subscripts must be integers') self.elec_neg = self.max_electronegativity() self.aos = {str(el): [[str(el), k, v] for k, v in Element(el).atomic_orbitals.items()] for el in self.elements} self.band_edges = self.obtain_band_edges()
def __init__(self, composition, charge=0.0, properties=None): """ Flexible Ion construction, similar to Composition. For more information, please see pymatgen.core.Composition """ self._composition = Composition(composition) self._charge = charge self._properties = properties if properties else {}
def test_equals(self): random_z = random.randint(1, 92) fixed_el = Element.from_Z(random_z) other_z = random.randint(1, 92) while other_z == random_z: other_z = random.randint(1, 92) comp1 = Composition({fixed_el: 1, Element.from_Z(other_z): 0}) other_z = random.randint(1, 92) while other_z == random_z: other_z = random.randint(1, 92) comp2 = Composition({fixed_el: 1, Element.from_Z(other_z): 0}) self.assertEqual( comp1, comp2, "Composition equality test failed. " + "%s should be equal to %s" % (comp1.formula, comp2.formula), ) self.assertEqual(comp1.__hash__(), comp2.__hash__(), "Hashcode equality test failed!")
def are_equal(self, sp1, sp2): """ True if element:amounts are exactly the same, i.e., oxidation state is not considered. Args: sp1: First species. A dict of {specie/element: amt} as per the definition in Site and PeriodicSite. sp2: Second species. A dict of {specie/element: amt} as per the definition in Site and PeriodicSite. Returns: Boolean indicating whether species are the same based on element and amounts. """ comp1 = Composition(sp1) comp2 = Composition(sp2) return comp1.get_el_amt_dict() == comp2.get_el_amt_dict()
def test_getmu_vertices_stability_phase(self): results = self.analyzer.getmu_vertices_stability_phase(Composition.from_formula("LiFeO2"), Element("O")) self.assertAlmostEqual(len(results), 6) test_equality = False for c in results: if abs(c[Element("O")]+7.115) < 1e-2 and abs(c[Element("Fe")]+6.596) < 1e-2 and \ abs(c[Element("Li")]+3.931) < 1e-2: test_equality = True self.assertTrue(test_equality,"there is an expected vertex missing in the list")
def test_as_dict(self): c = Composition.from_dict({"Fe": 4, "O": 6}) d = c.as_dict() correct_dict = {"Fe": 4.0, "O": 6.0} self.assertEqual(d["Fe"], correct_dict["Fe"]) self.assertEqual(d["O"], correct_dict["O"]) correct_dict = {"Fe": 2.0, "O": 3.0} d = c.to_reduced_dict self.assertEqual(d["Fe"], correct_dict["Fe"]) self.assertEqual(d["O"], correct_dict["O"])
def test_as_dict(self): c = Composition.from_dict({'Fe': 4, 'O': 6}) d = c.as_dict() correct_dict = {'Fe': 4.0, 'O': 6.0} self.assertEqual(d['Fe'], correct_dict['Fe']) self.assertEqual(d['O'], correct_dict['O']) correct_dict = {'Fe': 2.0, 'O': 3.0} d = c.to_reduced_dict self.assertEqual(d['Fe'], correct_dict['Fe']) self.assertEqual(d['O'], correct_dict['O'])
def test_indeterminate_formula(self): correct_formulas = [["Co1"], ["Co1", "C1 O1"], ["Co2 O3", "C1 O5"], ["N1 Ca1 Lu1", "U1 Al1 C1 N1"], ["N1 Ca1 Lu1", "U1 Al1 C1 N1"], ["Li1 Co1 P2 N1 O10", "Li1 P2 C1 N1 O11", "Li1 Co1 Po8 N1 O2", "Li1 Po8 C1 N1 O3"], ["Co2 P4 O4", "Co2 Po4", "P4 C2 O6", "Po4 C2 O2"], []] for i, c in enumerate(correct_formulas): self.assertEqual([Composition.from_formula(comp) for comp in c], self.indeterminate_comp[i])
def __init__(self, atoms_n_occu, coords, properties=None): """ Create a *non-periodic* site. Args: atoms_n_occu: Species on the site. Can be: i. A Composition object (preferred) ii. An element / specie specified either as a string symbols, e.g. "Li", "Fe2+", "P" or atomic numbers, e.g., 3, 56, or actual Element or Specie objects. iii.Dict of elements/species and occupancies, e.g., {"Fe" : 0.5, "Mn":0.5}. This allows the setup of disordered structures. coords: Cartesian coordinates of site. properties: Properties associated with the site as a dict, e.g. {"magmom": 5}. Defaults to None. """ if isinstance(atoms_n_occu, Composition): # Compositions are immutable, so don't need to copy (much faster) self._species = atoms_n_occu # Kludgy lookup of private attribute, but its faster totaloccu = atoms_n_occu._natoms if totaloccu > 1 + Composition.amount_tolerance: raise ValueError("Species occupancies sum to more than 1!") # Another kludgy lookup of private attribute, but its faster self._is_ordered = totaloccu == 1 and len(self._species._data) == 1 else: try: self._species = Composition({get_el_sp(atoms_n_occu): 1}) self._is_ordered = True except TypeError: self._species = Composition(atoms_n_occu) totaloccu = self._species.num_atoms if totaloccu > 1 + Composition.amount_tolerance: raise ValueError("Species occupancies sum to more than 1!") self._is_ordered = totaloccu == 1 and len(self._species) == 1 self._coords = np.array(coords) self._coords.setflags(write=False) self._properties = properties if properties else {}
def __str__(self): reactant_str = [] product_str = [] for i in range(self._num_comp): comp = self._all_comp[i] coeff = self._coeffs[i] red_comp = Composition.from_formula(comp.reduced_formula) scale_factor = comp.num_atoms / red_comp.num_atoms scaled_coeff = coeff * scale_factor if scaled_coeff < 0: reactant_str.append("{:.3f} {}".format(-scaled_coeff, comp.reduced_formula)) elif scaled_coeff > 0: product_str.append("{:.3f} {}".format(scaled_coeff, comp.reduced_formula)) return " + ".join(reactant_str) + " -> " + " + ".join(product_str)
def __init__(self, atoms_n_occu, coords, properties=None): """ Create a *non-periodic* site. Args: atoms_n_occu: Species on the site. Can be: i. A sequence of element / specie specified either as string symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers, e.g., (3, 56, ...) or actual Element or Specie objects. ii. List of dict of elements/species and occupancies, e.g., [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of disordered structures. coords: Cartesian coordinates of site. properties: Properties associated with the site as a dict, e.g. {"magmom": 5}. Defaults to None. """ if issubclass(atoms_n_occu.__class__, collections.Mapping): self._species = Composition({smart_element_or_specie(k): v for k, v in atoms_n_occu.items()}) totaloccu = self._species.num_atoms if totaloccu > 1: raise ValueError("Species occupancies sum to more than 1!") self._is_ordered = (totaloccu == 1 and len(self._species) == 1) else: self._species = Composition( {smart_element_or_specie(atoms_n_occu): 1}) self._is_ordered = True self._coords = coords self._properties = properties if properties else {} for k in self._properties.keys(): if k not in Site.supported_properties: raise ValueError("{} is not a supported property".format(k))
def parse_tok(t): if re.match("\w+-\d+", t): return {"task_id": t} elif "-" in t: elements = [parse_sym(sym) for sym in t.split("-")] chemsyss = [] for cs in itertools.product(*elements): if len(set(cs)) == len(cs): # Check for valid symbols cs = [Element(s).symbol for s in cs] chemsyss.append("-".join(sorted(cs))) return {"chemsys": {"$in": chemsyss}} else: all_formulas = set() parts = re.split(r"(\*|\{.*\})", t) parts = [parse_sym(s) for s in parts] for f in itertools.product(*parts): if len(set(f)) == len(f): c = Composition("".join(f)) #Check for valid Elements in keys. for e in c.keys(): Element(e.symbol) all_formulas.add(c.reduced_formula) return {"pretty_formula": {"$in": list(all_formulas)}}
def setUp(self): self.comp = list() self.comp.append(Composition("Li3Fe2(PO4)3")) self.comp.append(Composition("Li3Fe(PO4)O")) self.comp.append(Composition("LiMn2O4")) self.comp.append(Composition("Li4O4")) self.comp.append(Composition("Li3Fe2Mo3O12")) self.comp.append(Composition("Li3Fe2((PO4)3(CO3)5)2")) self.comp.append(Composition("Li1.5Si0.5")) self.comp.append(Composition("ZnOH")) self.indeterminate_comp = [] self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("Co1", True)) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("Co1", False)) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("co2o3")) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("ncalu")) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("calun")) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("liCoo2n (pO4)2")) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("(co)2 (PO)4")) self.indeterminate_comp.append(Composition.ranked_compositions_from_indeterminate_formula("Fee3"))
def test_oxi_state_decoration(self): # Basic test: Get compositions where each element is in a single charge state decorated = Composition("H2O").add_charges_from_oxi_state_guesses() self.assertIn(Specie("H", 1), decorated) self.assertEqual(2, decorated.get(Specie("H", 1))) # Test: More than one charge state per element decorated = Composition("Fe3O4").add_charges_from_oxi_state_guesses() self.assertEqual(1, decorated.get(Specie("Fe", 2))) self.assertEqual(2, decorated.get(Specie("Fe", 3))) self.assertEqual(4, decorated.get(Specie("O", -2))) # Test: No possible charge states # It should return an uncharged composition decorated = Composition("NiAl").add_charges_from_oxi_state_guesses() self.assertEqual(1, decorated.get(Specie("Ni", 0))) self.assertEqual(1, decorated.get(Specie("Al", 0)))
def test_init_numerical_tolerance(self): self.assertEqual(Composition({'B': 1, 'C': -1e-12}), Composition('B'))
def setUp(self): self.comp = list() self.comp.append(Composition("Li3Fe2(PO4)3")) self.comp.append(Composition("Li3Fe(PO4)O")) self.comp.append(Composition("LiMn2O4")) self.comp.append(Composition("Li4O4")) self.comp.append(Composition("Li3Fe2Mo3O12")) self.comp.append(Composition("Li3Fe2((PO4)3(CO3)5)2")) self.comp.append(Composition("Li1.5Si0.5")) self.comp.append(Composition("ZnOH")) self.indeterminate_comp = [] self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "Co1", True)) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "Co1", False)) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "co2o3")) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "ncalu")) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "calun")) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "liCoo2n (pO4)2")) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula( "(co)2 (PO)4")) self.indeterminate_comp.append( Composition.ranked_compositions_from_indeterminate_formula("Fee3"))
class Site(collections.Mapping, collections.Hashable, MSONable): """ A generalized *non-periodic* site. This is essentially a composition at a point in space, with some optional properties associated with it. A Composition is used to represent the atoms and occupancy, which allows for disordered site representation. Coords are given in standard cartesian coordinates. """ position_atol = 1e-5 def __init__(self, atoms_n_occu, coords, properties=None): """ Create a *non-periodic* site. Args: atoms_n_occu: Species on the site. Can be: i. A sequence of element / specie specified either as string symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers, e.g., (3, 56, ...) or actual Element or Specie objects. ii. List of dict of elements/species and occupancies, e.g., [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of disordered structures. coords: Cartesian coordinates of site. properties: Properties associated with the site as a dict, e.g. {"magmom": 5}. Defaults to None. """ if isinstance(atoms_n_occu, collections.Mapping): self._species = Composition(atoms_n_occu) totaloccu = self._species.num_atoms if totaloccu > 1 + Composition.amount_tolerance: raise ValueError("Species occupancies sum to more than 1!") self._is_ordered = totaloccu == 1 and len(self._species) == 1 else: self._species = Composition({get_el_sp(atoms_n_occu): 1}) self._is_ordered = True self._coords = coords self._properties = properties if properties else {} @property def properties(self): """ Returns a view of properties as a dict. """ return {k: v for k, v in self._properties.items()} def __getattr__(self, a): # overriding getattr doens't play nice with pickle, so we # can't use self._properties p = object.__getattribute__(self, '_properties') if a in p: return p[a] raise AttributeError(a) def distance(self, other): """ Get distance between two sites. Args: other: Other site. Returns: Distance (float) """ return np.linalg.norm(other.coords - self.coords) def distance_from_point(self, pt): """ Returns distance between the site and a point in space. Args: pt: Cartesian coordinates of point. Returns: Distance (float) """ return np.linalg.norm(np.array(pt) - self._coords) @property def species_string(self): """ String representation of species on the site. """ if self._is_ordered: return list(self._species.keys())[0].__str__() else: sorted_species = sorted(self._species.keys()) return ", ".join([ "{}:{:.3f}".format(sp, self._species[sp]) for sp in sorted_species ]) @property def species_and_occu(self): """ The species at the site, i.e., a Composition mapping type of element/species to occupancy. """ return self._species @property def specie(self): """ The Specie/Element at the site. Only works for ordered sites. Otherwise an AttributeError is raised. Use this property sparingly. Robust design should make use of the property species_and_occu instead. Raises: AttributeError if Site is not ordered. """ if not self._is_ordered: raise AttributeError("specie property only works for ordered " "sites!") return list(self._species.keys())[0] @property def coords(self): """ A copy of the cartesian coordinates of the site as a numpy array. """ return np.copy(self._coords) @property def is_ordered(self): """ True if site is an ordered site, i.e., with a single species with occupancy 1. """ return self._is_ordered @property def x(self): """ Cartesian x coordinate """ return self._coords[0] @property def y(self): """ Cartesian y coordinate """ return self._coords[1] @property def z(self): """ Cartesian z coordinate """ return self._coords[2] def __getitem__(self, el): """ Get the occupancy for element """ return self._species[el] def __eq__(self, other): """ Site is equal to another site if the species and occupancies are the same, and the coordinates are the same to some tolerance. numpy function `allclose` is used to determine if coordinates are close. """ if other is None: return False return self._species == other._species and \ np.allclose(self._coords, other._coords, atol=Site.position_atol) and \ self._properties == other._properties def __ne__(self, other): return not self.__eq__(other) def __hash__(self): """ Minimally effective hash function that just distinguishes between Sites with different elements. """ return sum([el.Z for el in self._species.keys()]) def __contains__(self, el): return el in self._species def __len__(self): return len(self._species) def __iter__(self): return self._species.__iter__() def __repr__(self): return "Site: {} ({:.4f}, {:.4f}, {:.4f})".format( self.species_string, *self._coords) def __lt__(self, other): """ Sets a default sort order for atomic species by electronegativity. Very useful for getting correct formulas. For example, FeO4PLi is automatically sorted in LiFePO4. """ if self._species.average_electroneg < other._species.average_electroneg: return True if self._species.average_electroneg > other._species.average_electroneg: return False if self.species_string < other.species_string: return True if self.species_string > other.species_string: return False return False def __str__(self): return "{} {}".format(self._coords, self.species_string) def as_dict(self): """ Json-serializable dict representation for Site. """ species_list = [] for spec, occu in self._species.items(): d = spec.as_dict() del d["@module"] del d["@class"] d["occu"] = occu species_list.append(d) return { "name": self.species_string, "species": species_list, "xyz": [float(c) for c in self._coords], "properties": self._properties, "@module": self.__class__.__module__, "@class": self.__class__.__name__ } @classmethod def from_dict(cls, d): """ 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 = Specie.from_dict(sp_occu) elif "oxidation_state" in sp_occu: sp = DummySpecie.from_dict(sp_occu) else: sp = Element(sp_occu["element"]) atoms_n_occu[sp] = sp_occu["occu"] props = d.get("properties", None) return cls(atoms_n_occu, d["xyz"], properties=props)
def disordered_formula(disordered_struct, symbols=('x', 'y', 'z'), fmt='plain'): """ Returns a formula of a form like AxB1-x (x=0.5) for disordered structures. Will only return a formula for disordered structures with one kind of disordered site at present. Args: disordered_struct: a disordered structure symbols: a tuple of characters to use for subscripts, by default this is ('x', 'y', 'z') but if you have more than three disordered species more symbols will need to be added fmt (str): 'plain', 'HTML' or 'LaTeX' Returns (str): a disordered formula string """ # this is in string utils and not in # Composition because we need to have access # to site occupancies to calculate this, so # have to pass the full structure as an argument # (alternatively this could be made a method on # Structure) from pymatgen.core.composition import Composition from pymatgen.core.periodic_table import get_el_sp if disordered_struct.is_ordered: raise ValueError("Structure is not disordered, " "so disordered formula not defined.") disordered_site_compositions = {site.species_and_occu for site in disordered_struct if not site.is_ordered} if len(disordered_site_compositions) > 1: # this probably won't happen too often raise ValueError("Ambiguous how to define disordered " "formula when more than one type of disordered " "site is present.") disordered_site_composition = disordered_site_compositions.pop() disordered_species = {str(sp) for sp, occu in disordered_site_composition.items()} if len(disordered_species) > len(symbols): # this probably won't happen too often either raise ValueError("Not enough symbols to describe disordered composition: " "{}".format(symbols)) symbols = list(symbols)[0:len(disordered_species) - 1] comp = disordered_struct.composition.get_el_amt_dict().items() # sort by electronegativity, as per composition comp = sorted(comp, key=lambda x: get_el_sp(x[0]).X) disordered_comp = [] variable_map = {} total_disordered_occu = sum([occu for sp, occu in comp if str(sp) in disordered_species]) # composition to get common factor factor_comp = disordered_struct.composition.as_dict() factor_comp['X'] = total_disordered_occu for sp in disordered_species: del factor_comp[str(sp)] factor_comp = Composition.from_dict(factor_comp) factor = factor_comp.get_reduced_formula_and_factor()[1] total_disordered_occu /= factor remainder = "{}-{}".format(formula_double_format(total_disordered_occu, ignore_ones=False), '-'.join(symbols)) for sp, occu in comp: sp = str(sp) if sp not in disordered_species: disordered_comp.append((sp, formula_double_format(occu/factor))) else: if len(symbols) > 0: symbol = symbols.pop(0) disordered_comp.append((sp, symbol)) variable_map[symbol] = occu / total_disordered_occu / factor else: disordered_comp.append((sp, remainder)) if fmt == 'LaTeX': sub_start = "_{" sub_end = "}" elif fmt == 'HTML': sub_start = "<sub>" sub_end = "</sub>" elif fmt != 'plain': raise ValueError("Unsupported output format, " "choose from: LaTeX, HTML, plain") disordered_formula = [] for sp, occu in disordered_comp: disordered_formula.append(sp) if occu: # can be empty string if 1 if fmt != 'plain': disordered_formula.append(sub_start) disordered_formula.append(occu) if fmt != 'plain': disordered_formula.append(sub_end) disordered_formula.append(" ") disordered_formula += ["{}={} ".format(k, formula_double_format(v)) for k, v in variable_map.items()] comp = disordered_struct.composition return "".join(map(str, disordered_formula))[0:-1]
def test_to_from_dict(self): d = self.vinput.as_dict() vinput = VaspInput.from_dict(d) comp = vinput["POSCAR"].structure.composition self.assertEqual(comp, Composition("Fe4P4O16"))
def as_dict_summary(self, print_subelectrodes=True): """ Args: print_subelectrodes: Also print data on all the possible subelectrodes Returns: a summary of this electrode"s properties in dictionary format """ d = {} framework_comp = Composition({ k: v for k, v in self.initial_comp.items() if k.symbol != self.working_ion.symbol }) d["framework"] = framework_comp.to_data_dict d["framework_pretty"] = framework_comp.reduced_formula d["average_voltage"] = self.get_average_voltage() d["max_voltage"] = self.max_voltage d["min_voltage"] = self.min_voltage d["max_delta_volume"] = self.max_delta_volume d["max_instability"] = 0 d["max_voltage_step"] = self.max_voltage_step d["nsteps"] = self.num_steps d["capacity_grav"] = self.get_capacity_grav() d["capacity_vol"] = self.get_capacity_vol() d["energy_grav"] = self.get_specific_energy() d["energy_vol"] = self.get_energy_density() d["working_ion"] = self.working_ion.symbol d["reactions"] = [] d["reactant_compositions"] = [] comps = [] frac = [] for pair in self.voltage_pairs: rxn = pair.rxn frac.append(pair.frac_charge) frac.append(pair.frac_discharge) d["reactions"].append(str(rxn)) for i in range(len(rxn.coeffs)): if abs(rxn.coeffs[i]) > 1e-5 and rxn.all_comp[i] not in comps: comps.append(rxn.all_comp[i]) if abs(rxn.coeffs[i]) > 1e-5 and rxn.all_comp[ i].reduced_formula != d["working_ion"]: reduced_comp = rxn.all_comp[i].reduced_composition comp_dict = reduced_comp.as_dict() d["reactant_compositions"].append(comp_dict) d["fracA_charge"] = min(frac) d["fracA_discharge"] = max(frac) d["nsteps"] = self.num_steps if print_subelectrodes: def f_dict(c): return c.get_summary_dict(print_subelectrodes=False) d["adj_pairs"] = list( map(f_dict, self.get_sub_electrodes(adjacent_only=True))) d["all_pairs"] = list( map(f_dict, self.get_sub_electrodes(adjacent_only=False))) return d
def test_hill_formula(self): c = Composition("CaCO3") self.assertEqual(c.hill_formula, "C Ca O3") c = Composition("C2H5OH") self.assertEqual(c.hill_formula, "C2 H6 O")
def apply_transformation(self, structure, return_ranked_list=False): """ Apply the transformation. Args: structure: input structure return_ranked_list (bool/int): Boolean stating whether or not multiple structures are returned. If return_ranked_list is an int, that number of structures is returned. Returns: Depending on returned_ranked list, either a transformed structure or a list of dictionaries, where each dictionary is of the form {"structure" = .... , "other_arguments"} the key "transformation" is reserved for the transformation that was actually applied to the structure. This transformation is parsed by the alchemy classes for generating a more specific transformation history. Any other information will be stored in the transformation_parameters dictionary in the transmuted structure class. """ sp = get_el_sp(self.specie_to_remove) specie_indices = [i for i in range(len(structure)) if structure[i].species == Composition({sp: 1})] trans = PartialRemoveSitesTransformation([specie_indices], [self.fraction_to_remove], algo=self.algo) return trans.apply_transformation(structure, return_ranked_list)
def defect_composition(self): temp_comp = self.bulk_structure.composition.as_dict() temp_comp[str(self.site.specie)] += 1 return Composition(temp_comp)
def test_negative_compositions(self): self.assertEqual( Composition("Li-1(PO-1)4", allow_negative=True).formula, "Li-1 P4 O-4") self.assertEqual( Composition("Li-1(PO-1)4", allow_negative=True).reduced_formula, "Li-1(PO-1)4", ) self.assertEqual( Composition("Li-2Mg4", allow_negative=True).reduced_composition, Composition("Li-1Mg2", allow_negative=True), ) self.assertEqual( Composition("Li-2.5Mg4", allow_negative=True).reduced_composition, Composition("Li-2.5Mg4", allow_negative=True), ) # test math c1 = Composition("LiCl", allow_negative=True) c2 = Composition("Li") self.assertEqual(c1 - 2 * c2, Composition({ "Li": -1, "Cl": 1 }, allow_negative=True)) self.assertEqual((c1 + c2).allow_negative, True) self.assertEqual(c1 / -1, Composition("Li-1Cl-1", allow_negative=True)) # test num_atoms c1 = Composition("Mg-1Li", allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.get_atomic_fraction("Mg"), 0.5) self.assertEqual(c1.get_atomic_fraction("Li"), 0.5) self.assertEqual(c1.fractional_composition, Composition("Mg-0.5Li0.5", allow_negative=True)) # test copy self.assertEqual(c1.copy(), c1) # test species c1 = Composition({"Mg": 1, "Mg2+": -1}, allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.element_composition, Composition()) self.assertEqual(c1.average_electroneg, 1.31)
class MolecularOrbitals: """ Represents the character of bands in a solid. The input is a chemical formula, since no structural characteristics are taken into account. The band character of a crystal emerges from the atomic orbitals of the constituant ions, hybridization/covalent bonds, and the spin-orbit interaction (ex: Fe2O3). Right now the orbitals are only built from the uncharged atomic species. Functionality can be improved by: 1) calculate charged ion orbital energies 2) incorportate the coordination enviornment to account for covalant bonds The atomic orbital energies are stored in pymatgen.core.periodic_table.JSON >>> MOs = MolecularOrbitals('SrTiO3') >>> MOs.band_edges {'H**O':['O','2p',-0.338381], 'LUMO':['Ti','3d',-0.17001], 'metal':False} """ def __init__(self, formula): """ Args: chemical formula as a string. formula must have integer subscripts Ex: 'SrTiO3' Attributes: composition: the composition as a dictionary. Ex: {'Sr': 1, 'Ti': 1, 'O', 3} elements: the dictionary keys for the composition elec_neg: the maximum pairwise electronegetivity difference aos: the consituant atomic orbitals for each element as a dictionary band_edges: dictionary containing the highest occupied molecular orbital (H**O), lowest unocupied molecular orbital (LUMO), and whether the material is predicted to be a metal """ self.composition = Composition(formula).as_dict() self.elements = self.composition.keys() for subscript in self.composition.values(): if not float(subscript).is_integer(): raise ValueError('composition subscripts must be integers') self.elec_neg = self.max_electronegativity() self.aos = { str(el): [[str(el), k, v] for k, v in Element(el).atomic_orbitals.items()] for el in self.elements } self.band_edges = self.obtain_band_edges() def max_electronegativity(self): """ returns the maximum pairwise electronegativity difference """ maximum = 0 for e1, e2 in combinations(self.elements, 2): if abs(Element(e1).X - Element(e2).X) > maximum: maximum = abs(Element(e1).X - Element(e2).X) return maximum def aos_as_list(self): """ Returns a list of atomic orbitals, sorted from lowest to highest energy """ return sorted(chain.from_iterable([ self.aos[el] * int(self.composition[el]) for el in self.elements ]), key=lambda x: x[2]) def obtain_band_edges(self): """ Fill up the atomic orbitals with available electrons. Return H**O, LUMO, and whether it's a metal. """ orbitals = self.aos_as_list() electrons = Composition(self.composition).total_electrons partial_filled = [] for orbital in orbitals: if electrons <= 0: break if 's' in orbital[1]: electrons += -2 elif 'p' in orbital[1]: electrons += -6 elif 'd' in orbital[1]: electrons += -10 elif 'f' in orbital[1]: electrons += -14 partial_filled.append(orbital) if electrons != 0: h**o = partial_filled[-1] lumo = partial_filled[-1] else: h**o = partial_filled[-1] try: lumo = orbitals[len(partial_filled)] except Exception: lumo = None return {'H**O': h**o, 'LUMO': lumo, 'metal': h**o == lumo}
def initial_comp(self) -> Composition: """ The pymatgen Composition representation of the initial composition """ return Composition(self._initial_comp_formula)
def from_steps(cls, step1, step2, normalization_els, framework_formula=None): """ Creates a ConversionVoltagePair from two steps in the element profile from a PD analysis. Args: step1: Starting step step2: Ending step normalization_els: Elements to normalize the reaction by. To ensure correct capacities. """ working_ion_entry = step1["element_reference"] working_ion = working_ion_entry.composition.elements[0].symbol working_ion_valence = max(Element(working_ion).oxidation_states) voltage = (-step1["chempot"] + working_ion_entry.energy_per_atom) / working_ion_valence mAh = ((step2["evolution"] - step1["evolution"]) * Charge(1, "e").to("C") * Time(1, "s").to("h") * N_A * 1000 * working_ion_valence) licomp = Composition(working_ion) prev_rxn = step1["reaction"] reactants = { comp: abs(prev_rxn.get_coeff(comp)) for comp in prev_rxn.products if comp != licomp } curr_rxn = step2["reaction"] products = { comp: abs(curr_rxn.get_coeff(comp)) for comp in curr_rxn.products if comp != licomp } reactants[licomp] = step2["evolution"] - step1["evolution"] rxn = BalancedReaction(reactants, products) for el, amt in normalization_els.items(): if rxn.get_el_amount(el) > 1e-6: rxn.normalize_to_element(el, amt) break prev_mass_dischg = (sum([ prev_rxn.all_comp[i].weight * abs(prev_rxn.coeffs[i]) for i in range(len(prev_rxn.all_comp)) ]) / 2) vol_charge = sum([ abs(prev_rxn.get_coeff(e.composition)) * e.structure.volume for e in step1["entries"] if e.composition.reduced_formula != working_ion ]) mass_discharge = (sum([ curr_rxn.all_comp[i].weight * abs(curr_rxn.coeffs[i]) for i in range(len(curr_rxn.all_comp)) ]) / 2) mass_charge = prev_mass_dischg mass_discharge = mass_discharge vol_discharge = sum([ abs(curr_rxn.get_coeff(e.composition)) * e.structure.volume for e in step2["entries"] if e.composition.reduced_formula != working_ion ]) totalcomp = Composition({}) for comp in prev_rxn.products: if comp.reduced_formula != working_ion: totalcomp += comp * abs(prev_rxn.get_coeff(comp)) frac_charge = totalcomp.get_atomic_fraction(Element(working_ion)) totalcomp = Composition({}) for comp in curr_rxn.products: if comp.reduced_formula != working_ion: totalcomp += comp * abs(curr_rxn.get_coeff(comp)) frac_discharge = totalcomp.get_atomic_fraction(Element(working_ion)) rxn = rxn entries_charge = step2["entries"] entries_discharge = step1["entries"] return cls( rxn=rxn, voltage=voltage, mAh=mAh, vol_charge=vol_charge, vol_discharge=vol_discharge, mass_charge=mass_charge, mass_discharge=mass_discharge, frac_charge=frac_charge, frac_discharge=frac_discharge, entries_charge=entries_charge, entries_discharge=entries_discharge, working_ion_entry=working_ion_entry, _framework_formula=framework_formula, )
def test_negative_compositions(self): self.assertEqual( Composition('Li-1(PO-1)4', allow_negative=True).formula, 'Li-1 P4 O-4') self.assertEqual( Composition('Li-1(PO-1)4', allow_negative=True).reduced_formula, 'Li-1(PO-1)4') self.assertEqual( Composition('Li-2Mg4', allow_negative=True).reduced_composition, Composition('Li-1Mg2', allow_negative=True)) self.assertEqual( Composition('Li-2.5Mg4', allow_negative=True).reduced_composition, Composition('Li-2.5Mg4', allow_negative=True)) #test math c1 = Composition('LiCl', allow_negative=True) c2 = Composition('Li') self.assertEqual(c1 - 2 * c2, Composition({ 'Li': -1, 'Cl': 1 }, allow_negative=True)) self.assertEqual((c1 + c2).allow_negative, True) self.assertEqual(c1 / -1, Composition('Li-1Cl-1', allow_negative=True)) #test num_atoms c1 = Composition('Mg-1Li', allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.get_atomic_fraction('Mg'), 0.5) self.assertEqual(c1.get_atomic_fraction('Li'), 0.5) self.assertEqual(c1.fractional_composition, Composition('Mg-0.5Li0.5', allow_negative=True)) #test copy self.assertEqual(c1.copy(), c1) #test species c1 = Composition({'Mg': 1, 'Mg2+': -1}, allow_negative=True) self.assertEqual(c1.num_atoms, 2) self.assertEqual(c1.element_composition, Composition()) self.assertEqual(c1.average_electroneg, 1.31)
def test_oxi_state_guesses(self): self.assertEqual( Composition("LiFeO2").oxi_state_guesses(), [{ "Li": 1, "Fe": 3, "O": -2 }]) self.assertEqual( Composition("Fe4O5").oxi_state_guesses(), [{ "Fe": 2.5, "O": -2 }]) # because pymatgen common oxidation states doesn't include V:3+, # this doesn't work self.assertEqual(Composition("V2O3").oxi_state_guesses(), []) # all_oxidation_states produces *many* possible responses self.assertEqual( len(Composition("MnO").oxi_state_guesses(all_oxi_states=True)), 4) self.assertEqual( Composition("V2O3").oxi_state_guesses( oxi_states_override={"V": [2, 3, 4, 5]}), [{ "V": 3, "O": -2 }]) # can't balance b/c missing V4+ self.assertEqual( Composition("VO2").oxi_state_guesses( oxi_states_override={"V": [2, 3, 5]}), []) # missing V4+, but can balance due to additional sites self.assertEqual( Composition("V2O4").oxi_state_guesses( oxi_states_override={"V": [2, 3, 5]}), [{ "V": 4, "O": -2 }]) # multiple solutions - Mn/Fe = 2+/4+ or 3+/3+ or 4+/2+ self.assertEqual( len( Composition("MnFeO3").oxi_state_guesses(oxi_states_override={ "Mn": [2, 3, 4], "Fe": [2, 3, 4] })), 3) # multiple solutions prefers 3/3 over 2/4 or 4/2 self.assertEqual( Composition("MnFeO3").oxi_state_guesses(oxi_states_override={ "Mn": [2, 3, 4], "Fe": [2, 3, 4] })[0], { "Mn": 3, "Fe": 3, "O": -2 }) # target charge of 1 self.assertEqual( Composition("V2O6").oxi_state_guesses( oxi_states_override={"V": [2, 3, 4, 5]}, target_charge=-2), [{ "V": 5, "O": -2 }])
def biased_hull(atomate_db, comp_list, anions=['N', 'O'], bias=[0]): with MPRester() as mpr: for pretty in comp_list: composition = Composition(pretty) composition = [str(i) for i in composition.elements] # anion_num = composition[2] # composition.pop() # composition.append(anions[0]) # composition.append(anions[1]) #First, build the phase diagram and hull orig_entries = mpr.get_entries_in_chemsys(composition) #orig_entries = mpr.get_entries_in_chemsys(chemsys_list[k]) entries = [] for i in range(len(bias)): entries.append(copy.deepcopy(orig_entries)) for j in range(0, len(entries[i])): temp = entries[i][j].parameters['potcar_symbols'] if temp in [['PBE ' + anions[0]], ['PBE ' + anions[1]], ['PBE ' + anions[0], 'PBE ' + anions[1]], ['PBE ' + anions[1], 'PBE ' + anions[0]]]: new_entry = ComputedEntry( entries[i][j].composition, entries[i][j].energy + bias[i]) #add arbitrary energy to gas phase entries[i][j] = copy.deepcopy(new_entry) #Then, find each entry in atomate_db which has this composition and get its hull energy print(pretty) structures = [] cursor = atomate_db.collection.find({ 'task_label': 'static', 'formula_pretty': pretty }) for structure in cursor: structures.append(structure) struct_entries = [] for structure in structures: temp = structure['calcs_reversed'][0] struct_entry = ComputedEntry( temp['composition_unit_cell'], temp['output']['energy'], parameters={ 'run_type': temp['run_type'], 'is_hubbard': structure['input']['is_hubbard'], 'pseudo_potential': structure['input']['pseudo_potential'], 'hubbards': structure['input']['hubbards'], 'potcar_symbols': structure['orig_inputs']['potcar']['symbols'], 'oxide_type': 'oxide' }, data={'oxide_type': 'oxide'}) for i in range(0, 4): struct_entry.parameters['potcar_symbols'][ i] = 'PBE ' + struct_entry.parameters[ 'potcar_symbols'][i] struct_entry = MaterialsProjectCompatibility().process_entries( [struct_entry ])[0] #takes list as argument and returns list struct_entries.append(struct_entry) bias_strings = [] stable_polymorph = {'id': 0, 'tilt_order': ''} for i in range(len(bias)): entries[i].extend(struct_entries) pd = PhaseDiagram(entries[i]) bias_string = 'ehull_' + str(bias[i]) + 'eV' bias_strings.append(bias_string) stable_polymorph[bias_strings[i]] = 1000 print(bias_strings) for j in range(0, len(struct_entries)): stability = pd.get_decomp_and_e_above_hull( struct_entries[j]) print(structures[j]['formula_pretty'], structures[j]['task_id'], [phase.composition for phase in stability[0]], stability[1]) if stability[1] < stable_polymorph[bias_strings[i]]: stable_polymorph['id'] = structures[j]['task_id'] stable_polymorph[bias_strings[i]] = stability[1] if 'tags' in structures[j]: if structures[j]['tags'][1] == 'tetra': stable_polymorph['tilt_order'] = structures[j][ 'tags'][2] else: stable_polymorph['tilt_order'] = structures[j][ 'tags'][1] output_dict[structures[j] ['formula_pretty']] = stable_polymorph return output_dict
def from_entries(cls, entry1, entry2, working_ion_entry): """ Args: entry1: Entry corresponding to one of the entries in the voltage step. entry2: Entry corresponding to the other entry in the voltage step. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. """ # initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction( working_element) > entry2.composition.get_atomic_fraction( working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({ el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym }) frame_discharge_comp = Composition({ el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym }) # Data validation # check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") # check that at least one of the entries contains the working element if (not comp_charge.get_atomic_fraction(working_element) > 0 and not comp_discharge.get_atomic_fraction(working_element) > 0): raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") # check that the entries do not contain the same amount of the workin # element if comp_charge.get_atomic_fraction( working_element) == comp_discharge.get_atomic_fraction( working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") # check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") # Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = abs(max(valence_list)) ( framework, norm_charge, ) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = frame_discharge_comp.get_reduced_composition_and_factor( )[1] # Initialize normalized properties if hasattr(entry_charge, "structure"): _vol_charge = entry_charge.structure.volume / norm_charge else: _vol_charge = entry_charge.data.get("volume") if hasattr(entry_discharge, "structure"): _vol_discharge = entry_discharge.structure.volume / norm_discharge else: _vol_discharge = entry_discharge.data.get("volume") comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition _mass_charge = comp_charge.weight / norm_charge _mass_discharge = comp_discharge.weight / norm_discharge _num_ions_transferred = ( comp_discharge[working_element] / norm_discharge) - (comp_charge[working_element] / norm_charge) _voltage = ( ((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / _num_ions_transferred + working_ion_entry.energy_per_atom) / working_ion_valence _mAh = _num_ions_transferred * Charge(1, "e").to("C") * Time( 1, "s").to("h") * N_A * 1000 * working_ion_valence _frac_charge = comp_charge.get_atomic_fraction(working_element) _frac_discharge = comp_discharge.get_atomic_fraction(working_element) vpair = cls( voltage=_voltage, mAh=_mAh, mass_charge=_mass_charge, mass_discharge=_mass_discharge, vol_charge=_vol_charge, vol_discharge=_vol_discharge, frac_charge=_frac_charge, frac_discharge=_frac_discharge, working_ion_entry=working_ion_entry, entry_charge=entry_charge, entry_discharge=entry_discharge, _framework_formula=framework.reduced_formula, ) # Step 4: add (optional) hull and muO2 data vpair.decomp_e_charge = entry_charge.data.get("decomposition_energy", None) vpair.decomp_e_discharge = entry_discharge.data.get( "decomposition_energy", None) vpair.muO2_charge = entry_charge.data.get("muO2", None) vpair.muO2_discharge = entry_discharge.data.get("muO2", None) return vpair
def from_dict(cls, d): dec = MontyDecoder() return cls(dec.process_decoded(d["voltage_pairs"]), dec.process_decoded(d["working_ion_entry"]), Composition(d["initial_comp"]))
def apply_transformation(self, structure, return_ranked_list=False): """ For this transformation, the apply_transformation method will return only the ordered structure with the lowest Ewald energy, to be consistent with the method signature of the other transformations. However, all structures are stored in the all_structures attribute in the transformation object for easy access. Args: structure: Oxidation state decorated disordered structure to order return_ranked_list (bool): Whether or not multiple structures are returned. If return_ranked_list is a number, that number of structures is returned. Returns: Depending on returned_ranked list, either a transformed structure or a list of dictionaries, where each dictionary is of the form {"structure" = .... , "other_arguments"} the key "transformation" is reserved for the transformation that was actually applied to the structure. This transformation is parsed by the alchemy classes for generating a more specific transformation history. Any other information will be stored in the transformation_parameters dictionary in the transmuted structure class. """ try: num_to_return = int(return_ranked_list) except ValueError: num_to_return = 1 num_to_return = max(1, num_to_return) if self.no_oxi_states: structure = Structure.from_sites(structure) for i, site in enumerate(structure): structure[i] = {"%s0+" % k.symbol: v for k, v in site.species.items()} equivalent_sites = [] exemplars = [] # generate list of equivalent sites to order # equivalency is determined by sp_and_occu and symmetry # if symmetrized structure is true for i, site in enumerate(structure): if site.is_ordered: continue for j, ex in enumerate(exemplars): sp = ex.species if not site.species.almost_equals(sp): continue if self.symmetrized_structures: sym_equiv = structure.find_equivalent_sites(ex) sym_test = site in sym_equiv else: sym_test = True if sym_test: equivalent_sites[j].append(i) break else: equivalent_sites.append([i]) exemplars.append(site) # generate the list of manipulations and input structure s = Structure.from_sites(structure) m_list = [] for g in equivalent_sites: total_occupancy = sum((structure[i].species for i in g), Composition()) total_occupancy = dict(total_occupancy.items()) # round total occupancy to possible values for k, v in total_occupancy.items(): if abs(v - round(v)) > 0.25: raise ValueError("Occupancy fractions not consistent with size of unit cell") total_occupancy[k] = int(round(v)) # start with an ordered structure initial_sp = max(total_occupancy.keys(), key=lambda x: abs(x.oxi_state)) for i in g: s[i] = initial_sp # determine the manipulations for k, v in total_occupancy.items(): if k == initial_sp: continue m = [ k.oxi_state / initial_sp.oxi_state if initial_sp.oxi_state else 0, v, list(g), k, ] m_list.append(m) # determine the number of empty sites empty = len(g) - sum(total_occupancy.values()) if empty > 0.5: m_list.append([0, empty, list(g), None]) matrix = EwaldSummation(s).total_energy_matrix ewald_m = EwaldMinimizer(matrix, m_list, num_to_return, self.algo) self._all_structures = [] lowest_energy = ewald_m.output_lists[0][0] num_atoms = sum(structure.composition.values()) for output in ewald_m.output_lists: s_copy = s.copy() # do deletions afterwards because they screw up the indices of the # structure del_indices = [] for manipulation in output[1]: if manipulation[1] is None: del_indices.append(manipulation[0]) else: s_copy[manipulation[0]] = manipulation[1] s_copy.remove_sites(del_indices) if self.no_oxi_states: s_copy.remove_oxidation_states() self._all_structures.append( { "energy": output[0], "energy_above_minimum": (output[0] - lowest_energy) / num_atoms, "structure": s_copy.get_sorted_structure(), } ) if return_ranked_list: return self._all_structures[:num_to_return] return self._all_structures[0]["structure"]
def test_oxi_state_guesses(self): self.assertEqual( Composition("LiFeO2").oxi_state_guesses(), ({ "Li": 1, "Fe": 3, "O": -2 }, )) self.assertEqual( Composition("Fe4O5").oxi_state_guesses(), ({ "Fe": 2.5, "O": -2 }, )) self.assertEqual( Composition("V2O3").oxi_state_guesses(), ({ "V": 3, "O": -2 }, )) # all_oxidation_states produces *many* possible responses self.assertEqual( len(Composition("MnO").oxi_state_guesses(all_oxi_states=True)), 4) # can't balance b/c missing V4+ self.assertEqual( Composition("VO2").oxi_state_guesses( oxi_states_override={"V": [2, 3, 5]}), [], ) # missing V4+, but can balance due to additional sites self.assertEqual( Composition("V2O4").oxi_state_guesses( oxi_states_override={"V": [2, 3, 5]}), ({ "V": 4, "O": -2 }, ), ) # multiple solutions - Mn/Fe = 2+/4+ or 3+/3+ or 4+/2+ self.assertEqual( len( Composition("MnFeO3").oxi_state_guesses(oxi_states_override={ "Mn": [2, 3, 4], "Fe": [2, 3, 4] })), 3, ) # multiple solutions prefers 3/3 over 2/4 or 4/2 self.assertEqual( Composition("MnFeO3").oxi_state_guesses(oxi_states_override={ "Mn": [2, 3, 4], "Fe": [2, 3, 4] })[0], { "Mn": 3, "Fe": 3, "O": -2 }, ) # target charge of 1 self.assertEqual( Composition("V2O6").oxi_state_guesses( oxi_states_override={"V": [2, 3, 4, 5]}, target_charge=-2), ({ "V": 5, "O": -2 }, ), ) # max_sites for very large composition - should timeout if incorrect self.assertEqual( Composition("Li10000Fe10000P10000O40000").oxi_state_guesses( max_sites=7)[0], { "Li": 1, "Fe": 2, "P": 5, "O": -2 }, ) # max_sites for very large composition - should timeout if incorrect self.assertEqual( Composition("Li10000Fe10000P10000O40000").oxi_state_guesses( max_sites=-1)[0], { "Li": 1, "Fe": 2, "P": 5, "O": -2 }, ) # negative max_sites less than -1 - should throw error if cannot reduce # to under the abs(max_sites) number of sites. Will also timeout if # incorrect. self.assertEqual( Composition("Sb10000O10000F10000").oxi_state_guesses( max_sites=-3)[0], { "Sb": 3, "O": -2, "F": -1 }, ) self.assertRaises(ValueError, Composition("LiOF").oxi_state_guesses, max_sites=-2) self.assertRaises(ValueError, Composition("V2O3").oxi_state_guesses, max_sites=1)
def test_chemical_system(self): formula = "NaCl" cmp = Composition(formula) self.assertEqual(cmp.chemical_system, "Cl-Na")
def test_getmu_range_stability_phase(self): results = self.analyzer.getmu_range_stability_phase(Composition.from_formula("LiFeO2"),Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007)
def from_entries( cls, entries: Iterable[Union[ComputedEntry, ComputedStructureEntry]], working_ion_entry: Union[ComputedEntry, ComputedStructureEntry, PDEntry], strip_structures: bool = False, ): """ Create a new InsertionElectrode. Args: entries: A list of ComputedEntries, ComputedStructureEntries, or subclasses representing the different topotactic states of the battery, e.g. TiO2 and LiTiO2. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. strip_structures: Since the electrode document only uses volume we can make the electrode object significantly leaner by dropping the structure data. If this parameter is set to True, the ComputedStructureEntry will be replaced with a ComputedEntry and the volume will be stored in ComputedEntry.data['volume']. If entries provided are ComputedEntries, must set strip_structures=False. """ if strip_structures: ents = [] for ient in entries: dd = ient.as_dict() ent = ComputedEntry.from_dict(dd) ent.data["volume"] = ient.structure.volume ents.append(ent) entries = ents _working_ion = working_ion_entry.composition.elements[0] _working_ion_entry = working_ion_entry # Prepare to make phase diagram: determine elements and set their energy # to be very high elements = set() for entry in entries: elements.update(entry.composition.elements) # Set an artificial high energy for each element for convex hull generation element_energy = max(entry.energy_per_atom for entry in entries) + 10 pdentries: List[Union[ComputedEntry, ComputedStructureEntry, PDEntry]] = [] pdentries.extend(entries) pdentries.extend( [PDEntry(Composition({el: 1}), element_energy) for el in elements]) # Make phase diagram to determine which entries are stable vs. unstable. # For each working ion concentration, we want one stable entry # to use in forming voltage pairs. PhaseDiagram allows for easy comparison # of entry energies. pd = PhaseDiagram(pdentries) def lifrac(e): return e.composition.get_atomic_fraction(_working_ion) # stable entries ordered by amount of Li asc _stable_entries = tuple( sorted((e for e in pd.stable_entries if e in entries), key=lifrac)) # unstable entries ordered by amount of Li asc _unstable_entries = tuple( sorted((e for e in pd.unstable_entries if e in entries), key=lifrac)) # create voltage pairs _vpairs: Tuple[AbstractVoltagePair] = tuple( # type: ignore InsertionVoltagePair.from_entries( _stable_entries[i], _stable_entries[i + 1], working_ion_entry, ) for i in range(len(_stable_entries) - 1)) framework = _vpairs[0].framework return cls( # pylint: disable=E1123 voltage_pairs=_vpairs, working_ion_entry=_working_ion_entry, stable_entries=_stable_entries, unstable_entries=_unstable_entries, framework_formula=framework.reduced_formula, )
def get_pd_species_list(self, composition_space, constraints, random): """ Returns a list containing the species in the random organism. Precondition: the composition space contains multiple endpoints (it's a fixed-composition search) Args: composition_space: the CompositionSpace of the search constraints: the Constraints of the search random: a copy of Python's built in PRNG Description: 1. Gets a random fraction of each composition space endpoint such that the fractions sum to 1. 2. Computes the fraction of each specie from the fraction of each endpoint and the amount of each specie within each endpoint. 3. Approximates the fraction of each specie as a rational number with a maximum possible denominator of self.max_num_atoms. 4. Takes the product of the denominators of all the species' rational fractions, and then multiplies each specie's rational fraction by this product to obtain the number of atoms of that species. 5. Checks if the total number of atoms exceeds self.max_num_atoms. If so, reduce the amount of each atom with a multiplicative factor. 6. Reduces the resulting composition (i.e., find the smallest number of atoms needed to describe the composition). 7. Optionally increases the number of atoms (w/o changing the composition) such that the min num atoms constraint is satisfied if possible. 8. Checks that the resulting number of atoms satisfies the maximum (self.max_num_atoms) number of atoms constraint, and optionally checks that the resulting composition is not equivalent to one of the endpoint compositions. """ # get random fractions for each endpoint that sum to one 1 (i.e., a # random location in the composition space fracs = self.get_random_endpoint_fractions(composition_space, random) composition_space.endpoints.sort() endpoint_fracs = {} for i in range(len(fracs)): endpoint_fracs[composition_space.endpoints[i]] = fracs[i] # compute amount of each element from amount of each endpoint all_elements = composition_space.get_all_elements() element_amounts = {} for element in all_elements: element_amounts[element] = 0 for formula in endpoint_fracs: for element in formula: element_amounts[ element] += endpoint_fracs[formula] * formula[element] # normalize the amounts of the elements amounts_sum = 0 for element in element_amounts: amounts_sum += element_amounts[element] for element in element_amounts: element_amounts[element] = element_amounts[element] / amounts_sum # approximate the decimal amount of each element as a fraction # (rational number) rational_amounts = {} for element in element_amounts: rational_amounts[element] = Fraction( element_amounts[element]).limit_denominator(self.max_num_atoms) # multiply the denominators together, then multiply each fraction # by this result to get the number of atoms of each element denom_product = 1.0 for element in rational_amounts: denom_product *= rational_amounts[element].denominator for element in rational_amounts: element_amounts[element] = round( float(denom_product) * rational_amounts[element]) # see how many total atoms we have num_atoms = 0 for element in element_amounts: num_atoms += element_amounts[element] # reduce the number of atoms of each element if needed if num_atoms > self.max_num_atoms: numerator = random.randint( int( round(0.5 * (constraints.min_num_atoms + self.max_num_atoms))), self.max_num_atoms) factor = numerator / num_atoms for element in element_amounts: element_amounts[element] = round(factor * element_amounts[element]) # make a Composition object from the amounts of each element random_composition = Composition(element_amounts) random_composition = random_composition.reduced_composition # possibly increase the number of atoms by a random (allowed) amount min_multiple = int( math.ceil(constraints.min_num_atoms / random_composition.num_atoms)) max_multiple = int( math.floor(self.max_num_atoms / random_composition.num_atoms)) if max_multiple > min_multiple: random_multiple = random.randint(min_multiple, max_multiple) bigger_composition = {} for element in random_composition: bigger_composition[element] = \ random_multiple*random_composition[element] random_composition = Composition(bigger_composition) # check the max number of atoms constraints (should be ok) if int(random_composition.num_atoms) > self.max_num_atoms: return None # check the composition - only allow endpoints if specified if not self.allow_endpoints: for endpoint in composition_space.endpoints: if endpoint.almost_equals( random_composition.reduced_composition): return None # save the element objects species = [] for specie in random_composition: for _ in range(int(random_composition[specie])): species.append(specie) return species
def generate_doc(self, dir_name, vasprun_files, outcar_files): """ Adapted from matgendb.creator.generate_doc """ try: # basic properties, incl. calcs_reversed and run_stats fullpath = os.path.abspath(dir_name) d = jsanitize(self.additional_fields, strict=True) d["schema"] = {"code": "atomate", "version": VaspDrone.__version__} d["dir_name"] = fullpath d["calcs_reversed"] = [ self.process_vasprun(dir_name, taskname, filename) for taskname, filename in vasprun_files.items() ] outcar_data = [ Outcar(os.path.join(dir_name, filename)).as_dict() for taskname, filename in outcar_files.items() ] run_stats = {} for i, d_calc in enumerate(d["calcs_reversed"]): run_stats[d_calc["task"]["name"]] = outcar_data[i].pop( "run_stats") if d_calc.get("output"): d_calc["output"].update({"outcar": outcar_data[i]}) else: d_calc["output"] = {"outcar": outcar_data[i]} try: overall_run_stats = {} for key in [ "Total CPU time used (sec)", "User time (sec)", "System time (sec)", "Elapsed time (sec)" ]: overall_run_stats[key] = sum( [v[key] for v in run_stats.values()]) run_stats["overall"] = overall_run_stats except: logger.error("Bad run stats for {}.".format(fullpath)) d["run_stats"] = run_stats # reverse the calculations data order so newest calc is first d["calcs_reversed"].reverse() # set root formula/composition keys based on initial and final calcs d_calc_init = d["calcs_reversed"][-1] d_calc_final = d["calcs_reversed"][0] d["chemsys"] = "-".join(sorted(d_calc_final["elements"])) comp = Composition(d_calc_final["composition_unit_cell"]) d["formula_anonymous"] = comp.anonymized_formula d["formula_reduced_abc"] = comp.reduced_composition.alphabetical_formula for root_key in [ "completed_at", "nsites", "composition_unit_cell", "composition_reduced", "formula_pretty", "elements", "nelements" ]: d[root_key] = d_calc_final[root_key] # store the input key based on initial calc # store any overrides to the exchange correlation functional xc = d_calc_init["input"]["incar"].get("GGA") if xc: xc = xc.upper() p = d_calc_init["input"]["potcar_type"][0].split("_") pot_type = p[0] functional = "lda" if len(pot_type) == 1 else "_".join(p[1:]) d["input"] = { "structure": d_calc_init["input"]["structure"], "is_hubbard": d_calc_init.pop("is_hubbard"), "hubbards": d_calc_init.pop("hubbards"), "is_lasph": d_calc_init["input"]["incar"].get("LASPH", False), "potcar_spec": d_calc_init["input"].get("potcar_spec"), "xc_override": xc, "pseudo_potential": { "functional": functional.lower(), "pot_type": pot_type.lower(), "labels": d_calc_init["input"]["potcar"] }, "parameters": d_calc_init["input"]["parameters"], "incar": d_calc_init["input"]["incar"] } # store the output key based on final calc d["output"] = { "structure": d_calc_final["output"]["structure"], "density": d_calc_final.pop("density"), "energy": d_calc_final["output"]["energy"], "energy_per_atom": d_calc_final["output"]["energy_per_atom"], "forces": d_calc_final["output"]["ionic_steps"][-1].get("forces"), "stress": d_calc_final["output"]["ionic_steps"][-1].get("stress") } # patch calculated magnetic moments into final structure if len(d_calc_final["output"]["outcar"]["magnetization"]) != 0: magmoms = [ m["tot"] for m in d_calc_final["output"]["outcar"]["magnetization"] ] s = Structure.from_dict(d["output"]["structure"]) s.add_site_property('magmom', magmoms) d["output"]["structure"] = s.as_dict() calc = d["calcs_reversed"][0] # copy band gap and properties into output d["output"].update({ "bandgap": calc["output"]["bandgap"], "cbm": calc["output"]["cbm"], "vbm": calc["output"]["vbm"], "is_gap_direct": calc["output"]["is_gap_direct"] }) try: d["output"].update({"is_metal": calc["output"]["is_metal"]}) if not calc["output"]["is_gap_direct"]: d["output"]["direct_gap"] = calc["output"]["direct_gap"] if "transition" in calc["output"]: d["output"]["transition"] = calc["output"]["transition"] except Exception: if self.bandstructure_mode is True: logger.error(traceback.format_exc()) logger.error("Error in " + os.path.abspath(dir_name) + ".\n" + traceback.format_exc()) raise # Store symmetry information sg = SpacegroupAnalyzer( Structure.from_dict(d_calc_final["output"]["structure"]), 0.1) if not sg.get_symmetry_dataset(): sg = SpacegroupAnalyzer( Structure.from_dict(d_calc_final["output"]["structure"]), 1e-3, 1) d["output"]["spacegroup"] = { "source": "spglib", "symbol": sg.get_space_group_symbol(), "number": sg.get_space_group_number(), "point_group": sg.get_point_group_symbol(), "crystal_system": sg.get_crystal_system(), "hall": sg.get_hall() } # store dieelctric and piezo information if d["input"]["parameters"].get("LEPSILON"): for k in [ 'epsilon_static', 'epsilon_static_wolfe', 'epsilon_ionic' ]: d["output"][k] = d_calc_final["output"][k] if SymmOp.inversion() not in sg.get_symmetry_operations(): for k in ["piezo_ionic_tensor", "piezo_tensor"]: d["output"][k] = d_calc_final["output"]["outcar"][k] d["state"] = "successful" if d_calc[ "has_vasp_completed"] else "unsuccessful" self.set_analysis(d) d["last_updated"] = datetime.datetime.utcnow() return d except Exception: logger.error(traceback.format_exc()) logger.error("Error in " + os.path.abspath(dir_name) + ".\n" + traceback.format_exc()) raise
def process_vasprun(self, dir_name, taskname, filename): """ Adapted from matgendb.creator Process a vasprun.xml file. """ vasprun_file = os.path.join(dir_name, filename) vrun = Vasprun(vasprun_file, parse_potcar_file=self.parse_potcar_file) d = vrun.as_dict() # rename formula keys for k, v in { "formula_pretty": "pretty_formula", "composition_reduced": "reduced_cell_formula", "composition_unit_cell": "unit_cell_formula" }.items(): d[k] = d.pop(v) for k in ["eigenvalues", "projected_eigenvalues" ]: # large storage space breaks some docs if k in d["output"]: del d["output"][k] comp = Composition(d["composition_unit_cell"]) d["formula_anonymous"] = comp.anonymized_formula d["formula_reduced_abc"] = comp.reduced_composition.alphabetical_formula d["dir_name"] = os.path.abspath(dir_name) d["completed_at"] = str( datetime.datetime.fromtimestamp(os.path.getmtime(vasprun_file))) d["density"] = vrun.final_structure.density # replace 'crystal' with 'structure' d["input"]["structure"] = d["input"].pop("crystal") d["output"]["structure"] = d["output"].pop("crystal") for k, v in { "energy": "final_energy", "energy_per_atom": "final_energy_per_atom" }.items(): d["output"][k] = d["output"].pop(v) # Process bandstructure and DOS if self.bandstructure_mode != False: bs = self.process_bandstructure(vrun) if bs: d["bandstructure"] = bs if self.parse_dos != False: dos = self.process_dos(vrun) if dos: d["dos"] = dos # Parse electronic information if possible. # For certain optimizers this is broken and we don't get an efermi resulting in the bandstructure try: bs = vrun.get_band_structure() bs_gap = bs.get_band_gap() d["output"]["vbm"] = bs.get_vbm()["energy"] d["output"]["cbm"] = bs.get_cbm()["energy"] d["output"]["bandgap"] = bs_gap["energy"] d["output"]["is_gap_direct"] = bs_gap["direct"] d["output"]["is_metal"] = bs.is_metal() if not bs_gap["direct"]: d["output"]["direct_gap"] = bs.get_direct_band_gap() if isinstance(bs, BandStructureSymmLine): d["output"]["transition"] = bs_gap["transition"] except Exception: logger.warning("Error in parsing bandstructure") if vrun.incar["IBRION"] == 1: logger.warning( "Vasp doesn't properly output efermi for IBRION == 1") if self.bandstructure_mode is True: logger.error(traceback.format_exc()) logger.error("Error in " + os.path.abspath(dir_name) + ".\n" + traceback.format_exc()) raise # Should roughly agree with information from .get_band_structure() above, subject to tolerances # If there is disagreement, it may be related to VASP incorrectly assigning the Fermi level try: band_props = vrun.eigenvalue_band_properties d["output"]["eigenvalue_band_properties"] = { "bandgap": band_props[0], "cbm": band_props[1], "vbm": band_props[2], "is_gap_direct": band_props[3] } except Exception: logger.warning("Error in parsing eigenvalue band properties") # store run name and location ,e.g. relax1, relax2, etc. d["task"] = {"type": taskname, "name": taskname} # include output file names d["output_file_paths"] = self.process_raw_data(dir_name, taskname=taskname) # parse axially averaged locpot if "locpot" in d["output_file_paths"] and self.parse_locpot: locpot = Locpot.from_file( os.path.join(dir_name, d["output_file_paths"]["locpot"])) d["output"]["locpot"] = { i: locpot.get_average_along_axis(i) for i in range(3) } if self.store_volumetric_data: for file in self.store_volumetric_data: if file in d["output_file_paths"]: try: # assume volumetric data is all in CHGCAR format data = Chgcar.from_file( os.path.join(dir_name, d["output_file_paths"][file])) d[file] = data.as_dict() except: raise ValueError("Failed to parse {} at {}.".format( file, d["output_file_paths"][file])) # parse force constants if hasattr(vrun, "force_constants"): d["output"]["force_constants"] = vrun.force_constants.tolist() d["output"][ "normalmode_eigenvals"] = vrun.normalmode_eigenvals.tolist() d["output"][ "normalmode_eigenvecs"] = vrun.normalmode_eigenvecs.tolist() # perform Bader analysis using Henkelman bader if self.parse_bader and "chgcar" in d["output_file_paths"]: suffix = '' if taskname == 'standard' else ".{}".format(taskname) bader = bader_analysis_from_path(dir_name, suffix=suffix) d["bader"] = bader return d
def __init__(self, entry1, entry2, working_ion_entry): #initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) \ > entry2.composition.get_atomic_fraction(working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym}) frame_discharge_comp = Composition({el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym}) #Data validation #check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") #check that at least one of the entries contains the working element if not comp_charge.get_atomic_fraction(working_element) > 0 and \ not comp_discharge.get_atomic_fraction(working_element) > 0: raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") #check that the entries do not contain the same amount of the workin #element if comp_charge.get_atomic_fraction(working_element) == \ comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") #check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == \ frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") #Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = max(valence_list) (self.framework, norm_charge) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = \ frame_discharge_comp.get_reduced_composition_and_factor()[1] self._working_ion_entry = working_ion_entry #Initialize normalized properties self._vol_charge = entry_charge.structure.volume / norm_charge self._vol_discharge = entry_discharge.structure.volume / norm_discharge comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition self._mass_charge = comp_charge.weight / norm_charge self._mass_discharge = comp_discharge.weight / norm_discharge self._num_ions_transferred = \ (comp_discharge[working_element] / norm_discharge) \ - (comp_charge[working_element] / norm_charge) self._voltage = \ (((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / \ self._num_ions_transferred + working_ion_entry.energy_per_atom) / working_ion_valence self._mAh = self._num_ions_transferred * Charge(1, "e").to("C") * \ Time(1, "s").to("h") * AVOGADROS_CONST * 1000 * working_ion_valence #Step 4: add (optional) hull and muO2 data self.decomp_e_charge = \ entry_charge.data.get("decomposition_energy", None) self.decomp_e_discharge = \ entry_discharge.data.get("decomposition_energy", None) self.muO2_charge = entry_charge.data.get("muO2", None) self.muO2_discharge = entry_discharge.data.get("muO2", None) self.entry_charge = entry_charge self.entry_discharge = entry_discharge self.normalization_charge = norm_charge self.normalization_discharge = norm_discharge self._frac_charge = comp_charge.get_atomic_fraction(working_element) self._frac_discharge = \ comp_discharge.get_atomic_fraction(working_element)
def test_calculate_bv_sum_unordered(self): s = Structure.from_file(os.path.join(test_dir, "LiMn2O4.json")) s[0].species = Composition("Li0.5Na0.5") neighbors = s.get_neighbors(s[0], 3.0) bv_sum = calculate_bv_sum_unordered(s[0], neighbors) self.assertAlmostEqual(bv_sum, 1.5494662306918852, places=5)
class ComputedEntry(PDEntry, MSONable): """ An lightweight ComputedEntry object containing key computed data for many purposes. Extends a PDEntry so that it can be used for phase diagram generation. The difference between a ComputedEntry and a standard PDEntry is that it includes additional parameters like a correction and run_parameters. """ def __init__(self, composition, energy, correction=0.0, parameters=None, data=None, entry_id=None, attribute=None): """ Initializes a ComputedEntry. Args: composition (Composition): Composition of the entry. For flexibility, this can take the form of all the typical input taken by a Composition, including a {symbol: amt} dict, a string formula, and others. energy (float): Energy of the entry. Usually the final calculated energy from VASP or other electronic structure codes. correction (float): A correction to be applied to the energy. This is used to modify the energy for certain analyses. Defaults to 0.0. parameters (dict): An optional dict of parameters associated with the entry. Defaults to None. data (dict): An optional dict of any additional data associated with the entry. Defaults to None. entry_id (obj): An optional id to uniquely identify the entry. attribute: Optional attribute of the entry. This can be used to specify that the entry is a newly found compound, or to specify a particular label for the entry, or else ... Used for further analysis and plotting purposes. An attribute can be anything but must be MSONable. """ self.uncorrected_energy = energy self.composition = Composition(composition) self.correction = correction self.parameters = parameters if parameters else {} self.data = data if data else {} self.entry_id = entry_id self.name = self.composition.reduced_formula self.attribute = attribute @property def energy(self): """ Returns the *corrected* energy of the entry. """ return self.uncorrected_energy + self.correction def __repr__(self): output = ["ComputedEntry {}".format(self.composition.formula), "Energy = {:.4f}".format(self.uncorrected_energy), "Correction = {:.4f}".format(self.correction), "Parameters:"] for k, v in self.parameters.items(): output.append("{} = {}".format(k, v)) output.append("Data:") for k, v in self.data.items(): output.append("{} = {}".format(k, v)) return "\n".join(output) def __str__(self): return self.__repr__() @classmethod def from_dict(cls, d): dec = MontyDecoder() return cls(d["composition"], d["energy"], d["correction"], dec.process_decoded(d.get("parameters", {})), dec.process_decoded(d.get("data", {})), entry_id=d.get("entry_id", None), attribute=d["attribute"] if "attribute" in d else None) def as_dict(self): return {"@module": self.__class__.__module__, "@class": self.__class__.__name__, "energy": self.uncorrected_energy, "composition": self.composition.as_dict(), "correction": self.correction, "parameters": json.loads(json.dumps(self.parameters, cls=MontyEncoder)), "data": json.loads(json.dumps(self.data, cls=MontyEncoder)), "entry_id": self.entry_id, "attribute": self.attribute}
def test_mixed_valence(self): comp = Composition({"Fe2+": 2, "Fe3+": 4, "Li+": 8}) self.assertEqual(comp.reduced_formula, "Li4Fe3") self.assertEqual(comp.alphabetical_formula, "Fe6 Li8") self.assertEqual(comp.formula, "Li8 Fe6")
def from_composition_and_pd(cls, comp, pd, working_ion_symbol="Li", allow_unstable=False): """ Convenience constructor to make a ConversionElectrode from a composition and a phase diagram. Args: comp: Starting composition for ConversionElectrode, e.g., Composition("FeF3") pd: A PhaseDiagram of the relevant system (e.g., Li-Fe-F) working_ion_symbol: Element symbol of working ion. Defaults to Li. allow_unstable: Allow compositions that are unstable """ working_ion = Element(working_ion_symbol) entry = None working_ion_entry = None for e in pd.stable_entries: if e.composition.reduced_formula == comp.reduced_formula: entry = e elif e.is_element and e.composition.reduced_formula == working_ion_symbol: working_ion_entry = e if not allow_unstable and not entry: raise ValueError( "Not stable compound found at composition {}.".format(comp)) profile = pd.get_element_profile(working_ion, comp) # Need to reverse because voltage goes form most charged to most # discharged. profile.reverse() if len(profile) < 2: return None working_ion_entry = working_ion_entry working_ion = working_ion_entry.composition.elements[0].symbol normalization_els = {} for el, amt in comp.items(): if el != Element(working_ion): normalization_els[el] = amt framework = comp.as_dict() if working_ion in framework: framework.pop(working_ion) framework = Composition(framework) vpairs = [ ConversionVoltagePair.from_steps( profile[i], profile[i + 1], normalization_els, framework_formula=framework.reduced_formula, ) for i in range(len(profile) - 1) ] return cls( voltage_pairs=vpairs, working_ion_entry=working_ion_entry, _initial_comp_formula=comp.reduced_formula, _framework_formula=framework.reduced_formula, )
def from_entries(cls, entries, working_ion_entry, strip_structures=False): """ Create a new InsertionElectrode. Args: entries: A list of ComputedStructureEntries (or subclasses) representing the different topotactic states of the battery, e.g. TiO2 and LiTiO2. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. strip_structures: Since the electrode document only uses volume we can make the electrode object significantly leaner by dropping the structure data. If this parameter is set to True, the ComputedStructureEntry will be replaced with ComputedEntry and the volume will be stored in ComputedEntry.data['volume'] """ if strip_structures: ents = [] for ient in entries: dd = ient.as_dict() ent = ComputedEntry.from_dict(dd) ent.data["volume"] = ient.structure.volume ents.append(ent) entries = ents _working_ion = working_ion_entry.composition.elements[0] _working_ion_entry = working_ion_entry # Prepare to make phase diagram: determine elements and set their energy # to be very high elements = set() for entry in entries: elements.update(entry.composition.elements) # Set an artificial energy for each element for convex hull generation element_energy = max([entry.energy_per_atom for entry in entries]) + 10 pdentries = [] pdentries.extend(entries) pdentries.extend( [PDEntry(Composition({el: 1}), element_energy) for el in elements]) # Make phase diagram to determine which entries are stable vs. unstable pd = PhaseDiagram(pdentries) def lifrac(e): return e.composition.get_atomic_fraction(_working_ion) # stable entries ordered by amount of Li asc _stable_entries = tuple( sorted([e for e in pd.stable_entries if e in entries], key=lifrac)) # unstable entries ordered by amount of Li asc _unstable_entries = tuple( sorted([e for e in pd.unstable_entries if e in entries], key=lifrac)) # create voltage pairs _vpairs = tuple([ InsertionVoltagePair.from_entries( _stable_entries[i], _stable_entries[i + 1], working_ion_entry, ) for i in range(len(_stable_entries) - 1) ]) framework = _vpairs[0].framework return cls( voltage_pairs=_vpairs, working_ion_entry=_working_ion_entry, _stable_entries=_stable_entries, _unstable_entries=_unstable_entries, _framework_formula=framework.reduced_formula, )
def from_steps(step1, step2, normalization_els): """ Creates a ConversionVoltagePair from two steps in the element profile from a PD analysis. Args: step1: Starting step step2: Ending step normalization_els: Elements to normalize the reaction by. To ensure correct capacities. """ working_ion_entry = step1["element_reference"] working_ion = working_ion_entry.composition.elements[0].symbol voltage = -step1["chempot"] + working_ion_entry.energy_per_atom mAh = (step2["evolution"] - step1["evolution"]) \ * Charge(1, "e").to("C") * Time(1, "s").to("h") * N_A * 1000 licomp = Composition(working_ion) prev_rxn = step1["reaction"] reactants = {comp: abs(prev_rxn.get_coeff(comp)) for comp in prev_rxn.products if comp != licomp} curr_rxn = step2["reaction"] products = {comp: abs(curr_rxn.get_coeff(comp)) for comp in curr_rxn.products if comp != licomp} reactants[licomp] = (step2["evolution"] - step1["evolution"]) rxn = BalancedReaction(reactants, products) for el, amt in normalization_els.items(): if rxn.get_el_amount(el) > 1e-6: rxn.normalize_to_element(el, amt) break prev_mass_dischg = sum([prev_rxn.all_comp[i].weight * abs(prev_rxn.coeffs[i]) for i in range(len(prev_rxn.all_comp))]) / 2 vol_charge = sum([abs(prev_rxn.get_coeff(e.composition)) * e.structure.volume for e in step1["entries"] if e.composition.reduced_formula != working_ion]) mass_discharge = sum([curr_rxn.all_comp[i].weight * abs(curr_rxn.coeffs[i]) for i in range(len(curr_rxn.all_comp))]) / 2 mass_charge = prev_mass_dischg mass_discharge = mass_discharge vol_discharge = sum([abs(curr_rxn.get_coeff(e.composition)) * e.structure.volume for e in step2["entries"] if e.composition.reduced_formula != working_ion]) totalcomp = Composition({}) for comp in prev_rxn.products: if comp.reduced_formula != working_ion: totalcomp += comp * abs(prev_rxn.get_coeff(comp)) frac_charge = totalcomp.get_atomic_fraction(Element(working_ion)) totalcomp = Composition({}) for comp in curr_rxn.products: if comp.reduced_formula != working_ion: totalcomp += comp * abs(curr_rxn.get_coeff(comp)) frac_discharge = totalcomp.get_atomic_fraction(Element(working_ion)) rxn = rxn entries_charge = step2["entries"] entries_discharge = step1["entries"] return ConversionVoltagePair(rxn, voltage, mAh, vol_charge, vol_discharge, mass_charge, mass_discharge, frac_charge, frac_discharge, entries_charge, entries_discharge, working_ion_entry)
def __init__( self, c1: Composition, c2: Composition, grand_pd: GrandPotentialPhaseDiagram, pd_non_grand: PhaseDiagram, include_no_mixing_energy: bool = False, norm: bool = True, use_hull_energy: bool = True, ): """ Args: c1: Reactant 1 composition c2: Reactant 2 composition grand_pd: Grand potential phase diagram object built from all elements in composition c1 and c2. include_no_mixing_energy: No_mixing_energy for a reactant is the opposite number of its energy above grand potential convex hull. In cases where reactions involve elements reservoir, this param determines whether no_mixing_energy of reactants will be included in the final reaction energy calculation. By definition, if pd is not a GrandPotentialPhaseDiagram object, this param is False. pd_non_grand: PhaseDiagram object but not GrandPotentialPhaseDiagram object built from elements in c1 and c2. norm: Whether or not the total number of atoms in composition of reactant will be normalized to 1. use_hull_energy: Whether or not use the convex hull energy for a given composition for reaction energy calculation. If false, the energy of ground state structure will be used instead. Note that in case when ground state can not be found for a composition, convex hull energy will be used associated with a warning message. """ if not isinstance(grand_pd, GrandPotentialPhaseDiagram): raise ValueError( "Please use the InterfacialReactivity class if using a regular phase diagram!" ) super().__init__(c1=c1, c2=c2, pd=grand_pd, norm=norm, use_hull_energy=use_hull_energy, bypass_grand_warning=True) self.pd_non_grand = pd_non_grand self.grand = True self.comp1 = Composition( {k: v for k, v in c1.items() if k not in grand_pd.chempots}) self.comp2 = Composition( {k: v for k, v in c2.items() if k not in grand_pd.chempots}) if self.norm: self.factor1 = self.comp1.num_atoms / c1.num_atoms self.factor2 = self.comp2.num_atoms / c2.num_atoms self.comp1 = self.comp1.fractional_composition self.comp2 = self.comp2.fractional_composition if include_no_mixing_energy: self.e1 = self._get_grand_potential(self.c1) self.e2 = self._get_grand_potential(self.c2) else: self.e1 = self.pd.get_hull_energy(self.comp1) self.e2 = self.pd.get_hull_energy(self.comp2)