Coverage for biobb_vs/utils/box.py: 22%

73 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-03 13:34 +0000

1#!/usr/bin/env python3 

2 

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

4from pathlib import PurePath 

5from typing import Optional 

6import numpy as np 

7from biobb_common.generic.biobb_object import BiobbObject 

8from biobb_common.tools import file_utils as fu 

9from biobb_common.tools.file_utils import launchlogger 

10 

11from biobb_vs.utils.common import ( 

12 check_input_path, 

13 check_output_path, 

14 get_box_coordinates, 

15) 

16 

17 

18class Box(BiobbObject): 

19 """ 

20 | biobb_vs Box 

21 | This class sets the center and the size of a rectangular parallelepiped box around a set of residues or a pocket. 

22 | Sets the center and the size of a rectangular parallelepiped box around a set of residues from a given PDB or a pocket from a given PQR. 

23 

24 Args: 

25 input_pdb_path (str): PDB file containing a selection of residue numbers or PQR file containing the pocket. File type: input. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/data/utils/input_box.pqr>`_. Accepted formats: pdb (edam:format_1476), pqr (edam:format_1476). 

26 output_pdb_path (str): PDB including the annotation of the box center and size as REMARKs. File type: output. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/reference/utils/ref_output_box.pdb>`_. Accepted formats: pdb (edam:format_1476). 

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

28 * **offset** (*float*) - (2.0) [0.1~1000|0.1] Extra distance (Angstroms) between the last residue atom and the box boundary. The box is centred on the mean of the selected coordinates, so a set of points that is asymmetric about its centre may extend slightly beyond the box faces. Choose an offset large enough to absorb that asymmetry, yet small enough not to enlarge the box past what the binding site needs, since a larger box spreads the same sampling effort over more space. 

29 * **box_coordinates** (*bool*) - (False) Add box coordinates as 8 ATOM records. 

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

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

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

33 

34 Examples: 

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

36 

37 from biobb_vs.utils.box import box 

38 prop = { 

39 'offset': 2, 

40 'box_coordinates': True 

41 } 

42 box(input_pdb_path='/path/to/myPocket.pqr', 

43 output_pdb_path='/path/to/newBox.pdb', 

44 properties=prop) 

45 

46 Info: 

47 * wrapped_software: 

48 * name: In house 

49 * license: Apache-2.0 

50 * ontology: 

51 * name: EDAM 

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

53 

54 """ 

55 

56 def __init__( 

57 self, input_pdb_path, output_pdb_path, properties=None, **kwargs 

58 ) -> None: 

59 properties = properties or {} 

60 

61 # Call parent class constructor 

62 super().__init__(properties) 

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

64 

65 # Input/Output files 

66 self.io_dict = { 

67 "in": {"input_pdb_path": input_pdb_path}, 

68 "out": {"output_pdb_path": output_pdb_path}, 

69 } 

70 

71 # Properties specific for BB 

72 self.offset = float(properties.get("offset", 2.0)) 

73 self.box_coordinates = float(properties.get("box_coordinates", False)) 

74 self.properties = properties 

75 

76 # Check the properties 

77 self.check_properties(properties) 

78 self.check_arguments() 

79 

80 def check_data_params(self, out_log, err_log): 

81 """Checks all the input/output paths and parameters""" 

82 self.io_dict["in"]["input_pdb_path"] = check_input_path( 

83 self.io_dict["in"]["input_pdb_path"], 

84 "input_pdb_path", 

85 self.out_log, 

86 self.__class__.__name__, 

87 ) 

88 self.io_dict["out"]["output_pdb_path"] = check_output_path( 

89 self.io_dict["out"]["output_pdb_path"], 

90 "output_pdb_path", 

91 False, 

92 self.out_log, 

93 self.__class__.__name__, 

94 ) 

95 

96 @launchlogger 

97 def launch(self) -> int: 

98 """Execute the :class:`Box <utils.box.Box>` utils.box.Box object.""" 

99 

100 # check input/output paths and parameters 

101 self.check_data_params(self.out_log, self.err_log) 

102 

103 # Setup Biobb 

104 if self.check_restart(): 

105 return 0 

106 self.stage_files() 

107 

108 # check if cavity (pdb) or pocket (pqr) 

109 input_type = PurePath(self.io_dict["in"]["input_pdb_path"]).suffix.lstrip(".") 

110 if input_type == "pdb": 

111 fu.log( 

112 "Loading residue PDB selection from %s" 

113 % (self.io_dict["in"]["input_pdb_path"]), 

114 self.out_log, 

115 self.global_log, 

116 ) 

117 else: 

118 fu.log( 

119 "Loading pocket PQR selection from %s" 

120 % (self.io_dict["in"]["input_pdb_path"]), 

121 self.out_log, 

122 self.global_log, 

123 ) 

124 

125 # get input_pdb_path atoms coordinates 

126 selection_atoms_num = 0 

127 x_coordslist = [] 

128 y_coordslist = [] 

129 z_coordslist = [] 

130 with open(self.io_dict["in"]["input_pdb_path"]) as infile: 

131 for line in infile: 

132 if line.startswith("HETATM") or line.startswith("ATOM"): 

133 x_coordslist.append(float(line[31:38].strip())) 

134 y_coordslist.append(float(line[39:46].strip())) 

135 z_coordslist.append(float(line[47:54].strip())) 

136 selection_atoms_num = selection_atoms_num + 1 

137 

138 # Compute binding site box size 

139 

140 # compute box center 

141 selection_box_center = [ 

142 np.average(x_coordslist), 

143 np.average(y_coordslist), 

144 np.average(z_coordslist), 

145 ] 

146 fu.log( 

147 "Binding site center (Angstroms): %10.3f%10.3f%10.3f" 

148 % ( 

149 selection_box_center[0], 

150 selection_box_center[1], 

151 selection_box_center[2], 

152 ), 

153 self.out_log, 

154 self.global_log, 

155 ) 

156 

157 # compute box size 

158 selection_coords_max = np.amax( 

159 [x_coordslist, y_coordslist, z_coordslist], axis=1 

160 ) 

161 selection_box_size = selection_coords_max - selection_box_center 

162 if self.offset: 

163 fu.log( 

164 "Adding %.1f Angstroms offset" % (self.offset), 

165 self.out_log, 

166 self.global_log, 

167 ) 

168 selection_box_size = [c + self.offset for c in selection_box_size] 

169 

170 # SIZE is written as the full edge length, which is what Vina reads from 

171 # --size_x/y/z. selection_box_size stays a half-extent because that is 

172 # what the corner coordinates and the volume are computed from. 

173 selection_box_edge = [2 * side for side in selection_box_size] 

174 fu.log( 

175 "Binding site box edge lengths (Angstroms): %10.3f%10.3f%10.3f" 

176 % (selection_box_edge[0], selection_box_edge[1], selection_box_edge[2]), 

177 self.out_log, 

178 self.global_log, 

179 ) 

180 

181 # compute volume 

182 vol = np.prod(selection_box_size) * 2**3 

183 fu.log("Volume (cubic Angstroms): %.0f" % (vol), self.out_log, self.global_log) 

184 

185 # add box details as PDB remarks 

186 remarks = "REMARK BOX CENTER:%10.3f%10.3f%10.3f" % ( 

187 selection_box_center[0], 

188 selection_box_center[1], 

189 selection_box_center[2], 

190 ) 

191 remarks += " SIZE:%10.3f%10.3f%10.3f" % ( 

192 selection_box_edge[0], 

193 selection_box_edge[1], 

194 selection_box_edge[2], 

195 ) 

196 

197 selection_box_coords_txt = "" 

198 # add (optional) box coordinates as 8 ATOM records 

199 if self.box_coordinates: 

200 fu.log("Adding box coordinates", self.out_log, self.global_log) 

201 selection_box_coords_txt = get_box_coordinates( 

202 selection_box_center, selection_box_size 

203 ) 

204 

205 with open(self.io_dict["out"]["output_pdb_path"], "w") as f: 

206 f.seek(0, 0) 

207 f.write(remarks.rstrip("\r\n") + "\n" + selection_box_coords_txt) 

208 

209 fu.log( 

210 "Saving output PDB file (with box setting annotations): %s" 

211 % (self.io_dict["out"]["output_pdb_path"]), 

212 self.out_log, 

213 self.global_log, 

214 ) 

215 

216 # Copy files to host 

217 self.copy_to_host() 

218 self.remove_tmp_files() 

219 

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

221 

222 return 0 

223 

224 

225def box( 

226 input_pdb_path: str, 

227 output_pdb_path: str, 

228 properties: Optional[dict] = None, 

229 **kwargs, 

230) -> int: 

231 """Create the :class:`Box <utils.box.Box>` class and 

232 execute the :meth:`launch() <utils.box.Box.launch>` method.""" 

233 return Box(**dict(locals())).launch() 

234 

235 

236box.__doc__ = Box.__doc__ 

237main = Box.get_main(box, "Sets the center and the size of a rectangular parallelepiped box around a set of residues from a given PDB or a pocket from a given PQR.") 

238 

239 

240if __name__ == "__main__": 

241 main()