Coverage for sparc/sparc_parsers/pseudopotential.py: 100%
92 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-12 01:13 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-12 01:13 +0000
1"""Provide a simple parser for pseudo potentials
3The psp8 format is defined in abinit manual
4https://docs.abinit.org/developers/psp8_info/
6The first
8"""
10import os
11import re
12import shutil
13from pathlib import Path
14from warnings import warn
16import numpy as np
17from ase.data import atomic_names, chemical_symbols
20class NotPSP8Format(Exception):
21 def __init__(self, message):
22 self.message = message
25class NoMatchingPseudopotential(FileNotFoundError):
26 def __init__(self, message):
27 self.message = message
30class MultiplePseudoPotentialFiles(Exception):
31 def __init__(self, message):
32 self.message = message
35def parse_psp8_header(text):
36 """Parse the first 4 lines of psp8 text
37 if parsing failed, raise exception
38 """
39 header = text.split("\n")[:4]
40 # Line 1
42 psp8_magic = r"^\s*(?P<symbol>[\w]+)\s+ONCVPSP-(?P<psp8ver>[\d.\w]+)\s+r\_core=(?P<r_core>.*?)$"
43 psp8_data = {}
44 match = re.search(psp8_magic, header[0])
45 if match is None:
46 raise NotPSP8Format(f"The pseudopotential file is not in PSP8 format!")
47 mgroup = match.groupdict()
48 psp8_data["symbol"] = mgroup["symbol"].strip()
49 psp8_data["psp8ver"] = mgroup["psp8ver"].strip()
50 psp8_data["r_core"] = np.fromstring(mgroup["r_core"].strip(), sep=" ", dtype=float)
51 # Line 2
52 zatom, zion, pspd, *_ = header[1].split()
53 psp8_data["zatom"] = float(zatom)
54 psp8_data["zion"] = float(zion)
55 # TODO: should we make date in datetime object?
56 psp8_data["pspd"] = str(pspd).strip()
58 # Line 3
59 pspcod, pspxc, lmax, lloc, mmax, r2well, *_ = header[2].split()
60 psp8_data["pspcod"] = int(pspcod)
61 psp8_data["pspxc"] = int(pspxc)
62 psp8_data["lmax"] = int(lmax)
63 psp8_data["lloc"] = int(lloc)
64 psp8_data["mmax"] = int(mmax)
65 psp8_data["r2well"] = int(r2well)
67 # Line 4
68 rchrg, fchrg, qchrg, *_ = header[3].split()
69 psp8_data["rchrg"] = float(rchrg)
70 psp8_data["fchrg"] = float(fchrg)
71 psp8_data["qchrg"] = float(qchrg)
73 # Sanity check the symbol and zatom
74 int_zatom = int(psp8_data["zatom"])
75 if chemical_symbols[int_zatom] != psp8_data["symbol"]:
76 raise NotPSP8Format(
77 (
78 f"The symbol defined in pseudo potential {psp8_data['symbol']} does not match "
79 f"the Z={int_zatom}!"
80 )
81 )
82 return psp8_data
85def infer_pseudo_path(symbol, search_path):
86 """Given an element symbol like 'Na', get the file name
87 of the search_path (resolved) that search through the search path
89 TODO: shall we support multiple directories?
90 TODO: add a `setup` option like VASP?
91 """
92 search_path = Path(search_path).resolve()
93 potfiles = (
94 list(search_path.glob("*.psp8"))
95 + list(search_path.glob("*.psp"))
96 + list(search_path.glob("*.pot"))
97 )
98 candidates = []
99 for pf in potfiles:
100 try:
101 psp8_data = parse_psp8_header(open(pf, "r").read())
102 except Exception as e:
103 print(e)
104 psp8_data = None
106 if psp8_data:
107 if psp8_data["symbol"] == symbol:
108 candidates.append(pf)
109 if len(candidates) == 0:
110 raise NoMatchingPseudopotential(
111 (
112 f"No pseudopotential file for {symbol} found "
113 "under the search path {search_path}!"
114 )
115 )
116 elif len(candidates) > 1:
117 msg = (
118 f"There are multiple psp8 files for {symbol}:\n"
119 f"{candidates}. Please select the desired pseudopotential file!"
120 )
121 raise MultiplePseudoPotentialFiles(msg)
123 else:
124 return candidates[0]
127def copy_psp_file(source_pot, target_dir, use_symbol=False):
128 """Copy the pseudo potential file `source_pot` under `target_dir`
130 if use_symbol is True, rename the potential to '{symbol}.psp8'
132 the function returns the name of the pseudo potential file
133 """
135 source_pot = Path(source_pot)
136 target_dir = Path(target_dir)
137 psp8_data = parse_psp8_header(open(source_pot, "r").read())
138 symbol = psp8_data["symbol"]
139 if use_symbol:
140 potname = f"{symbol}.psp8"
141 else:
142 potname = source_pot.name
144 target_pot = target_dir / potname
145 # shutil will copy
146 shutil.copy(source_pot, target_pot)
147 return potname
150def find_pseudo_path(symbol, search_path=None, pseudopotential_mapping={}):
151 """Get the pseudo potential file at best effort
153 Searching priorities
154 1) if pseudopotential_mapping has symbol as key, use the file name
155 There are two possibilities
156 i) filename does not contain directory information: i.e. Na-pbe.pot
157 use search_path / filename for the mapping
158 ii) filename contains directory information, directly use the file name
159 2) No pseudopotential_mapping is given, get the psp from search_path
160 """
161 mapping_psp = pseudopotential_mapping.get(symbol, None)
162 if mapping_psp is None:
163 if search_path is None:
164 raise NoMatchingPseudopotential(
165 (
166 f"No psudopotentials found for {symbol} "
167 "because neither search_path nor psp name is provided."
168 )
169 )
170 return infer_pseudo_path(symbol, search_path)
171 else:
172 str_psp = str(mapping_psp)
173 mapping_psp = Path(mapping_psp)
174 # if psp contains any path information (/, \\), treat is as a direct file
175 is_node_file_name = (mapping_psp.name == str_psp) and (os.sep not in str_psp)
176 if is_node_file_name:
177 if search_path is None:
178 raise NoMatchingPseudopotential(
179 (
180 f"You provide the pseudopotential name {mapping_psp} but no search path is defined. I cannot locate the pseudopotential file."
181 )
182 )
183 mapping_psp = Path(search_path) / str_psp
185 if not mapping_psp.is_file():
186 warn(
187 (
188 f"Pseudopotential file {mapping_psp} is defined by user input but cannot be found!\n"
189 "Please check your setup. I'll write the .ion file anyway."
190 )
191 )
192 return mapping_psp