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

100 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-12 01:13 +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 textwrap 

11from warnings import warn 

12 

13import numpy as np 

14from ase.units import Bohr 

15 

16# Safe wrappers for both string and fd 

17from ase.utils import reader, writer 

18 

19from ..api import SparcAPI 

20from .utils import ( 

21 bisect_and_strip, 

22 make_reverse_mapping, 

23 read_block_input, 

24 strip_comments, 

25) 

26 

27 

28class InvalidSortingComment(ValueError): 

29 def __init__(self, message): 

30 self.message = message 

31 

32 

33defaultAPI = SparcAPI() 

34 

35 

36@reader 

37def _read_ion(fileobj, validator=defaultAPI): 

38 """ 

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

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

41 

42 

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

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

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

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

47 """ 

48 contents = fileobj.read() 

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

50 data, comments = strip_comments(contents) 

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

52 sort, resort, new_comments = _read_sort_comment(comments) 

53 

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

55 atom_type_bounds = [i for i, x in enumerate(data) if "ATOM_TYPE" in x] + [len(data)] 

56 atom_blocks = [ 

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

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

59 ] 

60 

61 return { 

62 "ion": { 

63 "atom_blocks": atom_blocks, 

64 "comments": new_comments, 

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

66 } 

67 } 

68 

69 

70@writer 

71def _write_ion( 

72 fileobj, 

73 data_dict, 

74 validator=defaultAPI, 

75): 

76 """ 

77 Writes the ion file content from the atom_dict 

78 

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

80 

81 The data_dict takes similar format as _read_ion 

82 

83 Basically, we want to ensure 

84 data_dict = _read_ion("some.ion") 

85 _write_ion("some.ion", data_dict) 

86 shows the same format 

87 """ 

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

89 if ion_dict is None: 

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

91 if "atom_blocks" not in ion_dict: 

92 raise ValueError( 

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

94 ) 

95 

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

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

98 if len(comments) == 0: 

99 comments = [banner] 

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

101 comments = [banner] + comments 

102 

103 # Handle the sorting mapping 

104 # the line wrap is 80 words 

105 if "sorting" in ion_dict: 

106 # print(ion_dict["sorting"]) 

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

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

109 if len(resort) > 0: 

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

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

112 comments.extend(index_lines) 

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

114 

115 for line in comments: 

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

117 

118 fileobj.write("\n") 

119 blocks = ion_dict["atom_blocks"] 

120 for block in blocks: 

121 for key in [ 

122 "ATOM_TYPE", 

123 "N_TYPE_ATOM", 

124 "PSEUDO_POT", 

125 "COORD_FRAC", 

126 "COORD", 

127 "SPIN", 

128 "RELAX", 

129 ]: 

130 val = block.get(key, None) 

131 # print(key, val) 

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

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

134 # TODO: change the API version 

135 if val is None: 

136 continue 

137 

138 val_string = validator.convert_value_to_string(key, val) 

139 # print(val_string) 

140 # TODO: make sure 1 line is accepted 

141 # TODO: write pads to vector lines 

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

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

144 ): 

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

146 else: 

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

148 fileobj.write(output) 

149 # TODO: check extra keys 

150 # TODO: how to handle multiple psp files? 

151 # Write a split line 

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

153 fileobj.write("\n") 

154 return 

155 

156 

157def _ion_coord_to_ase_pos(data_dict, cell=None): 

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

159 

160 Arguments: 

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

162 

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

164 to the atom_blocks 

165 """ 

166 treated_blocks = [] 

167 can_have_coord_frac = cell is not None 

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

169 for i, block in enumerate(ion_atom_blocks): 

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

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

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

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

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

175 if coord is not None: 

176 coord = coord * Bohr 

177 else: 

178 coord_frac = block["COORD_FRAC"] 

179 # Cell is already in Bohr 

180 coord = np.dot(coord_frac, cell) 

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

182 return 

183 

184 

185def _read_sort_comment(lines): 

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

187 Format 

188 

189 ASE-SORT: 

190 r_i r_j r_k .... 

191 END ASE-SORT 

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

193 """ 

194 i = 0 

195 resort = [] 

196 record = False 

197 new_lines = [] 

198 while i < len(lines): 

199 line = lines[i] 

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

201 i += 1 

202 if key == "ASE-SORT": 

203 record = True 

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

205 record = False 

206 break 

207 elif record is True: 

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

209 else: 

210 # Put original lines in new_lines 

211 new_lines.append(line) 

212 # Put all remaining lines in new_lines 

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

214 line = lines[j] 

215 if "ASE-SORT" in line: 

216 raise InvalidSortingComment( 

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

218 ) 

219 new_lines.append(line) 

220 if record: 

221 warn( 

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

223 ) 

224 sort = make_reverse_mapping(resort) 

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

226 return sort, resort, new_lines