From: =?UTF-8?q?Se=C3=A1n=20Kavanagh?= <51478689+kavanase@users.noreply.github.com>
Subject: Use `np.nan` instead of 0 for no uncertainty with `ufloat`, to avoid unnecessary warnings (#4400)
Origin: https://github.com/materialsproject/pymatgen/commit/22834931904cfd138206a57684fff20ba0469580

* Use `np.nan` instead of 0 for no uncertainty with `ufloat`, as recommended

* Update rxn uncertainty handling and typing fix

---------

Co-authored-by: Shyue Ping Ong <shyuep@users.noreply.github.com>
---
 src/pymatgen/analysis/reaction_calculator.py | 13 +++++++---
 src/pymatgen/entries/compatibility.py        | 26 ++++++++++++--------
 src/pymatgen/entries/computed_entries.py     | 10 ++++----
 tests/analysis/test_reaction_calculator.py   |  2 +-
 4 files changed, 31 insertions(+), 20 deletions(-)

--- a/src/pymatgen/analysis/reaction_calculator.py
+++ b/src/pymatgen/analysis/reaction_calculator.py
@@ -10,7 +10,7 @@
 import numpy as np
 from monty.fractions import gcd_float
 from monty.json import MontyDecoder, MSONable
-from uncertainties import ufloat
+from uncertainties import UFloat, ufloat
 
 from pymatgen.core.composition import Composition
 from pymatgen.entries.computed_entries import ComputedEntry
@@ -100,7 +100,7 @@
     __repr__ = __str__
 
     @overload
-    def calculate_energy(self, energies: dict[Composition, ufloat]) -> ufloat:
+    def calculate_energy(self, energies: dict[Composition, ufloat]) -> UFloat:
         pass
 
     @overload
@@ -485,10 +485,15 @@
 
         for entry in self._reactant_entries + self._product_entries:
             comp, factor = entry.composition.get_reduced_composition_and_factor()
-            energy_ufloat = ufloat(entry.energy, entry.correction_uncertainty)
+            energy_ufloat = (
+                ufloat(entry.energy, entry.correction_uncertainty)
+                if entry.correction_uncertainty and not np.isnan(entry.correction_uncertainty)
+                else entry.energy
+            )
             calc_energies[comp] = min(calc_energies.get(comp, float("inf")), energy_ufloat / factor)
 
-        return self.calculate_energy(calc_energies).std_dev
+        ufloat_reaction_energy = self.calculate_energy(calc_energies)
+        return ufloat_reaction_energy.std_dev if isinstance(ufloat_reaction_energy, UFloat) else np.nan
 
     def as_dict(self) -> dict:
         """
--- a/src/pymatgen/entries/compatibility.py
+++ b/src/pymatgen/entries/compatibility.py
@@ -121,16 +121,14 @@
         """
         new_corr = self.get_correction(entry)
         old_std_dev = entry.correction_uncertainty
-        if np.isnan(old_std_dev):
-            old_std_dev = 0
-        old_corr = ufloat(entry.correction, old_std_dev)
+        old_corr = ufloat(entry.correction, 0 if np.isnan(old_std_dev) else old_std_dev)
         updated_corr = new_corr + old_corr
 
         # if there are no error values available for the corrections applied,
         # set correction uncertainty to not a number
-        uncertainty = np.nan if updated_corr.nominal_value != 0 and updated_corr.std_dev == 0 else updated_corr.std_dev
-
-        entry.energy_adjustments.append(ConstantEnergyAdjustment(updated_corr.nominal_value, uncertainty))
+        entry.energy_adjustments.append(
+            ConstantEnergyAdjustment(updated_corr.nominal_value, updated_corr.std_dev or np.nan)
+        )
 
         return entry
 
@@ -195,7 +193,7 @@
             ufloat: 0.0 +/- 0.0 (from uncertainties package)
         """
         if SETTINGS.get("PMG_POTCAR_CHECKS") is False or not self.check_potcar:
-            return ufloat(0.0, 0.0)
+            return ufloat(0.0, np.nan)
 
         potcar_spec = entry.parameters.get("potcar_spec")
         if self.check_hash:
@@ -211,7 +209,7 @@
         expected_psp = {self.valid_potcars.get(el.symbol) for el in entry.elements}
         if expected_psp != psp_settings:
             raise CompatibilityError(f"Incompatible POTCAR {psp_settings}, expected {expected_psp}")
-        return ufloat(0.0, 0.0)
+        return ufloat(0.0, np.nan)
 
 
 @cached_class
@@ -249,6 +247,8 @@
         if rform in self.cpd_energies:
             correction += self.cpd_energies[rform] * comp.num_atoms - entry.uncorrected_energy
 
+        if correction.std_dev == 0:
+            correction = ufloat(correction.nominal_value, np.nan)  # set std_dev to nan if no uncertainty
         return correction
 
 
@@ -286,7 +286,7 @@
         """
         comp = entry.composition
         if len(comp) == 1:  # Skip element entry
-            return ufloat(0.0, 0.0)
+            return ufloat(0.0, np.nan)
 
         correction = ufloat(0.0, 0.0)
 
@@ -345,6 +345,8 @@
             else:
                 correction += self.oxide_correction["oxide"] * comp["O"]
 
+        if correction.std_dev == 0:
+            correction = ufloat(correction.nominal_value, np.nan)  # set std_dev to nan if no uncertainty
         return correction
 
 
@@ -432,6 +434,8 @@
                 correction += ufloat(-1 * MU_H2O * nH2O, 0.0)
                 # correction += 0.5 * 2.46 * nH2O  # this is the old way this correction was calculated
 
+        if correction.std_dev == 0:
+            correction = ufloat(correction.nominal_value, np.nan)  # set std_dev to nan if no uncertainty
         return correction
 
 
@@ -537,6 +541,8 @@
             if sym in u_corr:
                 correction += ufloat(u_corr[sym], u_errors[sym]) * comp[el]
 
+        if correction.std_dev == 0:
+            correction = ufloat(correction.nominal_value, np.nan)  # set std_dev to nan if no uncertainty
         return correction
 
 
@@ -1077,7 +1083,7 @@
             )
 
         # check the POTCAR symbols
-        # this should return ufloat(0, 0) or raise a CompatibilityError or ValueError
+        # this should return ufloat(0, np.nan) or raise a CompatibilityError or ValueError
         if entry.parameters.get("software", "vasp") == "vasp":
             pc = PotcarCorrection(
                 MPRelaxSet,
--- a/src/pymatgen/entries/computed_entries.py
+++ b/src/pymatgen/entries/computed_entries.py
@@ -363,8 +363,8 @@
         Returns:
             float: the total energy correction / adjustment applied to the entry in eV.
         """
-        # adds to ufloat(0.0, 0.0) to ensure that no corrections still result in ufloat object
-        corr = ufloat(0.0, 0.0) + sum(ufloat(ea.value, ea.uncertainty) for ea in self.energy_adjustments)
+        # either sum of adjustments or ufloat with nan std_dev, so that no corrections still result in ufloat object:
+        corr = sum(ufloat(ea.value, ea.uncertainty) for ea in self.energy_adjustments) or ufloat(0.0, np.nan)
         return corr.nominal_value
 
     @correction.setter
@@ -386,11 +386,11 @@
         Returns:
             float: the uncertainty of the energy adjustments applied to the entry in eV.
         """
-        # adds to ufloat(0.0, 0.0) to ensure that no corrections still result in ufloat object
-        unc = ufloat(0.0, 0.0) + sum(
+        # either sum of adjustments or ufloat with nan std_dev, so that no corrections still result in ufloat object:
+        unc = sum(
             (ufloat(ea.value, ea.uncertainty) if not np.isnan(ea.uncertainty) else ufloat(ea.value, 0))
             for ea in self.energy_adjustments
-        )
+        ) or ufloat(0.0, np.nan)
 
         if unc.nominal_value != 0 and unc.std_dev == 0:
             return np.nan
--- a/tests/analysis/test_reaction_calculator.py
+++ b/tests/analysis/test_reaction_calculator.py
@@ -444,7 +444,7 @@
     def test_calculated_reaction_energy_uncertainty_for_no_uncertainty(self):
         # test that reaction_energy_uncertainty property doesn't cause errors
         # when products/reactants have no uncertainties
-        assert self.rxn.calculated_reaction_energy_uncertainty == 0
+        assert np.isnan(self.rxn.calculated_reaction_energy_uncertainty)
 
     def test_calculated_reaction_energy_uncertainty_for_nan(self):
         # test that reaction_energy_uncertainty property is nan when the uncertainty
