Skip to content

Commit f91f8d5

Browse files
Add support for vaspout.h5, improvements to potcar handling (#3680)
* First pass add functional Vaspout parser * Add POTCAR spec attrs to standardize spec, add from_spec method to Potcar class, standardize potcar checking * pre-commit auto-fixes * fix failing lobster tests * pre-commit auto-fixes * pre-commit auto-fixes * Add Vaspout tests, fix method * pre-commit and fix test names * fix remaining linting and mypy errors * pre-commit auto-fixes * Fix python 3.9 support, add charge and magnetization site_properties in Vaspout.ionic_steps * pre-commit auto-fixes * Fix projected DOS parsing * linting / typing * remove dunder methods from potcar spec * linting * increase support for auto k point generation * linting * precommit * add h5py to ci extra install for testing * make vaspout tests optional * explicit reason pass to pytest skipif * remove os system call * precommit * tweak dos parsing * precommit --------- Signed-off-by: Aaron Kaplan <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4e2cb01 commit f91f8d5

File tree

8 files changed

+858
-47
lines changed

8 files changed

+858
-47
lines changed

dev_scripts/potcar_scrambler.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class PotcarScrambler:
2727
2828
Used to generate copyright-compliant POTCARs for PMG tests.
2929
30-
In case of questions, contact Aaron Kaplan <[email protected]>.
30+
In case of questions, contact Aaron Kaplan <[email protected]>.
3131
3232
Recommended use:
3333
PotcarScrambler.from_file(
@@ -202,3 +202,21 @@ def potcar_cleanser() -> None:
202202
if __name__ == "__main__":
203203
potcar_cleanser()
204204
# generate_fake_potcar_libraries()
205+
206+
"""
207+
Note that vaspout.h5 files also contain full POTCARs. While the
208+
Vaspout class in `pymatgen.io.vasp.outputs` contains a method to
209+
replace the POTCAR with its spec (`remove_potcar_and_write_file`),
210+
for test purposes, its often useful to have a fake POTCAR in place
211+
of the real one.
212+
213+
To use the scrambler on a vaspout.h5:
214+
```
215+
vout = Vaspout("< path to vaspout.h5>")
216+
scrambled = PotcarScrambler(vout.potcar)
217+
vout.remove_potcar_and_write_file(
218+
filename = "< path to output vaspout.h5>",
219+
fake_potcar_str = scrambled.scrambled_potcars_str
220+
)
221+
```
222+
"""

src/pymatgen/io/vasp/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Oszicar,
1818
Outcar,
1919
Procar,
20+
Vaspout,
2021
Vasprun,
2122
VolumetricData,
2223
Wavecar,

src/pymatgen/io/vasp/inputs.py

+140-36
Original file line numberDiff line numberDiff line change
@@ -2436,22 +2436,28 @@ def data_stats(data_list: Sequence) -> dict:
24362436

24372437
data_match_tol: float = 1e-6
24382438
for ref_psp in possible_potcar_matches:
2439-
key_match = all(
2440-
set(ref_psp["keywords"][key]) == set(self._summary_stats["keywords"][key]) for key in ["header", "data"]
2441-
)
2442-
2443-
data_diff = [
2444-
abs(ref_psp["stats"][key][stat] - self._summary_stats["stats"][key][stat])
2445-
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
2446-
for key in ["header", "data"]
2447-
]
2448-
data_match = all(np.array(data_diff) < data_match_tol)
2449-
2450-
if key_match and data_match:
2439+
if self.compare_potcar_stats(ref_psp, self._summary_stats, tolerance=data_match_tol):
24512440
return True
24522441

24532442
return False
24542443

2444+
def spec(self, extra_spec: Sequence[str] | None = None) -> dict[str, Any]:
2445+
"""
2446+
POTCAR spec used in vasprun.xml.
2447+
2448+
Args:
2449+
extra_spec : Sequence[str] or None (default)
2450+
A list of extra POTCAR fields to include in the spec.
2451+
If None, defaults to no extra spec.
2452+
Returns:
2453+
dict of POTCAR spec
2454+
"""
2455+
extra_spec = extra_spec or []
2456+
spec = {"titel": self.TITEL, "hash": self.md5_header_hash, "summary_stats": self._summary_stats}
2457+
for attr in extra_spec:
2458+
spec[attr] = getattr(self, attr, None)
2459+
return spec
2460+
24552461
def write_file(self, filename: str) -> None:
24562462
"""Write PotcarSingle to a file.
24572463
@@ -2562,6 +2568,47 @@ def verify_potcar(self) -> tuple[bool, bool]:
25622568

25632569
return has_sha256, hash_is_valid
25642570

2571+
@staticmethod
2572+
def compare_potcar_stats(
2573+
potcar_stats_1: dict,
2574+
potcar_stats_2: dict,
2575+
tolerance: float = 1.0e-6,
2576+
check_potcar_fields: Sequence[str] = ["header", "data"],
2577+
) -> bool:
2578+
"""
2579+
Compare PotcarSingle._summary_stats to assess if they are the same within a tolerance.
2580+
2581+
Args:
2582+
potcar_stats_1 : dict
2583+
Dict of potcar summary stats from the first PotcarSingle, from the PotcarSingle._summary stats attr
2584+
potcar_stats_2 : dict
2585+
Second dict of summary stats
2586+
tolerance : float = 1.e-6
2587+
Tolerance to assess equality of numeric statistical values
2588+
check_potcar_fields : Sequence[str] = ["header", "data"]
2589+
The specific fields of the POTCAR to check, whether just the "header", just the "data", or both
2590+
2591+
Returns:
2592+
bool
2593+
Whether the POTCARs are identical according to their summary stats.
2594+
"""
2595+
2596+
key_match = all(
2597+
set(potcar_stats_1["keywords"].get(key)) == set(potcar_stats_2["keywords"].get(key))
2598+
for key in check_potcar_fields
2599+
)
2600+
2601+
data_match = False
2602+
if key_match:
2603+
data_diff = [
2604+
abs(potcar_stats_1["stats"].get(key, {}).get(stat) - potcar_stats_2["stats"].get(key, {}).get(stat))
2605+
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
2606+
for key in check_potcar_fields
2607+
]
2608+
data_match = all(np.array(data_diff) < tolerance)
2609+
2610+
return key_match and data_match
2611+
25652612
def identify_potcar(
25662613
self,
25672614
mode: Literal["data", "file"] = "data",
@@ -2599,19 +2646,9 @@ def identify_potcar(
25992646
if self.VRHFIN.replace(" ", "") != ref_psp["VRHFIN"]:
26002647
continue
26012648

2602-
key_match = all(
2603-
set(ref_psp["keywords"][key]) == set(self._summary_stats["keywords"][key]) for key in check_modes
2604-
)
2605-
2606-
data_diff = [
2607-
abs(ref_psp["stats"][key][stat] - self._summary_stats["stats"][key][stat])
2608-
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
2609-
for key in check_modes
2610-
]
2611-
2612-
data_match = all(np.array(data_diff) < data_tol)
2613-
2614-
if key_match and data_match:
2649+
if self.compare_potcar_stats(
2650+
ref_psp, self._summary_stats, tolerance=data_tol, check_potcar_fields=check_modes
2651+
):
26152652
identity["potcar_functionals"].append(func)
26162653
identity["potcar_symbols"].append(ref_psp["symbol"])
26172654

@@ -2861,8 +2898,28 @@ def symbols(self, symbols: Sequence[str]) -> None:
28612898

28622899
@property
28632900
def spec(self) -> list[dict]:
2864-
"""The atomic symbols and hash of all the atoms in the POTCAR file."""
2865-
return [{"symbol": psingle.symbol, "hash": psingle.md5_computed_file_hash} for psingle in self]
2901+
"""
2902+
POTCAR spec for all POTCARs in this instance.
2903+
2904+
Args:
2905+
extra_spec : Sequence[str] or None (default)
2906+
A list of extra POTCAR fields to include in the spec.
2907+
If None, defaults to ["symbol"] (needed for compatibility with LOBSTER).
2908+
2909+
Return:
2910+
list[dict], a list of PotcarSingle.spec dicts
2911+
"""
2912+
return [psingle.spec(extra_spec=["symbol"]) for psingle in self]
2913+
2914+
def write_potcar_spec(self, filename: str = "POTCAR.spec.json.gz") -> None:
2915+
"""
2916+
Write POTCAR spec to file.
2917+
2918+
Args:
2919+
filename : str = "POTCAR.spec.json.gz"
2920+
The name of a file to write the POTCAR spec to.
2921+
"""
2922+
dumpfn(self.spec, filename)
28662923

28672924
def as_dict(self) -> dict:
28682925
"""MSONable dict representation."""
@@ -2885,22 +2942,19 @@ def from_dict(cls, dct: dict) -> Self:
28852942
return Potcar(symbols=dct["symbols"], functional=dct["functional"])
28862943

28872944
@classmethod
2888-
def from_file(cls, filename: PathLike) -> Self:
2945+
def from_str(cls, data: str):
28892946
"""
2890-
Reads Potcar from file.
2947+
Read Potcar from a string.
28912948
2892-
Args:
2893-
filename: Filename
2949+
:param data: Potcar as a string.
28942950
28952951
Returns:
28962952
Potcar
28972953
"""
2898-
with zopen(filename, mode="rt", encoding="utf-8") as file:
2899-
fdata = file.read()
2900-
29012954
potcar = cls()
2902-
functionals: list[str | None] = []
2903-
for psingle_str in fdata.split("End of Dataset"):
2955+
2956+
functionals = []
2957+
for psingle_str in data.split("End of Dataset"):
29042958
if p_strip := psingle_str.strip():
29052959
psingle = PotcarSingle(f"{p_strip}\nEnd of Dataset\n")
29062960
potcar.append(psingle)
@@ -2912,6 +2966,20 @@ def from_file(cls, filename: PathLike) -> Self:
29122966
potcar.functional = functionals[0]
29132967
return potcar
29142968

2969+
@classmethod
2970+
def from_file(cls, filename: str):
2971+
"""
2972+
Reads Potcar from file.
2973+
2974+
:param filename: Filename
2975+
2976+
Returns:
2977+
Potcar
2978+
"""
2979+
with zopen(filename, mode="rt", encoding="utf-8") as file:
2980+
fdata = file.read()
2981+
return cls.from_str(fdata)
2982+
29152983
def write_file(self, filename: PathLike) -> None:
29162984
"""Write Potcar to a file.
29172985
@@ -2948,6 +3016,42 @@ def set_symbols(
29483016
else:
29493017
self.extend(PotcarSingle(sym_potcar_map[el]) for el in symbols)
29503018

3019+
@classmethod
3020+
def from_spec(cls, potcar_spec: list[dict], functionals: list[str] | None = None) -> Potcar:
3021+
"""
3022+
Generate a POTCAR from a list of POTCAR spec dicts.
3023+
3024+
If a set of POTCARs *for the same functional* cannot be found, raises a ValueError.
3025+
Args:
3026+
potcar_spec: list[dict]
3027+
List of POTCAR specs, from Potcar.spec or [PotcarSingle.spec]
3028+
functionals : list[str] or None (default)
3029+
If a list of strings, the functionals to restrict the search to.
3030+
3031+
Returns:
3032+
Potcar, a POTCAR using a single functional that matches the input spec.
3033+
"""
3034+
3035+
functionals = functionals or list(PotcarSingle._potcar_summary_stats)
3036+
for functional in functionals:
3037+
potcar = Potcar()
3038+
matched = [False for _ in range(len(potcar_spec))]
3039+
for ispec, spec in enumerate(potcar_spec):
3040+
titel = spec.get("titel", "")
3041+
titel_no_spc = titel.replace(" ", "")
3042+
symbol = titel.split(" ")[1].strip()
3043+
3044+
for stats in PotcarSingle._potcar_summary_stats[functional].get(titel_no_spc, []):
3045+
if PotcarSingle.compare_potcar_stats(spec["summary_stats"], stats):
3046+
potcar.append(PotcarSingle.from_symbol_and_functional(symbol=symbol, functional=functional))
3047+
matched[ispec] = True
3048+
break
3049+
3050+
if all(matched):
3051+
return potcar
3052+
3053+
raise ValueError("Cannot match the give POTCAR spec to a set of POTCARs generated with the same functional.")
3054+
29513055

29523056
class UnknownPotcarWarning(UserWarning):
29533057
"""Warning raised when POTCAR hashes do not pass validation."""

0 commit comments

Comments
 (0)