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

1#!/usr/bin/env python3 

2 

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

4 

5import re 

6import shutil 

7from pathlib import Path 

8from typing import Optional 

9 

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 

13 

14 

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. 

20 

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. 

31 

32 Examples: 

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

34 

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) 

41 

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 """ 

50 

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 {} 

61 

62 # Call parent class constructor 

63 super().__init__(properties) 

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

65 

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 } 

75 

76 # Properties specific for BB 

77 self.posres_name = properties.get("posres_name", "POSRES_LIGAND") 

78 

79 # Check the properties 

80 self.check_properties(properties) 

81 self.check_arguments() 

82 

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 

89 

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) 

97 

98 with open(top_file) as top_f: 

99 top_lines = top_f.readlines() 

100 top_f.close() 

101 fu.rm(top_file) 

102 

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 

115 

116 ligand_itp_path = self.io_dict["in"].get("input_itp_path") 

117 

118 # Read ligand itp contents 

119 with open(ligand_itp_path, 'r') as itp_file: 

120 ligand_itp_contents = itp_file.readlines() 

121 

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) 

138 

139 # If the ligand itp contains an [ atomtypes ] section, merge it into the main topology 

140 if lig_atomtypes_section: 

141 

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) 

154 

155 # If there is already an [ atomtypes ] section in the main topology 

156 if top_atomtypes_section: 

157 

158 # Remove the header and comments of the ligand [ atomtypes ] section 

159 lig_atomtypes_section = lig_atomtypes_section[2:] 

160 

161 # Remove the [ atomtypes ] section from top_lines 

162 top_lines = [line for line in top_lines if line not in top_atomtypes_section] 

163 

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 

167 

168 top_lines.insert(ff_index + 1, "\n") 

169 

170 # Merge both [ atomtypes ] sections 

171 atomtype_section = top_atomtypes_section + lig_atomtypes_section 

172 

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]) 

176 

177 # Update the index for the remaining directives 

178 at_index = ff_index + atomtype_index + 2 

179 else: 

180 at_index = ff_index 

181 

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") 

195 

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 

206 

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 

221 

222 if index_molecule: 

223 top_lines.insert(index_molecule + 1, molecule_string) 

224 else: 

225 top_lines.append(molecule_string) 

226 

227 new_top = fu.create_name( 

228 path=top_dir, prefix=self.prefix, step=self.step, name="ligand.top" 

229 ) 

230 

231 with open(new_top, "w") as new_top_f: 

232 new_top_f.write("".join(top_lines)) 

233 

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)) 

238 

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) 

241 

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 ) 

255 

256 # Remove temporal files 

257 self.tmp_files.append(top_dir) 

258 self.remove_tmp_files() 

259 

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

261 return 0 

262 

263 

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() 

275 

276 

277append_ligand.__doc__ = AppendLigand.__doc__ 

278main = AppendLigand.get_main(append_ligand, "This command takes a ligand ITP file and inserts it in a topology") 

279 

280 

281if __name__ == "__main__": 

282 main()