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

123 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-06-18 01:20 +0000

1# fmt: off 

2 

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

4 

5Felix Hanke hanke@liverpool.ac.uk 

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

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

8 

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

10""" 

11 

12import os 

13import re 

14 

15import numpy as np 

16 

17from ase.calculators.genericfileio import ( 

18 BaseProfile, 

19 CalculatorTemplate, 

20 GenericFileIOCalculator, 

21 read_stdout, 

22) 

23from ase.io.aims import write_aims, write_control 

24 

25 

26def get_aims_version(string): 

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

28 return match.group(1) 

29 

30 

31class AimsProfile(BaseProfile): 

32 configvars = {'default_species_directory'} 

33 

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

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

36 self.default_species_directory = default_species_directory 

37 

38 def get_calculator_command(self, inputfile): 

39 return [] 

40 

41 def version(self): 

42 return get_aims_version(read_stdout(self._split_command)) 

43 

44 

45class AimsTemplate(CalculatorTemplate): 

46 _label = 'aims' 

47 

48 def __init__(self): 

49 super().__init__( 

50 'aims', 

51 [ 

52 'energy', 

53 'free_energy', 

54 'forces', 

55 'stress', 

56 'stresses', 

57 'dipole', 

58 'magmom', 

59 ], 

60 ) 

61 

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

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

64 

65 def update_parameters(self, properties, parameters): 

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

67 

68 Parameters 

69 ---------- 

70 properties: list of str 

71 The list of properties to calculate 

72 parameters: dict 

73 The parameters used to perform the calculation. 

74 

75 Returns 

76 ------- 

77 dict 

78 The updated parameters object 

79 """ 

80 parameters = dict(parameters) 

81 property_flags = { 

82 'forces': 'compute_forces', 

83 'stress': 'compute_analytical_stress', 

84 'stresses': 'compute_heat_flux', 

85 } 

86 # Ensure FHI-aims will calculate all desired properties 

87 for property in properties: 

88 aims_name = property_flags.get(property, None) 

89 if aims_name is not None: 

90 parameters[aims_name] = True 

91 

92 if 'dipole' in properties: 

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

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

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

96 elif 'output' not in parameters: 

97 parameters['output'] = ['dipole'] 

98 

99 return parameters 

100 

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

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

103 

104 Parameters 

105 ---------- 

106 directory : Path 

107 The working directory to store the input files. 

108 atoms : atoms.Atoms 

109 The atoms object to perform the calculation on. 

110 parameters: dict 

111 The parameters used to perform the calculation. 

112 properties: list of str 

113 The list of properties to calculate 

114 """ 

115 parameters = self.update_parameters(properties, parameters) 

116 

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

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

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

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

121 

122 if scaled is None: 

123 scaled = np.all(atoms.pbc) 

124 if write_velocities is None: 

125 write_velocities = atoms.has('momenta') 

126 

127 if geo_constrain is None: 

128 geo_constrain = scaled and 'relax_geometry' in parameters 

129 

130 have_lattice_vectors = atoms.pbc.any() 

131 have_k_grid = ( 

132 'k_grid' in parameters 

133 or 'kpts' in parameters 

134 or 'k_grid_density' in parameters 

135 ) 

136 if have_lattice_vectors and not have_k_grid: 

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

138 if not have_lattice_vectors and have_k_grid: 

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

140 

141 geometry_in = directory / 'geometry.in' 

142 

143 write_aims( 

144 geometry_in, 

145 atoms, 

146 scaled, 

147 geo_constrain, 

148 write_velocities=write_velocities, 

149 ghosts=ghosts, 

150 ) 

151 

152 control = directory / 'control.in' 

153 

154 if ( 

155 'species_dir' not in parameters 

156 and profile.default_species_directory is not None 

157 ): 

158 parameters['species_dir'] = profile.default_species_directory 

159 

160 write_control(control, atoms, parameters) 

161 

162 def execute(self, directory, profile): 

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

164 errorfile=self.errorname) 

165 

166 def read_results(self, directory): 

167 from ase.io.aims import read_aims_results 

168 

169 dst = directory / self.outputname 

170 return read_aims_results(dst, index=-1) 

171 

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

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

174 

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

176 return [profile.command] 

177 

178 def socketio_parameters(self, unixsocket, port): 

179 if port: 

180 use_pimd_wrapper = ('localhost', port) 

181 else: 

182 # (INET port number should be unused.) 

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

184 

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

186 

187 

188class Aims(GenericFileIOCalculator): 

189 def __init__( 

190 self, 

191 profile=None, 

192 directory='.', 

193 **kwargs, 

194 ): 

195 """Construct the FHI-aims calculator. 

196 

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

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

199 native keywords. 

200 

201 

202 Arguments: 

203 

204 cubes: AimsCube object 

205 Cube file specification. 

206 

207 tier: int or array of ints 

208 Set basis set tier for all atomic species. 

209 

210 plus_u : dict 

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

212 

213 kwargs : dict 

214 Any of the base class arguments. 

215 

216 """ 

217 

218 super().__init__( 

219 template=AimsTemplate(), 

220 profile=profile, 

221 parameters=kwargs, 

222 directory=directory, 

223 ) 

224 

225 

226class AimsCube: 

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

228 

229 def __init__( 

230 self, 

231 origin=(0, 0, 0), 

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

233 points=(50, 50, 50), 

234 plots=(), 

235 ): 

236 """parameters: 

237 

238 origin, edges, points: 

239 Same as in the FHI-aims output 

240 plots: 

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

242 

243 self.name = 'AimsCube' 

244 self.origin = origin 

245 self.edges = edges 

246 self.points = points 

247 self.plots = plots 

248 

249 def ncubes(self): 

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

251 return len(self.plots) 

252 

253 def move_to_base_name(self, basename): 

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

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

256 easier tracking""" 

257 for plot in self.plots: 

258 found = False 

259 cube = plot.split() 

260 if ( 

261 cube[0] == 'total_density' 

262 or cube[0] == 'spin_density' 

263 or cube[0] == 'delta_density' 

264 ): 

265 found = True 

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

267 new_name = basename + '.' + old_name 

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

269 found = True 

270 state = int(cube[1]) 

271 s_state = cube[1] 

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

273 if state < i: 

274 s_state = '0' + s_state 

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

276 new_name = basename + '.' + old_name 

277 if found: 

278 # XXX Should not use platform dependent commands! 

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

280 

281 def add_plot(self, name): 

282 """in case you forgot one ...""" 

283 self.plots += [name] 

284 

285 def write(self, file): 

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

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

288 file.write(' cube origin ') 

289 for ival in self.origin: 

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

291 file.write('\n') 

292 for i in range(3): 

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

294 for ival in self.edges[i]: 

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

296 file.write('\n') 

297 if self.ncubes() > 1: 

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

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