Coverage for ase / io / gpaw_out.py: 83.80%

216 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 15:52 +0000

1# fmt: off 

2 

3import re 

4 

5import numpy as np 

6 

7from ase.atoms import Atoms 

8from ase.calculators.singlepoint import ( 

9 SinglePointDFTCalculator, 

10 SinglePointKPoint, 

11) 

12 

13 

14def index_startswith(lines: list[str], string: str) -> int: 

15 for i, line in enumerate(lines): 

16 if line.strip().startswith(string): 

17 return i 

18 raise ValueError 

19 

20 

21def index_pattern(lines: list[str], pattern: str) -> int: 

22 repat = re.compile(pattern) 

23 for i, line in enumerate(lines): 

24 if repat.match(line): 

25 return i 

26 raise ValueError 

27 

28 

29def read_forces(lines: list[str], 

30 ii: int, 

31 atoms: Atoms) -> tuple[list[tuple[float, float, float]], int]: 

32 f = [] 

33 for i in range(ii + 1, ii + 1 + len(atoms)): 

34 try: 

35 x, y, z = lines[i].split()[-3:] 

36 f.append((float(x), float(y), float(z))) 

37 except (ValueError, IndexError) as m: 

38 raise OSError(f'Malformed GPAW log file: {m}') 

39 return f, i 

40 

41 

42def read_stresses(lines: list[str], 

43 ii: int,) -> tuple[list[tuple[float, float, float]], int]: 

44 s = [] 

45 for i in range(ii + 1, ii + 4): 

46 try: 

47 x, y, z = lines[i].split()[-3:] 

48 s.append((float(x), float(y), float(z))) 

49 except (ValueError, IndexError) as m: 

50 raise OSError(f'Malformed GPAW log file: {m}') 

51 return s, i 

52 

53 

54def find_blocks(lines: list[str]) -> list[list[str]]: 

55 blocks = [] 

56 i1 = 0 

57 for i2, line in enumerate(lines): 

58 if line == 'positions:\n': 

59 if i1 > 0: 

60 blocks.append(lines[i1:i2]) 

61 i1 = i2 

62 blocks.append(lines[i1:]) 

63 return blocks 

64 

65 

66def read_gpaw_out(fileobj, index): # -> Union[Atoms, List[Atoms]]: 

67 """Read text output from GPAW calculation.""" 

68 lines = [line.lower() for line in fileobj.readlines()] 

69 

70 try: 

71 ii = index_startswith(lines, 'total charge:') 

72 except ValueError: 

73 q = None 

74 else: 

75 q = float(lines[ii].split()[2]) 

76 

77 images = [] 

78 for i, block in enumerate(find_blocks(lines)): 

79 # XXX wrong to pass the charge here, it could differ in each block! 

80 atoms = process_block(block, q, is_first=not images) 

81 if atoms is None: 

82 break 

83 

84 images.append(atoms) 

85 

86 if len(images) == 0: 

87 raise OSError('Corrupted GPAW-text file!') 

88 

89 return images[index] 

90 

91 

92def process_block(lines, q, is_first): 

93 try: 

94 i = lines.index('unit cell:\n') 

95 except ValueError: 

96 pass 

97 else: 

98 if lines[i + 2].startswith(' -'): 

99 del lines[i + 2] # old format 

100 cell: list[float | list[float]] = [] 

101 pbc = [] 

102 for line in lines[i + 2:i + 5]: 

103 words = line.split() 

104 if len(words) == 5: # old format 

105 cell.append(float(words[2])) 

106 pbc.append(words[1] == 'yes') 

107 else: # new format with GUC 

108 cell.append([float(word) for word in words[3:6]]) 

109 pbc.append(words[2] == 'yes') 

110 

111 symbols = [] 

112 positions = [] 

113 magmoms = [] 

114 for line in lines[1:]: 

115 words = line.split() 

116 if len(words) < 5: 

117 break 

118 n, symbol, x, y, z = words[:5] 

119 symbols.append(symbol.split('.')[0].title()) 

120 positions.append([float(x), float(y), float(z)]) 

121 if len(words) > 5: 

122 mom = float(words[-1].rstrip(')')) 

123 magmoms.append(mom) 

124 if len(symbols): 

125 atoms = Atoms(symbols=symbols, positions=positions, 

126 cell=cell, pbc=pbc) 

127 else: 

128 atoms = Atoms(cell=cell, pbc=pbc) 

129 

130 try: 

131 ii = index_pattern(lines, '\\d+ k-point') 

132 word = lines[ii].split() 

133 kx = int(word[2]) 

134 ky = int(word[4]) 

135 kz = int(word[6]) 

136 bz_kpts = (kx, ky, kz) 

137 ibz_kpts = int(lines[ii + 1].split()[0]) 

138 except (ValueError, TypeError, IndexError): 

139 bz_kpts = None 

140 ibz_kpts = None 

141 

142 try: 

143 i = index_startswith(lines, 'energy contributions relative to') + 2 

144 except ValueError: 

145 e = energy_contributions = None 

146 else: 

147 energy_contributions = {} 

148 while i < len(lines): 

149 fields = lines[i].split(':') 

150 if len(fields) == 2: 

151 name = fields[0] 

152 energy = float(fields[1]) 

153 energy_contributions[name] = energy 

154 if name in ['zero kelvin', 'extrapolated']: 

155 e = energy 

156 break 

157 i += 1 

158 else: # no break 

159 raise ValueError 

160 

161 try: 

162 ii = index_pattern(lines, '(fixed )?fermi level(s)?:') 

163 except ValueError: 

164 eFermi = None 

165 else: 

166 fields = lines[ii].split() 

167 try: 

168 def strip(string): 

169 for rubbish in '[],': 

170 string = string.replace(rubbish, '') 

171 return string 

172 eFermi = [float(strip(fields[-2])), 

173 float(strip(fields[-1]))] 

174 except ValueError: 

175 eFermi = float(fields[-1]) 

176 

177 # read Eigenvalues and occupations 

178 ii1 = ii2 = 1e32 

179 try: 

180 ii1 = index_startswith(lines, 'band eigenvalues occupancy') 

181 except ValueError: 

182 pass 

183 try: 

184 ii2 = index_startswith(lines, 'band eigenvalues occupancy') 

185 except ValueError: 

186 pass 

187 ii = min(ii1, ii2) 

188 if ii == 1e32: 

189 kpts = None 

190 else: 

191 ii += 1 

192 words = lines[ii].split() 

193 vals = [] 

194 while len(words) > 2: 

195 vals.append([float(w) for w in words]) 

196 ii += 1 

197 words = lines[ii].split() 

198 vals = np.array(vals).transpose() 

199 kpts = [SinglePointKPoint(1, 0, 0)] 

200 kpts[0].eps_n = vals[1] 

201 kpts[0].f_n = vals[2] 

202 if vals.shape[0] > 3: 

203 kpts.append(SinglePointKPoint(1, 1, 0)) 

204 kpts[1].eps_n = vals[3] 

205 kpts[1].f_n = vals[4] 

206 # read dipole moment 

207 try: 

208 ii = index_startswith(lines, 'dipole moment:') 

209 except ValueError: 

210 dipole = None 

211 else: 

212 line = lines[ii] 

213 for x in '()[],': 

214 line = line.replace(x, '') 

215 dipole = np.array([float(c) for c in line.split()[2:5]]) 

216 

217 try: 

218 ii = index_startswith(lines, 'local magnetic moments') 

219 except ValueError: 

220 pass 

221 else: 

222 magmoms = [] 

223 for j in range(ii + 1, ii + 1 + len(atoms)): 

224 line = lines[j] 

225 if '#' in line: # new GPAW format 

226 magmom = line.split()[-4].split(']')[0] 

227 else: 

228 magmom = line.split()[-1].rstrip(')') 

229 magmoms.append(float(magmom)) 

230 

231 try: 

232 ii = lines.index('forces in ev/ang:\n') 

233 except ValueError: 

234 f = None 

235 else: 

236 f, i = read_forces(lines, ii, atoms) 

237 

238 try: 

239 ii = lines.index('stress tensor:\n') 

240 except ValueError: 

241 stress_tensor = None 

242 else: 

243 stress_tensor, i = read_stresses(lines, ii) 

244 

245 try: 

246 parameters = {} 

247 ii = index_startswith(lines, 'vdw correction:') 

248 except ValueError: 

249 name = 'gpaw' 

250 else: 

251 name = lines[ii - 1].strip() 

252 # save uncorrected values 

253 parameters.update({ 

254 'calculator': 'gpaw', 

255 'uncorrected_energy': e, 

256 }) 

257 line = lines[ii + 1] 

258 assert line.startswith('energy:') 

259 e = float(line.split()[-1]) 

260 f, i = read_forces(lines, ii + 3, atoms) 

261 

262 if not is_first and e is None: 

263 return None 

264 

265 if q is not None and len(atoms) > 0: 

266 n = len(atoms) 

267 atoms.set_initial_charges([q / n] * n) 

268 

269 if magmoms: 

270 atoms.set_initial_magnetic_moments(magmoms) 

271 else: 

272 magmoms = None 

273 if e is not None or f is not None: 

274 calc = SinglePointDFTCalculator(atoms, energy=e, forces=f, 

275 dipole=dipole, magmoms=magmoms, 

276 efermi=eFermi, 

277 bzkpts=bz_kpts, ibzkpts=ibz_kpts, 

278 stress=stress_tensor) 

279 calc.name = name 

280 calc.parameters = parameters 

281 if energy_contributions is not None: 

282 calc.energy_contributions = energy_contributions 

283 if kpts is not None: 

284 calc.kpts = kpts 

285 atoms.calc = calc 

286 

287 return atoms