Coverage for biobb_amber/leap/leap_gen_top.py: 68%

115 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-06 14:11 +0000

1#!/usr/bin/env python3 

2 

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

4 

5import os 

6from pathlib import PurePath 

7from typing import List, Optional 

8 

9from biobb_common.generic.biobb_object import BiobbObject 

10from biobb_common.tools import file_utils as fu 

11from biobb_common.tools.file_utils import launchlogger 

12 

13from biobb_amber.leap.common import _from_string_to_list 

14 

15 

16class LeapGenTop(BiobbObject): 

17 """ 

18 | biobb_amber.leap.leap_gen_top LeapGenTop 

19 | Wrapper of the `AmberTools (AMBER MD Package) leap tool <https://ambermd.org/AmberTools.php>`_ module. 

20 | Generates a MD topology from a molecule structure using tLeap tool from the AmberTools MD package. 

21 

22 Args: 

23 input_pdb_path (str): Input 3D structure PDB file. File type: input. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/data/leap/structure.leapin.pdb>`_. Accepted formats: pdb (edam:format_1476). 

24 input_lib_path (str) (Optional): Input ligand library parameters file. File type: input. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/data/leap/ligand.lib>`_. Accepted formats: lib (edam:format_3889), zip (edam:format_3987). 

25 input_frcmod_path (str) (Optional): Input ligand frcmod parameters file. File type: input. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/data/leap/ligand.frcmod>`_. Accepted formats: frcmod (edam:format_3888), zip (edam:format_3987). 

26 input_params_path (str) (Optional): Additional leap parameter files to load with loadAmberParams Leap command. File type: input. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/data/leap/frcmod.ionsdang_spce.txt>`_. Accepted formats: in (edam:format_2330), leapin (edam:format_2330), txt (edam:format_2330), zip (edam:format_3987). 

27 input_prep_path (str) (Optional): Additional leap parameter files to load with loadAmberPrep Leap command. File type: input. Accepted formats: in (edam:format_2330), leapin (edam:format_2330), txt (edam:format_2330), zip (edam:format_3987). 

28 input_source_path (str) (Optional): Additional leap command files to load with source Leap command. File type: input. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/data/leap/leaprc.water.spce.txt>`_. Accepted formats: in (edam:format_2330), leapin (edam:format_2330), txt (edam:format_2330), zip (edam:format_3987). 

29 output_pdb_path (str): Output 3D structure PDB file matching the topology file. File type: output. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/reference/leap/structure.leap.pdb>`_. Accepted formats: pdb (edam:format_1476). 

30 output_top_path (str): Output topology file (AMBER ParmTop). File type: output. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/reference/leap/structure.leap.top>`_. Accepted formats: top (edam:format_3881), parmtop (edam:format_3881), prmtop (edam:format_3881). 

31 output_crd_path (str): Output coordinates file (AMBER crd). File type: output. `Sample file <https://github.com/bioexcel/biobb_amber/raw/master/biobb_amber/test/reference/leap/structure.leap.crd>`_. Accepted formats: crd (edam:format_3878), mdcrd (edam:format_3878), inpcrd (edam:format_3878). 

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

33 * **forcefield** (*list*) - (["protein.ff14SB","DNA.bsc1","gaff"]) Forcefields to be used for the structure generation. Each item should be either a path to a leaprc file or a string with the leaprc file name if the force field is included with Amber (e.g. "/path/to/leaprc.protein.ff14SB" or "protein.ff14SB"). Default values: ["protein.ff14SB","DNA.bsc1","gaff"]. 

34 * **binary_path** (*str*) - ("tleap") Path to the tleap executable binary. 

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

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

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

38 * **container_path** (*str*) - (None) Container path definition. 

39 * **container_image** (*str*) - ('afandiadib/ambertools:serial') Container image definition. 

40 * **container_volume_path** (*str*) - ('/tmp') Container volume path definition. 

41 * **container_working_dir** (*str*) - (None) Container working directory definition. 

42 * **container_user_id** (*str*) - (None) Container user_id definition. 

43 * **container_shell_path** (*str*) - ('/bin/bash') Path to default shell inside the container. 

44 

45 Examples: 

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

47 

48 from biobb_amber.leap.leap_gen_top import leap_gen_top 

49 prop = { 

50 'forcefield': ['protein.ff14SB'] 

51 } 

52 leap_gen_top(input_pdb_path='/path/to/structure.pdb', 

53 output_pdb_path='/path/to/newStructure.pdb', 

54 output_top_path='/path/to/newTopology.top', 

55 output_crd_path='/path/to/newCoordinates.crd', 

56 properties=prop) 

57 

58 Info: 

59 * wrapped_software: 

60 * name: AmberTools tLeap 

61 * version: >20.9 

62 * license: LGPL 2.1 

63 * ontology: 

64 * name: EDAM 

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

66 

67 """ 

68 

69 def __init__( 

70 self, 

71 input_pdb_path: str, 

72 output_pdb_path: str, 

73 output_top_path: str, 

74 output_crd_path: str, 

75 input_lib_path: Optional[str] = None, 

76 input_frcmod_path: Optional[str] = None, 

77 input_params_path: Optional[str] = None, 

78 input_prep_path: Optional[str] = None, 

79 input_source_path: Optional[str] = None, 

80 properties: Optional[dict] = None, 

81 **kwargs, 

82 ): 

83 properties = properties or {} 

84 

85 # Call parent class constructor 

86 super().__init__(properties) 

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

88 

89 # Input/Output files 

90 self.io_dict = { 

91 "in": { 

92 "input_pdb_path": input_pdb_path, 

93 "input_lib_path": input_lib_path, 

94 "input_frcmod_path": input_frcmod_path, 

95 "input_params_path": input_params_path, 

96 "input_prep_path": input_prep_path, 

97 "input_source_path": input_source_path, 

98 }, 

99 "out": { 

100 "output_pdb_path": output_pdb_path, 

101 "output_top_path": output_top_path, 

102 "output_crd_path": output_crd_path, 

103 }, 

104 } 

105 

106 # # Ligand Parameter lists 

107 # self.ligands_lib_list = [] 

108 # if input_lib_path: 

109 # self.ligands_lib_list.append(input_lib_path) 

110 # 

111 # self.ligands_frcmod_list = [] 

112 # if input_frcmod_path: 

113 # self.ligands_frcmod_list.append(input_frcmod_path) 

114 

115 # Properties specific for BB 

116 self.properties = properties 

117 

118 # Set default forcefields 

119 if self.container_path: 

120 # self.forcefield = "leaprc.protein.ff14SB" 

121 self.forcefield = _from_string_to_list( 

122 properties.get("forcefield", ['leaprc.protein.ff14SB', 'leaprc.DNA.bsc1', 'leaprc.gaff']) 

123 ) 

124 else: 

125 amber_home_path = os.getenv("AMBERHOME") 

126 protein_ff14SB_path = os.path.join(amber_home_path, 'dat', 'leap', 'cmd', 'leaprc.protein.ff14SB') 

127 dna_bsc1_path = os.path.join(amber_home_path, 'dat', 'leap', 'cmd', 'leaprc.DNA.bsc1') 

128 gaff_path = os.path.join(amber_home_path, 'dat', 'leap', 'cmd', 'leaprc.gaff') 

129 

130 self.forcefield = _from_string_to_list( 

131 properties.get("forcefield", [protein_ff14SB_path, dna_bsc1_path, gaff_path]) 

132 ) 

133 # Find the paths of the leaprc files if only the force field names are provided 

134 self.forcefield = self.find_leaprc_paths(self.forcefield) 

135 

136 self.binary_path = properties.get("binary_path", "tleap") 

137 

138 # Check the properties 

139 self.check_properties(properties) 

140 self.check_arguments() 

141 

142 def find_leaprc_paths(self, forcefields: List[str]) -> List[str]: 

143 """ 

144 Find the leaprc paths for the force fields provided. 

145 

146 For each item in the forcefields list, the function checks if the str is a path to an existing file. 

147 If not, it tries to find the file in the $AMBERHOME/dat/leap/cmd/ directory or the $AMBERHOME/dat/leap/cmd/oldff/ 

148 directory with and without the leaprc prefix. 

149 

150 Args: 

151 forcefields (List[str]): List of force fields to find the leaprc files for. 

152 

153 Returns: 

154 List[str]: List of leaprc file paths. 

155 """ 

156 

157 leaprc_paths = [] 

158 

159 for forcefield in forcefields: 

160 

161 num_paths = len(leaprc_paths) 

162 

163 # Check if the forcefield is a path to an existing file 

164 if os.path.exists(forcefield): 

165 leaprc_paths.append(forcefield) 

166 continue 

167 

168 # Check if the forcefield is in the leaprc directory 

169 leaprc_path = os.path.join(os.environ.get('AMBERHOME', ''), 'dat', 'leap', 'cmd', f"leaprc.{forcefield}") 

170 if os.path.exists(leaprc_path): 

171 leaprc_paths.append(leaprc_path) 

172 continue 

173 

174 # Check if the forcefield is in the oldff directory 

175 leaprc_path = os.path.join(os.environ.get('AMBERHOME', ''), 'dat', 'leap', 'cmd', 'oldff', f"leaprc.{forcefield}") 

176 if os.path.exists(leaprc_path): 

177 leaprc_paths.append(leaprc_path) 

178 continue 

179 

180 # Check if the forcefield is in the leaprc directory without the leaprc prefix 

181 leaprc_path = os.path.join(os.environ.get('AMBERHOME', ''), 'dat', 'leap', 'cmd', f"{forcefield}") 

182 if os.path.exists(leaprc_path): 

183 leaprc_paths.append(leaprc_path) 

184 continue 

185 

186 # Check if the forcefield is in the oldff directory without the leaprc prefix 

187 leaprc_path = os.path.join(os.environ.get('AMBERHOME', ''), 'dat', 'leap', 'cmd', 'oldff', f"{forcefield}") 

188 if os.path.exists(leaprc_path): 

189 leaprc_paths.append(leaprc_path) 

190 continue 

191 

192 new_num_paths = len(leaprc_paths) 

193 

194 if new_num_paths == num_paths: 

195 raise ValueError(f"Force field {forcefield} not found. Check the $AMBERHOME/dat/leap/cmd/ directory for available force fields or provide the path to an existing leaprc file.") 

196 

197 return leaprc_paths 

198 

199 def _resolve_input_files(self, io_key: str, tmp_folder: Optional[str]) -> List[str]: 

200 """Resolve an optional input (zip or single file) to the list of paths the tleap 

201 script must load. 

202 

203 In container mode the zipball is staged into the host sandbox (unique_dir), which is 

204 bind-mounted at ``container_volume_path``; it is unpacked on the host and the entries 

205 are referenced by their container path. In local mode the zipball is unpacked into a 

206 temporary folder and referenced by its host path. 

207 """ 

208 file_path = self.io_dict["in"][io_key] 

209 if file_path is None: 

210 return [] 

211 if not file_path.endswith(".zip"): 

212 return [self.stage_io_dict["in"][io_key]] 

213 if self.container_path: 

214 unique_dir = self.stage_io_dict["unique_dir"] 

215 host_zip = os.path.join(unique_dir, os.path.basename(file_path)) 

216 extracted = fu.unzip_list(host_zip, dest_dir=unique_dir, out_log=self.out_log) 

217 return [ 

218 os.path.join(self.container_volume_path, os.path.relpath(p, unique_dir)) 

219 for p in extracted 

220 ] 

221 return fu.unzip_list( 

222 self.stage_io_dict["in"][io_key], dest_dir=tmp_folder, out_log=self.out_log 

223 ) 

224 

225 # def check_data_params(self, out_log, err_log): 

226 # """ Checks input/output paths correctness """ 

227 

228 # # Check input(s) 

229 # self.io_dict["in"]["input_pdb_path"] = check_input_path(self.io_dict["in"]["input_pdb_path"], "input_pdb_path", False, out_log, self.__class__.__name__) 

230 # self.io_dict["in"]["input_lib_path"] = check_input_path(self.io_dict["in"]["input_lib_path"], "input_lib_path", True, out_log, self.__class__.__name__) 

231 # self.io_dict["in"]["input_frcmod_path"] = check_input_path(self.io_dict["in"]["input_frcmod_path"], "input_frcmod_path", True, out_log, self.__class__.__name__) 

232 # # self.io_dict["in"]["input_params_path"] = check_input_path(self.io_dict["in"]["input_params_path"], "input_params_path", True, out_log, self.__class__.__name__) 

233 # # self.io_dict["in"]["input_prep_path"] = check_input_path(self.io_dict["in"]["input_params_path"], "input_params_path", True, out_log, self.__class__.__name__) 

234 # # self.io_dict["in"]["input_source_path"] = check_input_path(self.io_dict["in"]["input_source_path"], "input_source_path", True, out_log, self.__class__.__name__) 

235 

236 # # Check output(s) 

237 # self.io_dict["out"]["output_pdb_path"] = check_output_path(self.io_dict["out"]["output_pdb_path"], "output_pdb_path", False, out_log, self.__class__.__name__) 

238 # self.io_dict["out"]["output_top_path"] = check_output_path(self.io_dict["out"]["output_top_path"], "output_top_path", False, out_log, self.__class__.__name__) 

239 # self.io_dict["out"]["output_crd_path"] = check_output_path(self.io_dict["out"]["output_crd_path"], "output_crd_path", False, out_log, self.__class__.__name__) 

240 

241 @launchlogger 

242 def launch(self): 

243 """Launches the execution of the LeapGenTop module.""" 

244 

245 # check input/output paths and parameters 

246 # self.check_data_params(self.out_log, self.err_log) 

247 

248 # Setup Biobb 

249 if self.check_restart(): 

250 return 0 

251 self.stage_files() 

252 

253 # Creating temporary folder & Leap configuration (instructions) file 

254 if self.container_path: 

255 instructions_file = str( 

256 PurePath(self.stage_io_dict["unique_dir"]).joinpath("leap.in") 

257 ) 

258 instructions_file_path = str( 

259 PurePath(self.container_volume_path).joinpath("leap.in") 

260 ) 

261 tmp_folder = None 

262 else: 

263 tmp_folder = fu.create_unique_dir() 

264 instructions_file = str(PurePath(tmp_folder).joinpath("leap.in")) 

265 fu.log("Creating %s temporary folder" % tmp_folder, self.out_log) 

266 instructions_file_path = instructions_file 

267 

268 ligands_lib_list = self._resolve_input_files("input_lib_path", tmp_folder) 

269 ligands_frcmod_list = self._resolve_input_files("input_frcmod_path", tmp_folder) 

270 amber_params_list = self._resolve_input_files("input_params_path", tmp_folder) 

271 amber_prep_list = self._resolve_input_files("input_prep_path", tmp_folder) 

272 leap_source_list = self._resolve_input_files("input_source_path", tmp_folder) 

273 

274 with open(instructions_file, "w") as leapin: 

275 # Forcefields loaded by default: 

276 # Protein: ff14SB (PARM99 + frcmod.ff99SB + frcmod.parmbsc0 + OL3 for RNA) 

277 # leapin.write("source leaprc.protein.ff14SB \n") 

278 # DNA: parmBSC1 (ParmBSC1 (ff99 + bsc0 + bsc1) for DNA. Ivani et al. Nature Methods 13: 55, 2016) 

279 # leapin.write("source leaprc.DNA.bsc1 \n") 

280 # Ligands: GAFF (General Amber Force field, J. Comput. Chem. 2004 Jul 15;25(9):1157-74) 

281 # leapin.write("source leaprc.gaff \n") 

282 

283 # Forcefields loaded from input forcefield property 

284 for t in self.forcefield: 

285 if self.container_path: 

286 leapin.write("source leaprc.{}\n".format(t)) 

287 else: 

288 leapin.write("source {}\n".format(t)) 

289 

290 # Additional Leap commands 

291 for leap_commands in leap_source_list: 

292 leapin.write("source " + leap_commands + "\n") 

293 

294 # Additional Amber parameters 

295 for amber_params in amber_params_list: 

296 leapin.write("loadamberparams " + amber_params + "\n") 

297 

298 # Additional Amber prep files 

299 for amber_prep in amber_prep_list: 

300 leapin.write("loadamberprep " + amber_prep + "\n") 

301 

302 # Ions libraries 

303 leapin.write("loadOff atomic_ions.lib \n") 

304 

305 # Ligand(s) libraries (if any) 

306 for amber_lib in ligands_lib_list: 

307 leapin.write("loadOff " + amber_lib + "\n") 

308 for amber_frcmod in ligands_frcmod_list: 

309 leapin.write("loadamberparams " + amber_frcmod + "\n") 

310 

311 # Loading PDB file 

312 leapin.write( 

313 "mol = loadpdb " + self.stage_io_dict["in"]["input_pdb_path"] + " \n" 

314 ) 

315 

316 # Saving output PDB file, coordinates and topology 

317 leapin.write( 

318 "savepdb mol " + self.stage_io_dict["out"]["output_pdb_path"] + " \n" 

319 ) 

320 leapin.write( 

321 "saveAmberParm mol " + self.stage_io_dict["out"]["output_top_path"] + " " + self.stage_io_dict["out"]["output_crd_path"] + "\n" 

322 ) 

323 leapin.write("quit \n") 

324 

325 # Command line 

326 self.cmd = [self.binary_path, "-f", instructions_file_path] 

327 

328 # Run Biobb block 

329 self.run_biobb() 

330 

331 # Copy files to host 

332 self.copy_to_host() 

333 

334 # remove temporary folder(s) 

335 self.tmp_files.extend([str(tmp_folder), "leap.log"]) 

336 self.remove_tmp_files() 

337 

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

339 

340 return self.return_code 

341 

342 

343def leap_gen_top( 

344 input_pdb_path: str, 

345 output_pdb_path: str, 

346 output_top_path: str, 

347 output_crd_path: str, 

348 input_lib_path: Optional[str] = None, 

349 input_frcmod_path: Optional[str] = None, 

350 input_params_path: Optional[str] = None, 

351 input_prep_path: Optional[str] = None, 

352 input_source_path: Optional[str] = None, 

353 properties: Optional[dict] = None, 

354 **kwargs, 

355) -> int: 

356 """Create the :class:`LeapGenTop <leap.leap_gen_top.LeapGenTop>` class and 

357 execute the :meth:`launch() <leap.leap_gen_top.LeapGenTop.launch>` method.""" 

358 return LeapGenTop(**dict(locals())).launch() 

359 

360 

361leap_gen_top.__doc__ = LeapGenTop.__doc__ 

362main = LeapGenTop.get_main(leap_gen_top, "Generating a MD topology from a molecule structure using tLeap program from AmberTools MD package.") 

363 

364if __name__ == "__main__": 

365 main()