Skip to content

Commit 3b6a057

Browse files
authored
Clarify return float/int type for core.Composition.reduced_composition and siblings, minor type clean up for core.Composition (#4265)
* add some types * more types * fix factor type * fix IUPAC writing * fix another factor type * fix factor * fix type * add test for composition error * add test for reduce formula
1 parent 4375e34 commit 3b6a057

File tree

3 files changed

+69
-48
lines changed

3 files changed

+69
-48
lines changed

src/pymatgen/core/composition.py

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from pymatgen.util.string import Stringify, formula_double_format
2525

2626
if TYPE_CHECKING:
27-
from collections.abc import Generator, ItemsView, Iterator
27+
from collections.abc import Generator, ItemsView, Iterator, Mapping
2828
from typing import Any, ClassVar, Literal
2929

3030
from typing_extensions import Self
@@ -115,11 +115,11 @@ class Composition(collections.abc.Hashable, collections.abc.Mapping, MSONable, S
115115
# Tolerance in distinguishing different composition amounts.
116116
# 1e-8 is fairly tight, but should cut out most floating point arithmetic
117117
# errors.
118-
amount_tolerance = 1e-8
119-
charge_balanced_tolerance = 1e-8
118+
amount_tolerance: ClassVar[float] = 1e-8
119+
charge_balanced_tolerance: ClassVar[float] = 1e-8
120120

121121
# Special formula handling for peroxides and certain elements. This is so
122-
# that formula output does not write LiO instead of Li2O2 for example.
122+
# that formula output does not write "LiO" instead of "Li2O2" for example.
123123
special_formulas: ClassVar[dict[str, str]] = {
124124
"LiO": "Li2O2",
125125
"NaO": "Na2O2",
@@ -134,7 +134,8 @@ class Composition(collections.abc.Hashable, collections.abc.Mapping, MSONable, S
134134
"H": "H2",
135135
}
136136

137-
oxi_prob = None # prior probability of oxidation used by oxi_state_guesses
137+
# Prior probability of oxidation used by oxi_state_guesses
138+
oxi_prob: ClassVar[dict | None] = None
138139

139140
def __init__(self, *args, strict: bool = False, **kwargs) -> None:
140141
"""Very flexible Composition construction, similar to the built-in Python
@@ -417,35 +418,38 @@ def get_reduced_composition_and_factor(self) -> tuple[Self, float]:
417418
"""Calculate a reduced composition and factor.
418419
419420
Returns:
420-
A normalized composition and a multiplicative factor, i.e.,
421-
Li4Fe4P4O16 returns (Composition("LiFePO4"), 4).
421+
tuple[Composition, float]: Normalized Composition and multiplicative factor,
422+
i.e. "Li4Fe4P4O16" returns (Composition("LiFePO4"), 4).
422423
"""
423-
factor = self.get_reduced_formula_and_factor()[1]
424+
factor: float = self.get_reduced_formula_and_factor()[1]
424425
return self / factor, factor
425426

426427
def get_reduced_formula_and_factor(self, iupac_ordering: bool = False) -> tuple[str, float]:
427428
"""Calculate a reduced formula and factor.
428429
429430
Args:
430431
iupac_ordering (bool, optional): Whether to order the
431-
formula by the iupac "electronegativity" series, defined in
432+
formula by the IUPAC "electronegativity" series, defined in
432433
Table VI of "Nomenclature of Inorganic Chemistry (IUPAC
433434
Recommendations 2005)". This ordering effectively follows
434-
the groups and rows of the periodic table, except the
435+
the groups and rows of the periodic table, except for
435436
Lanthanides, Actinides and hydrogen. Note that polyanions
436437
will still be determined based on the true electronegativity of
437438
the elements.
438439
439440
Returns:
440-
A pretty normalized formula and a multiplicative factor, i.e.,
441-
Li4Fe4P4O16 returns (LiFePO4, 4).
441+
tuple[str, float]: Normalized formula and multiplicative factor,
442+
i.e., "Li4Fe4P4O16" returns (LiFePO4, 4).
442443
"""
443-
all_int = all(abs(val - round(val)) < type(self).amount_tolerance for val in self.values())
444+
all_int: bool = all(abs(val - round(val)) < type(self).amount_tolerance for val in self.values())
444445
if not all_int:
445446
return self.formula.replace(" ", ""), 1
446-
el_amt_dict = {key: round(val) for key, val in self.get_el_amt_dict().items()}
447+
448+
el_amt_dict: dict[str, int] = {key: round(val) for key, val in self.get_el_amt_dict().items()}
449+
factor: float
447450
formula, factor = reduce_formula(el_amt_dict, iupac_ordering=iupac_ordering)
448451

452+
# Do not "completely reduce" certain formulas
449453
if formula in type(self).special_formulas:
450454
formula = type(self).special_formulas[formula]
451455
factor /= 2
@@ -458,10 +462,10 @@ def get_integer_formula_and_factor(
458462
"""Calculate an integer formula and factor.
459463
460464
Args:
461-
max_denominator (int): all amounts in the el:amt dict are
462-
first converted to a Fraction with this maximum denominator
465+
max_denominator (int): all amounts in the el_amt dict are
466+
first converted to a Fraction with this maximum denominator.
463467
iupac_ordering (bool, optional): Whether to order the
464-
formula by the iupac "electronegativity" series, defined in
468+
formula by the IUPAC "electronegativity" series, defined in
465469
Table VI of "Nomenclature of Inorganic Chemistry (IUPAC
466470
Recommendations 2005)". This ordering effectively follows
467471
the groups and rows of the periodic table, except the
@@ -470,13 +474,14 @@ def get_integer_formula_and_factor(
470474
the elements.
471475
472476
Returns:
473-
A pretty normalized formula and a multiplicative factor, i.e.,
474-
Li0.5O0.25 returns (Li2O, 0.25). O0.25 returns (O2, 0.125)
477+
A normalized formula and a multiplicative factor, i.e.,
478+
"Li0.5O0.25" returns (Li2O, 0.25). "O0.25" returns (O2, 0.125)
475479
"""
476-
el_amt = self.get_el_amt_dict()
477-
_gcd = gcd_float(list(el_amt.values()), 1 / max_denominator)
480+
el_amt: dict[str, float] = self.get_el_amt_dict()
481+
_gcd: float = gcd_float(list(el_amt.values()), 1 / max_denominator)
478482

479-
dct = {key: round(val / _gcd) for key, val in el_amt.items()}
483+
dct: dict[str, int] = {key: round(val / _gcd) for key, val in el_amt.items()}
484+
factor: float
480485
formula, factor = reduce_formula(dct, iupac_ordering=iupac_ordering)
481486
if formula in type(self).special_formulas:
482487
formula = type(self).special_formulas[formula]
@@ -485,8 +490,8 @@ def get_integer_formula_and_factor(
485490

486491
@property
487492
def reduced_formula(self) -> str:
488-
"""A pretty normalized formula, i.e., LiFePO4 instead of
489-
Li4Fe4P4O16.
493+
"""A normalized formula, i.e., "LiFePO4" instead of
494+
"Li4Fe4P4O16".
490495
"""
491496
return self.get_reduced_formula_and_factor()[0]
492497

@@ -1055,7 +1060,7 @@ def _get_oxi_state_guesses(
10551060
raise ValueError(f"Composition {comp} cannot accommodate max_sites setting!")
10561061

10571062
# Load prior probabilities of oxidation states, used to rank solutions
1058-
if not type(self).oxi_prob:
1063+
if type(self).oxi_prob is None:
10591064
all_data = loadfn(f"{MODULE_DIR}/../analysis/icsd_bv.yaml")
10601065
type(self).oxi_prob = {Species.from_str(sp): data for sp, data in all_data["occurrence"].items()}
10611066
oxi_states_override = oxi_states_override or {}
@@ -1319,38 +1324,38 @@ def _parse_chomp_and_rank(match, formula: str, m_dict: dict[str, float], m_point
13191324

13201325

13211326
def reduce_formula(
1322-
sym_amt: dict[str, float] | dict[str, int],
1327+
sym_amt: Mapping[str, float],
13231328
iupac_ordering: bool = False,
1324-
) -> tuple[str, float]:
1325-
"""Helper function to reduce a sym_amt dict to a reduced formula and factor.
1329+
) -> tuple[str, int]:
1330+
"""Helper function to reduce a symbol-amount mapping.
13261331
13271332
Args:
1328-
sym_amt (dict): {symbol: amount}.
1333+
sym_amt (dict[str, float]): Symbol to amount mapping.
13291334
iupac_ordering (bool, optional): Whether to order the
1330-
formula by the iupac "electronegativity" series, defined in
1335+
formula by the IUPAC "electronegativity" series, defined in
13311336
Table VI of "Nomenclature of Inorganic Chemistry (IUPAC
13321337
Recommendations 2005)". This ordering effectively follows
1333-
the groups and rows of the periodic table, except the
1338+
the groups and rows of the periodic table, except for
13341339
Lanthanides, Actinides and hydrogen. Note that polyanions
13351340
will still be determined based on the true electronegativity of
13361341
the elements.
13371342
13381343
Returns:
1339-
tuple[str, float]: reduced formula and factor.
1344+
tuple[str, int]: reduced formula and factor.
13401345
"""
1341-
syms = sorted(sym_amt, key=lambda x: [get_el_sp(x).X, x])
1346+
syms: list[str] = sorted(sym_amt, key=lambda x: [get_el_sp(x).X, x])
13421347

13431348
syms = list(filter(lambda x: abs(sym_amt[x]) > Composition.amount_tolerance, syms))
13441349

1345-
factor = 1
1346-
# Enforce integers for doing gcd.
1350+
# Enforce integer for calculating greatest common divisor
1351+
factor: int = 1
13471352
if all(int(i) == i for i in sym_amt.values()):
13481353
factor = abs(gcd(*(int(i) for i in sym_amt.values())))
13491354

1350-
poly_anions = []
1351-
# if the composition contains a poly anion
1355+
# If the composition contains polyanion
1356+
poly_anions: list[str] = []
13521357
if len(syms) >= 3 and get_el_sp(syms[-1]).X - get_el_sp(syms[-2]).X < 1.65:
1353-
poly_sym_amt = {syms[i]: sym_amt[syms[i]] / factor for i in [-2, -1]}
1358+
poly_sym_amt: dict[str, float] = {syms[i]: sym_amt[syms[i]] / factor for i in (-2, -1)}
13541359
poly_form, poly_factor = reduce_formula(poly_sym_amt, iupac_ordering=iupac_ordering)
13551360

13561361
if poly_factor != 1:
@@ -1369,9 +1374,17 @@ def reduce_formula(
13691374
return "".join([*reduced_form, *poly_anions]), factor
13701375

13711376

1377+
class CompositionError(Exception):
1378+
"""Composition exceptions."""
1379+
1380+
13721381
class ChemicalPotential(dict, MSONable):
1373-
"""Represent set of chemical potentials. Can be: multiplied/divided by a Number
1374-
multiplied by a Composition (returns an energy) added/subtracted with other ChemicalPotentials.
1382+
"""Represent set of chemical potentials.
1383+
1384+
Can be:
1385+
- multiplied/divided by a Number
1386+
- multiplied by a Composition (returns an energy)
1387+
- added/subtracted with other ChemicalPotentials
13751388
"""
13761389

13771390
def __init__(self, *args, **kwargs) -> None:
@@ -1424,7 +1437,3 @@ def get_energy(self, composition: Composition, strict: bool = True) -> float:
14241437
if strict and (missing := set(composition) - set(self)):
14251438
raise ValueError(f"Potentials not specified for {missing}")
14261439
return sum(self.get(key, 0) * val for key, val in composition.items())
1427-
1428-
1429-
class CompositionError(Exception):
1430-
"""Exception class for composition errors."""

src/pymatgen/core/ion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def get_reduced_formula_and_factor(
141141
142142
Args:
143143
iupac_ordering (bool, optional): Whether to order the
144-
formula by the iupac "electronegativity" series, defined in
144+
formula by the IUPAC "electronegativity" series, defined in
145145
Table VI of "Nomenclature of Inorganic Chemistry (IUPAC
146146
Recommendations 2005)". This ordering effectively follows
147147
the groups and rows of the periodic table, except the

tests/core/test_composition.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pytest import approx
1313

1414
from pymatgen.core import Composition, DummySpecies, Element, Species
15-
from pymatgen.core.composition import ChemicalPotential
15+
from pymatgen.core.composition import ChemicalPotential, CompositionError, reduce_formula
1616
from pymatgen.util.testing import PymatgenTest
1717

1818

@@ -316,7 +316,7 @@ def test_reduced_formula(self):
316316
all_formulas = [c.reduced_formula for c in self.comps]
317317
assert all_formulas == correct_reduced_formulas
318318

319-
# test iupac reduced formula (polyanions should still appear at the end)
319+
# test IUPAC reduced formula (polyanions should still appear at the end)
320320
all_formulas = [c.get_reduced_formula_and_factor(iupac_ordering=True)[0] for c in self.comps]
321321
assert all_formulas == correct_reduced_formulas
322322
assert Composition("H6CN").get_integer_formula_and_factor(iupac_ordering=True)[0] == "CNH6"
@@ -347,7 +347,7 @@ def test_integer_formula(self):
347347
assert formula == "Li(BH)6"
348348
assert factor == approx(1 / 6)
349349

350-
# test iupac reduced formula (polyanions should still appear at the end)
350+
# test IUPAC reduced formula (polyanions should still appear at the end)
351351
all_formulas = [c.get_integer_formula_and_factor(iupac_ordering=True)[0] for c in self.comps]
352352
assert all_formulas == correct_reduced_formulas
353353
assert Composition("H6CN0.5").get_integer_formula_and_factor(iupac_ordering=True) == ("C2NH12", 0.5)
@@ -860,6 +860,18 @@ def test_isotopes(self):
860860
assert "Deuterium" in [elem.long_name for elem in composition.elements]
861861

862862

863+
def test_reduce_formula():
864+
assert reduce_formula({"Li": 2, "Mn": 4, "O": 8}) == ("LiMn2O4", 2)
865+
assert reduce_formula({"Li": 4, "O": 4}) == ("LiO", 4)
866+
assert reduce_formula({"Zn": 2, "O": 2, "H": 2}) == ("ZnHO", 2)
867+
868+
869+
def test_composition_error():
870+
error = CompositionError("Composition error")
871+
assert isinstance(error, CompositionError)
872+
assert str(error) == "Composition error"
873+
874+
863875
class TestChemicalPotential:
864876
def test_init(self):
865877
dct = {"Fe": 1, Element("Fe"): 1}

0 commit comments

Comments
 (0)