Coverage for biobb_amber/leap/leap_add_ions.py: 60%

219 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 LeapAddIons class and the command line interface.""" 

4 

5import os 

6import re 

7import shutil 

8from decimal import Decimal 

9from pathlib import PurePath 

10from typing import List, Optional 

11 

12from biobb_common.generic.biobb_object import BiobbObject 

13from biobb_common.tools import file_utils as fu 

14from biobb_common.tools.file_utils import launchlogger 

15 

16from biobb_amber.leap.common import _from_string_to_list 

17 

18 

19class LeapAddIons(BiobbObject): 

20 """ 

21 | biobb_amber LeapAddIons 

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

23 | Adds counterions to a system box for an AMBER MD system using tLeap tool from the AmberTools MD package. 

24 

25 Args: 

26 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.ions.input.pdb>`_. Accepted formats: pdb (edam:format_1476). 

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

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

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

30 input_prep_path (str) (Optional): Additional leap parameter files to load with loadAmberPrep 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). 

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

32 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.ions.pdb>`_. Accepted formats: pdb (edam:format_1476). 

33 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.ions.top>`_. Accepted formats: top (edam:format_3881), parmtop (edam:format_3881), prmtop (edam:format_3881). 

34 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.ions.crd>`_. Accepted formats: crd (edam:format_3878), mdcrd (edam:format_3878), inpcrd (edam:format_3878). 

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

36 * **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"]. 

37 * **water_type** (*str*) - ("TIP3PBOX") Water molecule parameters to be used for the topology. Values: POL3BOX, QSPCFWBOX, SPCBOX, SPCFWBOX, TIP3PBOX, TIP3PFBOX, TIP4PBOX, TIP4PEWBOX, OPCBOX, OPC3BOX, TIP5PBOX. 

38 * **box_type** (*str*) - ("truncated_octahedron") Type for the MD system box. Values: cubic, truncated_octahedron. 

39 * **ions_type** (*str*) - ("ionsjc_tip3p") Ions type. Values: ionsjc_tip3p, ionsjc_spce, ionsff99_tip3p, ions_charmm22, ionsjc_tip4pew, None. 

40 * **neutralise** (*bool*) - ("True") Energetically neutralise the system adding the necessary counterions. 

41 * **ionic_concentration** (*float*) - (50) Additional ionic concentration to include in the system box. Units in mM/L. 

42 * **positive_ions_number** (*int*) - (0) Number of additional positive ions to include in the system box. 

43 * **negative_ions_number** (*int*) - (0) Number of additional negative ions to include in the system box. 

44 * **positive_ions_type** (*str*) - ("Na+") Type of additional positive ions to include in the system box. Values: Na+,K+. 

45 * **negative_ions_type** (*str*) - ("Cl-") Type of additional negative ions to include in the system box. Values: Cl-. 

46 * **binary_path** (*str*) - ("tleap") Path to the tleap 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) Container path definition. 

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

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

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

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

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

56 

57 Examples: 

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

59 

60 from biobb_amber.leap.leap_add_ions import leap_add_ions 

61 prop = { 

62 'forcefield': ['protein.ff14SB'], 

63 'water_type': 'TIP3PBOX', 

64 'neutralise' : True 

65 } 

66 leap_add_ions(input_pdb_path='/path/to/structure.pdb', 

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

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

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

70 properties=prop) 

71 

72 Info: 

73 * wrapped_software: 

74 * name: AmberTools tLeap 

75 * version: >20.9 

76 * license: LGPL 2.1 

77 * ontology: 

78 * name: EDAM 

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

80 

81 """ 

82 

83 def __init__( 

84 self, 

85 input_pdb_path: str, 

86 output_pdb_path: str, 

87 output_top_path: str, 

88 output_crd_path: str, 

89 input_lib_path: Optional[str] = None, 

90 input_frcmod_path: Optional[str] = None, 

91 input_params_path: Optional[str] = None, 

92 input_prep_path: Optional[str] = None, 

93 input_source_path: Optional[str] = None, 

94 properties: Optional[dict] = None, 

95 **kwargs, 

96 ): 

97 properties = properties or {} 

98 

99 # Call parent class constructor 

100 super().__init__(properties) 

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

102 

103 # Input/Output files 

104 self.io_dict = { 

105 "in": { 

106 "input_pdb_path": input_pdb_path, 

107 "input_lib_path": input_lib_path, 

108 "input_frcmod_path": input_frcmod_path, 

109 "input_params_path": input_params_path, 

110 "input_prep_path": input_prep_path, 

111 "input_source_path": input_source_path, 

112 }, 

113 "out": { 

114 "output_pdb_path": output_pdb_path, 

115 "output_top_path": output_top_path, 

116 "output_crd_path": output_crd_path, 

117 }, 

118 } 

119 

120 # Ligand Parameter lists 

121 self.ligands_lib_list = [] 

122 if input_lib_path: 

123 self.ligands_lib_list.append(input_lib_path) 

124 

125 self.ligands_frcmod_list = [] 

126 if input_frcmod_path: 

127 self.ligands_frcmod_list.append(input_frcmod_path) 

128 

129 # Properties specific for BB 

130 self.properties = properties 

131 

132 # Set default forcefields 

133 if self.container_path: 

134 self.forcefield = _from_string_to_list( 

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

136 ) 

137 else: 

138 amber_home_path = os.getenv("AMBERHOME") 

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

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

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

142 

143 self.forcefield = _from_string_to_list( 

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

145 ) 

146 

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

148 self.forcefield = self.find_leaprc_paths(self.forcefield) 

149 

150 self.water_type = properties.get("water_type", "TIP3PBOX") 

151 self.box_type = properties.get("box_type", "truncated_octahedron") 

152 self.ions_type = properties.get("ions_type", "ionsjc_tip3p") 

153 self.neutralise = properties.get("neutralise", True) 

154 self.ionic_concentration = properties.get("ionic_concentration", 50) 

155 self.positive_ions_number = properties.get("positive_ions_number", 0) 

156 self.positive_ions_type = properties.get("positive_ions_type", "Na+") 

157 self.negative_ions_number = properties.get("negative_ions_number", 0) 

158 self.negative_ions_type = properties.get("negative_ions_type", "Cl-") 

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

160 

161 # Check the properties 

162 self.check_properties(properties) 

163 self.check_arguments() 

164 

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

166 """ 

167 Find the leaprc paths for the force fields provided. 

168 

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

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

171 directory with and without the leaprc prefix. 

172 

173 Args: 

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

175 

176 Returns: 

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

178 """ 

179 

180 leaprc_paths = [] 

181 

182 for forcefield in forcefields: 

183 

184 num_paths = len(leaprc_paths) 

185 

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

187 if os.path.exists(forcefield): 

188 leaprc_paths.append(forcefield) 

189 continue 

190 

191 # Check if the forcefield is in the leaprc directory 

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

193 if os.path.exists(leaprc_path): 

194 leaprc_paths.append(leaprc_path) 

195 continue 

196 

197 # Check if the forcefield is in the oldff directory 

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

199 if os.path.exists(leaprc_path): 

200 leaprc_paths.append(leaprc_path) 

201 continue 

202 

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

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

205 if os.path.exists(leaprc_path): 

206 leaprc_paths.append(leaprc_path) 

207 continue 

208 

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

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

211 if os.path.exists(leaprc_path): 

212 leaprc_paths.append(leaprc_path) 

213 continue 

214 

215 new_num_paths = len(leaprc_paths) 

216 

217 if new_num_paths == num_paths: 

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

219 

220 return leaprc_paths 

221 

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

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

224 

225 # # Check input(s) 

226 # 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__) 

227 # 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__) 

228 # 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__) 

229 # # 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__) 

230 # # 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__) 

231 

232 # # Check output(s) 

233 # 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__) 

234 # 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__) 

235 # 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__) 

236 

237 def find_out_number_of_ions(self): 

238 """Computes the number of positive and negative ions from the input ionic 

239 concentration and the number of water molecules in the system box.""" 

240 

241 # Finding number of water molecules in the box 

242 cnt = 0 

243 with open(self.io_dict["in"]["input_pdb_path"]) as fp: 

244 for line in fp: 

245 # Water molecules are identified with different resids 

246 # by different forcefields / MD packages 

247 pq = re.compile((r"WAT|HOH|TP3|TIP3|SOL"), re.M) 

248 if pq.search(line): 

249 # Incrementing number of water molecules just in the water 

250 # oxygen atom, ignoring hydrogen or dummy atoms 

251 pq2 = re.compile(r"H|EPW|EP1|EP2", re.M) 

252 if not pq2.search(line): 

253 cnt = cnt + 1 

254 

255 # To get to X mM ions we need 

256 # n(NaCl) = #waters / 55 Mol * X M 

257 # where X M = X mM / 1000 

258 self.nio = int((cnt / 55) * (self.ionic_concentration / 1000)) 

259 

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

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

262 script must load. 

263 

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

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

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

267 temporary folder and referenced by its host path. 

268 """ 

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

270 if file_path is None: 

271 return [] 

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

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

274 if self.container_path: 

275 unique_dir = self.stage_io_dict["unique_dir"] 

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

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

278 return [ 

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

280 for p in extracted 

281 ] 

282 return fu.unzip_list( 

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

284 ) 

285 

286 @launchlogger 

287 def launch(self): 

288 """Launches the execution of the LeapAddIons module.""" 

289 

290 # check input/output paths and parameters 

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

292 

293 # Setup Biobb 

294 if self.check_restart(): 

295 return 0 

296 self.stage_files() 

297 

298 # Water Type 

299 # leaprc.water.tip4pew, tip4pd, tip3p, spceb, spce, opc, fb4, fb3 

300 # Values: POL3BOX, QSPCFWBOX, SPCBOX, SPCFWBOX, TIP3PBOX, TIP3PFBOX, TIP4PBOX, TIP4PEWBOX, OPCBOX, OPC3BOX, TIP5PBOX. 

301 source_wat_command = "source leaprc.water.tip3p" 

302 if self.water_type == "TIP4PEWBOX": 

303 source_wat_command = "leaprc.water.tip4pew" 

304 if self.water_type == "TIP4PBOX": 

305 source_wat_command = "leaprc.water.tip4pd" 

306 if re.match(r"SPC", self.water_type): 

307 source_wat_command = "source leaprc.water.spce" 

308 if re.match(r"OPC", self.water_type): 

309 source_wat_command = "source leaprc.water.opc" 

310 

311 # Counterions 

312 ions_command = "" 

313 if self.neutralise: 

314 # ions_command = ions_command + "addions mol " + self.negative_ions_type + " 0 \n" 

315 # ions_command = ions_command + "addions mol " + self.positive_ions_type + " 0 \n" 

316 ions_command = ( 

317 ions_command + "addionsRand mol " + self.negative_ions_type + " 0 \n" 

318 ) 

319 ions_command = ( 

320 ions_command + "addionsRand mol " + self.positive_ions_type + " 0 \n" 

321 ) 

322 

323 if ( 

324 self.ionic_concentration and self.negative_ions_number == 0 and self.positive_ions_number == 0 

325 ): 

326 self.find_out_number_of_ions() 

327 nneg = self.nio # Update with function 

328 npos = self.nio # Update with function 

329 # ions_command = ions_command + "addions mol " + self.negative_ions_type + " " + str(nneg) + " \n" 

330 # ions_command = ions_command + "addions mol " + self.positive_ions_type + " " + str(npos) + " \n" 

331 ions_command = ( 

332 ions_command + "addionsRand mol " + self.negative_ions_type + " " + str(nneg) + " \n" 

333 ) 

334 ions_command = ( 

335 ions_command + "addionsRand mol " + self.positive_ions_type + " " + str(npos) + " \n" 

336 ) 

337 else: 

338 if self.negative_ions_number != 0: 

339 # ions_command = ions_command + "addions mol " + self.negative_ions_type + " " + str(self.negative_ions_number) + " \n" 

340 ions_command = ( 

341 ions_command + "addionsRand mol " + self.negative_ions_type + " " + str(self.negative_ions_number) + " \n" 

342 ) 

343 if self.positive_ions_number != 0: 

344 # ions_command = ions_command + "addions mol " + self.positive_ions_type + " " + str(self.positive_ions_number) + " \n" 

345 ions_command = ( 

346 ions_command + "addionsRand mol " + self.positive_ions_type + " " + str(self.positive_ions_number) + " \n" 

347 ) 

348 

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

350 if self.container_path: 

351 instructions_file = str( 

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

353 ) 

354 instructions_file_path = str( 

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

356 ) 

357 tmp_folder = None 

358 else: 

359 tmp_folder = fu.create_unique_dir() 

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

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

362 instructions_file_path = instructions_file 

363 

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

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

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

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

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

369 

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

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

372 # Forcefields loaded by default: 

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

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

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

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

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

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

379 

380 # Forcefields loaded from input forcefield property 

381 for t in self.forcefield: 

382 if self.container_path: 

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

384 else: 

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

386 

387 # Additional Leap commands 

388 for leap_commands in leap_source_list: 

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

390 

391 # Water Model loaded from input water_model property 

392 leapin.write(source_wat_command + " \n") 

393 

394 # Ions Type 

395 if self.ions_type != "None": 

396 leapin.write("loadamberparams frcmod." + self.ions_type + "\n") 

397 

398 # Additional Amber parameters 

399 for amber_params in amber_params_list: 

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

401 

402 # Additional Amber prep files 

403 for amber_prep in amber_prep_list: 

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

405 

406 # Ligand(s) libraries (if any) 

407 for amber_lib in ligands_lib_list: 

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

409 for amber_frcmod in ligands_frcmod_list: 

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

411 

412 # Loading PDB file 

413 leapin.write( 

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

415 ) 

416 

417 # Adding ions 

418 leapin.write(ions_command) 

419 

420 # Generating box 

421 leapin.write("setBox mol vdw \n") 

422 

423 # Saving output PDB file, coordinates and topology 

424 leapin.write( 

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

426 ) 

427 leapin.write( 

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

429 ) 

430 leapin.write("quit \n") 

431 

432 # Container path 

433 if self.container_path: 

434 if not self.container_working_dir: 

435 fu.log('WARNING: container_working_dir property was not set. Defining it with the same value as container_volume_path', self.out_log, self.global_log) 

436 self.container_working_dir = self.container_volume_path 

437 

438 # Command line 

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

440 

441 # Run Biobb block 

442 self.run_biobb() 

443 

444 # Copy files to host 

445 self.copy_to_host() 

446 

447 if self.box_type != "cubic": 

448 fu.log( 

449 "Fixing truncated octahedron Box in the topology and coordinates files", 

450 self.out_log, 

451 self.global_log, 

452 ) 

453 

454 # Taking box info from input PDB file, CRYST1 tag (first line) 

455 with open(self.io_dict["in"]["input_pdb_path"]) as file: 

456 lines = file.readlines() 

457 pdb_line = lines[0] 

458 

459 if "OCTBOX" not in pdb_line: 

460 fu.log( 

461 "WARNING: box info not found in input PDB file (OCTBOX). Needed to correctly assign the octahedron box. Assuming cubic box.", 

462 self.out_log, 

463 self.global_log, 

464 ) 

465 else: 

466 # PDB info: CRYST1 86.316 86.316 86.316 109.47 109.47 109.47 P 1 

467 # PDB info: OCTBOX 86.1942924 86.1942924 86.1942924 109.4712190 109.4712190 109.4712190 

468 # regex_box = 'CRYST1\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*P 1' 

469 regex_box = r"OCTBOX\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)\s*(\d+\.\d+)" 

470 box = re.findall(regex_box, pdb_line)[0] 

471 box_line = "" 

472 for coord in box: 

473 box_line += "{:12.7f}".format(float(coord)) 

474 

475 # PRMTOP info: 1.09471219E+02 8.63157502E+01 8.63157502E+01 8.63157502E+01 

476 top_box_line = "" 

477 top_box_line += " %.8E" % Decimal(float(box[3])) 

478 top_box_line += " %.8E" % Decimal(float(box[0])) 

479 top_box_line += " %.8E" % Decimal(float(box[1])) 

480 top_box_line += " %.8E" % Decimal(float(box[2])) 

481 

482 # Removing box generated by tleap from the crd file (last line) 

483 with open(self.io_dict["out"]["output_crd_path"]) as file: 

484 lines = file.readlines() 

485 crd_lines = lines[:-1] 

486 

487 # Adding old box coordinates (taken from the input pdb) 

488 crd_lines.append(box_line) 

489 

490 with open(self.io_dict["out"]["output_crd_path"], "w") as file: 

491 for line in crd_lines: 

492 file.write(str(line)) 

493 file.write("\n") 

494 

495 # Now fixing IFBOX param in prmtop. 

496 box_flag = False 

497 ifbox_flag = 0 

498 # %FLAG BOX_DIMENSIONS 

499 # %FORMAT(5E16.8) 

500 # 1.09471219E+02 8.63157502E+01 8.63157502E+01 8.63157502E+01 

501 

502 tmp_parmtop = str( 

503 # PurePath(str(tmp_folder)).joinpath("top_temp.parmtop") 

504 PurePath(self.stage_io_dict["unique_dir"]).joinpath("top_temp.parmtop") 

505 ) 

506 shutil.copyfile(self.io_dict["out"]["output_top_path"], tmp_parmtop) 

507 

508 with open(self.io_dict["out"]["output_top_path"], "w") as new_top: 

509 with open(tmp_parmtop, "r") as old_top: 

510 for line in old_top: 

511 if "BOX_DIMENSIONS" in line: 

512 box_flag = True 

513 new_top.write(line) 

514 elif box_flag and "FORMAT" not in line: 

515 new_top.write(top_box_line + "\n") 

516 box_flag = False 

517 elif ( 

518 "FLAG POINTERS" in line or ifbox_flag == 1 or ifbox_flag == 2 or ifbox_flag == 3 

519 ): 

520 ifbox_flag += 1 

521 new_top.write(line) 

522 elif ifbox_flag == 4: 

523 # new_top.write(top_box_line + "\n") 

524 new_top.write(line[:56] + " 2" + line[64:]) 

525 ifbox_flag += 1 

526 else: 

527 new_top.write(line) 

528 

529 # remove temporary folder(s) 

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

531 self.remove_tmp_files() 

532 

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

534 

535 return self.return_code 

536 

537 

538def leap_add_ions( 

539 input_pdb_path: str, 

540 output_pdb_path: str, 

541 output_top_path: str, 

542 output_crd_path: str, 

543 input_lib_path: Optional[str] = None, 

544 input_frcmod_path: Optional[str] = None, 

545 input_params_path: Optional[str] = None, 

546 input_prep_path: Optional[str] = None, 

547 input_source_path: Optional[str] = None, 

548 properties: Optional[dict] = None, 

549 **kwargs, 

550) -> int: 

551 """Create the :class:`LeapAddIons <leap.leap_add_ions.LeapAddIons>` class and 

552 execute the :meth:`launch() <leap.leap_add_ions.LeapAddIons.launch>` method.""" 

553 return LeapAddIons(**dict(locals())).launch() 

554 

555 

556leap_add_ions.__doc__ = LeapAddIons.__doc__ 

557main = LeapAddIons.get_main(leap_add_ions, "Adds counterions to a system box for an AMBER MD system using tLeap.") 

558 

559if __name__ == "__main__": 

560 main()