Skip to content

Commit f31d6ab

Browse files
authored
Raise ValueError for float('NaN') in Composition (#3519)
* raise ValueError for float('NaN') in Composition * test err msg for float('NaN') in TestComposition.test_formula
1 parent 11714b9 commit f31d6ab

File tree

2 files changed

+25
-18
lines changed

2 files changed

+25
-18
lines changed

pymatgen/core/composition.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import warnings
1212
from functools import total_ordering
1313
from itertools import combinations_with_replacement, product
14+
from math import isnan
1415
from typing import TYPE_CHECKING, Union, cast
1516

1617
from monty.fractions import gcd, gcd_float
@@ -122,17 +123,19 @@ def __init__(self, *args, strict: bool = False, **kwargs) -> None:
122123
if len(args) == 1 and isinstance(args[0], Composition):
123124
elem_map = args[0]
124125
elif len(args) == 1 and isinstance(args[0], str):
125-
elem_map = self._parse_formula(args[0]) # type: ignore
126+
elem_map = self._parse_formula(args[0]) # type: ignore[assignment]
127+
elif len(args) == 1 and isinstance(args[0], float) and isnan(args[0]):
128+
raise ValueError("float('NaN') is not a valid Composition, did you mean str('NaN')?")
126129
else:
127130
elem_map = dict(*args, **kwargs) # type: ignore
128131
elem_amt = {}
129-
self._natoms = 0
132+
self._n_atoms = 0
130133
for key, val in elem_map.items():
131134
if val < -Composition.amount_tolerance and not self.allow_negative:
132135
raise ValueError("Amounts in Composition cannot be negative!")
133136
if abs(val) >= Composition.amount_tolerance:
134137
elem_amt[get_el_sp(key)] = val
135-
self._natoms += abs(val)
138+
self._n_atoms += abs(val)
136139
self._data = elem_amt
137140
if strict and not self.valid:
138141
raise ValueError(f"Composition is not valid, contains: {', '.join(map(str, self.elements))}")
@@ -201,8 +204,8 @@ def __add__(self, other: object) -> Composition:
201204

202205
new_el_map: dict[SpeciesLike, float] = collections.defaultdict(float)
203206
new_el_map.update(self)
204-
for k, v in other.items():
205-
new_el_map[get_el_sp(k)] += v
207+
for key, val in other.items():
208+
new_el_map[get_el_sp(key)] += val
206209
return Composition(new_el_map, allow_negative=self.allow_negative)
207210

208211
def __sub__(self, other: object) -> Composition:
@@ -219,8 +222,8 @@ def __sub__(self, other: object) -> Composition:
219222

220223
new_el_map: dict[SpeciesLike, float] = collections.defaultdict(float)
221224
new_el_map.update(self)
222-
for k, v in other.items():
223-
new_el_map[get_el_sp(k)] -= v
225+
for key, val in other.items():
226+
new_el_map[get_el_sp(key)] -= val
224227
return Composition(new_el_map, allow_negative=self.allow_negative)
225228

226229
def __mul__(self, other: object) -> Composition:
@@ -323,7 +326,7 @@ def fractional_composition(self) -> Composition:
323326
1.
324327
E.g. "Fe2 O3".fractional_composition = "Fe0.4 O0.6".
325328
"""
326-
return self / self._natoms
329+
return self / self._n_atoms
327330

328331
@property
329332
def reduced_composition(self) -> Composition:
@@ -362,7 +365,7 @@ def get_reduced_formula_and_factor(self, iupac_ordering: bool = False) -> tuple[
362365
all_int = all(abs(x - round(x)) < Composition.amount_tolerance for x in self.values())
363366
if not all_int:
364367
return self.formula.replace(" ", ""), 1
365-
d = {k: int(round(v)) for k, v in self.get_el_amt_dict().items()}
368+
d = {key: int(round(val)) for key, val in self.get_el_amt_dict().items()}
366369
formula, factor = reduce_formula(d, iupac_ordering=iupac_ordering)
367370

368371
if formula in Composition.special_formulas:
@@ -395,7 +398,7 @@ def get_integer_formula_and_factor(
395398
el_amt = self.get_el_amt_dict()
396399
gcd = gcd_float(list(el_amt.values()), 1 / max_denominator)
397400

398-
dct = {k: round(v / gcd) for k, v in el_amt.items()}
401+
dct = {key: round(val / gcd) for key, val in el_amt.items()}
399402
formula, factor = reduce_formula(dct, iupac_ordering=iupac_ordering)
400403
if formula in Composition.special_formulas:
401404
formula = Composition.special_formulas[formula]
@@ -439,7 +442,7 @@ def elements(self) -> list[Element | Species | DummySpecies]:
439442
return list(self)
440443

441444
def __str__(self) -> str:
442-
return " ".join(f"{k}{formula_double_format(v, ignore_ones=False)}" for k, v in self.as_dict().items())
445+
return " ".join(f"{key}{formula_double_format(val, ignore_ones=False)}" for key, val in self.as_dict().items())
443446

444447
def to_pretty_string(self) -> str:
445448
"""
@@ -453,7 +456,7 @@ def num_atoms(self) -> float:
453456
"""Total number of atoms in Composition. For negative amounts, sum
454457
of absolute values.
455458
"""
456-
return self._natoms
459+
return self._n_atoms
457460

458461
@property
459462
def weight(self) -> float:
@@ -469,7 +472,7 @@ def get_atomic_fraction(self, el: SpeciesLike) -> float:
469472
Returns:
470473
Atomic fraction for element el in Composition
471474
"""
472-
return abs(self[el]) / self._natoms
475+
return abs(self[el]) / self._n_atoms
473476

474477
def get_wt_fraction(self, el: SpeciesLike) -> float:
475478
"""Calculate weight fraction of an Element or Species.
@@ -605,7 +608,7 @@ def valid(self) -> bool:
605608
return not any(isinstance(el, DummySpecies) for el in self.elements)
606609

607610
def __repr__(self) -> str:
608-
formula = " ".join(f"{k}{':' if hasattr(k, 'oxi_state') else ''}{v:g}" for k, v in self.items())
611+
formula = " ".join(f"{key}{':' if hasattr(key, 'oxi_state') else ''}{val:g}" for key, val in self.items())
609612
cls_name = type(self).__name__
610613
return f"{cls_name}({formula!r})"
611614

@@ -1250,20 +1253,20 @@ def __init__(self, *args, **kwargs) -> None:
12501253
**kwargs: any valid dict init arguments.
12511254
"""
12521255
dct = dict(*args, **kwargs)
1253-
super().__init__((get_el_sp(k), v) for k, v in dct.items())
1256+
super().__init__((get_el_sp(key), val) for key, val in dct.items())
12541257
if len(dct) != len(self):
12551258
raise ValueError("Duplicate potential specified")
12561259

12571260
def __mul__(self, other: object) -> ChemicalPotential:
12581261
if isinstance(other, (int, float)):
1259-
return ChemicalPotential({k: v * other for k, v in self.items()})
1262+
return ChemicalPotential({key: val * other for key, val in self.items()})
12601263
return NotImplemented
12611264

12621265
__rmul__ = __mul__
12631266

12641267
def __truediv__(self, other: object) -> ChemicalPotential:
12651268
if isinstance(other, (int, float)):
1266-
return ChemicalPotential({k: v / other for k, v in self.items()})
1269+
return ChemicalPotential({key: val / other for key, val in self.items()})
12671270
return NotImplemented
12681271

12691272
__div__ = __truediv__
@@ -1290,7 +1293,7 @@ def get_energy(self, composition: Composition, strict: bool = True) -> float:
12901293
if strict and set(composition) > set(self):
12911294
s = set(composition) - set(self)
12921295
raise ValueError(f"Potentials not specified for {s}")
1293-
return sum(self.get(k, 0) * v for k, v in composition.items())
1296+
return sum(self.get(key, 0) * val for key, val in composition.items())
12941297

12951298
def __repr__(self) -> str:
12961299
return f"ChemPots: {super()!r}"

tests/core/test_composition.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ def test_formula(self):
186186

187187
assert Composition("Na 3 Zr (PO 4) 3").reduced_formula == "Na3Zr(PO4)3"
188188

189+
assert Composition("NaN").reduced_formula == "NaN"
190+
with pytest.raises(ValueError, match=r"float\('NaN'\) is not a valid Composition, did you mean str\('NaN'\)\?"):
191+
Composition(float("NaN"))
192+
189193
# test bad formulas raise ValueError
190194
for bad_formula in ("", " ", " 2", "4 2", "6123", "1.2", "1/2", "1.2.3"):
191195
with pytest.raises(ValueError, match=f"Invalid formula={bad_formula!r}"):

0 commit comments

Comments
 (0)