Repository: materialsproject/pymatgen

Issue:
`UFloat` Warnings with `ComputedEntry.energy`
### Python version

Python 3.10-3.12

### Pymatgen version

2025.5.2

### Operating system version

_No response_

### Current behavior

From running our `doped` tests with the latest `pymatgen` release, we get many of these warnings (which breaks tests):
```
Using UFloat objects with std_dev==0 may give unexpected results.
```

From setting the warning level to error, the traceback shows:
```
File ~/Packages/doped/doped/core.py:1428, in DefectEntry.sc_entry_energy(self)
   1425     return self._sc_entry_energy
   1427 self._sc_entry_hash = hash(self.sc_entry)
-> 1428 self._sc_entry_energy = self.sc_entry.energy
   1430 return self._sc_entry_energy

File ~/Packages/pymatgen/src/pymatgen/entries/computed_entries.py:350, in ComputedEntry.energy(self)
    347 @property
    348 def energy(self) -> float:
    349     """The *corrected* energy of the entry."""
--> 350     return self.uncorrected_energy + self.correction

File ~/Packages/pymatgen/src/pymatgen/entries/computed_entries.py:367, in ComputedEntry.correction(self)
    362 """
    363 Returns:
    364     float: the total energy correction / adjustment applied to the entry in eV.
    365 """
    366 # adds to ufloat(0.0, 0.0) to ensure that no corrections still result in ufloat object
--> 367 corr = ufloat(0.0, 0.0) + sum(ufloat(ea.value, ea.uncertainty) for ea in self.energy_adjustments)
    368 return corr.nominal_value

File ~/miniconda3/lib/python3.10/site-packages/uncertainties/core.py:1024, in ufloat(nominal_value, std_dev, tag)
    992 """
    993 Create an uncertainties Variable
    994 
   (...)
   1021    and `tag` which match the input values.
   1022 """
   1023 if std_dev == 0:
-> 1024     warn("Using UFloat objects with std_dev==0 may give unexpected results.")
   1025 return Variable(nominal_value, std_dev, tag=tag)

UserWarning: Using UFloat objects with std_dev==0 may give unexpected results.
```

I think this is due to a recent update in `uncertainties`, which now includes this warning, which occurs when there are no energy adjustments with uncertainties in `ComputedEntry.energy_adjustments`. Some discussion here: https://github.com/lmfit/lmfit-py/issues/999

### Expected Behavior

Not totally sure, but it seems like that warning is there to stay, so might make sense to catch/handle it here somehow?
Only use `ufloat` when there is an energy adjustment with non-zero uncertainty, otherwise just normal floats for correction values?

### Minimal example

```Python
from pymatgen.entries.computed_entries import ComputedEntry

ce = ComputedEntry(composition="H", energy=1)
ce.energy  # shows warning
```

### Relevant files to reproduce this bug

_No response_

Discussion:
@shyuep: I am fine with either solution proposed. Just a note that I don't think this is considered a bug per se.@kavanase: Yeah not a bug per se, but unnecessary warnings. It was the closest GitHub template that seemed to fit, but yep not really a bug.@kavanase: I've added a (backwards-compatible) fix in #4400, and tests are passing locally. I've also confirmed this fixes the issue of many of these warnings being thrown in our packages.

Solution patch:
diff --git a/src/pymatgen/analysis/reaction_calculator.py b/src/pymatgen/analysis/reaction_calculator.py
index 7266301eb0..c69e437c09 100644
--- a/src/pymatgen/analysis/reaction_calculator.py
+++ b/src/pymatgen/analysis/reaction_calculator.py
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, overload
 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 @@ class BalancedReaction(MSONable):
     __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 @@ class ComputedReaction(Reaction):
 
         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:
         """
diff --git a/src/pymatgen/entries/compatibility.py b/src/pymatgen/entries/compatibility.py
index a0034ee03f..bcc5fdfec8 100644
--- a/src/pymatgen/entries/compatibility.py
+++ b/src/pymatgen/entries/compatibility.py
@@ -121,16 +121,14 @@ class Correction(abc.ABC):
         """
         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 @@ class PotcarCorrection(Correction):
             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 @@ class PotcarCorrection(Correction):
         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 @@ class GasCorrection(Correction):
         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 @@ class AnionCorrection(Correction):
         """
         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 @@ class AnionCorrection(Correction):
             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 @@ class AqueousCorrection(Correction):
                 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 @@ class UCorrection(Correction):
             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
 
 
@@ -1076,7 +1082,7 @@ class MaterialsProject2020Compatibility(Compatibility):
             )
 
         # 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,
diff --git a/src/pymatgen/entries/computed_entries.py b/src/pymatgen/entries/computed_entries.py
index 870f280693..3955fd9de2 100644
--- a/src/pymatgen/entries/computed_entries.py
+++ b/src/pymatgen/entries/computed_entries.py
@@ -363,8 +363,8 @@ class ComputedEntry(Entry):
         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 @@ class ComputedEntry(Entry):
         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


Test patch:
diff --git a/tests/analysis/test_reaction_calculator.py b/tests/analysis/test_reaction_calculator.py
index cb6363a011..9af48a2057 100644
--- a/tests/analysis/test_reaction_calculator.py
+++ b/tests/analysis/test_reaction_calculator.py
@@ -441,7 +441,7 @@ class TestComputedReaction:
     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