Skip to content

Commit e1f260c

Browse files
authored
Breaking: Return True for Element in Composition if Species.symbol matches Element (#3184)
* refactor get_el_sp() * fix Composition.__contains__() by handling Species and DummySpecies correctly * test all edge cases of Composition.__contains__ - Species in Composition - Element in Composition with Species - str in Composition with Element - int (atomic number) in Composition - float in Composition - DummySpecies in Composition * revert accidental removal of if Element("O") in comp: * fix test_process_entry_with_oxidation_state > assert len(processed_entry.energy_adjustments) == 1 E AssertionError: assert 2 == 1 * fix test_get_supercell_size() expected wrong error msg * fix expected err msg
1 parent d97e77d commit e1f260c

File tree

7 files changed

+81
-46
lines changed

7 files changed

+81
-46
lines changed

pymatgen/analysis/tests/test_structure_matcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_get_supercell_size(self):
7777
assert sm._get_supercell_size(s2, s1) == (1, True)
7878

7979
sm = StructureMatcher(supercell_size="wfieoh")
80-
with pytest.raises(ValueError, match="Can't parse Element or String from str: wfieoh"):
80+
with pytest.raises(ValueError, match="Can't parse Element or Species from str: wfieoh"):
8181
sm._get_supercell_size(s1, s2)
8282

8383
def test_cmp_fstruct(self):

pymatgen/core/composition.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ def __iter__(self) -> Iterator[Species | Element | DummySpecies]:
151151
def __contains__(self, key) -> bool:
152152
try:
153153
sp = get_el_sp(key)
154-
return sp in self._data
154+
if isinstance(sp, Species):
155+
return sp in self._data
156+
# Element or str
157+
return any(sp.symbol == s.symbol for s in self._data)
155158
except ValueError as exc:
156159
raise TypeError(f"Invalid {key=} for Composition") from exc
157160

@@ -160,9 +163,9 @@ def __eq__(self, other: object) -> bool:
160163
if not isinstance(other, (Composition, dict)):
161164
return NotImplemented
162165

163-
# elements with amounts < Composition.amount_tolerance don't show up
164-
# in the el_map, so checking len enables us to only check one
165-
# composition's elements
166+
# elements with amounts < Composition.amount_tolerance don't show up
167+
# in the el_map, so checking len enables us to only check one
168+
# composition's elements
166169
if len(self) != len(other):
167170
return False
168171

pymatgen/core/periodic_table.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,15 +1540,15 @@ class DummySpecie(DummySpecies):
15401540

15411541

15421542
@functools.lru_cache
1543-
def get_el_sp(obj) -> Element | Species | DummySpecies:
1544-
"""
1545-
Utility method to get an Element or Species from an input obj.
1543+
def get_el_sp(obj: int | SpeciesLike) -> Element | Species | DummySpecies:
1544+
"""Utility method to get an Element, Species or DummySpecies from any input.
1545+
15461546
If obj is in itself an element or a specie, it is returned automatically.
1547-
If obj is an int or a string representing an integer, the Element
1548-
with the atomic number obj is returned.
1549-
If obj is a string, Species parsing will be attempted (e.g., Mn2+), failing
1550-
which Element parsing will be attempted (e.g., Mn), failing which
1551-
DummyElement parsing will be attempted.
1547+
If obj is an int or a string representing an integer, the Element with the
1548+
atomic number obj is returned.
1549+
If obj is a string, Species parsing will be attempted (e.g. Mn2+). Failing that
1550+
Element parsing will be attempted (e.g. Mn). Failing that DummyElement parsing
1551+
will be attempted.
15521552
15531553
Args:
15541554
obj (Element/Species/str/int): An arbitrary object. Supported objects
@@ -1560,28 +1560,28 @@ def get_el_sp(obj) -> Element | Species | DummySpecies:
15601560
that can be determined.
15611561
15621562
Raises:
1563-
ValueError if obj cannot be converted into an Element or Species.
1563+
ValueError: if obj cannot be converted into an Element or Species.
15641564
"""
15651565
if isinstance(obj, (Element, Species, DummySpecies)):
15661566
return obj
15671567

15681568
try:
1569-
c = float(obj)
1570-
i = int(c)
1571-
i = i if i == c else None # type: ignore
1569+
flt = float(obj)
1570+
integer = int(flt)
1571+
integer = integer if integer == flt else None # type: ignore
1572+
return Element.from_Z(integer)
15721573
except (ValueError, TypeError):
1573-
i = None
1574-
1575-
if i is not None:
1576-
return Element.from_Z(i)
1574+
pass
15771575

15781576
try:
1579-
return Species.from_str(obj)
1577+
return Species.from_str(obj) # type: ignore
15801578
except (ValueError, KeyError):
1581-
try:
1582-
return Element(obj)
1583-
except (ValueError, KeyError):
1584-
try:
1585-
return DummySpecies.from_str(obj)
1586-
except Exception:
1587-
raise ValueError(f"Can't parse Element or String from {type(obj).__name__}: {obj}.")
1579+
pass
1580+
try:
1581+
return Element(obj) # type: ignore
1582+
except (ValueError, KeyError):
1583+
pass
1584+
try:
1585+
return DummySpecies.from_str(obj) # type: ignore
1586+
except Exception:
1587+
raise ValueError(f"Can't parse Element or Species from {type(obj).__name__}: {obj}.")

pymatgen/core/tests/test_composition.py

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

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

1818

@@ -52,6 +52,7 @@ def test_immutable(self):
5252
assert "'Composition' object does not support item deletion" in str(exc.value)
5353

5454
def test_in(self):
55+
# test the Composition.__contains__ magic method
5556
assert "Fe" in self.comps[0]
5657
assert "Fe" not in self.comps[2]
5758
assert Element("Fe") in self.comps[0]
@@ -62,6 +63,46 @@ def test_in(self):
6263
with pytest.raises(KeyError, match="Invalid key='Vac'"):
6364
self.comps[0]["Vac"]
6465

66+
# Test Species in Composition
67+
comp = Composition({Species("Fe2+"): 2})
68+
assert Species("Fe2+") in comp
69+
assert Species("Fe3+") not in comp
70+
assert "Fe" in comp
71+
assert Element("Fe") in comp
72+
73+
# Test Element in Composition with Species
74+
comp = Composition({Species("Fe2+"): 2})
75+
assert Element("Fe") in comp
76+
assert Element("O") not in comp
77+
assert "Fe" in comp
78+
assert "O" not in comp
79+
80+
# Test str in Composition with Element
81+
comp = Composition({"Fe": 2})
82+
assert "Fe" in comp
83+
assert "O" not in comp
84+
assert Element("Fe") in comp
85+
assert Element("O") not in comp
86+
87+
# Test int (atomic number) in Composition
88+
comp = Composition({Element("Fe"): 2})
89+
assert 26 in comp # atomic number for Fe
90+
assert 8 not in comp # atomic number for O
91+
92+
# Test float in Composition
93+
comp = Composition({Element("Fe"): 2})
94+
with pytest.raises(TypeError, match="expected string or bytes-like object"):
95+
assert 1.5 in comp
96+
97+
# Test DummySpecies in Composition
98+
comp = Composition({DummySpecies("X"): 2})
99+
assert DummySpecies("X") in comp
100+
assert DummySpecies("A") not in comp
101+
assert "X" in comp
102+
assert "Y" not in comp
103+
assert Element("Fe") not in comp
104+
assert Species("Fe2+") not in comp
105+
65106
def test_hill_formula(self):
66107
c = Composition("CaCO3")
67108
assert c.hill_formula == "C Ca O3"
@@ -528,21 +569,12 @@ def test_oxi_state_guesses(self):
528569
{"V": 5, "O": -2},
529570
)
530571

572+
expected_oxi_guesses = dict(Li=1, Fe=2, P=5, O=-2)
531573
# max_sites for very large composition - should timeout if incorrect
532-
assert Composition("Li10000Fe10000P10000O40000").oxi_state_guesses(max_sites=7)[0] == {
533-
"Li": 1,
534-
"Fe": 2,
535-
"P": 5,
536-
"O": -2,
537-
}
574+
assert Composition("Li10000Fe10000P10000O40000").oxi_state_guesses(max_sites=7)[0] == expected_oxi_guesses
538575

539576
# max_sites for very large composition - should timeout if incorrect
540-
assert Composition("Li10000Fe10000P10000O40000").oxi_state_guesses(max_sites=-1)[0] == {
541-
"Li": 1,
542-
"Fe": 2,
543-
"P": 5,
544-
"O": -2,
545-
}
577+
assert Composition("Li10000Fe10000P10000O40000").oxi_state_guesses(max_sites=-1)[0] == expected_oxi_guesses
546578

547579
# negative max_sites less than -1 - should throw error if cannot reduce
548580
# to under the abs(max_sites) number of sites. Will also timeout if
@@ -572,7 +604,7 @@ def test_oxi_state_decoration(self):
572604
assert decorated.get(Species("Ni", 0)) == 1
573605
assert decorated.get(Species("Al", 0)) == 1
574606

575-
def test_Metallofullerene(self):
607+
def test_metallofullerene(self):
576608
# Test: Parse Metallofullerene formula (e.g. Y3N@C80)
577609
formula = "Y3N@C80"
578610
sym_dict = {"Y": 3, "N": 1, "C": 80}

pymatgen/entries/compatibility.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def get_correction(self, entry) -> ufloat:
278278
correction += self.sulfide_correction[sf_type] * comp["S"]
279279

280280
# Check for oxide, peroxide, superoxide, and ozonide corrections.
281-
if True:
281+
if Element("O") in comp:
282282
if self.correct_peroxide:
283283
if entry.data.get("oxide_type"):
284284
if entry.data["oxide_type"] in self.oxide_correction:

pymatgen/entries/tests/test_compatibility.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,7 @@ def test_process_entry_with_oxidation_state(self):
10201020
compat = MaterialsProject2020Compatibility(check_potcar=False)
10211021
[processed_entry] = compat.process_entries(entry, clean=True, inplace=False)
10221022

1023-
assert len(processed_entry.energy_adjustments) == 1
1023+
assert len(processed_entry.energy_adjustments) == 2
10241024

10251025

10261026
class MITCompatibilityTest(unittest.TestCase):

test_files/.pytest-split-durations

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,7 @@
758758
"pymatgen/core/tests/test_bonds.py::FuncTest::test_obtain_all_bond_lengths": 0.0001646250020712614,
759759
"pymatgen/core/tests/test_composition.py::ChemicalPotentialTest::test_init": 0.00024487500195391476,
760760
"pymatgen/core/tests/test_composition.py::ChemicalPotentialTest::test_math": 0.0002614989934954792,
761-
"pymatgen/core/tests/test_composition.py::CompositionTest::test_Metallofullerene": 0.0008804169774521142,
761+
"pymatgen/core/tests/test_composition.py::CompositionTest::test_metallofullerene": 0.0008804169774521142,
762762
"pymatgen/core/tests/test_composition.py::CompositionTest::test_add": 0.0006662499945377931,
763763
"pymatgen/core/tests/test_composition.py::CompositionTest::test_almost_equals": 0.0006189589767018333,
764764
"pymatgen/core/tests/test_composition.py::CompositionTest::test_alphabetical_formula": 0.0006650839932262897,

0 commit comments

Comments
 (0)