Coverage for /builds/ase/ase/ase/calculators/gromacs.py: 86.11%

144 statements  

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

1# fmt: off 

2 

3"""This module defines an ASE interface to GROMACS. 

4 

5http://www.gromacs.org/ 

6It is VERY SLOW compared to standard Gromacs 

7(due to slow formatted io required here). 

8 

9Mainly intended to be the MM part in the ase QM/MM 

10 

11Markus.Kaukonen@iki.fi 

12 

13To be done: 

141) change the documentation for the new file-io-calculator (test works now) 

152) change gromacs program names 

16-now: hard coded 

17-future: set as dictionary in params_runs 

18 

19""" 

20 

21import os 

22import subprocess 

23from glob import glob 

24 

25import numpy as np 

26 

27from ase import units 

28from ase.calculators.calculator import ( 

29 CalculatorSetupError, 

30 FileIOCalculator, 

31 all_changes, 

32) 

33from ase.io.gromos import read_gromos, write_gromos 

34 

35 

36def parse_gromacs_version(output): 

37 import re 

38 match = re.search(r'GROMACS version\:\s*(\S+)', output, re.M) 

39 return match.group(1) 

40 

41 

42def get_gromacs_version(executable): 

43 output = subprocess.check_output([executable, '--version'], 

44 encoding='utf-8') 

45 return parse_gromacs_version(output) 

46 

47 

48def do_clean(name='#*'): 

49 """ remove files matching wildcards """ 

50 myfiles = glob(name) 

51 for myfile in myfiles: 

52 try: 

53 os.remove(myfile) 

54 except OSError: 

55 pass 

56 

57 

58class Gromacs(FileIOCalculator): 

59 """Class for doing GROMACS calculations. 

60 Before running a gromacs calculation you must prepare the input files 

61 separately (pdb2gmx and grompp for instance.) 

62 

63 Input parameters for gromacs runs (the .mdp file) 

64 are given in self.params and can be set when initializing the calculator 

65 or by method set_own. 

66 for example:: 

67 

68 CALC_MM_RELAX = Gromacs() 

69 CALC_MM_RELAX.set_own_params('integrator', 'steep', 

70 'use steepest descent') 

71 

72 Run command line arguments for gromacs related programs: 

73 pdb2gmx, grompp, mdrun, energy, traj. These can be given as:: 

74 

75 CALC_MM_RELAX = Gromacs() 

76 CALC_MM_RELAX.set_own_params_runs('force_field', 'oplsaa') 

77 """ 

78 

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

80 discard_results_on_any_change = True 

81 

82 default_parameters = dict( 

83 define='-DFLEXIBLE', 

84 integrator='cg', 

85 nsteps='10000', 

86 nstfout='10', 

87 nstlog='10', 

88 nstenergy='10', 

89 nstlist='10', 

90 ns_type='grid', 

91 pbc='xyz', 

92 rlist='1.15', 

93 coulombtype='PME-Switch', 

94 rcoulomb='0.8', 

95 vdwtype='shift', 

96 rvdw='0.8', 

97 rvdw_switch='0.75', 

98 DispCorr='Ener') 

99 

100 def __init__(self, restart=None, 

101 ignore_bad_restart_file=FileIOCalculator._deprecated, 

102 label='gromacs', atoms=None, 

103 do_qmmm=False, clean=True, 

104 water_model='tip3p', force_field='oplsaa', command=None, 

105 **kwargs): 

106 """Construct GROMACS-calculator object. 

107 

108 Parameters 

109 ========== 

110 label: str 

111 Prefix to use for filenames (label.in, label.txt, ...). 

112 Default is 'gromacs'. 

113 

114 do_qmmm : bool 

115 Is gromacs used as mm calculator for a qm/mm calculation 

116 

117 clean : bool 

118 Remove gromacs backup files 

119 and old gormacs.* files 

120 

121 water_model: str 

122 Water model to be used in gromacs runs (see gromacs manual) 

123 

124 force_field: str 

125 Force field to be used in gromacs runs 

126 

127 command : str 

128 Gromacs executable; if None (default), choose available one from 

129 ('gmx', 'gmx_d', 'gmx_mpi', 'gmx_mpi_d') 

130 """ 

131 

132 self.do_qmmm = do_qmmm 

133 self.water_model = water_model 

134 self.force_field = force_field 

135 self.clean = clean 

136 self.params_doc = {} 

137 # add comments for gromacs input file 

138 self.params_doc['define'] = \ 

139 'flexible/ rigid water' 

140 self.params_doc['integrator'] = \ 

141 'md: molecular dynamics(Leapfrog), \n' + \ 

142 '; md-vv: molecular dynamics(Velocity Verlet), \n' + \ 

143 '; steep: steepest descent minimization, \n' + \ 

144 '; cg: conjugate cradient minimization \n' 

145 

146 self.positions = None 

147 self.atoms = None 

148 

149 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file, 

150 label, atoms, command=command, 

151 **kwargs) 

152 self.set(**kwargs) 

153 # default values for runtime parameters 

154 # can be changed by self.set_own_params_runs('key', 'value') 

155 self.params_runs = {} 

156 self.params_runs['index_filename'] = 'index.ndx' 

157 self.params_runs['init_structure'] = self.label + '.pdb' 

158 self.params_runs['water'] = self.water_model 

159 self.params_runs['force_field'] = self.force_field 

160 

161 # these below are required by qm/mm 

162 self.topology_filename = self.label + '.top' 

163 

164 # clean up gromacs backups 

165 if self.clean: 

166 do_clean('gromacs.???') 

167 

168 # write input files for gromacs program energy 

169 self.write_energy_files() 

170 

171 if self.do_qmmm: 

172 self.parameters['integrator'] = 'md' 

173 self.parameters['nsteps'] = '0' 

174 

175 def _get_name(self): 

176 return 'Gromacs' 

177 

178 def _execute_gromacs(self, command): 

179 """ execute gmx command 

180 Parameters 

181 ---------- 

182 command : str 

183 """ 

184 if self.command: 

185 subprocess.check_call(self.command + ' ' + command, shell=True) 

186 else: 

187 raise CalculatorSetupError('Missing gromacs executable') 

188 

189 def generate_g96file(self): 

190 """ from current coordinates (self.structure_file) 

191 write a structure file in .g96 format 

192 """ 

193 # generate structure file in g96 format 

194 write_gromos(self.label + '.g96', self.atoms) 

195 

196 def run_editconf(self): 

197 """ run gromacs program editconf, typically to set a simulation box 

198 writing to the input structure""" 

199 subcmd = 'editconf' 

200 command = ' '.join([ 

201 subcmd, 

202 '-f', self.label + '.g96', 

203 '-o', self.label + '.g96', 

204 self.params_runs.get('extra_editconf_parameters', ''), 

205 f'> {self.label}.{subcmd}.log 2>&1']) 

206 self._execute_gromacs(command) 

207 

208 def run_genbox(self): 

209 """Run gromacs program genbox, typically to solvate the system 

210 writing to the input structure 

211 as extra parameter you need to define the file containing the solvent 

212 

213 for instance:: 

214 

215 CALC_MM_RELAX = Gromacs() 

216 CALC_MM_RELAX.set_own_params_runs( 

217 'extra_genbox_parameters', '-cs spc216.gro') 

218 """ 

219 subcmd = 'genbox' 

220 command = ' '.join([ 

221 subcmd, 

222 '-cp', self.label + '.g96', 

223 '-o', self.label + '.g96', 

224 '-p', self.label + '.top', 

225 self.params_runs.get('extra_genbox_parameters', ''), 

226 f'> {self.label}.{subcmd}.log 2>&1']) 

227 self._execute_gromacs(command) 

228 

229 def run(self): 

230 """ runs a gromacs-mdrun with the 

231 current atom-configuration """ 

232 

233 # clean up gromacs backups 

234 if self.clean: 

235 do_clean('#*') 

236 

237 subcmd = 'mdrun' 

238 command = [subcmd] 

239 if self.do_qmmm: 

240 command += [ 

241 '-s', self.label + '.tpr', 

242 '-o', self.label + '.trr', 

243 '-e', self.label + '.edr', 

244 '-g', self.label + '.log', 

245 '-rerun', self.label + '.g96', 

246 self.params_runs.get('extra_mdrun_parameters', ''), 

247 '> QMMM.log 2>&1'] 

248 command = ' '.join(command) 

249 self._execute_gromacs(command) 

250 else: 

251 command += [ 

252 '-s', self.label + '.tpr', 

253 '-o', self.label + '.trr', 

254 '-e', self.label + '.edr', 

255 '-g', self.label + '.log', 

256 '-c', self.label + '.g96', 

257 self.params_runs.get('extra_mdrun_parameters', ''), 

258 '> MM.log 2>&1'] 

259 command = ' '.join(command) 

260 self._execute_gromacs(command) 

261 

262 atoms = read_gromos(self.label + '.g96') 

263 self.atoms = atoms.copy() 

264 

265 def generate_topology_and_g96file(self): 

266 """ from coordinates (self.label.+'pdb') 

267 and gromacs run input file (self.label + '.mdp) 

268 generate topology (self.label+'top') 

269 and structure file in .g96 format (self.label + '.g96') 

270 """ 

271 # generate structure and topology files 

272 # In case of predefinded topology file this is not done 

273 subcmd = 'pdb2gmx' 

274 command = ' '.join([ 

275 subcmd, 

276 '-f', self.params_runs['init_structure'], 

277 '-o', self.label + '.g96', 

278 '-p', self.label + '.top', 

279 '-ff', self.params_runs['force_field'], 

280 '-water', self.params_runs['water'], 

281 self.params_runs.get('extra_pdb2gmx_parameters', ''), 

282 f'> {self.label}.{subcmd}.log 2>&1']) 

283 self._execute_gromacs(command) 

284 

285 atoms = read_gromos(self.label + '.g96') 

286 self.atoms = atoms.copy() 

287 

288 def generate_gromacs_run_file(self): 

289 """ Generates input file for a gromacs mdrun 

290 based on structure file and topology file 

291 resulting file is self.label + '.tpr 

292 """ 

293 

294 # generate gromacs run input file (gromacs.tpr) 

295 try: 

296 os.remove(self.label + '.tpr') 

297 except OSError: 

298 pass 

299 

300 subcmd = 'grompp' 

301 command = ' '.join([ 

302 subcmd, 

303 '-f', self.label + '.mdp', 

304 '-c', self.label + '.g96', 

305 '-p', self.label + '.top', 

306 '-o', self.label + '.tpr', 

307 '-maxwarn', '100', 

308 self.params_runs.get('extra_grompp_parameters', ''), 

309 f'> {self.label}.{subcmd}.log 2>&1']) 

310 self._execute_gromacs(command) 

311 

312 def write_energy_files(self): 

313 """write input files for gromacs force and energy calculations 

314 for gromacs program energy""" 

315 filename = 'inputGenergy.txt' 

316 with open(filename, 'w') as output: 

317 output.write('Potential \n') 

318 output.write(' \n') 

319 output.write(' \n') 

320 

321 filename = 'inputGtraj.txt' 

322 with open(filename, 'w') as output: 

323 output.write('System \n') 

324 output.write(' \n') 

325 output.write(' \n') 

326 

327 def set_own_params(self, key, value, docstring=""): 

328 """Set own gromacs parameter with doc strings.""" 

329 self.parameters[key] = value 

330 self.params_doc[key] = docstring 

331 

332 def set_own_params_runs(self, key, value): 

333 """Set own gromacs parameter for program parameters 

334 Add spaces to avoid errors """ 

335 self.params_runs[key] = ' ' + value + ' ' 

336 

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

338 """Write input parameters to input file.""" 

339 

340 FileIOCalculator.write_input(self, atoms, properties, system_changes) 

341 # print self.parameters 

342 with open(self.label + '.mdp', 'w') as myfile: 

343 for key, val in self.parameters.items(): 

344 if val is not None: 

345 docstring = self.params_doc.get(key, '') 

346 myfile.write('%-35s = %s ; %s\n' 

347 % (key, val, ';' + docstring)) 

348 

349 def update(self, atoms): 

350 """ set atoms and do the calculation """ 

351 # performs an update of the atoms 

352 self.atoms = atoms.copy() 

353 # must be g96 format for accuracy, alternatively binary formats 

354 write_gromos(self.label + '.g96', atoms) 

355 # does run to get forces and energies 

356 self.calculate() 

357 

358 def calculate(self, atoms=None, properties=['energy', 'forces'], 

359 system_changes=all_changes): 

360 """ runs a gromacs-mdrun and 

361 gets energy and forces 

362 rest below is to make gromacs calculator 

363 compactible with ase-Calculator class 

364 

365 atoms: Atoms object 

366 Contains positions, unit-cell, ... 

367 properties: list of str 

368 List of what needs to be calculated. Can be any combination 

369 of 'energy', 'forces' 

370 system_changes: list of str 

371 List of what has changed since last calculation. Can be 

372 any combination of these five: 'positions', 'numbers', 'cell', 

373 'pbc', 'initial_charges' and 'initial_magmoms'. 

374 

375 """ 

376 

377 self.run() 

378 if self.clean: 

379 do_clean('#*') 

380 # get energy 

381 try: 

382 os.remove('tmp_ene.del') 

383 except OSError: 

384 pass 

385 

386 subcmd = 'energy' 

387 command = ' '.join([ 

388 subcmd, 

389 '-f', self.label + '.edr', 

390 '-o', self.label + '.Energy.xvg', 

391 '< inputGenergy.txt', 

392 f'> {self.label}.{subcmd}.log 2>&1']) 

393 self._execute_gromacs(command) 

394 with open(self.label + '.Energy.xvg') as fd: 

395 lastline = fd.readlines()[-1] 

396 energy = float(lastline.split()[1]) 

397 # We go for ASE units ! 

398 self.results['energy'] = energy * units.kJ / units.mol 

399 # energies are about 100 times bigger in Gromacs units 

400 # when compared to ase units 

401 

402 subcmd = 'traj' 

403 command = ' '.join([ 

404 subcmd, 

405 '-f', self.label + '.trr', 

406 '-s', self.label + '.tpr', 

407 '-of', self.label + '.Force.xvg', 

408 '< inputGtraj.txt', 

409 f'> {self.label}.{subcmd}.log 2>&1']) 

410 self._execute_gromacs(command) 

411 with open(self.label + '.Force.xvg') as fd: 

412 lastline = fd.readlines()[-1] 

413 forces = np.array([float(f) for f in lastline.split()[1:]]) 

414 # We go for ASE units !gromacsForce.xvg 

415 tmp_forces = forces / units.nm * units.kJ / units.mol 

416 tmp_forces = np.reshape(tmp_forces, (-1, 3)) 

417 self.results['forces'] = tmp_forces