@@ -2436,22 +2436,28 @@ def data_stats(data_list: Sequence) -> dict:
2436
2436
2437
2437
data_match_tol : float = 1e-6
2438
2438
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 ):
2451
2440
return True
2452
2441
2453
2442
return False
2454
2443
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
+
2455
2461
def write_file (self , filename : str ) -> None :
2456
2462
"""Write PotcarSingle to a file.
2457
2463
@@ -2562,6 +2568,47 @@ def verify_potcar(self) -> tuple[bool, bool]:
2562
2568
2563
2569
return has_sha256 , hash_is_valid
2564
2570
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
+
2565
2612
def identify_potcar (
2566
2613
self ,
2567
2614
mode : Literal ["data" , "file" ] = "data" ,
@@ -2599,19 +2646,9 @@ def identify_potcar(
2599
2646
if self .VRHFIN .replace (" " , "" ) != ref_psp ["VRHFIN" ]:
2600
2647
continue
2601
2648
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
+ ):
2615
2652
identity ["potcar_functionals" ].append (func )
2616
2653
identity ["potcar_symbols" ].append (ref_psp ["symbol" ])
2617
2654
@@ -2861,8 +2898,28 @@ def symbols(self, symbols: Sequence[str]) -> None:
2861
2898
2862
2899
@property
2863
2900
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 )
2866
2923
2867
2924
def as_dict (self ) -> dict :
2868
2925
"""MSONable dict representation."""
@@ -2885,22 +2942,19 @@ def from_dict(cls, dct: dict) -> Self:
2885
2942
return Potcar (symbols = dct ["symbols" ], functional = dct ["functional" ])
2886
2943
2887
2944
@classmethod
2888
- def from_file (cls , filename : PathLike ) -> Self :
2945
+ def from_str (cls , data : str ) :
2889
2946
"""
2890
- Reads Potcar from file .
2947
+ Read Potcar from a string .
2891
2948
2892
- Args:
2893
- filename: Filename
2949
+ :param data: Potcar as a string.
2894
2950
2895
2951
Returns:
2896
2952
Potcar
2897
2953
"""
2898
- with zopen (filename , mode = "rt" , encoding = "utf-8" ) as file :
2899
- fdata = file .read ()
2900
-
2901
2954
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" ):
2904
2958
if p_strip := psingle_str .strip ():
2905
2959
psingle = PotcarSingle (f"{ p_strip } \n End of Dataset\n " )
2906
2960
potcar .append (psingle )
@@ -2912,6 +2966,20 @@ def from_file(cls, filename: PathLike) -> Self:
2912
2966
potcar .functional = functionals [0 ]
2913
2967
return potcar
2914
2968
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
+
2915
2983
def write_file (self , filename : PathLike ) -> None :
2916
2984
"""Write Potcar to a file.
2917
2985
@@ -2948,6 +3016,42 @@ def set_symbols(
2948
3016
else :
2949
3017
self .extend (PotcarSingle (sym_potcar_map [el ]) for el in symbols )
2950
3018
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
+
2951
3055
2952
3056
class UnknownPotcarWarning (UserWarning ):
2953
3057
"""Warning raised when POTCAR hashes do not pass validation."""
0 commit comments