Coverage for biobb_gromacs / gromacs / mdrun_plumed.py: 49%

170 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-05 08:26 +0000

1#!/usr/bin/env python3 

2 

3"""Module containing the MDrun class and the command line interface.""" 

4import os 

5import shutil 

6from typing import Optional 

7from biobb_common.generic.biobb_object import BiobbObject 

8from biobb_common.tools import file_utils as fu 

9from biobb_common.tools.file_utils import launchlogger 

10from biobb_gromacs.gromacs.common import get_gromacs_version 

11 

12 

13class MdrunPlumed(BiobbObject): 

14 """ 

15 | biobb_gromacs MdrunPlumed 

16 | Wrapper of the `GROMACS mdrun <http://manual.gromacs.org/current/onlinehelp/gmx-mdrun.html>`_ module. 

17 | MDRun is the main computational chemistry engine within GROMACS. It performs Molecular Dynamics simulations, but it can also perform Stochastic Dynamics, Energy Minimization, test particle insertion or (re)calculation of energies. 

18 

19 Args: 

20 input_tpr_path (str): Path to the portable binary run input file TPR. File type: input. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/data/gromacs/mdrun.tpr>`_. Accepted formats: tpr (edam:format_2333). 

21 output_gro_path (str): Path to the output GROMACS structure GRO file. File type: output. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/reference/gromacs/ref_mdrun.gro>`_. Accepted formats: gro (edam:format_2033). 

22 output_edr_path (str): Path to the output GROMACS portable energy file EDR. File type: output. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/reference/gromacs/ref_mdrun.edr>`_. Accepted formats: edr (edam:format_2330). 

23 output_log_path (str): Path to the output GROMACS trajectory log file LOG. File type: output. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/reference/gromacs/ref_mdrun.log>`_. Accepted formats: log (edam:format_2330). 

24 output_trr_path (str) (Optional): Path to the GROMACS uncompressed raw trajectory file TRR. File type: output. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/reference/gromacs/ref_mdrun.trr>`_. Accepted formats: trr (edam:format_3910). 

25 input_cpt_path (str) (Optional): Path to the input GROMACS checkpoint file CPT. File type: input. Accepted formats: cpt (edam:format_2333). 

26 output_xtc_path (str) (Optional): Path to the GROMACS compressed trajectory file XTC. File type: output. Accepted formats: xtc (edam:format_3875). 

27 output_cpt_path (str) (Optional): Path to the output GROMACS checkpoint file CPT. File type: output. Accepted formats: cpt (edam:format_2333). 

28 output_dhdl_path (str) (Optional): Path to the output dhdl.xvg file only used when free energy calculation is turned on. File type: output. Accepted formats: xvg (edam:format_2033). 

29 input_plumed_path (str) (Optional): Path to the main PLUMED input file. If provided, PLUMED will be used during the simulation. All files used by the main PLUMED input file must exist in the input_plumed_folder and be called with just their name. Make sure to provide a GROMACS version with the PLUMED patch. File type: input. Accepted formats: dat (edam:format_2330). 

30 input_plumed_folder (dir) (Optional): Path to the folder with all files needed by the main PLUMED input file, see input_plumed_path. File type: input. Accepted formats: directory (edam:format_1915) 

31 output_plumed_folder (dir) (Optional): Folder where PLUMED generated output files will be saved. File type: output. Accepted formats: directory (edam:format_1915) 

32 properties (dict - Python dictionary object containing the tool parameters, not input/output files): 

33 * **mpi_bin** (*str*) - (None) Path to the MPI runner. Usually "mpirun" or "srun". 

34 * **mpi_np** (*int*) - (0) [0~1000|1] Number of MPI processes. Usually an integer bigger than 1. 

35 * **mpi_flags** (*str*) - (None) Path to the MPI hostlist file. 

36 * **checkpoint_time** (*int*) - (15) [0~1000|1] Checkpoint writing interval in minutes. Only enabled if an output_cpt_path is provided. 

37 * **noappend** (*bool*) - (False) Include the noappend flag to open new output files and add the simulation part number to all output file names 

38 * **num_threads** (*int*) - (0) [0~1000|1] Let GROMACS guess. The number of threads that are going to be used. 

39 * **num_threads_mpi** (*int*) - (0) [0~1000|1] Let GROMACS guess. The number of GROMACS MPI threads that are going to be used. 

40 * **num_threads_omp** (*int*) - (0) [0~1000|1] Let GROMACS guess. The number of GROMACS OPENMP threads that are going to be used. 

41 * **num_threads_omp_pme** (*int*) - (0) [0~1000|1] Let GROMACS guess. The number of GROMACS OPENMP_PME threads that are going to be used. 

42 * **use_gpu** (*bool*) - (False) Use settings appropriate for GPU. Adds: -nb gpu -pme gpu 

43 * **gpu_id** (*str*) - (None) list of unique GPU device IDs available to use. 

44 * **gpu_tasks** (*str*) - (None) list of GPU device IDs, mapping each PP task on each node to a device. 

45 * **gmx_lib** (*str*) - (None) Path set GROMACS GMXLIB environment variable. 

46 * **binary_path** (*str*) - ("gmx") Path to the GROMACS executable binary. 

47 * **remove_tmp** (*bool*) - (True) [WF property] Remove temporal files. 

48 * **restart** (*bool*) - (False) [WF property] Do not execute if output files exist. 

49 * **sandbox_path** (*str*) - ("./") [WF property] Parent path to the sandbox directory. 

50 * **container_path** (*str*) - (None) Path to the binary executable of your container. 

51 * **container_image** (*str*) - (None) Container Image identifier. 

52 * **container_volume_path** (*str*) - ("/data") Path to an internal directory in the container. 

53 * **container_working_dir** (*str*) - (None) Path to the internal CWD in the container. 

54 * **container_user_id** (*str*) - (None) User number id to be mapped inside the container. 

55 * **container_shell_path** (*str*) - ("/bin/bash") Path to the binary executable of the container shell. 

56 

57 Examples: 

58 This is a use example of how to use the building block from Python:: 

59 

60 from biobb_gromacs.gromacs.mdrun_plumed import mdrun_plumed 

61 prop = { 'num_threads': 0, 

62 'binary_path': 'gmx' } 

63 mdrun_plumed(input_tpr_path='/path/to/myPortableBinaryRunInputFile.tpr', 

64 output_trr_path='/path/to/newTrajectory.trr', 

65 output_gro_path='/path/to/newStructure.gro', 

66 output_edr_path='/path/to/newEnergy.edr', 

67 output_log_path='/path/to/newSimulationLog.log', 

68 properties=prop) 

69 

70 Info: 

71 * wrapped_software: 

72 * name: GROMACS Mdrun with PLUMED 

73 * version: 2025.2 

74 * license: LGPL 2.1 

75 * multinode: mpi 

76 * ontology: 

77 * name: EDAM 

78 * schema: http://edamontology.org/EDAM.owl 

79 """ 

80 

81 def __init__(self, input_tpr_path: str, output_gro_path: str, output_edr_path: str, 

82 output_log_path: str, output_trr_path: Optional[str] = None, input_cpt_path: Optional[str] = None, 

83 output_xtc_path: Optional[str] = None, output_cpt_path: Optional[str] = None, 

84 output_dhdl_path: Optional[str] = None, input_plumed_path: Optional[str] = None, 

85 input_plumed_folder: Optional[str] = None, output_plumed_folder: Optional[str] = None, 

86 properties: Optional[dict] = None, **kwargs) -> None: 

87 properties = properties or {} 

88 

89 # Call parent class constructor 

90 super().__init__(properties) 

91 self.locals_var_dict = locals().copy() 

92 

93 # Input/Output files 

94 self.io_dict = { 

95 "in": {"input_tpr_path": input_tpr_path, "input_cpt_path": input_cpt_path, 

96 "input_plumed_path": input_plumed_path, "input_plumed_folder": input_plumed_folder}, 

97 "out": {"output_trr_path": output_trr_path, "output_gro_path": output_gro_path, 

98 "output_edr_path": output_edr_path, "output_log_path": output_log_path, 

99 "output_xtc_path": output_xtc_path, "output_cpt_path": output_cpt_path, 

100 "output_dhdl_path": output_dhdl_path, "output_plumed_folder": output_plumed_folder} 

101 } 

102 

103 # Properties specific for BB 

104 # general mpi properties 

105 self.mpi_bin = properties.get('mpi_bin') 

106 self.mpi_np = properties.get('mpi_np') 

107 self.mpi_flags = properties.get('mpi_flags') 

108 # gromacs cpu mpi/openmp properties 

109 self.num_threads = str(properties.get('num_threads', '')) 

110 self.num_threads_mpi = str(properties.get('num_threads_mpi', '')) 

111 self.num_threads_omp = str(properties.get('num_threads_omp', '')) 

112 self.num_threads_omp_pme = str( 

113 properties.get('num_threads_omp_pme', '')) 

114 # gromacs gpus 

115 self.use_gpu = properties.get( 

116 'use_gpu', False) # Adds: -nb gpu -pme gpu 

117 self.gpu_id = str(properties.get('gpu_id', '')) 

118 self.gpu_tasks = str(properties.get('gpu_tasks', '')) 

119 # gromacs 

120 self.checkpoint_time = properties.get('checkpoint_time') 

121 self.noappend = properties.get('noappend', False) 

122 

123 # Properties common in all GROMACS BB 

124 self.gmx_lib = properties.get('gmx_lib', None) 

125 self.binary_path: str = properties.get('binary_path', 'gmx') 

126 self.gmx_nobackup = properties.get('gmx_nobackup', True) 

127 self.gmx_nocopyright = properties.get('gmx_nocopyright', True) 

128 if self.gmx_nobackup: 

129 self.binary_path += ' -nobackup' 

130 if self.gmx_nocopyright: 

131 self.binary_path += ' -nocopyright' 

132 if (not self.mpi_bin) and (not self.container_path): 

133 self.gmx_version = get_gromacs_version(self.binary_path) 

134 

135 # Check the properties 

136 self.check_properties(properties) 

137 self.check_arguments() 

138 

139 @launchlogger 

140 def launch(self) -> int: 

141 """Execute the :class:`MdrunPlumed <gromacs.mdrun_plumed.MdrunPlumed>` object.""" 

142 

143 # Setup Biobb 

144 if self.check_restart(): 

145 return 0 

146 

147 # Optional output files (if not added mrun will create them using a generic name) 

148 if not self.stage_io_dict["out"].get("output_trr_path"): 

149 self.stage_io_dict["out"]["output_trr_path"] = fu.create_name( 

150 prefix=self.prefix, step=self.step, name='trajectory.trr') 

151 self.tmp_files.append(self.stage_io_dict["out"]["output_trr_path"]) 

152 

153 self.stage_files() 

154 

155 self.cmd = [self.binary_path, 'mdrun', 

156 '-o', self.stage_io_dict["out"]["output_trr_path"], 

157 '-s', self.stage_io_dict["in"]["input_tpr_path"], 

158 '-c', self.stage_io_dict["out"]["output_gro_path"], 

159 '-e', self.stage_io_dict["out"]["output_edr_path"], 

160 '-g', self.stage_io_dict["out"]["output_log_path"]] 

161 

162 if self.stage_io_dict["in"].get("input_plumed_path"): 

163 self.cmd.append('-plumed') 

164 self.cmd.append(self.stage_io_dict["in"]["input_plumed_path"]) 

165 

166 if self.stage_io_dict["in"].get("input_cpt_path"): 

167 self.cmd.append('-cpi') 

168 self.cmd.append(self.stage_io_dict["in"]["input_cpt_path"]) 

169 if self.stage_io_dict["out"].get("output_xtc_path"): 

170 self.cmd.append('-x') 

171 self.cmd.append(self.stage_io_dict["out"]["output_xtc_path"]) 

172 else: 

173 self.tmp_files.append('traj_comp.xtc') 

174 if self.stage_io_dict["out"].get("output_cpt_path"): 

175 self.cmd.append('-cpo') 

176 self.cmd.append(self.stage_io_dict["out"]["output_cpt_path"]) 

177 if self.checkpoint_time: 

178 self.cmd.append('-cpt') 

179 self.cmd.append(str(self.checkpoint_time)) 

180 if self.stage_io_dict["out"].get("output_dhdl_path"): 

181 self.cmd.append('-dhdl') 

182 self.cmd.append(self.stage_io_dict["out"]["output_dhdl_path"]) 

183 

184 # general mpi properties 

185 if self.mpi_bin: 

186 mpi_cmd = [self.mpi_bin] 

187 if self.mpi_np: 

188 mpi_cmd.append('-n') 

189 mpi_cmd.append(str(self.mpi_np)) 

190 if self.mpi_flags: 

191 mpi_cmd.extend(self.mpi_flags) 

192 self.cmd = mpi_cmd + self.cmd 

193 

194 # gromacs cpu mpi/openmp properties 

195 if self.num_threads: 

196 fu.log( 

197 f'User added number of gmx threads: {self.num_threads}', self.out_log) 

198 self.cmd.append('-nt') 

199 self.cmd.append(self.num_threads) 

200 if self.num_threads_mpi: 

201 fu.log( 

202 f'User added number of gmx mpi threads: {self.num_threads_mpi}', self.out_log) 

203 self.cmd.append('-ntmpi') 

204 self.cmd.append(self.num_threads_mpi) 

205 if self.num_threads_omp: 

206 fu.log( 

207 f'User added number of gmx omp threads: {self.num_threads_omp}', self.out_log) 

208 self.cmd.append('-ntomp') 

209 self.cmd.append(self.num_threads_omp) 

210 if self.num_threads_omp_pme: 

211 fu.log( 

212 f'User added number of gmx omp_pme threads: {self.num_threads_omp_pme}', self.out_log) 

213 self.cmd.append('-ntomp_pme') 

214 self.cmd.append(self.num_threads_omp_pme) 

215 # GMX gpu properties 

216 if self.use_gpu: 

217 fu.log('Adding GPU specific settings adds: -nb gpu -pme gpu', self.out_log) 

218 self.cmd += ["-nb", "gpu", "-pme", "gpu"] 

219 if self.gpu_id: 

220 fu.log( 

221 f'list of unique GPU device IDs available to use: {self.gpu_id}', self.out_log) 

222 self.cmd.append('-gpu_id') 

223 self.cmd.append(self.gpu_id) 

224 if self.gpu_tasks: 

225 fu.log( 

226 f'list of GPU device IDs, mapping each PP task on each node to a device: {self.gpu_tasks}', self.out_log) 

227 self.cmd.append('-gputasks') 

228 self.cmd.append(self.gpu_tasks) 

229 

230 if self.noappend: 

231 self.cmd.append('-noappend') 

232 

233 if self.gmx_lib: 

234 self.env_vars_dict['GMXLIB'] = self.gmx_lib 

235 

236 # Run Biobb block 

237 self.run_biobb() 

238 

239 # Copy files to host 

240 self.copy_to_host() 

241 

242 # Remove temporal files 

243 self.remove_tmp_files() 

244 

245 self.check_arguments(output_files_created=True, raise_exception=False) 

246 return self.return_code 

247 

248 def stage_files(self): 

249 """ 

250 Stage the input/output files in a temporal unique directory aka sandbox. 

251 

252 Overwrite the parent class method to handle PLUMED input files. 

253 """ 

254 

255 # If PLUMED is requested, change the working directory to the sandbox 

256 if self.io_dict["in"].get("input_plumed_path"): 

257 fu.log("PLUMED detected: Enabling chdir_sandbox to ensure relative paths work.", self.out_log) 

258 self.chdir_sandbox = True 

259 

260 super().stage_files() 

261 

262 # If plumed folder is provided, flatten its contents into the sandbox 

263 if self.stage_io_dict["in"].get("input_plumed_folder"): 

264 plumed_folder = self.stage_io_dict["in"]["input_plumed_folder"] 

265 for item in os.listdir(plumed_folder): 

266 s = os.path.join(plumed_folder, item) 

267 d = os.path.join(self.stage_io_dict["unique_dir"], item) 

268 if os.path.isdir(s): 

269 shutil.copytree(s, d, dirs_exist_ok=True) 

270 else: 

271 shutil.copy2(s, d) 

272 

273 def copy_to_host(self): 

274 """ 

275 Updates the path to the original output files in the sandbox, 

276 to catch changes due to noappend restart. 

277 

278 GROMACS mdrun will change the output file names from md.gro to md.part0001.gro 

279 if the noappend flag is used. 

280 """ 

281 import pathlib 

282 

283 def capture_part_pattern(filename): 

284 """ 

285 Captures the 'part' pattern followed by digits from a string. 

286 """ 

287 import re 

288 pattern = r'part\d+' 

289 

290 match = re.search(pattern, filename) 

291 if match: 

292 return match.group(0) 

293 else: 

294 return None 

295 

296 if self.noappend: 

297 # List files in the staging directory 

298 staging_path = self.stage_io_dict["unique_dir"] 

299 files_in_staging = list(pathlib.Path(staging_path).glob('*')) 

300 

301 # Find the part000x pattern in the output files 

302 for file in files_in_staging: 

303 part_pattern = capture_part_pattern(file.name) 

304 if part_pattern: 

305 break 

306 

307 # Update expected output files 

308 for file_ref, stage_file_path in self.stage_io_dict["out"].items(): 

309 if stage_file_path: 

310 # Find the parent and the file name in the sandbox 

311 parent_path = pathlib.Path(stage_file_path).parent 

312 file_stem = pathlib.Path(stage_file_path).stem 

313 file_suffix = pathlib.Path(stage_file_path).suffix 

314 

315 # Rename all output files except checkpoint files 

316 if file_suffix != '.cpt': 

317 # Create the new file name with the part pattern 

318 if part_pattern: 

319 new_file_name = f"{file_stem}.{part_pattern}{file_suffix}" 

320 new_file_path = parent_path / new_file_name 

321 # Update the stage_io_dict with the new file path 

322 self.stage_io_dict["out"][file_ref] = str( 

323 new_file_path) 

324 

325 super().copy_to_host() 

326 

327 # Bulk Copy PLUMED outputs 

328 if self.io_dict["out"].get("output_plumed_folder"): 

329 dest_folder = self.io_dict["out"]["output_plumed_folder"] 

330 os.makedirs(dest_folder, exist_ok=True) 

331 

332 unique_dir = self.stage_io_dict["unique_dir"] 

333 # We ignore files that were inputs 

334 input_filenames = [os.path.basename(f) for f in self.io_dict["in"].values() if f] 

335 # We ignore standard GMX outputs already copied 

336 gmx_output_filenames = [os.path.basename(f) for f in self.stage_io_dict["out"].values() if f and isinstance(f, str)] 

337 

338 fu.log(f"Searching for PLUMED outputs in {unique_dir}...", self.out_log) 

339 for item in os.listdir(unique_dir): 

340 if item not in input_filenames and item not in gmx_output_filenames: 

341 if os.path.isdir(os.path.join(unique_dir, item)): 

342 # Skip directories 

343 continue 

344 # NOTE: Here we could list specific PLUMED output patterns or skip files contained in the input_plumed_folder 

345 src = os.path.join(unique_dir, item) 

346 dst = os.path.join(dest_folder, item) 

347 fu.log(f"Copying PLUMED output: {item} --> {dest_folder}", self.out_log) 

348 shutil.copy2(src, dst) 

349 

350 

351def mdrun_plumed(input_tpr_path: str, output_gro_path: str, output_edr_path: str, 

352 output_log_path: str, output_trr_path: Optional[str] = None, input_cpt_path: Optional[str] = None, 

353 output_xtc_path: Optional[str] = None, output_cpt_path: Optional[str] = None, 

354 output_dhdl_path: Optional[str] = None, input_plumed_path: Optional[str] = None, 

355 input_plumed_folder: Optional[str] = None, output_plumed_folder: Optional[str] = None, 

356 properties: Optional[dict] = None, **kwargs) -> int: 

357 """Create :class:`MdrunPlumed <gromacs.mdrun_plumed.MdrunPlumed>` class and 

358 execute the :meth:`launch() <gromacs.mdrun_plumed.MdrunPlumed.launch>` method.""" 

359 return MdrunPlumed(**dict(locals())).launch() 

360 

361 

362mdrun_plumed.__doc__ = MdrunPlumed.__doc__ 

363main = MdrunPlumed.get_main(mdrun_plumed, "Wrapper for the GROMACS mdrun with PLUMED module.") 

364 

365 

366if __name__ == '__main__': 

367 main()