Esempio n. 1
0
def test_constant_energy_adjustment():
    ea = ConstantEnergyAdjustment(8)
    assert ea.name == "Constant energy adjustment"
    assert ea.value == 8
    assert ea.explain == "Constant energy adjustment (8.000 eV)"
    ead = ea.as_dict()
    ea2 = ConstantEnergyAdjustment.from_dict(ead)
    assert str(ead) == str(ea2.as_dict())
Esempio n. 2
0
 def test_conflicting_correction_adjustment(self):
     """
     Should raise a ValueError if a user tries to manually set both the correction
     and energy_adjustment, even if the values match.
     """
     ea = ConstantEnergyAdjustment(-10, name="Dummy adjustment")
     with pytest.raises(ValueError, match="Argument conflict!"):
         ComputedEntry("Fe6O9", 6.9, correction=-10, energy_adjustments=[ea])
Esempio n. 3
0
 def setUp(self):
     self.entry = ComputedEntry(vasprun.final_structure.composition,
                                vasprun.final_energy,
                                parameters=vasprun.incar)
     self.entry2 = ComputedEntry({"Fe": 2, "O": 3}, 2.3)
     self.entry3 = ComputedEntry("Fe2O3", 2.3)
     self.entry4 = ComputedEntry("Fe2O3", 2.3, entry_id=1)
     self.entry5 = ComputedEntry("Fe6O9", 6.9)
     ea = ConstantEnergyAdjustment(-5, name="Dummy adjustment")
     self.entry6 = ComputedEntry("Fe6O9", 6.9, correction=-10)
     self.entry7 = ComputedEntry("Fe6O9", 6.9, energy_adjustments=[ea])
 def test_normalize_energy_adjustments(self):
     ealist = [ManualEnergyAdjustment(5),
               ConstantEnergyAdjustment(5),
               CompositionEnergyAdjustment(1, 5, uncertainty_per_atom=0, name="Na"),
               TemperatureEnergyAdjustment(0.005, 100, 10, uncertainty_per_degK=0)
               ]
     entry = ComputedEntry("Na5Cl5", 6.9, energy_adjustments=ealist)
     assert entry.correction == 20
     entry.normalize()
     assert entry.correction == 4
     for ea in entry.energy_adjustments:
         assert ea.value == 1
Esempio n. 5
0
    def test_normalize_not_in_place(self):
        ealist = [
            ManualEnergyAdjustment(5),
            ConstantEnergyAdjustment(5),
            CompositionEnergyAdjustment(1, 5, uncertainty_per_atom=0, name="Na"),
            TemperatureEnergyAdjustment(0.005, 100, 10, uncertainty_per_deg=0),
        ]
        entry = ComputedEntry("Na5Cl5", 6.9, energy_adjustments=ealist)

        normed_entry = entry.normalize(inplace=False)
        entry.normalize()

        self.assertEqual(normed_entry.as_dict(), entry.as_dict())
Esempio n. 6
0
    def get_adjustments(self, entry):
        """
        Get the list of energy adjustments to be applied to an entry.
        """
        adjustment_list = []
        # try:
        corrections = self.get_corrections_dict(entry)
        for k, v in corrections.items():
            adjustment_list.append(
                ConstantEnergyAdjustment(
                    v,
                    name=k,
                    cls=self.as_dict(),
                ))

        return adjustment_list
Esempio n. 7
0
    def get_adjustments(self, entry, mixing_state_data: pd.DataFrame = None):
        """
        Returns the corrections applied to a particular entry. Note that get_adjustments is not
        intended to be called directly in the R2SCAN mixing scheme. Call process_entries instead,
        and it will pass the required arguments to get_adjustments.

        Args:
            entry: A ComputedEntry object. The entry must be a member of the list of entries
                used to create mixing_state_data.
            mixing_state_data: A DataFrame containing information about which Entries
                correspond to the same materials, which are stable on the phase diagrams of
                the respective run_types, etc. Can be generated from a list of entries using
                MaterialsProjectDFTMixingScheme.get_mixing_state_data. This argument is included to
                facilitate use of the mixing scheme in high-throughput databases where an alternative
                to get_mixing_state_data is desirable for performance reasons. In general, it should
                always be left at the default value (None) to avoid inconsistencies between the mixing
                state data and the properties of the ComputedStructureEntry.

        Returns:
            [EnergyAdjustment]: Energy adjustments to be applied to entry.

        Raises:
            CompatibilityError if the DFT mixing scheme cannot be applied to the entry.
        """
        adjustments: List[ConstantEnergyAdjustment] = []
        run_type = entry.parameters.get("run_type")

        if mixing_state_data is None:
            raise CompatibilityError(
                "WARNING! `mixing_state_data` DataFrame is None. No energy adjustments will be applied."
            )

        if not all(mixing_state_data["hull_energy_1"].notna()):
            if any(mixing_state_data["entry_id_1"].notna()):
                raise CompatibilityError(
                    f"WARNING! {self.run_type_1} entries do not form a complete PhaseDiagram."
                    " No energy adjustments will be applied.")

        if run_type not in self.valid_rtypes_1 + self.valid_rtypes_2:
            raise CompatibilityError(
                f"WARNING! Invalid run_type {run_type} for entry {entry.entry_id}. Must be one of "
                f"{self.valid_rtypes_1 + self.valid_rtypes_2}. This entry will be ignored."
            )

        # Verify that the entry is included in the mixing state data
        if (entry.entry_id not in mixing_state_data["entry_id_1"].values) and (
                entry.entry_id not in mixing_state_data["entry_id_2"].values):
            raise CompatibilityError(
                f"WARNING! Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                f"because it was not found in the mixing state data. This can occur when there are duplicate "
                "structures. In such cases, only the lowest energy entry with that structure appears in the "
                "mixing state data.")

        # Verify that the entry's energy has not been modified since mixing state data was generated
        if (entry.energy_per_atom not in mixing_state_data["energy_1"].values
            ) and (entry.energy_per_atom
                   not in mixing_state_data["energy_2"].values):
            raise CompatibilityError(
                f"WARNING! Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                "because it's energy has been modified since the mixing state data was generated."
            )

        # Compute the energy correction for mixing. The correction value depends on how many of the
        # run_type_1 stable entries are present as run_type_2 calculations

        # First case - ALL run_type_1 stable entries are present in run_type_2
        # In this scenario we construct the hull using run_type_2 energies. We discard any
        # run_type_1 entries that already exist in run_type_2 and correct other run_type_1
        # energies to have the same e_above_hull on the run_type_2 hull as they had on the run_type_1 hull
        if all(mixing_state_data[mixing_state_data["is_stable_1"]]
               ["entry_id_2"].notna()):
            if run_type in self.valid_rtypes_2:  # pylint: disable=R1705
                # For run_type_2 entries, there is no correction
                return adjustments

            # Discard GGA ground states whose structures already exist in R2SCAN.
            else:
                df_slice = mixing_state_data[(
                    mixing_state_data["entry_id_1"] == entry.entry_id)]

                if df_slice["entry_id_2"].notna().item():
                    # there is a matching run_type_2 entry, so we will discard this entry
                    if df_slice["is_stable_1"].item():
                        # this is a GGA ground state.
                        raise CompatibilityError(
                            f"Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                            f"because it is a {self.run_type_1} ground state that matches a {self.run_type_2} "
                            "material.")

                    raise CompatibilityError(
                        f"Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                        f"because there is a matching {self.run_type_2} material."
                    )

                # If a GGA is not present in R2SCAN, correct its energy to give the same
                # e_above_hull on the R2SCAN hull that it would have on the GGA hull
                hull_energy_1 = df_slice["hull_energy_1"].iloc[0]
                hull_energy_2 = df_slice["hull_energy_2"].iloc[0]
                correction = (hull_energy_2 -
                              hull_energy_1) * entry.composition.num_atoms

                adjustments.append(
                    ConstantEnergyAdjustment(
                        correction,
                        0.0,
                        name=
                        f"MP {self.run_type_1}/{self.run_type_2} mixing adjustment",
                        cls=self.as_dict(),
                        description=
                        f"Place {self.run_type_1} energy onto the {self.run_type_2} hull",
                    ))
                return adjustments

        # Second case - there are run_type_2 energies available for at least some run_type_1
        # stable entries. Here, we can correct run_type_2 energies at certain compositions
        # to preserve their e_above_hull on the run_type_1 hull
        elif any(mixing_state_data[mixing_state_data["is_stable_1"]]
                 ["entry_id_2"].notna()):
            if run_type in self.valid_rtypes_1:  # pylint: disable=R1705
                df_slice = mixing_state_data[mixing_state_data["entry_id_1"] ==
                                             entry.entry_id]

                if df_slice["entry_id_2"].notna().item():
                    # there is a matching run_type_2 entry. We should discard this entry
                    if df_slice["is_stable_1"].item():
                        # this is a GGA ground state.
                        raise CompatibilityError(
                            f"Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                            f"because it is a {self.run_type_1} ground state that matches a {self.run_type_2} "
                            "material.")

                    raise CompatibilityError(
                        f"Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                        f"because there is a matching {self.run_type_2} material"
                    )

                # For other run_type_1 entries, there is no correction
                return adjustments

            else:
                # for run_type_2, determine whether there is a run_type_2 ground state at this composition
                df_slice = mixing_state_data[mixing_state_data["formula"] ==
                                             entry.composition.reduced_formula]

                if any(df_slice[df_slice["is_stable_1"]]
                       ["entry_id_2"].notna()):
                    # there is a run_type_2 entry corresponding to the run_type_1 ground state
                    # adjust the run_type_2 energy to preserve the e_above_hull
                    gs_energy_type_2 = df_slice[
                        df_slice["is_stable_1"]]["energy_2"].item()
                    e_above_hull = entry.energy_per_atom - gs_energy_type_2
                    hull_energy_1 = df_slice["hull_energy_1"].iloc[0]
                    correction = (
                        hull_energy_1 + e_above_hull -
                        entry.energy_per_atom) * entry.composition.num_atoms
                    adjustments.append(
                        ConstantEnergyAdjustment(
                            correction,
                            0.0,
                            name=
                            f"MP {self.run_type_1}/{self.run_type_2} mixing adjustment",
                            cls=self.as_dict(),
                            description=
                            f"Place {self.run_type_2} energy onto the {self.run_type_1} hull",
                        ))
                    return adjustments

                # this composition is not stable in run_type_1. If the run_type_2 entry matches a run_type_1
                # entry, we can adjust the run_type_2 energy to match the run_type_1 energy.
                if any(df_slice[df_slice["entry_id_2"] == entry.entry_id]
                       ["entry_id_1"].notna()):
                    # adjust the energy of the run_type_2 entry to match that of the run_type_1 entry
                    type_1_energy = df_slice[df_slice["entry_id_2"] == entry.
                                             entry_id]["energy_1"].iloc[0]
                    correction = (type_1_energy - entry.energy_per_atom
                                  ) * entry.composition.num_atoms
                    adjustments.append(
                        ConstantEnergyAdjustment(
                            correction,
                            0.0,
                            name=
                            f"MP {self.run_type_1}/{self.run_type_2} mixing adjustment",
                            cls=self.as_dict(),
                            description=
                            f"Replace {self.run_type_2} energy with {self.run_type_1} energy",
                        ))
                    return adjustments

                # there is no run_type_1 entry that matches this material, and no ground state. Discard.
                raise CompatibilityError(
                    f"Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                    f"because there is no matching {self.run_type_1} entry and no {self.run_type_2} "
                    "ground state at this composition.")

        # Third case - there are no run_type_2 energies available for any run_type_1
        # ground states. There's no way to use the run_type_2 energies in this case.
        elif all(mixing_state_data[mixing_state_data["is_stable_1"]]
                 ["entry_id_2"].isna()):
            if run_type in self.valid_rtypes_1:
                # nothing to do for run_type_1, return as is
                return adjustments

            # for run_type_2, discard the entry
            raise CompatibilityError(
                f"Discarding {run_type} entry {entry.entry_id} for {entry.composition.formula} "
                f"because there are no {self.run_type_2} ground states at this composition."
            )

        # this statement is here to make pylint happy by guaranteeing a return or raise
        else:
            raise CompatibilityError(
                "WARNING! If you see this Exception it means you have encountered"
                f"an edge case in {self.__class__.__name__}. Inspect your input carefully and post a bug report."
            )
def test_constant_energy_adjustment():
    ea = ConstantEnergyAdjustment(8)
    assert ea.name == "Constant energy adjustment"
    assert ea.value == 8
    assert ea.description == "Constant energy adjustment (8.000 eV)"
Esempio n. 9
0
    def get_adjustments(self, entry: ComputedEntry):
        """
        Returns the corrections applied to a particular entry.

        Args:
            entry: A ComputedEntry object.

        Returns:
            [EnergyAdjustment]: Energy adjustments to be applied to entry.

        Raises:
            CompatibilityError if the required O2 and H2O energies have not been provided to
            MaterialsProjectAqueousCompatibility during init or in the list of entries passed to process_entries.
        """
        adjustments = []
        if self.o2_energy is None or self.h2o_energy is None or self.h2o_adjustments is None:
            raise CompatibilityError(
                "You did not provide the required O2 and H2O energies. "
                "{} needs these energies in order to compute "
                "the appropriate energy adjustments. Either specify the energies as arguments "
                "to {}.__init__ or run process_entries on a list that includes ComputedEntry for "
                "the ground state of O2 and H2O.".format(
                    type(self).__name__,
                    type(self).__name__))

        # compute the free energies of H2 and H2O (eV/atom) to guarantee that the
        # formationfree energy of H2O is equal to -2.4583 eV/H2O from experiments
        # (MU_H2O from pourbaix module)

        # Free energy of H2 in eV/atom, fitted using Eq. 40 of Persson et al. PRB 2012 85(23)
        # for this calculation ONLY, we need the (corrected) DFT energy of water
        self.h2_energy = round(
            0.5 * (3 * (self.h2o_energy - self.cpd_entropies["H2O"]) -
                   (self.o2_energy - self.cpd_entropies["O2"]) - MU_H2O), 6)

        # Free energy of H2O, fitted for consistency with the O2 and H2 energies.
        self.fit_h2o_energy = round(
            (2 * self.h2_energy +
             (self.o2_energy - self.cpd_entropies["O2"]) + MU_H2O) / 3, 6)

        comp = entry.composition
        rform = comp.reduced_formula

        # pin the energy of all H2 entries to h2_energy
        if rform == "H2":
            adjustments.append(
                ConstantEnergyAdjustment(
                    self.h2_energy * comp.num_atoms - entry.energy,
                    name="MP Aqueous H2 / H2O referencing",
                    cls=self.as_dict(),
                    description=
                    "Adjusts the H2 and H2O energy to reproduce the experimental "
                    "Gibbs formation free energy of H2O, based on the DFT energy "
                    "of Oxygen"))

        # pin the energy of all H2O entries to fit_h2o_energy
        elif rform == "H2O":
            adjustments.append(
                ConstantEnergyAdjustment(
                    self.fit_h2o_energy * comp.num_atoms - entry.energy,
                    name="MP Aqueous H2 / H2O referencing",
                    cls=self.as_dict(),
                    description=
                    "Adjusts the H2 and H2O energy to reproduce the experimental "
                    "Gibbs formation free energy of H2O, based on the DFT energy "
                    "of Oxygen"))

        # add minus T delta S to the DFT energy (enthalpy) of compounds that are
        # molecular-like at room temperature
        elif rform in self.cpd_entropies and rform != "H2O":
            adjustments.append(
                TemperatureEnergyAdjustment(
                    -1 * self.cpd_entropies[rform] / 298,
                    298,
                    comp.num_atoms,
                    name="Compound entropy at room temperature",
                    cls=self.as_dict(),
                    description=
                    "Adds the entropy (T delta S) to energies of compounds that "
                    "are gaseous or liquid at standard state"))

        # TODO - detection of embedded water molecules is not very sophisticated
        # Should be replaced with some kind of actual structure detection

        # For any compound except water, check to see if it is a hydrate (contains)
        # H2O in its structure. If so, adjust the energy to remove MU_H2O ev per
        # embedded water molecule.
        # in other words, we assume that the DFT energy of such a compound is really
        # a superposition of the "real" solid DFT energy (FeO in this case) and the free
        # energy of some water molecules
        # e.g. that E_FeO.nH2O = E_FeO + n * g_H2O
        # so, to get the most accurate gibbs free energy, we want to replace
        # g_FeO.nH2O = E_FeO.nH2O + dE_Fe + (n+1) * dE_O + 2n dE_H
        # with
        # g_FeO = E_FeO.nH2O + dE_Fe + dE_O + n g_H2O
        # where E is DFT energy, dE is an energy correction, and g is gibbs free energy
        # This means we have to 1) remove energy corrections associated with H and O in water
        # and then 2) remove the free energy of the water molecules
        if not rform == "H2O":
            # count the number of whole water molecules in the composition
            nH2O = int(min(comp["H"] / 2.0, comp["O"]))
            if nH2O > 0:
                # first, remove any H or O corrections already applied to H2O in the
                # formation energy so that we don't double count them
                # next, remove MU_H2O for each water molecule present
                hydrate_adjustment = -1 * (self.h2o_adjustments * 3 + MU_H2O)

                adjustments.append(
                    CompositionEnergyAdjustment(
                        hydrate_adjustment,
                        nH2O,
                        name="MP Aqueous hydrate",
                        cls=self.as_dict(),
                        description=
                        "Adjust the energy of solid hydrate compounds (compounds "
                        "containing H2O molecules in their structure) so that the "
                        "free energies of embedded H2O molecules match the experimental"
                        " value enforced by the MP Aqueous energy referencing scheme."
                    ))

        return adjustments