diff --git a/dev_scripts/potcar_scrambler.py b/dev_scripts/potcar_scrambler.py index 4a5bb292a82..f32e866e572 100644 --- a/dev_scripts/potcar_scrambler.py +++ b/dev_scripts/potcar_scrambler.py @@ -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. @@ -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): @@ -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 diff --git a/src/pymatgen/io/vasp/inputs.py b/src/pymatgen/io/vasp/inputs.py index afcd84e689d..ebfa0d16f43 100644 --- a/src/pymatgen/io/vasp/inputs.py +++ b/src/pymatgen/io/vasp/inputs.py @@ -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.", @@ -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 diff --git a/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Be.gz b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Be.gz new file mode 100644 index 00000000000..fea276fc36b Binary files /dev/null and b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Be.gz differ diff --git a/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Ce.gz b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Ce.gz new file mode 100644 index 00000000000..b707501c295 Binary files /dev/null and b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Ce.gz differ diff --git a/tests/files/io/vasp/inputs/fake_potcars/POTCAR.H.25.gz b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.H.25.gz new file mode 100644 index 00000000000..021fb061cb0 Binary files /dev/null and b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.H.25.gz differ diff --git a/tests/files/io/vasp/inputs/fake_potcars/POTCAR.K_sv.gz b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.K_sv.gz new file mode 100644 index 00000000000..c15cf40303c Binary files /dev/null and b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.K_sv.gz differ diff --git a/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Li.gz b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Li.gz new file mode 100644 index 00000000000..c5e59a5b323 Binary files /dev/null and b/tests/files/io/vasp/inputs/fake_potcars/POTCAR.Li.gz differ diff --git a/tests/io/vasp/test_inputs.py b/tests/io/vasp/test_inputs.py index c97a21bab55..157b24382da 100644 --- a/tests/io/vasp/test_inputs.py +++ b/tests/io/vasp/test_inputs.py @@ -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():