Skip to content

Commit 5028956

Browse files
authored
Merge pull request #6035 from brockdyer03/pp-zval-parsers
Nexus: Add Z-valence parsers for UPF, XML, and POTCAR files
2 parents 79324ed + dbede60 commit 5028956

2 files changed

Lines changed: 482 additions & 0 deletions

File tree

nexus/nexus/pseudopotential.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
#====================================================================#
3838

3939
import os
40+
from os import PathLike
4041
from pathlib import Path
42+
import re
4143
import numpy as np
4244
from .execute import execute
4345
from .fileio import TextFile
@@ -88,6 +90,187 @@ def pp_elem_label(filename,guard=False):
8890
#end def pp_elem_label
8991

9092

93+
def read_upf_z_valence(file: PathLike) -> int | float:
94+
"""Read Z-valence from a UPF-compliant pseudopotential file."""
95+
# Bind these to the function so we only compile them once.
96+
if not (
97+
hasattr(read_upf_z_valence, "zval_xml_like_pattern")
98+
and hasattr(read_upf_z_valence, "zval_old_pattern")
99+
):
100+
# Regex:
101+
# `z[_ ]?valence` -> "z_valence" or "z valence"
102+
# ` *=? *` -> "=" or " =" or " = " or " " (whitespace optional)
103+
# `([\d\.eEdD]+)` -> Capturing group gets any numbers in scientific notation.
104+
# `\"? *() *\"?` -> Anything between quotes or not, with optional whitespace around it too.
105+
# Note: Both of these are similar, but the first is key then value and the second is value then key.
106+
zval_xml_like_pattern = re.compile(
107+
pattern = r'z[_ ]?valence *=? *\"? *([\d\.eEdD]+) *\"?',
108+
flags = re.IGNORECASE,
109+
)
110+
zval_old_pattern = re.compile(
111+
pattern = r'\"? *([ \d\.eEdD]+) *\"? *=? *z[_ ]?valence',
112+
flags = re.IGNORECASE,
113+
)
114+
read_upf_z_valence.zval_xml_like_pattern = zval_xml_like_pattern
115+
read_upf_z_valence.zval_old_pattern = zval_old_pattern
116+
#end if
117+
118+
zval = None
119+
with open(file, "r") as pseudo:
120+
found_header_start = False
121+
while not found_header_start:
122+
line = pseudo.readline()
123+
if "<PP_HEADER" in line:
124+
found_header_start = True
125+
126+
if "/>" in line or "</PP_HEADER>" in line: # One-line header
127+
zval = re.search(
128+
pattern = read_upf_z_valence.zval_xml_like_pattern,
129+
string = line,
130+
)
131+
else:
132+
# We're at the header, but we don't know where the Z-valence is.
133+
# Search until we hit a line with a proper end token, or until we hit 200 lines.
134+
i = 0
135+
while i < 200:
136+
i += 1
137+
line = pseudo.readline().lower()
138+
if "valence" in line:
139+
zval = re.search(
140+
pattern = read_upf_z_valence.zval_xml_like_pattern,
141+
string = line,
142+
)
143+
if zval is None:
144+
zval = re.search(
145+
pattern = read_upf_z_valence.zval_old_pattern,
146+
string = line,
147+
)
148+
break
149+
elif "/>" in line or "</PP_HEADER>" in line:
150+
break
151+
#end if
152+
#end while
153+
#end if
154+
155+
if zval is None:
156+
error(
157+
f"Could not find Z valence in file: {file!s}\n"
158+
"You may need to provide the Z valence manually!"
159+
)
160+
else:
161+
zval = float(zval.group(1).lower().replace("d", "e"))
162+
163+
if zval <= 0 or zval > 118:
164+
error(
165+
f"Invalid Z-valence found in file, must be in range (0, 118], but is {zval}!"
166+
)
167+
# Round to 8 digits
168+
if round(zval, 8).is_integer():
169+
return int(zval)
170+
else:
171+
return zval
172+
#end def read_upf_z_valence
173+
174+
175+
def read_xml_z_valence(file: PathLike) -> int | float:
176+
"""Read the Z-valence from a QMCPACK-compatible XML pseudopotential file."""
177+
# Bind these to the function so we only compile them once.
178+
if not hasattr(read_xml_z_valence, "zval_pattern"):
179+
# Regex:
180+
# `zval *= *` -> "zval=" or "zval = " or "zval =" or "zval= "
181+
# `([\d\.eEdD]+)` -> Capturing group gets any numbers in scientific notation.
182+
# `\"? *() *\"?` -> Anything between quotes or not, with optional whitespace around it too.
183+
read_xml_z_valence.zval_pattern = re.compile(r'zval *= *\" *([\d\.eEdD]+) *\"')
184+
185+
header_lines = []
186+
with open(file, "r") as xml:
187+
header_started = False
188+
for line in xml:
189+
if "<header" in line:
190+
header_started = True
191+
192+
if header_started:
193+
header_lines.append(line)
194+
195+
if "/>" in line or "</header>" in line:
196+
if line not in header_lines:
197+
header_lines.append(line)
198+
break
199+
200+
header = " ".join(header_lines)
201+
zval = re.search(read_xml_z_valence.zval_pattern, header)
202+
203+
if zval is None:
204+
error(
205+
f"Could not find Z valence in file: {file!s}\n"
206+
"You may need to provide the Z valence manually!"
207+
)
208+
else:
209+
zval = float(zval.group(1).lower().replace("d", "e"))
210+
211+
if zval <= 0 or zval > 118:
212+
error(
213+
f"Invalid Z-valence found in file, must be in range (0, 118], but is {zval}!"
214+
)
215+
# Round to 8 digits
216+
if round(zval, 8).is_integer():
217+
return int(zval)
218+
else:
219+
return zval
220+
#end def read_xml_z_valence
221+
222+
223+
def read_potcar_z_valence(file: PathLike) -> int | float:
224+
"""Read the Z-valence from a POTCAR file.
225+
226+
This function uses the format specifications from the VASP wiki, and
227+
assumes that the file is a valid POTCAR, so the second line should
228+
be the Z-valence [1]_.
229+
230+
References
231+
----------
232+
.. [1] https://vasp.at/wiki/POTCAR#File_format
233+
"""
234+
if not hasattr(read_potcar_z_valence, "zval_pattern"):
235+
# Regex:
236+
# `ZVAL ?= ?` -> "ZVAL=" or "ZVAL = " or "ZVAL =" or "ZVAL= "
237+
# `([\d\.eEdD]+)` -> Capturing group gets any numbers in scientific notation.
238+
read_potcar_z_valence.zval_pattern = re.compile(r"ZVAL ?= ?([\d\.eEdD]+)")
239+
file = Path(file).resolve()
240+
with open(file, "r") as potcar:
241+
potcar.readline() # Skip first line
242+
z_valence = potcar.readline().strip()
243+
try:
244+
zval = float(z_valence)
245+
except ValueError: # Improperly formatted POTCAR, but try alternative location
246+
for line in potcar:
247+
if "ZVAL" in line:
248+
zval = re.search(
249+
pattern = read_potcar_z_valence.zval_pattern,
250+
string = line
251+
)
252+
break
253+
254+
if zval is None:
255+
error(
256+
f"Could not find Z valence in file: {file!s}\n"
257+
"You may need to provide the Z valence manually!"
258+
)
259+
else:
260+
zval = float(zval.group(1).lower().replace("d", "e"))
261+
262+
if zval <= 0 or zval > 118:
263+
error(
264+
f"Invalid Z-valence found in file, must be in range (0, 118], but is {zval}!"
265+
)
266+
# Round to 8 digits
267+
if round(zval, 8).is_integer():
268+
return int(zval)
269+
else:
270+
return zval
271+
#end def read_potcar_z_valence
272+
273+
91274
# basic interface for nexus, only gamess really needs this for now
92275
class PseudoFile(DevBase):
93276
def __init__(self,filepath=None):

0 commit comments

Comments
 (0)