Coverage for /builds/ase/ase/ase/io/elk.py: 86.49%

259 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3import collections 

4from pathlib import Path 

5 

6import numpy as np 

7 

8from ase import Atoms 

9from ase.units import Bohr, Hartree 

10from ase.utils import reader, writer 

11 

12elk_parameters = {'swidth': Hartree} 

13 

14 

15@reader 

16def read_elk(fd): 

17 """Import ELK atoms definition. 

18 

19 Reads unitcell, atom positions, magmoms from elk.in/GEOMETRY.OUT file. 

20 """ 

21 

22 lines = fd.readlines() 

23 

24 scale = np.ones(4) # unit cell scale 

25 positions = [] 

26 cell = [] 

27 symbols = [] 

28 magmoms = [] 

29 

30 # find cell scale 

31 for n, line in enumerate(lines): 

32 if line.split() == []: 

33 continue 

34 if line.strip() == 'scale': 

35 scale[0] = float(lines[n + 1]) 

36 elif line.startswith('scale'): 

37 scale[int(line.strip()[-1])] = float(lines[n + 1]) 

38 for n, line in enumerate(lines): 

39 if line.split() == []: 

40 continue 

41 if line.startswith('avec'): 

42 cell = np.array( 

43 [[float(v) * scale[1] for v in lines[n + 1].split()], 

44 [float(v) * scale[2] for v in lines[n + 2].split()], 

45 [float(v) * scale[3] for v in lines[n + 3].split()]]) 

46 if line.startswith('atoms'): 

47 lines1 = lines[n + 1:] # start subsearch 

48 spfname = [] 

49 natoms = [] 

50 atpos = [] 

51 bfcmt = [] 

52 for n1, line1 in enumerate(lines1): 

53 if line1.split() == []: 

54 continue 

55 if 'spfname' in line1: 

56 spfnamenow = lines1[n1].split()[0] 

57 spfname.append(spfnamenow) 

58 natomsnow = int(lines1[n1 + 1].split()[0]) 

59 natoms.append(natomsnow) 

60 atposnow = [] 

61 bfcmtnow = [] 

62 for line in lines1[n1 + 2:n1 + 2 + natomsnow]: 

63 atposnow.append([float(v) for v in line.split()[0:3]]) 

64 if len(line.split()) == 6: # bfcmt present 

65 bfcmtnow.append( 

66 [float(v) for v in line.split()[3:]]) 

67 atpos.append(atposnow) 

68 bfcmt.append(bfcmtnow) 

69 # symbols, positions, magmoms based on ELK spfname, atpos, and bfcmt 

70 symbols = '' 

71 positions = [] 

72 magmoms = [] 

73 for n, s in enumerate(spfname): 

74 symbols += str(s[1:].split('.')[0]) * natoms[n] 

75 positions += atpos[n] # assumes fractional coordinates 

76 if len(bfcmt[n]) > 0: 

77 # how to handle cases of magmoms being one or three dim array? 

78 magmoms += [m[-1] for m in bfcmt[n]] 

79 atoms = Atoms(symbols, scaled_positions=positions, cell=[1, 1, 1]) 

80 if len(magmoms) > 0: 

81 atoms.set_initial_magnetic_moments(magmoms) 

82 # final cell scale 

83 cell = cell * scale[0] * Bohr 

84 

85 atoms.set_cell(cell, scale_atoms=True) 

86 atoms.pbc = True 

87 return atoms 

88 

89 

90@writer 

91def write_elk_in(fd, atoms, parameters=None): 

92 if parameters is None: 

93 parameters = {} 

94 

95 parameters = dict(parameters) 

96 

97 species_path = parameters.pop('species_dir', None) 

98 species_path = parameters.pop('sppath', species_path) 

99 

100 if parameters.get('spinpol') is None: 

101 if atoms.get_initial_magnetic_moments().any(): 

102 parameters['spinpol'] = True 

103 

104 if 'xctype' in parameters: 

105 if 'xc' in parameters: 

106 raise RuntimeError("You can't use both 'xctype' and 'xc'!") 

107 

108 if parameters.get('autokpt'): 

109 if 'kpts' in parameters: 

110 raise RuntimeError("You can't use both 'autokpt' and 'kpts'!") 

111 if 'ngridk' in parameters: 

112 raise RuntimeError( 

113 "You can't use both 'autokpt' and 'ngridk'!") 

114 if 'ngridk' in parameters: 

115 if 'kpts' in parameters: 

116 raise RuntimeError("You can't use both 'ngridk' and 'kpts'!") 

117 

118 if parameters.get('autoswidth'): 

119 if 'smearing' in parameters: 

120 raise RuntimeError( 

121 "You can't use both 'autoswidth' and 'smearing'!") 

122 if 'swidth' in parameters: 

123 raise RuntimeError( 

124 "You can't use both 'autoswidth' and 'swidth'!") 

125 

126 inp = {} 

127 inp.update(parameters) 

128 

129 if 'xc' in parameters: 

130 xctype = {'LDA': 3, # PW92 

131 'PBE': 20, 

132 'REVPBE': 21, 

133 'PBESOL': 22, 

134 'WC06': 26, 

135 'AM05': 30, 

136 'mBJLDA': (100, 208, 12)}[parameters['xc']] 

137 inp['xctype'] = xctype 

138 del inp['xc'] 

139 

140 if 'kpts' in parameters: 

141 # XXX should generalize kpts handling. 

142 from ase.calculators.calculator import kpts2mp 

143 mp = kpts2mp(atoms, parameters['kpts']) 

144 inp['ngridk'] = tuple(mp) 

145 vkloff = [] # is this below correct? 

146 for nk in mp: 

147 if nk % 2 == 0: # shift kpoint away from gamma point 

148 vkloff.append(0.5) 

149 else: 

150 vkloff.append(0) 

151 inp['vkloff'] = vkloff 

152 del inp['kpts'] 

153 

154 if 'smearing' in parameters: 

155 name = parameters.smearing[0].lower() 

156 if name == 'methfessel-paxton': 

157 stype = parameters.smearing[2] 

158 else: 

159 stype = {'gaussian': 0, 

160 'fermi-dirac': 3, 

161 }[name] 

162 inp['stype'] = stype 

163 inp['swidth'] = parameters.smearing[1] 

164 del inp['smearing'] 

165 

166 # convert keys to ELK units 

167 for key, value in inp.items(): 

168 if key in elk_parameters: 

169 inp[key] /= elk_parameters[key] 

170 

171 # write all keys 

172 for key, value in inp.items(): 

173 fd.write(f'{key}\n') 

174 if isinstance(value, bool): 

175 fd.write(f'.{("false", "true")[value]}.\n\n') 

176 elif isinstance(value, (int, float)): 

177 fd.write(f'{value}\n\n') 

178 else: 

179 fd.write('%s\n\n' % ' '.join([str(x) for x in value])) 

180 

181 # cell 

182 fd.write('avec\n') 

183 for vec in atoms.cell: 

184 fd.write('%.14f %.14f %.14f\n' % tuple(vec / Bohr)) 

185 fd.write('\n') 

186 

187 # atoms 

188 species = {} 

189 symbols = [] 

190 for a, (symbol, m) in enumerate( 

191 zip(atoms.get_chemical_symbols(), 

192 atoms.get_initial_magnetic_moments())): 

193 if symbol in species: 

194 species[symbol].append((a, m)) 

195 else: 

196 species[symbol] = [(a, m)] 

197 symbols.append(symbol) 

198 fd.write('atoms\n%d\n' % len(species)) 

199 # scaled = atoms.get_scaled_positions(wrap=False) 

200 scaled = np.linalg.solve(atoms.cell.T, atoms.positions.T).T 

201 for symbol in symbols: 

202 fd.write(f"'{symbol}.in' : spfname\n") 

203 fd.write('%d\n' % len(species[symbol])) 

204 for a, m in species[symbol]: 

205 fd.write('%.14f %.14f %.14f 0.0 0.0 %.14f\n' % 

206 (tuple(scaled[a]) + (m,))) 

207 fd.write('\n') 

208 

209 # Elk seems to concatenate path and filename in such a way 

210 # that we must put a / at the end: 

211 if species_path is not None: 

212 fd.write('sppath\n') 

213 fd.write(f"'{species_path.rstrip('/')}/'\n\n") 

214 

215 

216class ElkReader: 

217 def __init__(self, path): 

218 self.path = Path(path) 

219 

220 def _read_everything(self): 

221 yield from self._read_energy() 

222 

223 with (self.path / 'INFO.OUT').open() as fd: 

224 yield from parse_elk_info(fd) 

225 

226 with (self.path / 'EIGVAL.OUT').open() as fd: 

227 yield from parse_elk_eigval(fd) 

228 

229 with (self.path / 'KPOINTS.OUT').open() as fd: 

230 yield from parse_elk_kpoints(fd) 

231 

232 def read_everything(self): 

233 dct = dict(self._read_everything()) 

234 

235 # The eigenvalue/occupation tables do not say whether there are 

236 # two spins, so we have to reshape them from 1 x K x SB to S x K x B: 

237 spinpol = dct.pop('spinpol') 

238 if spinpol: 

239 for name in 'eigenvalues', 'occupations': 

240 array = dct[name] 

241 _, nkpts, nbands_double = array.shape 

242 assert _ == 1 

243 assert nbands_double % 2 == 0 

244 nbands = nbands_double // 2 

245 newarray = np.empty((2, nkpts, nbands)) 

246 newarray[0, :, :] = array[0, :, :nbands] 

247 newarray[1, :, :] = array[0, :, nbands:] 

248 if name == 'eigenvalues': 

249 # Verify that eigenvalues are still sorted: 

250 diffs = np.diff(newarray, axis=2) 

251 assert all(diffs.flat[:] > 0) 

252 dct[name] = newarray 

253 return dct 

254 

255 def _read_energy(self): 

256 txt = (self.path / 'TOTENERGY.OUT').read_text() 

257 tokens = txt.split() 

258 energy = float(tokens[-1]) * Hartree 

259 yield 'free_energy', energy 

260 yield 'energy', energy 

261 

262 

263def parse_elk_kpoints(fd): 

264 header = next(fd) 

265 lhs, rhs = header.split(':', 1) 

266 assert 'k-point, vkl, wkpt' in rhs, header 

267 nkpts = int(lhs.strip()) 

268 

269 kpts = np.empty((nkpts, 3)) 

270 weights = np.empty(nkpts) 

271 

272 for ikpt in range(nkpts): 

273 line = next(fd) 

274 tokens = line.split() 

275 kpts[ikpt] = np.array(tokens[1:4]).astype(float) 

276 weights[ikpt] = float(tokens[4]) 

277 yield 'ibz_kpoints', kpts 

278 yield 'kpoint_weights', weights 

279 

280 

281def parse_elk_info(fd): 

282 dct = collections.defaultdict(list) 

283 fd = iter(fd) 

284 

285 spinpol = None 

286 converged = False 

287 actually_did_not_converge = False 

288 # Legacy code kept track of both these things, which is strange. 

289 # Why could a file both claim to converge *and* not converge? 

290 

291 # We loop over all lines and extract also data that occurs 

292 # multiple times (e.g. in multiple SCF steps) 

293 for line in fd: 

294 # "name of quantity : 1 2 3" 

295 tokens = line.split(':', 1) 

296 if len(tokens) == 2: 

297 lhs, rhs = tokens 

298 dct[lhs.strip()].append(rhs.strip()) 

299 

300 elif line.startswith('Convergence targets achieved'): 

301 converged = True 

302 elif 'reached self-consistent loops maximum' in line.lower(): 

303 actually_did_not_converge = True 

304 

305 if 'Spin treatment' in line: 

306 # (Somewhat brittle doing multi-line stuff here.) 

307 line = next(fd) 

308 spinpol = line.strip() == 'spin-polarised' 

309 

310 yield 'converged', converged and not actually_did_not_converge 

311 if spinpol is None: 

312 raise RuntimeError('Could not determine spin treatment') 

313 yield 'spinpol', spinpol 

314 

315 if 'Fermi' in dct: 

316 yield 'fermi_level', float(dct['Fermi'][-1]) * Hartree 

317 

318 if 'total force' in dct: 

319 forces = [] 

320 for line in dct['total force']: 

321 forces.append(line.split()) 

322 yield 'forces', np.array(forces, float) * (Hartree / Bohr) 

323 

324 

325def parse_elk_eigval(fd): 

326 

327 def match_int(line, word): 

328 number, colon, word1 = line.split() 

329 assert word1 == word 

330 assert colon == ':' 

331 return int(number) 

332 

333 def skip_spaces(line=''): 

334 while not line.strip(): 

335 line = next(fd) 

336 return line 

337 

338 line = skip_spaces() 

339 nkpts = match_int(line, 'nkpt') # 10 : nkpts 

340 line = next(fd) 

341 nbands = match_int(line, 'nstsv') # 15 : nstsv 

342 

343 eigenvalues = np.empty((nkpts, nbands)) 

344 occupations = np.empty((nkpts, nbands)) 

345 kpts = np.empty((nkpts, 3)) 

346 

347 for ikpt in range(nkpts): 

348 line = skip_spaces() 

349 tokens = line.split() 

350 assert tokens[-1] == 'vkl', tokens 

351 assert ikpt + 1 == int(tokens[0]) 

352 kpts[ikpt] = np.array(tokens[1:4]).astype(float) 

353 

354 line = next(fd) # "(state, eigenvalue and occupancy below)" 

355 assert line.strip().startswith('(state,'), line 

356 for iband in range(nbands): 

357 line = next(fd) 

358 tokens = line.split() # (band number, eigenval, occ) 

359 assert iband + 1 == int(tokens[0]) 

360 eigenvalues[ikpt, iband] = float(tokens[1]) 

361 occupations[ikpt, iband] = float(tokens[2]) 

362 

363 yield 'ibz_kpoints', kpts 

364 yield 'eigenvalues', eigenvalues[None] * Hartree 

365 yield 'occupations', occupations[None]