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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-06 14:11 +0000
1#!/usr/bin/env python3
3"""Module containing the LeapGenTop class and the command line interface."""
5import os
6from pathlib import PurePath
7from typing import List, Optional
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
13from biobb_amber.leap.common import _from_string_to_list
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.
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.
45 Examples:
46 This is a use example of how to use the building block from Python::
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)
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
67 """
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 {}
85 # Call parent class constructor
86 super().__init__(properties)
87 self.locals_var_dict = locals().copy()
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 }
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)
115 # Properties specific for BB
116 self.properties = properties
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')
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)
136 self.binary_path = properties.get("binary_path", "tleap")
138 # Check the properties
139 self.check_properties(properties)
140 self.check_arguments()
142 def find_leaprc_paths(self, forcefields: List[str]) -> List[str]:
143 """
144 Find the leaprc paths for the force fields provided.
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.
150 Args:
151 forcefields (List[str]): List of force fields to find the leaprc files for.
153 Returns:
154 List[str]: List of leaprc file paths.
155 """
157 leaprc_paths = []
159 for forcefield in forcefields:
161 num_paths = len(leaprc_paths)
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
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
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
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
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
192 new_num_paths = len(leaprc_paths)
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.")
197 return leaprc_paths
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.
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 )
225 # def check_data_params(self, out_log, err_log):
226 # """ Checks input/output paths correctness """
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__)
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__)
241 @launchlogger
242 def launch(self):
243 """Launches the execution of the LeapGenTop module."""
245 # check input/output paths and parameters
246 # self.check_data_params(self.out_log, self.err_log)
248 # Setup Biobb
249 if self.check_restart():
250 return 0
251 self.stage_files()
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
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)
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")
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))
290 # Additional Leap commands
291 for leap_commands in leap_source_list:
292 leapin.write("source " + leap_commands + "\n")
294 # Additional Amber parameters
295 for amber_params in amber_params_list:
296 leapin.write("loadamberparams " + amber_params + "\n")
298 # Additional Amber prep files
299 for amber_prep in amber_prep_list:
300 leapin.write("loadamberprep " + amber_prep + "\n")
302 # Ions libraries
303 leapin.write("loadOff atomic_ions.lib \n")
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")
311 # Loading PDB file
312 leapin.write(
313 "mol = loadpdb " + self.stage_io_dict["in"]["input_pdb_path"] + " \n"
314 )
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")
325 # Command line
326 self.cmd = [self.binary_path, "-f", instructions_file_path]
328 # Run Biobb block
329 self.run_biobb()
331 # Copy files to host
332 self.copy_to_host()
334 # remove temporary folder(s)
335 self.tmp_files.extend([str(tmp_folder), "leap.log"])
336 self.remove_tmp_files()
338 self.check_arguments(output_files_created=True, raise_exception=False)
340 return self.return_code
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()
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.")
364if __name__ == "__main__":
365 main()