Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for vaspout.h5, improvements to potcar handling #3680

Draft
wants to merge 32 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
700d255
First pass add functional Vaspout parser
esoteric-ephemera Mar 9, 2024
6231f44
Merge branch 'materialsproject:master' into vaspout
esoteric-ephemera Mar 20, 2024
dbea9d8
Add POTCAR spec attrs to standardize spec, add from_spec method to Po…
esoteric-ephemera Mar 21, 2024
31ba296
pre-commit auto-fixes
pre-commit-ci[bot] Mar 21, 2024
4b792ff
fix failing lobster tests
esoteric-ephemera Mar 21, 2024
c1fde3c
pre-commit auto-fixes
pre-commit-ci[bot] Mar 21, 2024
eef3ee3
Merge branch 'master' into vaspout
esoteric-ephemera Mar 22, 2024
99dfb9a
pre-commit auto-fixes
pre-commit-ci[bot] Mar 22, 2024
e107740
Add Vaspout tests, fix method
esoteric-ephemera Mar 22, 2024
6a60d61
pre-commit and fix test names
esoteric-ephemera Mar 22, 2024
2cc49ed
fix remaining linting and mypy errors
esoteric-ephemera Mar 22, 2024
4261948
Merge branch 'master' into vaspout
esoteric-ephemera Apr 11, 2024
d6a616b
pre-commit auto-fixes
pre-commit-ci[bot] Apr 11, 2024
c3d29d5
Fix python 3.9 support, add charge and magnetization site_properties …
esoteric-ephemera Apr 11, 2024
5a0c985
pre-commit auto-fixes
pre-commit-ci[bot] Apr 11, 2024
dd4fceb
Fix projected DOS parsing
esoteric-ephemera Apr 11, 2024
bd6792a
linting / typing
esoteric-ephemera Apr 11, 2024
aa7c013
remove dunder methods from potcar spec
esoteric-ephemera Apr 11, 2024
68ee0ff
resolve merge conflicts
esoteric-ephemera Apr 26, 2024
ee05396
linting
esoteric-ephemera Apr 26, 2024
0bafff1
increase support for auto k point generation
esoteric-ephemera Apr 26, 2024
b6427a6
linting
esoteric-ephemera Apr 26, 2024
c9475a6
resolve merge conflicts
esoteric-ephemera Jul 25, 2024
1db644d
precommit
esoteric-ephemera Jul 25, 2024
b68ae9c
add h5py to ci extra install for testing
esoteric-ephemera Jul 25, 2024
747a950
make vaspout tests optional
esoteric-ephemera Jul 25, 2024
9dac8ca
explicit reason pass to pytest skipif
esoteric-ephemera Jul 26, 2024
cb5ad84
Merge branch 'master' into vaspout
esoteric-ephemera Jul 31, 2024
ccb57c1
Merge branch 'master' into vaspout
esoteric-ephemera Aug 21, 2024
4cb802b
remove os system call
esoteric-ephemera Aug 21, 2024
641a76f
resolve merge conflicts, fix vaspout file compression
esoteric-ephemera Jan 13, 2025
34d5a34
precommit
esoteric-ephemera Jan 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion dev_scripts/potcar_scrambler.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class PotcarScrambler:

Used to generate copyright-compliant POTCARs for PMG tests.

In case of questions, contact Aaron Kaplan <[email protected]>.
In case of questions, contact Aaron Kaplan <[email protected]>.

Recommended use:
PotcarScrambler.from_file(
Expand Down Expand Up @@ -199,3 +199,21 @@ def potcar_cleanser() -> None:
if __name__ == "__main__":
potcar_cleanser()
# generate_fake_potcar_libraries()

"""
Note that vaspout.h5 files also contain full POTCARs. While the
Vaspout class in `pymatgen.io.vasp.outputs` contains a method to
replace the POTCAR with its spec (`remove_potcar_and_write_file`),
for test purposes, its often useful to have a fake POTCAR in place
of the real one.

To use the scrambler on a vaspout.h5:
```
vout = Vaspout("< path to vaspout.h5>")
scrambled = PotcarScrambler(vout.potcar)
vout.remove_potcar_and_write_file(
filename = "< path to output vaspout.h5>",
fake_potcar_str = scrambled.scrambled_potcars_str
)
```
"""
1 change: 1 addition & 0 deletions src/pymatgen/io/vasp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Oszicar,
Outcar,
Procar,
Vaspout,
Vasprun,
VolumetricData,
Wavecar,
Expand Down
176 changes: 140 additions & 36 deletions src/pymatgen/io/vasp/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2391,22 +2391,28 @@ def data_stats(data_list: Sequence) -> dict:

data_match_tol: float = 1e-6
for ref_psp in possible_potcar_matches:
key_match = all(
set(ref_psp["keywords"][key]) == set(self._summary_stats["keywords"][key]) for key in ["header", "data"]
)

data_diff = [
abs(ref_psp["stats"][key][stat] - self._summary_stats["stats"][key][stat])
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
for key in ["header", "data"]
]
data_match = all(np.array(data_diff) < data_match_tol)

if key_match and data_match:
if self.compare_potcar_stats(ref_psp, self._summary_stats, tolerance=data_match_tol):
return True

return False

def spec(self, extra_spec: Sequence[str] | None = None) -> dict[str, Any]:
"""
POTCAR spec used in vasprun.xml.

Args:
extra_spec : Sequence[str] or None (default)
A list of extra POTCAR fields to include in the spec.
If None, defaults to no extra spec.
Returns:
dict of POTCAR spec
"""
extra_spec = extra_spec or []
spec = {"titel": self.TITEL, "hash": self.md5_header_hash, "summary_stats": self._summary_stats}
for attr in extra_spec:
spec[attr] = getattr(self, attr, None)
return spec

def write_file(self, filename: str) -> None:
"""Write PotcarSingle to a file.

Expand Down Expand Up @@ -2517,6 +2523,47 @@ def verify_potcar(self) -> tuple[bool, bool]:

return has_sha256, hash_is_valid

@staticmethod
def compare_potcar_stats(
potcar_stats_1: dict,
potcar_stats_2: dict,
tolerance: float = 1.0e-6,
check_potcar_fields: Sequence[str] = ["header", "data"],
) -> bool:
"""
Compare PotcarSingle._summary_stats to assess if they are the same within a tolerance.

Args:
potcar_stats_1 : dict
Dict of potcar summary stats from the first PotcarSingle, from the PotcarSingle._summary stats attr
potcar_stats_2 : dict
Second dict of summary stats
tolerance : float = 1.e-6
Tolerance to assess equality of numeric statistical values
check_potcar_fields : Sequence[str] = ["header", "data"]
The specific fields of the POTCAR to check, whether just the "header", just the "data", or both

Returns:
bool
Whether the POTCARs are identical according to their summary stats.
"""

key_match = all(
set(potcar_stats_1["keywords"].get(key)) == set(potcar_stats_2["keywords"].get(key))
for key in check_potcar_fields
)

data_match = False
if key_match:
data_diff = [
abs(potcar_stats_1["stats"].get(key, {}).get(stat) - potcar_stats_2["stats"].get(key, {}).get(stat))
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
for key in check_potcar_fields
]
data_match = all(np.array(data_diff) < tolerance)

return key_match and data_match

def identify_potcar(
self,
mode: Literal["data", "file"] = "data",
Expand Down Expand Up @@ -2554,19 +2601,9 @@ def identify_potcar(
if self.VRHFIN.replace(" ", "") != ref_psp["VRHFIN"]:
continue

key_match = all(
set(ref_psp["keywords"][key]) == set(self._summary_stats["keywords"][key]) for key in check_modes
)

data_diff = [
abs(ref_psp["stats"][key][stat] - self._summary_stats["stats"][key][stat])
for stat in ["MEAN", "ABSMEAN", "VAR", "MIN", "MAX"]
for key in check_modes
]

data_match = all(np.array(data_diff) < data_tol)

if key_match and data_match:
if self.compare_potcar_stats(
ref_psp, self._summary_stats, tolerance=data_tol, check_potcar_fields=check_modes
):
identity["potcar_functionals"].append(func)
identity["potcar_symbols"].append(ref_psp["symbol"])

Expand Down Expand Up @@ -2816,8 +2853,28 @@ def symbols(self, symbols: Sequence[str]) -> None:

@property
def spec(self) -> list[dict]:
"""The atomic symbols and hash of all the atoms in the POTCAR file."""
return [{"symbol": psingle.symbol, "hash": psingle.md5_computed_file_hash} for psingle in self]
"""
POTCAR spec for all POTCARs in this instance.

Args:
extra_spec : Sequence[str] or None (default)
A list of extra POTCAR fields to include in the spec.
If None, defaults to ["symbol"] (needed for compatibility with LOBSTER).

Return:
list[dict], a list of PotcarSingle.spec dicts
"""
return [psingle.spec(extra_spec=["symbol"]) for psingle in self]

def write_potcar_spec(self, filename: str = "POTCAR.spec.json.gz") -> None:
"""
Write POTCAR spec to file.

Args:
filename : str = "POTCAR.spec.json.gz"
The name of a file to write the POTCAR spec to.
"""
dumpfn(self.spec, filename)

def as_dict(self) -> dict:
"""MSONable dict representation."""
Expand All @@ -2840,22 +2897,19 @@ def from_dict(cls, dct: dict) -> Self:
return Potcar(symbols=dct["symbols"], functional=dct["functional"])

@classmethod
def from_file(cls, filename: PathLike) -> Self:
def from_str(cls, data: str):
"""
Reads Potcar from file.
Read Potcar from a string.

Args:
filename: Filename
:param data: Potcar as a string.

Returns:
Potcar
"""
with zopen(filename, mode="rt", encoding="utf-8") as file:
fdata = file.read()

potcar = cls()
functionals: list[str | None] = []
for psingle_str in fdata.split("End of Dataset"):

functionals = []
for psingle_str in data.split("End of Dataset"):
if p_strip := psingle_str.strip():
psingle = PotcarSingle(f"{p_strip}\nEnd of Dataset\n")
potcar.append(psingle)
Expand All @@ -2867,6 +2921,20 @@ def from_file(cls, filename: PathLike) -> Self:
potcar.functional = functionals[0]
return potcar

@classmethod
def from_file(cls, filename: str):
"""
Reads Potcar from file.

:param filename: Filename

Returns:
Potcar
"""
with zopen(filename, mode="rt", encoding="utf-8") as file:
fdata = file.read()
return cls.from_str(fdata)

def write_file(self, filename: PathLike) -> None:
"""Write Potcar to a file.

Expand Down Expand Up @@ -2903,6 +2971,42 @@ def set_symbols(
else:
self.extend(PotcarSingle.from_symbol_and_functional(el, functional) for el in symbols)

@classmethod
def from_spec(cls, potcar_spec: list[dict], functionals: list[str] | None = None) -> Potcar:
"""
Generate a POTCAR from a list of POTCAR spec dicts.

If a set of POTCARs *for the same functional* cannot be found, raises a ValueError.
Args:
potcar_spec: list[dict]
List of POTCAR specs, from Potcar.spec or [PotcarSingle.spec]
functionals : list[str] or None (default)
If a list of strings, the functionals to restrict the search to.

Returns:
Potcar, a POTCAR using a single functional that matches the input spec.
"""

functionals = functionals or list(PotcarSingle._potcar_summary_stats)
for functional in functionals:
potcar = Potcar()
matched = [False for _ in range(len(potcar_spec))]
for ispec, spec in enumerate(potcar_spec):
titel = spec.get("titel", "")
titel_no_spc = titel.replace(" ", "")
symbol = titel.split(" ")[1].strip()

for stats in PotcarSingle._potcar_summary_stats[functional].get(titel_no_spc, []):
if PotcarSingle.compare_potcar_stats(spec["summary_stats"], stats):
potcar.append(PotcarSingle.from_symbol_and_functional(symbol=symbol, functional=functional))
matched[ispec] = True
break

if all(matched):
return potcar

raise ValueError("Cannot match the give POTCAR spec to a set of POTCARs generated with the same functional.")


class UnknownPotcarWarning(UserWarning):
"""Warning raised when POTCAR hashes do not pass validation."""
Expand Down
Loading
Loading