diff --git a/src/pymatgen/analysis/magnetism/jahnteller.py b/src/pymatgen/analysis/magnetism/jahnteller.py index 90727f19e15..ae53c08473b 100644 --- a/src/pymatgen/analysis/magnetism/jahnteller.py +++ b/src/pymatgen/analysis/magnetism/jahnteller.py @@ -345,7 +345,7 @@ def _get_number_of_d_electrons(species: Species) -> float: # taken from get_crystal_field_spin elec = species.element.full_electronic_structure - if len(elec) < 4 or elec[-1][1] != "s" or elec[-2][1] != "d": + if len(elec) < 4 or elec[-2][1] != "s" or elec[-1][1] != "d": raise AttributeError(f"Invalid element {species.symbol} for crystal field calculation.") n_electrons = int(elec[-1][2] + elec[-2][2] - species.oxi_state) # type: ignore[operator] if n_electrons < 0 or n_electrons > 10: diff --git a/src/pymatgen/core/periodic_table.py b/src/pymatgen/core/periodic_table.py index ddc31e6bb1e..7fe90d45ce5 100644 --- a/src/pymatgen/core/periodic_table.py +++ b/src/pymatgen/core/periodic_table.py @@ -33,6 +33,28 @@ _pt_row_sizes = (2, 8, 8, 18, 18, 32, 32) +_madelung = [ + (1, "s"), + (2, "s"), + (2, "p"), + (3, "s"), + (3, "p"), + (4, "s"), + (3, "d"), + (4, "p"), + (5, "s"), + (4, "d"), + (5, "p"), + (6, "s"), + (4, "f"), + (5, "d"), + (6, "p"), + (7, "s"), + (5, "f"), + (6, "d"), + (7, "p"), +] + @functools.total_ordering @unique @@ -422,11 +444,12 @@ def icsd_oxidation_states(self) -> tuple[int, ...]: @property def full_electronic_structure(self) -> list[tuple[int, str, int]]: """Full electronic structure as list of tuples, in order of increasing - principal (n) and angular momentum (l) quantum numbers. + energy level (according to the Madelung rule). Therefore, the final + element in the list gives the electronic structure of the valence shell. For example, the electronic structure for Fe is represented as: [(1, "s", 2), (2, "s", 2), (2, "p", 6), (3, "s", 2), (3, "p", 6), - (3, "d", 6), (4, "s", 2)]. + (4, "s", 2), (3, "d", 6)]. References: Kramida, A., Ralchenko, Yu., Reader, J., and NIST ASD Team (2023). NIST @@ -445,7 +468,13 @@ def parse_orbital(orb_str): if data[0][0] == "[": sym = data[0].replace("[", "").replace("]", "") data = list(Element(sym).full_electronic_structure) + data[1:] - return data + # sort the final electronic structure by increasing energy level + return sorted(data, key=lambda x: _madelung.index((x[0], x[1]))) + + @property + def n_electrons(self) -> int: + """Total number of electrons in the Element.""" + return sum([t[-1] for t in self.full_electronic_structure]) @property def valence(self) -> tuple[int | np.nan, int]: @@ -1117,7 +1146,8 @@ def electronic_structure(self) -> str: @property def full_electronic_structure(self) -> list[tuple[int, str, int]]: """Full electronic structure as list of tuples, in order of increasing - principal (n) and angular momentum (l) quantum numbers. + energy level (according to the Madelung rule). Therefore, the final + element in the list gives the electronic structure of the valence shell. For example, the electronic structure for Fe+2 is represented as: [(1, "s", 2), (2, "s", 2), (2, "p", 6), (3, "s", 2), (3, "p", 6), @@ -1140,7 +1170,15 @@ def parse_orbital(orb_str): if data[0][0] == "[": sym = data[0].replace("[", "").replace("]", "") data = list(Element(sym).full_electronic_structure) + data[1:] - return data + # sort the final electronic structure by increasing energy level + return sorted(data, key=lambda x: _madelung.index((x[0], x[1]))) + + # NOTE - copied exactly from Element. Refactoring / inheritance may improve + # robustness + @property + def n_electrons(self) -> int: + """Total number of electrons in the Species.""" + return sum([t[-1] for t in self.full_electronic_structure]) # NOTE - copied exactly from Element. Refactoring / inheritance may improve # robustness @@ -1319,7 +1357,7 @@ def get_crystal_field_spin( raise ValueError("Invalid coordination or spin config") elec = self.element.full_electronic_structure - if len(elec) < 4 or elec[-1][1] != "s" or elec[-2][1] != "d": + if len(elec) < 4 or elec[-2][1] != "s" or elec[-1][1] != "d": raise AttributeError(f"Invalid element {self.symbol} for crystal field calculation") assert self.oxi_state is not None diff --git a/tests/core/test_periodic_table.py b/tests/core/test_periodic_table.py index f92335d5266..586a6a513bf 100644 --- a/tests/core/test_periodic_table.py +++ b/tests/core/test_periodic_table.py @@ -74,8 +74,8 @@ def test_full_electronic_structure(self): (2, "p", 6), (3, "s", 2), (3, "p", 6), - (3, "d", 6), (4, "s", 2), + (3, "d", 6), ], "Li": [(1, "s", 2), (2, "s", 1)], "U": [ @@ -84,19 +84,19 @@ def test_full_electronic_structure(self): (2, "p", 6), (3, "s", 2), (3, "p", 6), - (3, "d", 10), (4, "s", 2), + (3, "d", 10), (4, "p", 6), - (4, "d", 10), (5, "s", 2), + (4, "d", 10), (5, "p", 6), + (6, "s", 2), (4, "f", 14), (5, "d", 10), - (6, "s", 2), (6, "p", 6), + (7, "s", 2), (5, "f", 3), (6, "d", 1), - (7, "s", 2), ], } for k, v in cases.items(): @@ -169,6 +169,11 @@ def test_from_row_and_group(self): for k, v in cases.items(): assert ElementBase.from_row_and_group(v[0], v[1]) == Element(k) + def test_n_electrons(self): + cases = {"O": 8, "Fe": 26, "Li": 3, "Be": 4} + for k, v in cases.items(): + assert Element(k).n_electrons == v + def test_valence(self): cases = {"O": (1, 4), "Fe": (2, 6), "Li": (0, 1), "Be": (0, 2)} for k, v in cases.items(): @@ -602,18 +607,20 @@ def test_sort(self): def test_species_electronic_structure(self): assert Species("Fe", 0).electronic_structure == "[Ar].3d6.4s2" + assert Species("Fe", 0).n_electrons == 26 assert Species("Fe", 0).full_electronic_structure == [ (1, "s", 2), (2, "s", 2), (2, "p", 6), (3, "s", 2), (3, "p", 6), - (3, "d", 6), (4, "s", 2), + (3, "d", 6), ] assert Species("Fe", 0).valence == (2, 6) assert Species("Fe", 2).electronic_structure == "[Ar].3d6" + assert Species("Fe", 2).n_electrons == 24 assert Species("Fe", 2).full_electronic_structure == [ (1, "s", 2), (2, "s", 2), @@ -625,6 +632,7 @@ def test_species_electronic_structure(self): assert Species("Fe", 2).valence == (2, 6) assert Species("Fe", 3).electronic_structure == "[Ar].3d5" + assert Species("Fe", 3).n_electrons == 23 assert Species("Fe", 3).full_electronic_structure == [ (1, "s", 2), (2, "s", 2), @@ -635,12 +643,36 @@ def test_species_electronic_structure(self): ] assert Species("Fe", 3).valence == (2, 5) + assert Species("Th", 4).electronic_structure == "[Hg].6p6" + assert Species("Th", 4).full_electronic_structure == [ + (1, "s", 2), + (2, "s", 2), + (2, "p", 6), + (3, "s", 2), + (3, "p", 6), + (4, "s", 2), + (3, "d", 10), + (4, "p", 6), + (5, "s", 2), + (4, "d", 10), + (5, "p", 6), + (6, "s", 2), + (4, "f", 14), + (5, "d", 10), + (6, "p", 6), + ] + assert Species("Th", 4).valence == (1, 6) + assert Species("Li", 1).electronic_structure == "1s2" + assert Species("Li", 1).n_electrons == 2 # alkali metals, all p for el in ["Na", "K", "Rb", "Cs"]: assert Species(el, 1).electronic_structure.split(".")[-1][1::] == "p6", f"Failure for {el} +1" for el in ["Ca", "Mg", "Ba", "Sr"]: assert Species(el, 2).electronic_structure.split(".")[-1][1::] == "p6", f"Failure for {el} +2" + # valence shell should be f (l=3) for all lanthanide ions except La+3 and Lu+3 + for el in ["Ce", "Nd", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu"]: + assert Species(el, 3).valence[0] == 3, f"Failure for {el} +3" for el in Element: for ox in el.common_oxidation_states: diff --git a/tests/io/vasp/test_inputs.py b/tests/io/vasp/test_inputs.py index aba859bea1f..4ce0c20e95f 100644 --- a/tests/io/vasp/test_inputs.py +++ b/tests/io/vasp/test_inputs.py @@ -1099,8 +1099,8 @@ def test_nelectrons(self): assert self.psingle_Fe.nelectrons == 8 def test_electron_config(self): - assert self.psingle_Mn_pv.electron_configuration == [(4, "s", 2), (3, "d", 5), (3, "p", 6)] - assert self.psingle_Fe.electron_configuration == [(4, "s", 2), (3, "d", 6)] + assert self.psingle_Mn_pv.electron_configuration == [(3, "d", 5), (4, "s", 2), (3, "p", 6)] + assert self.psingle_Fe.electron_configuration == [(3, "d", 6), (4, "s", 2)] def test_attributes(self): for key, val in self.Mn_pv_attrs.items():