Coverage for /builds/alexhroom/ase/ase/calculators/aims.py: 42.28%

123 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2024-08-05 14:37 +0000

1"""This module defines an ASE interface to FHI-aims. 

2 

3Felix Hanke hanke@liverpool.ac.uk 

4Jonas Bjork j.bjork@liverpool.ac.uk 

5Simon P. Rittmeyer simon.rittmeyer@tum.de 

6 

7Edits on (24.11.2021) by Thomas A. R. Purcell purcell@fhi-berlin.mpg.de 

8""" 

9 

10import os 

11import re 

12 

13import numpy as np 

14 

15from ase.calculators.genericfileio import (BaseProfile, CalculatorTemplate, 

16 GenericFileIOCalculator, 

17 read_stdout) 

18from ase.io.aims import write_aims, write_control 

19 

20 

21def get_aims_version(string): 

22 match = re.search(r'\s*FHI-aims version\s*:\s*(\S+)', string, re.M) 

23 return match.group(1) 

24 

25 

26class AimsProfile(BaseProfile): 

27 configvars = {'default_species_directory'} 

28 

29 def __init__(self, command, default_species_directory=None, **kwargs): 

30 super().__init__(command, **kwargs) 

31 self.default_species_directory = default_species_directory 

32 

33 def get_calculator_command(self, inputfile): 

34 return [] 

35 

36 def version(self): 

37 return get_aims_version(read_stdout(self._split_command)) 

38 

39 

40class AimsTemplate(CalculatorTemplate): 

41 _label = 'aims' 

42 

43 def __init__(self): 

44 super().__init__( 

45 'aims', 

46 [ 

47 'energy', 

48 'free_energy', 

49 'forces', 

50 'stress', 

51 'stresses', 

52 'dipole', 

53 'magmom', 

54 ], 

55 ) 

56 

57 self.outputname = f'{self._label}.out' 

58 self.errorname = f'{self._label}.err' 

59 

60 def update_parameters(self, properties, parameters): 

61 """Check and update the parameters to match the desired calculation 

62 

63 Parameters 

64 ---------- 

65 properties: list of str 

66 The list of properties to calculate 

67 parameters: dict 

68 The parameters used to perform the calculation. 

69 

70 Returns 

71 ------- 

72 dict 

73 The updated parameters object 

74 """ 

75 parameters = dict(parameters) 

76 property_flags = { 

77 'forces': 'compute_forces', 

78 'stress': 'compute_analytical_stress', 

79 'stresses': 'compute_heat_flux', 

80 } 

81 # Ensure FHI-aims will calculate all desired properties 

82 for property in properties: 

83 aims_name = property_flags.get(property, None) 

84 if aims_name is not None: 

85 parameters[aims_name] = True 

86 

87 if 'dipole' in properties: 

88 if 'output' in parameters and 'dipole' not in parameters['output']: 

89 parameters['output'] = list(parameters['output']) 

90 parameters['output'].append('dipole') 

91 elif 'output' not in parameters: 

92 parameters['output'] = ['dipole'] 

93 

94 return parameters 

95 

96 def write_input(self, profile, directory, atoms, parameters, properties): 

97 """Write the geometry.in and control.in files for the calculation 

98 

99 Parameters 

100 ---------- 

101 directory : Path 

102 The working directory to store the input files. 

103 atoms : atoms.Atoms 

104 The atoms object to perform the calculation on. 

105 parameters: dict 

106 The parameters used to perform the calculation. 

107 properties: list of str 

108 The list of properties to calculate 

109 """ 

110 parameters = self.update_parameters(properties, parameters) 

111 

112 ghosts = parameters.pop('ghosts', None) 

113 geo_constrain = parameters.pop('geo_constrain', None) 

114 scaled = parameters.pop('scaled', None) 

115 write_velocities = parameters.pop('write_velocities', None) 

116 

117 if scaled is None: 

118 scaled = np.all(atoms.pbc) 

119 if write_velocities is None: 

120 write_velocities = atoms.has('momenta') 

121 

122 if geo_constrain is None: 

123 geo_constrain = scaled and 'relax_geometry' in parameters 

124 

125 have_lattice_vectors = atoms.pbc.any() 

126 have_k_grid = ( 

127 'k_grid' in parameters 

128 or 'kpts' in parameters 

129 or 'k_grid_density' in parameters 

130 ) 

131 if have_lattice_vectors and not have_k_grid: 

132 raise RuntimeError('Found lattice vectors but no k-grid!') 

133 if not have_lattice_vectors and have_k_grid: 

134 raise RuntimeError('Found k-grid but no lattice vectors!') 

135 

136 geometry_in = directory / 'geometry.in' 

137 

138 write_aims( 

139 geometry_in, 

140 atoms, 

141 scaled, 

142 geo_constrain, 

143 write_velocities=write_velocities, 

144 ghosts=ghosts, 

145 ) 

146 

147 control = directory / 'control.in' 

148 

149 if ( 

150 'species_dir' not in parameters 

151 and profile.default_species_directory is not None 

152 ): 

153 parameters['species_dir'] = profile.default_species_directory 

154 

155 write_control(control, atoms, parameters) 

156 

157 def execute(self, directory, profile): 

158 profile.run(directory, None, self.outputname, 

159 errorfile=self.errorname) 

160 

161 def read_results(self, directory): 

162 from ase.io.aims import read_aims_results 

163 

164 dst = directory / self.outputname 

165 return read_aims_results(dst, index=-1) 

166 

167 def load_profile(self, cfg, **kwargs): 

168 return AimsProfile.from_config(cfg, self.name, **kwargs) 

169 

170 def socketio_argv(self, profile, unixsocket, port): 

171 return [profile.command] 

172 

173 def socketio_parameters(self, unixsocket, port): 

174 if port: 

175 use_pimd_wrapper = ('localhost', port) 

176 else: 

177 # (INET port number should be unused.) 

178 use_pimd_wrapper = (f'UNIX:{unixsocket}', 31415) 

179 

180 return dict(use_pimd_wrapper=use_pimd_wrapper, compute_forces=True) 

181 

182 

183class Aims(GenericFileIOCalculator): 

184 def __init__( 

185 self, 

186 profile=None, 

187 directory='.', 

188 **kwargs, 

189 ): 

190 """Construct the FHI-aims calculator. 

191 

192 The keyword arguments (kwargs) can be one of the ASE standard 

193 keywords: 'xc', 'kpts' and 'smearing' or any of FHI-aims' 

194 native keywords. 

195 

196 

197 Arguments: 

198 

199 cubes: AimsCube object 

200 Cube file specification. 

201 

202 tier: int or array of ints 

203 Set basis set tier for all atomic species. 

204 

205 plus_u : dict 

206 For DFT+U. Adds a +U term to one specific shell of the species. 

207 

208 kwargs : dict 

209 Any of the base class arguments. 

210 

211 """ 

212 

213 super().__init__( 

214 template=AimsTemplate(), 

215 profile=profile, 

216 parameters=kwargs, 

217 directory=directory, 

218 ) 

219 

220 

221class AimsCube: 

222 'Object to ensure the output of cube files, can be attached to Aims object' 

223 

224 def __init__( 

225 self, 

226 origin=(0, 0, 0), 

227 edges=[(0.1, 0.0, 0.0), (0.0, 0.1, 0.0), (0.0, 0.0, 0.1)], 

228 points=(50, 50, 50), 

229 plots=(), 

230 ): 

231 """parameters: 

232 

233 origin, edges, points: 

234 Same as in the FHI-aims output 

235 plots: 

236 what to print, same names as in FHI-aims""" 

237 

238 self.name = 'AimsCube' 

239 self.origin = origin 

240 self.edges = edges 

241 self.points = points 

242 self.plots = plots 

243 

244 def ncubes(self): 

245 """returns the number of cube files to output""" 

246 return len(self.plots) 

247 

248 def move_to_base_name(self, basename): 

249 """when output tracking is on or the base namem is not standard, 

250 this routine will rename add the base to the cube file output for 

251 easier tracking""" 

252 for plot in self.plots: 

253 found = False 

254 cube = plot.split() 

255 if ( 

256 cube[0] == 'total_density' 

257 or cube[0] == 'spin_density' 

258 or cube[0] == 'delta_density' 

259 ): 

260 found = True 

261 old_name = cube[0] + '.cube' 

262 new_name = basename + '.' + old_name 

263 if cube[0] == 'eigenstate' or cube[0] == 'eigenstate_density': 

264 found = True 

265 state = int(cube[1]) 

266 s_state = cube[1] 

267 for i in [10, 100, 1000, 10000]: 

268 if state < i: 

269 s_state = '0' + s_state 

270 old_name = cube[0] + '_' + s_state + '_spin_1.cube' 

271 new_name = basename + '.' + old_name 

272 if found: 

273 # XXX Should not use platform dependent commands! 

274 os.system('mv ' + old_name + ' ' + new_name) 

275 

276 def add_plot(self, name): 

277 """in case you forgot one ...""" 

278 self.plots += [name] 

279 

280 def write(self, file): 

281 """write the necessary output to the already opened control.in""" 

282 file.write('output cube ' + self.plots[0] + '\n') 

283 file.write(' cube origin ') 

284 for ival in self.origin: 

285 file.write(str(ival) + ' ') 

286 file.write('\n') 

287 for i in range(3): 

288 file.write(' cube edge ' + str(self.points[i]) + ' ') 

289 for ival in self.edges[i]: 

290 file.write(str(ival) + ' ') 

291 file.write('\n') 

292 if self.ncubes() > 1: 

293 for i in range(self.ncubes() - 1): 

294 file.write('output cube ' + self.plots[i + 1] + '\n')