Skip to content

Commit

Permalink
enhance unit test and add scrambled POTCARs
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielYang59 committed Feb 1, 2025
1 parent cd92872 commit a13b2b6
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 11 deletions.
13 changes: 8 additions & 5 deletions dev_scripts/potcar_scrambler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

class PotcarScrambler:
"""
Takes a POTCAR and replaces its values with completely random values
Takes a POTCAR and replaces its values with completely random values.
Does type matching and attempts precision matching on floats to ensure
file is read correctly by Potcar and PotcarSingle classes.
Expand All @@ -40,14 +40,15 @@ class PotcarScrambler:

def __init__(self, potcars: Potcar | PotcarSingle) -> None:
self.PSP_list = [potcars] if isinstance(potcars, PotcarSingle) else potcars
self.scrambled_potcars_str = ""
self.scrambled_potcars_str: str = ""
for psp in self.PSP_list:
scrambled_potcar_str = self.scramble_single_potcar(psp)
self.scrambled_potcars_str += scrambled_potcar_str

def _rand_float_from_str_with_prec(self, input_str: str, bloat: float = 1.5) -> float:
n_prec = len(input_str.split(".")[1])
bd = max(1, bloat * abs(float(input_str))) # ensure we don't get 0
"""Generate a random float from str to replace true values."""
n_prec: int = len(input_str.split(".")[1])
bd: float = max(1.0, bloat * abs(float(input_str))) # ensure we don't get 0
return round(bd * np.random.default_rng().random(), n_prec)

def _read_fortran_str_and_scramble(self, input_str: str, bloat: float = 1.5):
Expand Down Expand Up @@ -124,14 +125,16 @@ def scramble_single_potcar(self, potcar: PotcarSingle) -> str:
return scrambled_potcar_str

def to_file(self, filename: str) -> None:
"""Write scrambled POTCAR to file."""
with zopen(filename, mode="wt", encoding="utf-8") as file:
file.write(self.scrambled_potcars_str)

@classmethod
def from_file(cls, input_filename: str, output_filename: str | None = None) -> Self:
"""Read a POTCAR from file and generate a scrambled version."""
psp = Potcar.from_file(input_filename)
psp_scrambled = cls(psp)
if output_filename:
if output_filename is not None:
psp_scrambled.to_file(output_filename)
return psp_scrambled

Expand Down
15 changes: 11 additions & 4 deletions src/pymatgen/io/vasp/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2128,11 +2128,13 @@ def __repr__(self) -> str:

@property
def electron_configuration(self) -> list[tuple[int, str, int]] | None:
"""Valence electronic configuration corresponding to the ZVAL.
"""Valence electronic configuration corresponding to the ZVAL,
read from the "Atomic configuration" section of POTCAR.
If the POTCAR defines a non-integer number of electrons,
the configuration is not well-defined, and None is returned.
"""
# TODO: test non integer cases
if not self.nelectrons.is_integer():
warnings.warn(
"POTCAR has non-integer charge, electron configuration not well-defined.",
Expand All @@ -2144,10 +2146,15 @@ def electron_configuration(self) -> list[tuple[int, str, int]] | None:
full_config: list[tuple[int, str, int]] = el.full_electronic_structure
nelect: float = self.nelectrons
config: list[tuple[int, str, int]] = []

while nelect > 0:
e_config: tuple[int, str, int] = full_config.pop(-1)
config.append(e_config)
nelect -= e_config[-1]
n, l, num_e = full_config.pop(-1)
# Skip fully filled d/f orbitals if there are higher n orbitals
if l in {"d", "f"} and num_e in {10, 14} and any(n_ > n for n_, _, _ in full_config):
continue
config.append((n, l, num_e))
nelect -= num_e

return config

@property
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
51 changes: 49 additions & 2 deletions tests/io/vasp/test_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,13 +1377,60 @@ def test_nelectrons(self):
assert self.psingle_Mn_pv.nelectrons == 13
assert self.psingle_Fe.nelectrons == 8

def test_electron_config(self):
def test_electron_configuration(self):
# TODO: use `approx` to compare floats

# Test s-block (Li: 2s1)
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Li.gz").electron_configuration == [
(2, "s", 1),
]

# Test p-block (O: 2s2 sp4)
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.O.gz").electron_configuration == [
(2, "s", 2),
(2, "p", 4),
]

# Test d-block (Fe: 4s1 3d7)
assert self.psingle_Fe.electron_configuration == [(3, "d", 7), (4, "s", 1)]

# Test f-block (Ce: 5s2 6s2 5p6 5d1 4f1)
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Ce.gz").electron_configuration == [
(5, "s", 2),
(6, "s", 2),
(5, "p", 6),
(5, "d", 1),
(4, "f", 1),
]

# Test "sv" POTCARs (K_sv: 3s2 4s1 3p6)
assert PotcarSingle.from_file(
f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.K_sv.gz"
).electron_configuration == [
(3, "s", 2),
(4, "s", 1),
(3, "p", 6),
]

# Test "pv" POTCARs
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)]

# Test non-integer occupancy (Be: 2s1.99 2p0.01)
assert PotcarSingle.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Be.gz").electron_configuration == [
(2, "s", 1.99),
(2, "p", 0.01),
]

# Test another non-integer occupancy (H.25: 1s0.25)
assert PotcarSingle.from_file(
f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.H.25.gz"
).electron_configuration == [
(1, "s", 0.25),
]

def test_attributes(self):
for key, val in self.Mn_pv_attrs.items():
Expand Down

0 comments on commit a13b2b6

Please sign in to comment.