Coverage for sparc/sparc_parsers/ion.py: 94%

151 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-18 16:19 +0000

1""" 

2Created on Thu Oct 18 14:16:21 2018 

3 

4Ben Comer (Georgia Tech) 

5 

6This file has been heavily modified since SPARC 0.1 

7 

8TODO: more descriptions about this file io parser 

9""" 

10import re 

11import textwrap 

12from warnings import warn 

13 

14import numpy as np 

15from ase.units import Bohr 

16 

17# Safe wrappers for both string and fd 

18from ase.utils import reader, writer 

19 

20from ..api import SparcAPI 

21from .utils import ( 

22 bisect_and_strip, 

23 make_reverse_mapping, 

24 read_block_input, 

25 strip_comments, 

26) 

27 

28 

29class InvalidSortingComment(ValueError): 

30 def __init__(self, message): 

31 self.message = message 

32 

33 

34defaultAPI = SparcAPI() 

35 

36 

37@reader 

38def _read_ion(fileobj, validator=defaultAPI): 

39 """ 

40 Read information from the .ion file. Note, this method does not return an atoms object, 

41 but rather return a dict. Thus the label option is not necessary to keep 

42 

43 

44 Reads an ion file. Because some of the information necessary to create 

45 an atoms object is found in the .inpt file, this function also attemtps to read 

46 that as a source of data. If the file is not found or the information is invalid, 

47 it will look for it in the comments of the ion file, as written. 

48 """ 

49 contents = fileobj.read() 

50 # label = get_label(fileobj, ".ion") 

51 data, comments = strip_comments(contents) 

52 # We do not read the cell at this time! 

53 sort, resort, new_comments = _read_sort_comment(comments) 

54 

55 # find the index for all atom type lines. They should be at the top of their block 

56 # @TT 2025.06.04 HUBBARD block comes at the end of the 

57 # ion file defines the last line in data 

58 hubbard_bounds = [i for i, x in enumerate(data) if "HUBBARD" in x] 

59 if len(hubbard_bounds) == 0: 

60 atom_type_bounds_lim = len(data) 

61 elif len(hubbard_bounds) == 1: 

62 # The hubbard bounds 

63 atom_type_bounds_lim = hubbard_bounds[0] 

64 else: 

65 # TODO: make it a format error 

66 raise ValueError("Bad .ion file format, multiple HUBBARD sections exist.") 

67 

68 atom_type_bounds = [i for i, x in enumerate(data) if re.match("^ATOM_TYPE", x)] 

69 atom_type_bounds += [atom_type_bounds_lim] 

70 atom_blocks = [ 

71 read_block_input(data[start:end], validator=validator) 

72 for start, end in zip(atom_type_bounds[:-1], atom_type_bounds[1:]) 

73 ] 

74 

75 extra_blocks = {} 

76 # Now handle hubbard information --> extra 

77 # the hubbard block is currently the last to appear in 

78 # .ion file 

79 if len(hubbard_bounds) == 1: 

80 hubbard_settings = _parse_hubbard_block( 

81 data[hubbard_bounds[0] :], validator=validator 

82 ) 

83 extra_blocks["hubbard"] = hubbard_settings 

84 

85 return { 

86 "ion": { 

87 "atom_blocks": atom_blocks, 

88 "comments": new_comments, 

89 "extra": extra_blocks, 

90 "sorting": {"sort": sort, "resort": resort}, 

91 } 

92 } 

93 

94 

95@writer 

96def _write_ion( 

97 fileobj, 

98 data_dict, 

99 validator=defaultAPI, 

100): 

101 """ 

102 Writes the ion file content from the atom_dict 

103 

104 Please note this is not a Atoms-compatible function! 

105 

106 The data_dict takes similar format as _read_ion 

107 

108 Basically, we want to ensure 

109 data_dict = _read_ion("some.ion") 

110 _write_ion("some.ion", data_dict) 

111 shows the same format 

112 """ 

113 ion_dict = data_dict.get("ion", None) 

114 if ion_dict is None: 

115 raise ValueError("No ion data provided in the input!") 

116 if "atom_blocks" not in ion_dict: 

117 raise ValueError( 

118 "Must provide a data-section in the data_dict (blocks of atomic information)" 

119 ) 

120 

121 comments = ion_dict.get("comments", []) 

122 banner = "Ion File Generated by SPARC ASE Calculator" 

123 if len(comments) == 0: 

124 comments = [banner] 

125 elif "ASE" not in comments[0]: 

126 comments = [banner] + comments 

127 

128 # Handle the sorting mapping 

129 # the line wrap is 80 words 

130 if "sorting" in ion_dict: 

131 # print(ion_dict["sorting"]) 

132 resort = ion_dict["sorting"].get("resort", []) 

133 # Write resort information only when it's actually useful 

134 if len(resort) > 0: 

135 comments.append("ASE-SORT:") 

136 index_lines = textwrap.wrap(" ".join(map(str, resort)), width=80) 

137 comments.extend(index_lines) 

138 comments.append("END ASE-SORT") 

139 

140 for line in comments: 

141 fileobj.write(f"# {line}\n") 

142 

143 fileobj.write("\n") 

144 blocks = ion_dict["atom_blocks"] 

145 for block in blocks: 

146 for key in [ 

147 "ATOM_TYPE", 

148 "N_TYPE_ATOM", 

149 "PSEUDO_POT", 

150 "COORD_FRAC", 

151 "COORD", 

152 "SPIN", 

153 "RELAX", 

154 ]: 

155 val = block.get(key, None) 

156 # print(key, val) 

157 if (key not in ["RELAX", "COORD", "COORD_FRAC", "SPIN"]) and (val is None): 

158 raise ValueError(f"Key {key} is not provided! Abort writing ion file") 

159 # TODO: change the API version 

160 if val is None: 

161 continue 

162 

163 val_string = validator.convert_value_to_string(key, val) 

164 # print(val_string) 

165 # TODO: make sure 1 line is accepted 

166 # TODO: write pads to vector lines 

167 if (val_string.count("\n") > 0) or ( 

168 key in ["COORD_FRAC", "COORD", "RELAX", "SPIN"] 

169 ): 

170 output = f"{key}:\n{val_string}\n" 

171 else: 

172 output = f"{key}: {val_string}\n" 

173 fileobj.write(output) 

174 # TODO: check extra keys 

175 # TODO: how to handle multiple psp files? 

176 # Write a split line 

177 # TODO: do we need to distinguish the last line? 

178 fileobj.write("\n") 

179 

180 # @TT 2025.06.04 add support for HUBBARD parameters 

181 extra_blocks = ion_dict.get("extra", {}) 

182 if "hubbard" in extra_blocks: 

183 _check_hubbard_block(ion_dict, extra_blocks["hubbard"]) 

184 _write_hubbard_block(fileobj, extra_blocks["hubbard"], validator) 

185 return 

186 

187 

188def _ion_coord_to_ase_pos(data_dict, cell=None): 

189 """Convert the COORD or COORD_FRAC from atom blocks to ASE's positions 

190 

191 Arguments: 

192 cell: a unit cell in ASE-unit (i.e. parsed from inpt._inpt_cell_to_ase_cell) 

193 

194 This function modifies the data_dict in-place to add a field '_ase_positions' 

195 to the atom_blocks 

196 """ 

197 treated_blocks = [] 

198 can_have_coord_frac = cell is not None 

199 ion_atom_blocks = data_dict["ion"]["atom_blocks"] 

200 for i, block in enumerate(ion_atom_blocks): 

201 if ("COORD" in block.keys()) and ("COORD_FRAC" in block.keys()): 

202 raise KeyError("COORD and COORD_FRAC cannot co-exist!") 

203 if (not can_have_coord_frac) and ("COORD_FRAC" in block.keys()): 

204 raise KeyError("COORD_FRAC must be acompanied by a cell!") 

205 coord = block.get("COORD", None) 

206 if coord is not None: 

207 coord = coord * Bohr 

208 else: 

209 coord_frac = block["COORD_FRAC"] 

210 # Cell is already in Bohr 

211 coord = np.dot(coord_frac, cell) 

212 data_dict["ion"]["atom_blocks"][i]["_ase_positions"] = coord 

213 return 

214 

215 

216def _read_sort_comment(lines): 

217 """Parse the atom sorting info from the comment lines 

218 Format 

219 

220 ASE-SORT: 

221 r_i r_j r_k .... 

222 END ASE-SORT 

223 where r_i etc are the indices in the original ASE atoms object 

224 """ 

225 i = 0 

226 resort = [] 

227 record = False 

228 new_lines = [] 

229 while i < len(lines): 

230 line = lines[i] 

231 key, value = bisect_and_strip(line, ":") 

232 i += 1 

233 if key == "ASE-SORT": 

234 record = True 

235 elif key == "END ASE-SORT": 

236 record = False 

237 break 

238 elif record is True: 

239 resort += list(map(int, line.strip().split(" "))) 

240 else: 

241 # Put original lines in new_lines 

242 new_lines.append(line) 

243 # Put all remaining lines in new_lines 

244 for j in range(i, len(lines)): 

245 line = lines[j] 

246 if "ASE-SORT" in line: 

247 raise InvalidSortingComment( 

248 "There appears to be multiple sorting information in the ion comment section!" 

249 ) 

250 new_lines.append(line) 

251 if record: 

252 warn( 

253 "ASE atoms resort comment block is not properly formatted, this may cause data loss!" 

254 ) 

255 sort = make_reverse_mapping(resort) 

256 assert set(sort) == set(resort), "Sort and resort info are of different length!" 

257 return sort, resort, new_lines 

258 

259 

260def _parse_hubbard_block(block, validator=defaultAPI): 

261 """Parse the hubbard blocks into the following list 

262 [{"U_ATOM_TYPE": <atom-name>, 

263 "U_VAL": array}, 

264 ] 

265 

266 A hubbard block (after stripping the extra comments) may look like: 

267 ['HUBBARD:' 

268 'U_ATOM_TYPE: Ni', 

269 'U_VAL: 0 0 0.05 0'] 

270 

271 The U_ATOM_TYPE and U_VAL must come in pairs ordered 

272 """ 

273 if "HUBBARD:" not in block[0]: 

274 raise ValueError("Ill-formatted HUBBARD block in .ion file!") 

275 if (len(block) - 1) % 2 != 0: 

276 raise ValueError("U_ATOM_TYPE and U_VAL are not paired in the HUBBARD block!") 

277 u_pairs = [] 

278 for i in range((len(block) - 1) // 2): 

279 u_sub_block = block[i + 1 : i + 3] 

280 u_dict = read_block_input(u_sub_block, validator) 

281 u_pairs.append(u_dict) 

282 return u_pairs 

283 

284 

285def _check_hubbard_block(ion_dict, hubbard_u_pairs): 

286 """Sanity check for hubbard parameters 

287 1. U_ATOM_TYPE must match one existing element 

288 2. No duplicated element of U_ATOM_TYPE 

289 3. U value must be 4-tuples 

290 """ 

291 structure_elements = set([entry["ATOM_TYPE"] for entry in ion_dict["atom_blocks"]]) 

292 hubbard_elements = set() 

293 for pair in hubbard_u_pairs: 

294 elem = pair["U_ATOM_TYPE"] 

295 if elem not in structure_elements: 

296 raise ValueError( 

297 f"Element {elem} in the HUBBARD setting does not exist in the input structure!" 

298 ) 

299 if elem in hubbard_elements: 

300 raise ValueError(f"Element {elem} is duplicated in the HUBBARD setting!") 

301 hubbard_elements.add(elem) 

302 val = pair["U_VAL"] 

303 if len(val) != 4: 

304 raise ValueError(f"U_VAL for element {elem} must have length of 4!") 

305 

306 

307def _write_hubbard_block(fileobj, u_pairs=[], validator=defaultAPI): 

308 """Write the HUBBARD U-blocks at the end of the .ion file 

309 format 

310 

311 HUBBARD: 

312 U_ATOM_TYPE: Ni 

313 U_VAL: 0 0 0.05 0 

314 

315 U_ATOM_TYPE: Cr 

316 U_VAL: 0 0 0.05 0 

317 """ 

318 if len(u_pairs) == 0: 

319 return 

320 fileobj.write("HUBBARD:\n") 

321 for u_pair in u_pairs: 

322 # TODO: add value checker 

323 for key in ("U_ATOM_TYPE", "U_VAL"): 

324 val = u_pair[key] 

325 val_string = validator.convert_value_to_string(key, val) 

326 fileobj.write(f"{key}: {val_string}\n") 

327 fileobj.write("\n") 

328 return