2727from monty .json import MontyDecoder , MSONable
2828from monty .os import cd
2929from monty .os .path import zpath
30- from monty .serialization import loadfn
30+ from monty .serialization import dumpfn , loadfn
3131from tabulate import tabulate
3232
3333from pymatgen .core import SETTINGS
@@ -1597,13 +1597,26 @@ class PotcarSingle:
15971597 are raised if a POTCAR hash fails validation.
15981598 """
15991599
1600+ """
1601+ NB: there are multiple releases of the {LDA,PBE} {52,54} POTCARs
1602+ the original (univie) releases include no SHA256 hashes nor COPYR fields
1603+ in the PSCTR/header field.
1604+ We indicate the older release in `functional_dir` as PBE_52, PBE_54, LDA_52, LDA_54.
1605+ The newer release is indicated as PBE_52_W_HASH, etc.
1606+ """
16001607 functional_dir = dict (
16011608 PBE = "POT_GGA_PAW_PBE" ,
16021609 PBE_52 = "POT_GGA_PAW_PBE_52" ,
1610+ PBE_52_W_HASH = "POTPAW_PBE_52" ,
16031611 PBE_54 = "POT_GGA_PAW_PBE_54" ,
1612+ PBE_54_W_HASH = "POTPAW_PBE_54" ,
1613+ PBE_64 = "POT_PAW_PBE_64" ,
16041614 LDA = "POT_LDA_PAW" ,
16051615 LDA_52 = "POT_LDA_PAW_52" ,
1616+ LDA_52_W_HASH = "POTPAW_LDA_52" ,
16061617 LDA_54 = "POT_LDA_PAW_54" ,
1618+ LDA_54_W_HASH = "POTPAW_LDA_54" ,
1619+ LDA_64 = "POT_LDA_PAW_64" ,
16071620 PW91 = "POT_GGA_PAW_PW91" ,
16081621 LDA_US = "POT_LDA_US" ,
16091622 PW91_US = "POT_GGA_US_PW91" ,
@@ -2106,8 +2119,8 @@ def md5_header_hash(self) -> str:
21062119 def is_valid (self ) -> bool :
21072120 """
21082121 Check that POTCAR matches reference metadata.
2109- Parsed metadata is stored in self._meta as a human-readable dict,
2110- self._meta = {
2122+ Parsed metadata is stored in self._summary_stats as a human-readable dict,
2123+ self._summary_stats = {
21112124 "keywords": {
21122125 "header": list[str],
21132126 "data": list[str],
@@ -2135,17 +2148,17 @@ def is_valid(self) -> bool:
21352148 Note also that POTCARs can contain **different** data keywords
21362149
21372150 All keywords found in the header, essentially self.keywords, and the data block
2138- (<Data Keyword> above) are stored in self._meta ["keywords"]
2151+ (<Data Keyword> above) are stored in self._summary_stats ["keywords"]
21392152
21402153 To avoid issues of copyright, statistics (mean, mean of abs vals, variance, max, min)
21412154 for the numeric values in the header and data sections of POTCAR are stored
2142- in self._meta ["stats"]
2155+ in self._summary_stats ["stats"]
21432156
21442157 tol is then used to match statistical values within a tolerance
21452158 """
21462159 functional_lexch = {
2147- "PE" : ["PBE" , "PBE_52" , "PBE_54" ],
2148- "CA" : ["LDA" , "LDA_52" , "LDA_54" , "LDA_US" , "Perdew_Zunger81" ],
2160+ "PE" : ["PBE" , "PBE_52" , "PBE_52_W_HASH" , " PBE_54" , "PBE_54_W_HASH" , "PBE_64 " ],
2161+ "CA" : ["LDA" , "LDA_52" , "LDA_52_W_HASH" , " LDA_54" , "LDA_54_W_HASH" , "LDA_64 " , "LDA_US" , "Perdew_Zunger81" ],
21492162 "91" : ["PW91" , "PW91_US" ],
21502163 }
21512164
@@ -2164,8 +2177,9 @@ def is_valid(self) -> bool:
21642177 )
21652178
21662179 def parse_fortran_style_str (input_str : str ) -> Any :
2167- """Parse any input string as bool, int, float, or failing that, str. Used to parse FORTRAN-generated
2168- POTCAR files where it's unknown a priori what type of data will be encountered.
2180+ """Parse any input string as bool, int, float, or failing that, str.
2181+ Used to parse FORTRAN-generated POTCAR files where it's unknown
2182+ a priori what type of data will be encountered.
21692183 """
21702184 input_str = input_str .strip ()
21712185
@@ -2225,7 +2239,9 @@ def data_stats(data_list: Sequence) -> dict:
22252239 "MAX" : arr .max (),
22262240 }
22272241
2228- summary_stats = { # for this PotcarSingle instance
2242+ # NB: to add future summary stats in a way that's consistent with PMG,
2243+ # it's easiest to save the summary stats as an attr of PotcarSingle
2244+ self ._summary_stats = { # for this PotcarSingle instance
22292245 "keywords" : {
22302246 "header" : [kwd .lower () for kwd in self .keywords ],
22312247 "data" : psp_keys ,
@@ -2239,12 +2255,12 @@ def data_stats(data_list: Sequence) -> dict:
22392255 data_match_tol = 1e-6
22402256 for ref_psp in possible_potcar_matches :
22412257 key_match = all (
2242- set (ref_psp ["keywords" ][key ]) == set (summary_stats ["keywords" ][key ]) # type: ignore
2258+ set (ref_psp ["keywords" ][key ]) == set (self . _summary_stats ["keywords" ][key ]) # type: ignore
22432259 for key in ["header" , "data" ]
22442260 )
22452261
22462262 data_diff = [
2247- abs (ref_psp ["stats" ][key ][stat ] - summary_stats ["stats" ][key ][stat ]) # type: ignore
2263+ abs (ref_psp ["stats" ][key ][stat ] - self . _summary_stats ["stats" ][key ][stat ]) # type: ignore
22482264 for stat in ["MEAN" , "ABSMEAN" , "VAR" , "MIN" , "MAX" ]
22492265 for key in ["header" , "data" ]
22502266 ]
@@ -2274,6 +2290,57 @@ def __repr__(self) -> str:
22742290 return f"{ cls_name } ({ symbol = } , { functional = } , { TITEL = } , { VRHFIN = } , { n_valence_elec = :.0f} )"
22752291
22762292
2293+ def _gen_potcar_summary_stats (
2294+ append : bool = False ,
2295+ vasp_psp_dir : str | None = None ,
2296+ summary_stats_filename : str = f"{ module_dir } /potcar_summary_stats.json.gz" ,
2297+ ):
2298+ """
2299+ This function solely intended to be used for PMG development to regenerate the
2300+ potcar_summary_stats.json.gz file used to validate POTCARs
2301+
2302+ THIS FUNCTION IS DESTRUCTIVE. It will completely overwrite your potcar_summary_stats.json.gz.
2303+
2304+ Args:
2305+ append (bool): Change whether data is appended to the existing potcar_summary_stats.json.gz,
2306+ or if a completely new file is generated. Defaults to False.
2307+ PMG_VASP_PSP_DIR (str): Change where this function searches for POTCARs
2308+ defaults to the PMG_VASP_PSP_DIR environment variable if not set. Defaults to None.
2309+ summary_stats_filename (str): Name of the output summary stats file. Defaults to
2310+ '<pymatgen_install_dir>/io/vasp/potcar_summary_stats.json.gz'.
2311+ """
2312+ func_dir_exist : dict [str , str ] = {}
2313+ vasp_psp_dir = vasp_psp_dir or SETTINGS .get ("PMG_VASP_PSP_DIR" )
2314+ for func in PotcarSingle .functional_dir :
2315+ cpsp_dir = f"{ vasp_psp_dir } /{ PotcarSingle .functional_dir [func ]} "
2316+ if os .path .isdir (cpsp_dir ):
2317+ func_dir_exist [func ] = PotcarSingle .functional_dir [func ]
2318+ else :
2319+ warnings .warn (f"missing { PotcarSingle .functional_dir [func ]} POTCAR directory" )
2320+
2321+ # use append = True if a new POTCAR library is released to add new summary stats
2322+ # without completely regenerating the dict of summary stats
2323+ # use append = False to completely regenerate the summary stats dict
2324+ new_summary_stats = loadfn (summary_stats_filename ) if append else {}
2325+
2326+ for func in func_dir_exist :
2327+ new_summary_stats .setdefault (func , {}) # initialize dict if key missing
2328+
2329+ potcar_list = [
2330+ * glob (f"{ vasp_psp_dir } /{ func_dir_exist [func ]} /POTCAR*" ),
2331+ * glob (f"{ vasp_psp_dir } /{ func_dir_exist [func ]} /*/POTCAR*" ),
2332+ ]
2333+ for potcar in potcar_list :
2334+ psp = PotcarSingle .from_file (potcar )
2335+ new_summary_stats [func ][psp .TITEL .replace (" " , "" )] = {
2336+ "LEXCH" : psp .LEXCH ,
2337+ "VRHFIN" : psp .VRHFIN .replace (" " , "" ),
2338+ ** psp ._summary_stats ,
2339+ }
2340+
2341+ dumpfn (new_summary_stats , summary_stats_filename )
2342+
2343+
22772344class Potcar (list , MSONable ):
22782345 """
22792346 Object for reading and writing POTCAR files for calculations. Consists of a
0 commit comments