Skip to content

Commit e914d3f

Browse files
committed
rewrite electron config parser
1 parent 0d13e37 commit e914d3f

File tree

2 files changed

+105
-58
lines changed

2 files changed

+105
-58
lines changed

src/pymatgen/io/vasp/inputs.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,35 +2127,52 @@ def __repr__(self) -> str:
21272127
return f"{cls_name}({symbol=}, {functional=}, {TITEL=}, {VRHFIN=}, {n_valence_elec=:.0f})"
21282128

21292129
@property
2130-
def electron_configuration(self) -> list[tuple[int, str, int]] | None:
2130+
def electron_configuration(self) -> list[tuple[int, str, float]]:
21312131
"""Valence electronic configuration corresponding to the ZVAL,
21322132
read from the "Atomic configuration" section of POTCAR.
21332133
2134-
If the POTCAR defines a non-integer number of electrons,
2135-
the configuration is not well-defined, and None is returned.
2134+
Returns:
2135+
list[tuple[int, str, float]]: A list of tuples containing:
2136+
- n (int): Principal quantum number.
2137+
- subshell (str): Subshell notation (s, p, d, f).
2138+
- occ (float): Occupation number, limited to ZVAL.
21362139
"""
2137-
# TODO: test non integer cases
2138-
if not self.nelectrons.is_integer():
2139-
warnings.warn(
2140-
"POTCAR has non-integer charge, electron configuration not well-defined.",
2141-
stacklevel=2,
2142-
)
2143-
return None
2140+
# Find "Atomic configuration" section
2141+
match = re.search(r"Atomic configuration", self.data)
2142+
if match is None:
2143+
raise RuntimeError("Cannot find atomic configuration section in POTCAR.")
21442144

2145-
el: Element = Element.from_Z(self.atomic_no)
2146-
full_config: list[tuple[int, str, int]] = el.full_electronic_structure
2147-
nelect: float = self.nelectrons
2148-
config: list[tuple[int, str, int]] = []
2145+
start_idx: int = self.data[: match.start()].count("\n")
21492146

2150-
while nelect > 0:
2151-
n, l, num_e = full_config.pop(-1)
2152-
# Skip fully filled d/f orbitals if there are higher n orbitals
2153-
if l in {"d", "f"} and num_e in {10, 14} and any(n_ > n for n_, _, _ in full_config):
2154-
continue
2155-
config.append((n, l, num_e))
2156-
nelect -= num_e
2147+
lines = self.data.splitlines()
2148+
2149+
# Extract all subshells
2150+
match_entries = re.search(r"(\d+)\s+entries", lines[start_idx + 1])
2151+
if match_entries is None:
2152+
raise RuntimeError("Cannot find entries in POTCAR.")
2153+
num_entries: int = int(match_entries.group(1))
2154+
2155+
l_map: dict[int, str] = {0: "s", 1: "p", 2: "d", 3: "f", 4: "g", 5: "h"}
2156+
all_config: list[tuple[int, str, float]] = []
2157+
for line in lines[start_idx + 3 : start_idx + 3 + num_entries]:
2158+
parts = line.split()
2159+
n, l, _j, _E, occ = int(parts[0]), int(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
2160+
2161+
all_config.append((n, l_map[l], occ))
2162+
2163+
# Get valence electron configuration (defined by ZVAL)
2164+
valence_config: list[tuple[int, str, float]] = []
2165+
total_electrons = 0.0
2166+
2167+
for n, subshell, occ in reversed(all_config):
2168+
if occ >= 0.01: # TODO: hard-coded occupancy cutoff
2169+
valence_config.append((n, subshell, occ))
2170+
total_electrons += occ
2171+
2172+
if total_electrons >= self.zval:
2173+
break
21572174

2158-
return config
2175+
return list(reversed(valence_config))
21592176

21602177
@property
21612178
def element(self) -> str:

tests/io/vasp/test_inputs.py

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,59 +1378,89 @@ def test_nelectrons(self):
13781378
assert self.psingle_Fe.nelectrons == 8
13791379

13801380
def test_electron_configuration(self):
1381-
# TODO: use `approx` to compare floats
1381+
def assert_config_equal(actual_config, expected_config) -> None:
1382+
"""
1383+
Helper function to assert that the electron configuration is equal.
1384+
Each configuration contains: (n: int, l: str, occ: float).
1385+
"""
1386+
assert len(actual_config) == len(expected_config), "Configurations have different lengths"
1387+
1388+
for expected, actual in zip(expected_config, actual_config, strict=False):
1389+
assert expected[0] == actual[0], f"Principal quantum number mismatch: {expected[0]} != {actual[0]}"
1390+
assert expected[1] == actual[1], f"Subshell mismatch: {expected[1]} != {actual[1]}"
1391+
1392+
assert expected[2] == approx(actual[2]), f"Occupation number mismatch: {expected[2]} != {actual[2]}"
13821393

13831394
# Test s-block (Li: 2s1)
1384-
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Li.gz").electron_configuration == [
1385-
(2, "s", 1),
1386-
]
1395+
assert_config_equal(
1396+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Li.gz").electron_configuration,
1397+
[
1398+
(2.0, "s", 1.0),
1399+
],
1400+
)
13871401

13881402
# Test p-block (O: 2s2 sp4)
1389-
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.O.gz").electron_configuration == [
1390-
(2, "s", 2),
1391-
(2, "p", 4),
1392-
]
1403+
assert_config_equal(
1404+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.O.gz").electron_configuration,
1405+
[
1406+
(2, "s", 2.0),
1407+
(2, "p", 4.0),
1408+
],
1409+
)
13931410

1394-
# Test d-block (Fe: 4s1 3d7)
1395-
assert self.psingle_Fe.electron_configuration == [(3, "d", 7), (4, "s", 1)]
1411+
# Test d-block (Fe: 3d7 4s1)
1412+
assert_config_equal(
1413+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz").electron_configuration,
1414+
[(3, "d", 7.0), (4, "s", 1.0)],
1415+
)
13961416

13971417
# Test f-block (Ce: 5s2 6s2 5p6 5d1 4f1)
1398-
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Ce.gz").electron_configuration == [
1399-
(5, "s", 2),
1400-
(6, "s", 2),
1401-
(5, "p", 6),
1402-
(5, "d", 1),
1403-
(4, "f", 1),
1404-
]
1418+
assert_config_equal(
1419+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Ce.gz").electron_configuration,
1420+
[
1421+
(5, "s", 2),
1422+
(6, "s", 2),
1423+
(5, "p", 6),
1424+
(5, "d", 1),
1425+
(4, "f", 1),
1426+
],
1427+
)
14051428

14061429
# Test "sv" POTCARs (K_sv: 3s2 4s1 3p6)
1430+
assert_config_equal(
1431+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.K_sv.gz").electron_configuration,
1432+
[
1433+
(3, "s", 2),
1434+
(4, "s", 1),
1435+
(3, "p", 6),
1436+
],
1437+
)
1438+
1439+
# Test "pv" POTCARs (Fe_pv: 3p6 3d7 4s1)
14071440
assert PotcarSingle.from_file(
1408-
f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.K_sv.gz"
1441+
f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe_pv.gz"
14091442
).electron_configuration == [
1410-
(3, "s", 2),
1411-
(4, "s", 1),
1412-
(3, "p", 6),
1413-
]
1414-
1415-
# Test "pv" POTCARs
1416-
assert self.psingle_Mn_pv.electron_configuration == [
1417-
(3, "d", 5),
1418-
(4, "s", 2),
14191443
(3, "p", 6),
1444+
(3, "d", 7),
1445+
(4, "s", 1),
14201446
]
14211447

14221448
# Test non-integer occupancy (Be: 2s1.99 2p0.01)
1423-
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Be.gz").electron_configuration == [
1424-
(2, "s", 1.99),
1425-
(2, "p", 0.01),
1426-
]
1449+
assert_config_equal(
1450+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Be.gz").electron_configuration,
1451+
[
1452+
(2, "s", 1.99),
1453+
(2, "p", 0.01),
1454+
],
1455+
)
14271456

14281457
# Test another non-integer occupancy (H.25: 1s0.25)
1429-
assert PotcarSingle.from_file(
1430-
f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.H.25.gz"
1431-
).electron_configuration == [
1432-
(1, "s", 0.25),
1433-
]
1458+
assert_config_equal(
1459+
PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.H.25.gz").electron_configuration,
1460+
[
1461+
(1, "s", 0.25),
1462+
],
1463+
)
14341464

14351465
def test_attributes(self):
14361466
for key, val in self.Mn_pv_attrs.items():

0 commit comments

Comments
 (0)