Coverage for biobb_gromacs / gromacs_extra / append_ligand.py: 72%
123 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-05 08:26 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-05 08:26 +0000
1#!/usr/bin/env python3
3"""Module containing the AppendLigand class and the command line interface."""
5import re
6import shutil
7from pathlib import Path
8from typing import Optional
10from biobb_common.generic.biobb_object import BiobbObject
11from biobb_common.tools import file_utils as fu
12from biobb_common.tools.file_utils import launchlogger
15class AppendLigand(BiobbObject):
16 """
17 | biobb_gromacs AppendLigand
18 | This class takes a ligand ITP file and inserts it in a topology.
19 | This module automatizes the process of inserting a ligand ITP file in a GROMACS topology.
21 Args:
22 input_top_zip_path (str): Path the input topology TOP and ITP files zipball. File type: input. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/data/gromacs_extra/ndx2resttop.zip>`_. Accepted formats: zip (edam:format_3987).
23 input_itp_path (str): Path to the ligand ITP file to be inserted in the topology. File type: input. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/data/gromacs_extra/pep_ligand.itp>`_. Accepted formats: itp (edam:format_3883).
24 output_top_zip_path (str): Path/Name the output topology TOP and ITP files zipball. File type: output. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/reference/gromacs_extra/ref_appendligand.zip>`_. Accepted formats: zip (edam:format_3987).
25 input_posres_itp_path (str) (Optional): Path to the position restriction ITP file. File type: input. Accepted formats: itp (edam:format_3883).
26 properties (dic):
27 * **posres_name** (*str*) - ("POSRES_LIGAND") String to be included in the ifdef clause.
28 * **remove_tmp** (*bool*) - (True) [WF property] Remove temporal files.
29 * **restart** (*bool*) - (False) [WF property] Do not execute if output files exist.
30 * **sandbox_path** (*str*) - ("./") [WF property] Parent path to the sandbox directory.
32 Examples:
33 This is a use example of how to use the building block from Python::
35 from biobb_gromacs.gromacs_extra.append_ligand import append_ligand
36 prop = { 'posres_name': 'POSRES_LIGAND' }
37 append_ligand(input_top_zip_path='/path/to/myTopology.zip',
38 input_itp_path='/path/to/myTopologyAddOn.itp',
39 output_top_zip_path='/path/to/newTopology.zip',
40 properties=prop)
42 Info:
43 * wrapped_software:
44 * name: In house
45 * license: Apache-2.0
46 * ontology:
47 * name: EDAM
48 * schema: http://edamontology.org/EDAM.owl
49 """
51 def __init__(
52 self,
53 input_top_zip_path: str,
54 input_itp_path: str,
55 output_top_zip_path: str,
56 input_posres_itp_path: Optional[str] = None,
57 properties: Optional[dict] = None,
58 **kwargs,
59 ) -> None:
60 properties = properties or {}
62 # Call parent class constructor
63 super().__init__(properties)
64 self.locals_var_dict = locals().copy()
66 # Input/Output files
67 self.io_dict = {
68 "in": {
69 "input_top_zip_path": input_top_zip_path,
70 "input_itp_path": input_itp_path,
71 "input_posres_itp_path": input_posres_itp_path,
72 },
73 "out": {"output_top_zip_path": output_top_zip_path},
74 }
76 # Properties specific for BB
77 self.posres_name = properties.get("posres_name", "POSRES_LIGAND")
79 # Check the properties
80 self.check_properties(properties)
81 self.check_arguments()
83 @launchlogger
84 def launch(self) -> int:
85 """Execute the :class:`AppendLigand <gromacs_extra.append_ligand.AppendLigand>` object."""
86 # Setup Biobb
87 if self.check_restart():
88 return 0
90 # Unzip topology
91 top_file = fu.unzip_top(
92 zip_file=str(self.io_dict["in"].get("input_top_zip_path")),
93 out_log=self.out_log,
94 )
95 top_dir = str(Path(top_file).parent)
96 itp_name = str(Path(str(self.io_dict["in"].get("input_itp_path"))).name)
98 with open(top_file) as top_f:
99 top_lines = top_f.readlines()
100 top_f.close()
101 fu.rm(top_file)
103 forcefield_pattern = r"#include.*forcefield.itp\""
104 if top_lines:
105 for ff_index, line in enumerate(top_lines):
106 if re.search(forcefield_pattern, line):
107 break
108 else:
109 fu.log(
110 f'FATAL: Input topfile {top_file} from input_top_zip_path {self.io_dict["in"].get("input_top_zip_path")} is empty.',
111 self.out_log,
112 self.global_log,
113 )
114 return 1
116 ligand_itp_path = self.io_dict["in"].get("input_itp_path")
118 # Read ligand itp contents
119 with open(ligand_itp_path, 'r') as itp_file:
120 ligand_itp_contents = itp_file.readlines()
122 # Separate ligand [ atomtypes ] section from the rest
123 lig_atomtypes_section = []
124 remaining_itp_contents = []
125 in_atomtypes_section = False
126 for line in ligand_itp_contents:
127 if line.strip().startswith("[ atomtypes ]"):
128 in_atomtypes_section = True
129 lig_atomtypes_section.append(line)
130 elif in_atomtypes_section:
131 if line.strip() == "" or line.startswith("["):
132 in_atomtypes_section = False
133 remaining_itp_contents.append(line)
134 else:
135 lig_atomtypes_section.append(line)
136 else:
137 remaining_itp_contents.append(line)
139 # If the ligand itp contains an [ atomtypes ] section, merge it into the main topology
140 if lig_atomtypes_section:
142 # Look for the [ atomtypes ] section in the main topology
143 top_atomtypes_section = []
144 in_atomtypes_section = False
145 for line in top_lines:
146 if line.strip().startswith("[ atomtypes ]"):
147 in_atomtypes_section = True
148 top_atomtypes_section.append(line)
149 elif in_atomtypes_section:
150 if line.strip() == "" or line.startswith("["):
151 in_atomtypes_section = False
152 else:
153 top_atomtypes_section.append(line)
155 # If there is already an [ atomtypes ] section in the main topology
156 if top_atomtypes_section:
158 # Remove the header and comments of the ligand [ atomtypes ] section
159 lig_atomtypes_section = lig_atomtypes_section[2:]
161 # Remove the [ atomtypes ] section from top_lines
162 top_lines = [line for line in top_lines if line not in top_atomtypes_section]
164 # NOTE: Check for repeated atoms in the [ atomtypes ] section
165 # NOTE: raise error if there are conflicts - atoms named equally with different parameters
166 # NOTE: raise error if there are different number of columns in the atomtypes sections
168 top_lines.insert(ff_index + 1, "\n")
170 # Merge both [ atomtypes ] sections
171 atomtype_section = top_atomtypes_section + lig_atomtypes_section
173 # Write the merged [ atomtypes ] section into the main topology after the forcefield include
174 for atomtype_index in range(len(atomtype_section)):
175 top_lines.insert(ff_index + atomtype_index + 2, atomtype_section[atomtype_index])
177 # Update the index for the remaining directives
178 at_index = ff_index + atomtype_index + 2
179 else:
180 at_index = ff_index
182 top_lines.insert(at_index + 1, "\n")
183 top_lines.insert(at_index + 2, "; Including ligand ITP\n")
184 top_lines.insert(at_index + 3, '#include "' + itp_name + '"\n')
185 top_lines.insert(at_index + 4, "\n")
186 if self.io_dict["in"].get("input_posres_itp_path"):
187 top_lines.insert(at_index + 5, "; Ligand position restraints" + "\n")
188 top_lines.insert(at_index + 6, "#ifdef " + self.posres_name + "\n")
189 top_lines.insert(
190 at_index + 7,
191 '#include "' + str(Path(self.io_dict["in"].get("input_posres_itp_path", "")).name) + '"\n'
192 )
193 top_lines.insert(at_index + 8, "#endif" + "\n")
194 top_lines.insert(at_index + 9, "\n")
196 inside_moleculetype_section = False
197 with open(self.io_dict["in"].get("input_itp_path", "")) as itp_file:
198 moleculetype_pattern = r"\[ moleculetype \]"
199 for line in itp_file:
200 if re.search(moleculetype_pattern, line):
201 inside_moleculetype_section = True
202 continue
203 if inside_moleculetype_section and not line.startswith(";"):
204 moleculetype = line.strip().split()[0].strip()
205 break
207 molecules_pattern = r"\[ molecules \]"
208 inside_molecules_section = False
209 index_molecule = None
210 molecule_string = (
211 str(moleculetype) + int(20 - len(moleculetype)) * " " + "1" + "\n"
212 )
213 for index, line in enumerate(top_lines):
214 if re.search(molecules_pattern, line):
215 inside_molecules_section = True
216 continue
217 if (
218 inside_molecules_section and not line.startswith(";") and line.upper().startswith("PROTEIN")
219 ):
220 index_molecule = index
222 if index_molecule:
223 top_lines.insert(index_molecule + 1, molecule_string)
224 else:
225 top_lines.append(molecule_string)
227 new_top = fu.create_name(
228 path=top_dir, prefix=self.prefix, step=self.step, name="ligand.top"
229 )
231 with open(new_top, "w") as new_top_f:
232 new_top_f.write("".join(top_lines))
234 # Create a new itp ligand file without the [ atomtypes ] section
235 new_ligand_tip_path = str(Path(top_dir) / itp_name)
236 with open(new_ligand_tip_path, 'w') as new_itp_file:
237 new_itp_file.write("".join(remaining_itp_contents))
239 if self.io_dict["in"].get("input_posres_itp_path"):
240 shutil.copy2(self.io_dict["in"].get("input_posres_itp_path", ""), top_dir)
242 # zip topology
243 fu.log(
244 "Compressing topology to: %s"
245 % self.io_dict["out"].get("output_top_zip_path"),
246 self.out_log,
247 self.global_log,
248 )
249 fu.zip_top(
250 zip_file=self.io_dict["out"].get("output_top_zip_path", ""),
251 top_file=new_top,
252 out_log=self.out_log,
253 remove_original_files=self.remove_tmp
254 )
256 # Remove temporal files
257 self.tmp_files.append(top_dir)
258 self.remove_tmp_files()
260 self.check_arguments(output_files_created=True, raise_exception=False)
261 return 0
264def append_ligand(
265 input_top_zip_path: str,
266 input_itp_path: str,
267 output_top_zip_path: str,
268 input_posres_itp_path: Optional[str] = None,
269 properties: Optional[dict] = None,
270 **kwargs,
271) -> int:
272 """Create :class:`AppendLigand <gromacs_extra.append_ligand.AppendLigand>` class and
273 execute the :meth:`launch() <gromacs_extra.append_ligand.AppendLigand.launch>` method."""
274 return AppendLigand(**dict(locals())).launch()
277append_ligand.__doc__ = AppendLigand.__doc__
278main = AppendLigand.get_main(append_ligand, "This command takes a ligand ITP file and inserts it in a topology")
281if __name__ == "__main__":
282 main()