Esempio n. 1
0
 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)}}
Esempio n. 2
0
 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)}}
Esempio n. 3
0
    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)
Esempio n. 4
0
    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 {}
Esempio n. 5
0
    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)
Esempio n. 6
0
 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)
Esempio n. 7
0
 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))
Esempio n. 8
0
 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))
Esempio n. 9
0
 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
Esempio n. 10
0
 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)
Esempio n. 11
0
 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")
Esempio n. 12
0
    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
Esempio n. 13
0
    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()
Esempio n. 14
0
 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 {}
Esempio n. 15
0
 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!")
Esempio n. 16
0
    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()
Esempio n. 17
0
 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")
Esempio n. 18
0
 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"])
Esempio n. 19
0
 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'])
Esempio n. 20
0
 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])
Esempio n. 21
0
    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 {}
Esempio n. 22
0
 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)
Esempio n. 23
0
    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))
Esempio n. 24
0
 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)}}
Esempio n. 25
0
    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"))
Esempio n. 26
0
    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)))
Esempio n. 27
0
 def test_init_numerical_tolerance(self):
     self.assertEqual(Composition({'B': 1, 'C': -1e-12}), Composition('B'))
Esempio n. 28
0
    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"))
Esempio n. 29
0
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)
Esempio n. 30
0
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]
Esempio n. 31
0
 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"))
Esempio n. 32
0
    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
Esempio n. 33
0
 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")
Esempio n. 34
0
    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)
Esempio n. 35
0
 def defect_composition(self):
     temp_comp = self.bulk_structure.composition.as_dict()
     temp_comp[str(self.site.specie)] += 1
     return Composition(temp_comp)
Esempio n. 36
0
    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)
Esempio n. 37
0
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}
Esempio n. 38
0
 def initial_comp(self) -> Composition:
     """
     The pymatgen Composition representation of the initial composition
     """
     return Composition(self._initial_comp_formula)
Esempio n. 39
0
    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,
        )
Esempio n. 40
0
    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)
Esempio n. 41
0
    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
Esempio n. 43
0
    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
Esempio n. 44
0
 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"]))
Esempio n. 45
0
    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"]
Esempio n. 46
0
    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)
Esempio n. 47
0
    def test_chemical_system(self):

        formula = "NaCl"
        cmp = Composition(formula)
        self.assertEqual(cmp.chemical_system, "Cl-Na")
Esempio n. 48
0
 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)
Esempio n. 49
0
    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,
        )
Esempio n. 50
0
    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
Esempio n. 51
0
    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
Esempio n. 52
0
    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
Esempio n. 53
0
    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)
Esempio n. 54
0
 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)
Esempio n. 55
0
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}
Esempio n. 56
0
 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")
Esempio n. 57
0
    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,
        )
Esempio n. 58
0
    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,
        )
Esempio n. 59
0
    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)
Esempio n. 60
0
    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)