Coverage for /builds/ase/ase/ase/calculators/demonnano.py: 44.00%

150 statements  

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

1# fmt: off 

2 

3# flake8: noqa 

4"""This module defines an ASE interface to deMon-nano. 

5 

6Link to the open-source DFTB code deMon-nano: 

7http://demon-nano.ups-tlse.fr/ 

8 

9export ASE_DEMONNANO_COMMAND="/path/to/bin/deMon.username.x" 

10export DEMONNANO_BASIS_PATH="/path/to/basis/" 

11 

12The file 'deMon.inp' contains the input geometry and parameters 

13The file 'deMon.out' contains the results 

14 

15""" 

16import os 

17import os.path as op 

18# import subprocess 

19import pathlib as pl 

20 

21import numpy as np 

22 

23import ase.data 

24import ase.io 

25from ase.calculators.calculator import FileIOCalculator, Parameters, ReadError 

26from ase.units import Bohr, Hartree 

27 

28 

29class DemonNanoParameters(Parameters): 

30 """Parameters class for the calculator. 

31 

32 The options here are the most important ones that the user needs to be 

33 aware of. Further options accepted by deMon can be set in the dictionary 

34 input_arguments. 

35 

36 """ 

37 

38 def __init__( 

39 self, 

40 label='.', 

41 atoms=None, 

42 command=None, 

43 basis_path=None, 

44 restart_path='.', 

45 print_out='ASE', 

46 title='deMonNano input file', 

47 forces=False, 

48 input_arguments=None): 

49 kwargs = locals() 

50 kwargs.pop('self') 

51 Parameters.__init__(self, **kwargs) 

52 

53 

54class DemonNano(FileIOCalculator): 

55 """Calculator interface to the deMon-nano code. """ 

56 

57 implemented_properties = ['energy', 'forces'] 

58 

59 def __init__(self, **kwargs): 

60 """ASE interface to the deMon-nano code. 

61 

62 The deMon-nano code can be obtained from http://demon-nano.ups-tlse.fr/ 

63 

64 The ASE_DEMONNANO_COMMAND environment variable must be set to run the executable, in bash it would be set along the lines of 

65 export ASE_DEMONNANO_COMMAND="pathway-to-deMon-binary/deMon.username.x" 

66 

67 Parameters: 

68 

69 label : str 

70 relative path to the run directory 

71 atoms : Atoms object 

72 the atoms object 

73 command : str 

74 Command to run deMon. If not present, the environment variable ASE_DEMONNANO_COMMAND is used 

75 basis_path : str 

76 Relative path to the directory containing DFTB-SCC or DFTB-0 parameters 

77 If not present, the environment variable DEMONNANO_BASIS_PATH is used 

78 restart_path : str 

79 Relative path to the deMon restart dir 

80 title : str 

81 Title in the deMon input file. 

82 forces : bool 

83 If True a force calculation is enforced 

84 print_out : str | list 

85 Options for the printing in deMon 

86 input_arguments : dict 

87 Explicitly given input arguments. The key is the input keyword 

88 and the value is either a str, a list of str (will be written on the same line as the keyword), 

89 or a list of lists of str (first list is written on the first line, the others on following lines.) 

90 """ 

91 

92 parameters = DemonNanoParameters(**kwargs) 

93 

94 # basis path 

95 basis_path = parameters['basis_path'] 

96 if basis_path is None: 

97 basis_path = self.cfg.get('DEMONNANO_BASIS_PATH') 

98 

99 if basis_path is None: 

100 mess = 'The "DEMONNANO_BASIS_PATH" environment is not defined.' 

101 raise ValueError(mess) 

102 else: 

103 parameters['basis_path'] = basis_path 

104 

105 # Call the base class. 

106 FileIOCalculator.__init__( 

107 self, 

108 **parameters) 

109 

110 def __getitem__(self, key): 

111 """Convenience method to retrieve a parameter as 

112 calculator[key] rather than calculator.parameters[key] 

113 

114 Parameters: 

115 key : str, the name of the parameters to get. 

116 """ 

117 return self.parameters[key] 

118 

119 def write_input(self, atoms, properties=None, system_changes=None): 

120 """Write input (in)-file. 

121 See calculator.py for further details. 

122 

123 Parameters: 

124 atoms : The Atoms object to write. 

125 properties : The properties which should be calculated. 

126 system_changes : List of properties changed since last run. 

127 

128 """ 

129 # Call base calculator. 

130 FileIOCalculator.write_input( 

131 self, 

132 atoms=atoms, 

133 properties=properties, 

134 system_changes=system_changes) 

135 

136 if system_changes is None and properties is None: 

137 return 

138 

139 filename = self.label + '/deMon.inp' 

140 

141 # Start writing the file. 

142 with open(filename, 'w') as fd: 

143 # write keyword argument keywords 

144 value = self.parameters['title'] 

145 self._write_argument('TITLE', value, fd) 

146 fd.write('\n') 

147 

148 # obtain forces through a single BOMD step 

149 # only if forces is in properties, or if keyword forces is True 

150 value = self.parameters['forces'] 

151 if 'forces' in properties or value: 

152 self._write_argument('MDYNAMICS', 'ZERO', fd) 

153 self._write_argument('MDSTEP', 'MAX=1', fd) 

154 # default timestep is 0.25 fs if not enough - uncomment the line below 

155 # self._write_argument('TIMESTEP', '0.1', fd) 

156 

157 # print argument, here other options could change this 

158 value = self.parameters['print_out'] 

159 assert (isinstance(value, str)) 

160 

161 if len(value) != 0: 

162 self._write_argument('PRINT', value, fd) 

163 fd.write('\n') 

164 

165 # write general input arguments 

166 self._write_input_arguments(fd) 

167 

168 if 'BASISPATH' not in self.parameters['input_arguments']: 

169 value = self.parameters['basis_path'] 

170 fd.write(value) 

171 fd.write('\n') 

172 

173 # write geometry 

174 self._write_atomic_coordinates(fd, atoms) 

175 

176 # write xyz file for good measure. 

177 ase.io.write(self.label + '/deMon_atoms.xyz', self.atoms) 

178 

179 def read(self, restart_path): 

180 """Read parameters from directory restart_path.""" 

181 

182 self.set_label(restart_path) 

183 rpath = pl.Path(restart_path) 

184 

185 if not (rpath / 'deMon.inp').exists(): 

186 raise ReadError('The restart_path file {} does not exist' 

187 .format(rpath)) 

188 

189 self.atoms = self.deMon_inp_to_atoms(rpath / 'deMon.inp') 

190 

191 self.read_results() 

192 

193 def _write_input_arguments(self, fd): 

194 """Write directly given input-arguments.""" 

195 input_arguments = self.parameters['input_arguments'] 

196 

197 # Early return 

198 if input_arguments is None: 

199 return 

200 

201 for key, value in input_arguments.items(): 

202 self._write_argument(key, value, fd) 

203 

204 def _write_argument(self, key, value, fd): 

205 """Write an argument to file. 

206 key : a string coresponding to the input keyword 

207 value : the arguments, can be a string, a number or a list 

208 fd : and open file 

209 """ 

210 if key == 'BASISPATH': 

211 # Write a basis path to file. 

212 # Has to be in lowercase for deMon-nano to work 

213 line = value.lower() 

214 fd.write(line) 

215 fd.write('\n') 

216 elif not isinstance(value, (tuple, list)): 

217 # for only one argument, write on same line 

218 line = key.upper() 

219 line += ' ' + str(value).upper() 

220 fd.write(line) 

221 fd.write('\n') 

222 

223 # for a list, write first argument on the first line, 

224 # then the rest on new lines 

225 else: 

226 line = key 

227 if not isinstance(value[0], (tuple, list)): 

228 for i in range(len(value)): 

229 line += ' ' + str(value[i].upper()) 

230 fd.write(line) 

231 fd.write('\n') 

232 else: 

233 for i in range(len(value)): 

234 for j in range(len(value[i])): 

235 line += ' ' + str(value[i][j]).upper() 

236 fd.write(line) 

237 fd.write('\n') 

238 line = '' 

239 

240 def _write_atomic_coordinates(self, fd, atoms): 

241 """Write atomic coordinates. 

242 Parameters: 

243 - fd: An open file object. 

244 - atoms: An atoms object. 

245 """ 

246 # fd.write('#\n') 

247 # fd.write('# Atomic coordinates\n') 

248 # fd.write('#\n') 

249 fd.write('GEOMETRY CARTESIAN ANGSTROM\n') 

250 

251 for sym, pos in zip(atoms.symbols, atoms.positions): 

252 fd.write('{:9s} {:10.5f} {:10.5f} {:10.5f}\n'.format(sym, *pos)) 

253 

254 fd.write('\n') 

255 

256# Analysis routines 

257 def read_results(self): 

258 """Read the results from output files.""" 

259 self.read_energy() 

260 self.read_forces(self.atoms) 

261 # self.read_eigenvalues() 

262 

263 def read_energy(self): 

264 """Read energy from deMon.ase output file.""" 

265 

266 epath = pl.Path(self.label) 

267 

268 if not (epath / 'deMon.ase').exists(): 

269 raise ReadError('The deMonNano output file for ASE {} does not exist' 

270 .format(epath)) 

271 

272 filename = self.label + '/deMon.ase' 

273 

274 if op.isfile(filename): 

275 with open(filename) as fd: 

276 lines = fd.readlines() 

277 

278 for i in range(len(lines)): 

279 if lines[i].startswith(' DFTB total energy [Hartree]'): 

280 self.results['energy'] = float(lines[i + 1]) * Hartree 

281 break 

282 

283 def read_forces(self, atoms): 

284 """Read forces from the deMon.ase file.""" 

285 

286 natoms = len(atoms) 

287 epath = pl.Path(self.label) 

288 

289 if not (epath / 'deMon.ase').exists(): 

290 raise ReadError('The deMonNano output file for ASE {} does not exist' 

291 .format(epath)) 

292 

293 filename = self.label + '/deMon.ase' 

294 

295 with open(filename) as fd: 

296 lines = fd.readlines() 

297 

298 # find line where the forces start 

299 flag_found = False 

300 for i in range(len(lines)): 

301 if 'DFTB gradients at 0 time step in a.u.' in lines[i]: 

302 start = i + 1 

303 flag_found = True 

304 break 

305 

306 if flag_found: 

307 self.results['forces'] = np.zeros((natoms, 3), float) 

308 for i in range(natoms): 

309 line = [s for s in lines[i + start].strip().split(' ') 

310 if len(s) > 0] 

311 f = -np.array([float(x) for x in line[1:4]]) 

312 # output forces in a.u. 

313 # self.results['forces'][i, :] = f 

314 # output forces with real dimension 

315 self.results['forces'][i, :] = f * (Hartree / Bohr) 

316 

317 def deMon_inp_to_atoms(self, filename): 

318 """Routine to read deMon.inp and convert it to an atoms object.""" 

319 

320 read_flag = False 

321 chem_symbols = [] 

322 xyz = [] 

323 

324 with open(filename) as fd: 

325 for line in fd: 

326 if 'GEOMETRY' in line: 

327 read_flag = True 

328 if 'ANGSTROM' in line: 

329 coord_units = 'Ang' 

330 elif 'BOHR' in line: 

331 coord_units = 'Bohr' 

332 

333 if read_flag: 

334 tokens = line.split() 

335 symbol = tokens[0] 

336 xyz_loc = np.array(tokens[1:4]).astype(float) 

337 if read_flag and tokens: 

338 chem_symbols.append(symbol) 

339 xyz.append(xyz_loc) 

340 

341 if coord_units == 'Bohr': 

342 xyz *= Bohr 

343 

344 # set atoms object 

345 atoms = ase.Atoms(symbols=chem_symbols, positions=xyz) 

346 

347 return atoms