Coverage for biobb_vs/gnina/gnina_run.py: 18%

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

4from pathlib import PurePath 

5from typing import Optional 

6 

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.gnina.common import check_input_path, check_output_path, process_output_gnina 

12 

13 

14class GninaRun(BiobbObject): 

15 """ 

16 | biobb_vs GninaRun 

17 | Wrapper of the gnina software. 

18 | This class performs docking of a ligand to a receptor, optionally rescoring the poses with a convolutional neural network, via the `gnina <https://github.com/gnina/gnina>`_ software. 

19 

20 Args: 

21 input_ligand_path (str): Path to the input ligand. It may hold several ligands and it must hold genuine 3D coordinates, as gnina samples torsions but never bond lengths, bond angles or ring conformations. File type: input. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/data/gnina/gnina_ligand.sdf>`_. Accepted formats: sdf (edam:format_3814), mol2 (edam:format_3816), pdb (edam:format_1476), pdbqt (edam:format_1476). 

22 input_receptor_path (str): Path to the input receptor. Every atom of this file is treated as rigid receptor, so any crystal ligand must be removed beforehand. Provide a PDBQT file for full control over protonation, as PDBQT input is passed to gnina unmodified. Charges are not taken into account, just hydrogen donor/acceptor character which depends on the protonation state. File type: input. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/data/vina/vina_receptor.pdbqt>`_. Accepted formats: pdb (edam:format_1476), pdbqt (edam:format_1476). 

23 input_box_path (str) (Optional): Path to the PDB file with the box center and size annotated as a REMARK, as written by the box and box_residues building blocks. Mutually exclusive with input_autobox_path. File type: input. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/data/vina/vina_box.pdb>`_. Accepted formats: pdb (edam:format_1476). 

24 input_autobox_path (str) (Optional): Path to a reference structure whose bounding coordinates define the docking box, for example a crystal ligand, an fpocket pocket or the whole receptor. It only needs atoms with Cartesian coordinates, it does not need to be a real molecule. Mutually exclusive with input_box_path. File type: input. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/data/gnina/gnina_autobox.pdb>`_. Accepted formats: sdf (edam:format_3814), mol2 (edam:format_3816), pdb (edam:format_1476), pdbqt (edam:format_1476), pqr (edam:format_1476). 

25 output_sdf_path (str): Path to the output file with the docked poses and their scores as SD data fields. Use a .sdf.gz extension to obtain gzip compressed output. File type: output. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/reference/gnina/ref_output_gnina.sdf>`_. Accepted formats: sdf (edam:format_3814), gz (edam:format_3989). 

26 output_summary_path (str) (Optional): Path to the JSON summary file, holding one entry per output pose with the ligand it belongs to and every score gnina assigned to it. File type: output. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/reference/gnina/ref_output_summary.json>`_. Accepted formats: json (edam:format_3464). 

27 output_log_path (str) (Optional): Path to the log file written by gnina. File type: output. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/reference/gnina/ref_output_gnina.log>`_. Accepted formats: log (edam:format_2330). 

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

29 * **cpu** (*int*) - (1) [1~1000|1] Number of CPU cores to use. Keep it lower than or equal to exhaustiveness, and always set it explicitly on a shared machine. 

30 * **exhaustiveness** (*int*) - (8) [1~10000|1] Number of independent Monte Carlo search chains. This is the main sampling knob, but it gives diminishing returns past the default for a targeted pocket. 

31 * **num_modes** (*int*) - (9) [1~1000|1] Maximum number of binding modes written out. 

32 * **min_rmsd_filter** (*float*) - (1.0) [0~100|0.1] RMSD in Angstroms below which a pose is dropped as redundant with a better ranked one. 

33 * **num_mc_saved** (*int*) - (None) [1~10000|1] Number of top poses retained in each Monte Carlo chain, gnina defaults to 50 when unset. 

34 * **seed** (*int*) - (None) Explicit random seed. Docking is stochastic, so set it for reproducible runs. 

35 * **scoring** (*str*) - (None) Built-in empirical scoring function, gnina uses its own default when unset. Values: default (the gnina default empirical scoring function), vina (the AutoDock Vina scoring function), vinardo (a reparameterization of the Vina terms that often does better for virtual screening), ad4_scoring (the AutoDock4 scoring function), dkoes_fast (a fast variant of the dkoes scoring function), dkoes_scoring (the dkoes scoring function), dkoes_scoring_old (the legacy dkoes scoring function). 

36 * **cnn_scoring** (*str*) - (None) Where the convolutional neural network is used in the pipeline, gnina defaults to rescore when unset. Values: none (empirical scoring only throughout, by far the fastest), rescore (the network only re-ranks the final pool of poses), refinement (the network also locally minimizes poses after the Monte Carlo search, around ten times slower), metrorescore (network rescoring combined with Metropolis sampling), metrorefine (network refinement combined with Metropolis sampling), all (the network scores the whole search, very slow). 

37 * **cnn** (*str*) - (None) Name of a built-in convolutional neural network model, or a name ending in _ensemble to evaluate every built-in model sharing that prefix. gnina defaults to an ensemble of three models when unset. 

38 * **pose_sort_order** (*str*) - (None) How the internal pose pool is sorted before the redundancy filter and the num_modes cutoff are applied, so it can surface a different set of poses and not merely reorder them. gnina defaults to CNNscore when unset. Values: CNNscore (sort by network pose score, which answers whether a pose is right), CNNaffinity (sort by predicted affinity, which is what ranks compounds in a screen), Energy (sort by empirical energy). 

39 * **autobox_add** (*float*) - (None) [0~100|0.1] Buffer in Angstroms added on every side of the box derived from input_autobox_path, gnina defaults to 4 when unset. A larger box does not slow gnina down, but it does loosen the constraint on sampling. 

40 * **autobox_extend** (*bool*) - (None) Enlarge the box derived from input_autobox_path when needed so the input ligand can rotate freely inside it, gnina enables this when unset. 

41 * **minimize** (*bool*) - (False) Energy minimize the poses given in input_ligand_path instead of searching for new ones. 

42 * **score_only** (*bool*) - (False) Score the poses given in input_ligand_path without searching or minimizing. 

43 * **local_only** (*bool*) - (False) Restrict the search to a local one inside the box. 

44 * **no_gpu** (*bool*) - (False) Disable GPU acceleration even when a GPU is available. 

45 * **device** (*int*) - (None) [0~16|1] Index of the GPU device to use. 

46 * **quiet** (*bool*) - (False) Suppress the gnina output messages. 

47 * **binary_path** (*str*) - ('gnina') Path to the gnina executable in your local computer. gnina is not distributed with this package, install it from its binary release or run it through a container. 

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

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

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

51 * **container_path** (*str*) - (None) Container path definition. 

52 * **container_image** (*str*) - ('gnina/gnina:latest') Container image definition. 

53 * **container_volume_path** (*str*) - ('/data') Container volume path definition. 

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

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

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

57 

58 Examples: 

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

60 

61 from biobb_vs.gnina.gnina_run import gnina_run 

62 prop = { 

63 'cnn_scoring': 'rescore', 

64 'scoring': 'vinardo', 

65 'exhaustiveness': 8, 

66 'cpu': 4, 

67 'seed': 42 

68 } 

69 gnina_run(input_ligand_path='/path/to/myLigand.sdf', 

70 input_receptor_path='/path/to/myReceptor.pdbqt', 

71 input_box_path='/path/to/myBox.pdb', 

72 output_sdf_path='/path/to/newPoses.sdf', 

73 output_summary_path='/path/to/newSummary.json', 

74 output_log_path='/path/to/newLog.log', 

75 properties=prop) 

76 

77 Instead of a box file, the docking box may be drawn around a reference structure, 

78 which is gnina's own idiom and needs no box file at all. An fpocket pocket works as 

79 a reference, and so does the receptor itself for whole protein docking:: 

80 

81 gnina_run(input_ligand_path='/path/to/myLigand.sdf', 

82 input_receptor_path='/path/to/myReceptor.pdbqt', 

83 input_autobox_path='/path/to/myPocket.pqr', 

84 output_sdf_path='/path/to/newPoses.sdf', 

85 properties={'autobox_add': 4}) 

86 

87 To reach a GPU from inside a container, ask the container runtime for it through the 

88 container_generic_command property, as in {'container_path': 'docker', 

89 'container_generic_command': 'run --gpus all'} for Docker or 'run --nv' for Singularity. 

90 

91 Info: 

92 * wrapped_software: 

93 * name: gnina 

94 * version: >=1.3 

95 * license: Apache-2.0 and GPL-2.0 

96 * ontology: 

97 * name: EDAM 

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

99 

100 """ 

101 

102 # gnina flags taking a value, as (flag, property name) pairs 

103 VALUE_FLAGS = ( 

104 ("--cpu", "cpu"), 

105 ("--exhaustiveness", "exhaustiveness"), 

106 ("--num_modes", "num_modes"), 

107 ("--min_rmsd_filter", "min_rmsd_filter"), 

108 ("--num_mc_saved", "num_mc_saved"), 

109 ("--seed", "seed"), 

110 ("--scoring", "scoring"), 

111 ("--cnn_scoring", "cnn_scoring"), 

112 ("--cnn", "cnn"), 

113 ("--pose_sort_order", "pose_sort_order"), 

114 ("--device", "device"), 

115 ) 

116 

117 # gnina flags that are bare switches 

118 SWITCH_FLAGS = ( 

119 ("--minimize", "minimize"), 

120 ("--score_only", "score_only"), 

121 ("--local_only", "local_only"), 

122 ("--no_gpu", "no_gpu"), 

123 ("--quiet", "quiet"), 

124 ) 

125 

126 def __init__(self, input_ligand_path, input_receptor_path, output_sdf_path, 

127 input_box_path=None, input_autobox_path=None, 

128 output_summary_path=None, output_log_path=None, 

129 properties=None, **kwargs) -> None: 

130 properties = properties or {} 

131 

132 # Call parent class constructor 

133 super().__init__(properties) 

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

135 

136 # Input/Output files 

137 self.io_dict = { 

138 "in": { 

139 "input_ligand_path": input_ligand_path, 

140 "input_receptor_path": input_receptor_path, 

141 "input_box_path": input_box_path, 

142 "input_autobox_path": input_autobox_path 

143 }, 

144 "out": { 

145 "output_sdf_path": output_sdf_path, 

146 "output_summary_path": output_summary_path, 

147 "output_log_path": output_log_path 

148 } 

149 } 

150 

151 # Properties specific for BB 

152 self.cpu = properties.get('cpu', 1) 

153 self.exhaustiveness = properties.get('exhaustiveness', 8) 

154 self.num_modes = properties.get('num_modes', 9) 

155 self.min_rmsd_filter = properties.get('min_rmsd_filter', 1.0) 

156 self.num_mc_saved = properties.get('num_mc_saved', None) 

157 self.seed = properties.get('seed', None) 

158 self.scoring = properties.get('scoring', None) 

159 self.cnn_scoring = properties.get('cnn_scoring', None) 

160 self.cnn = properties.get('cnn', None) 

161 self.pose_sort_order = properties.get('pose_sort_order', None) 

162 self.autobox_add = properties.get('autobox_add', None) 

163 self.autobox_extend = properties.get('autobox_extend', None) 

164 self.minimize = properties.get('minimize', False) 

165 self.score_only = properties.get('score_only', False) 

166 self.local_only = properties.get('local_only', False) 

167 self.no_gpu = properties.get('no_gpu', False) 

168 self.device = properties.get('device', None) 

169 self.quiet = properties.get('quiet', False) 

170 self.binary_path = properties.get('binary_path', 'gnina') 

171 self.properties = properties 

172 

173 # Check the properties 

174 self.check_properties(properties) 

175 self.check_arguments() 

176 

177 def check_data_params(self, out_log, err_log): 

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

179 self.io_dict["in"]["input_ligand_path"] = check_input_path(self.io_dict["in"]["input_ligand_path"], "input_ligand_path", out_log, self.__class__.__name__) 

180 self.io_dict["in"]["input_receptor_path"] = check_input_path(self.io_dict["in"]["input_receptor_path"], "input_receptor_path", out_log, self.__class__.__name__) 

181 self.io_dict["out"]["output_sdf_path"] = check_output_path(self.io_dict["out"]["output_sdf_path"], "output_sdf_path", False, out_log, self.__class__.__name__) 

182 self.io_dict["out"]["output_summary_path"] = check_output_path(self.io_dict["out"]["output_summary_path"], "output_summary_path", True, out_log, self.__class__.__name__) 

183 self.io_dict["out"]["output_log_path"] = check_output_path(self.io_dict["out"]["output_log_path"], "output_log_path", True, out_log, self.__class__.__name__) 

184 

185 # gnina needs a search space, given either as a box file or as a reference structure 

186 if bool(self.io_dict["in"]["input_box_path"]) == bool(self.io_dict["in"]["input_autobox_path"]): 

187 fu.log(self.__class__.__name__ + ': Provide exactly one of input_box_path or input_autobox_path to define the docking box, exiting', out_log) 

188 raise SystemExit(self.__class__.__name__ + ': Provide exactly one of input_box_path or input_autobox_path to define the docking box') 

189 

190 if self.io_dict["in"]["input_box_path"]: 

191 self.io_dict["in"]["input_box_path"] = check_input_path(self.io_dict["in"]["input_box_path"], "input_box_path", out_log, self.__class__.__name__) 

192 # parse it now so an unusable box file is caught before any work is done 

193 self.calculate_box(self.io_dict["in"]["input_box_path"]) 

194 else: 

195 self.io_dict["in"]["input_autobox_path"] = check_input_path(self.io_dict["in"]["input_autobox_path"], "input_autobox_path", out_log, self.__class__.__name__) 

196 

197 def calculate_box(self, box_file_path): 

198 """ Reads the docking box out of the REMARK line written by the box building blocks 

199 

200 Returns the box center and its edge lengths, as strings. SIZE is the 

201 full edge length of the box, which is what gnina expects in 

202 --size_x/y/z, so it is passed through unchanged. 

203 

204 Does not log, as it is called both to validate the box file up front and 

205 to build the command line. 

206 """ 

207 with open(box_file_path, "r") as box_file: 

208 for line in box_file: 

209 if line.startswith("REMARK BOX CENTER"): 

210 fields = line.split() 

211 center = [float(coord) for coord in fields[3:6]] 

212 size = [float(side) for side in fields[-3:]] 

213 return [str(coord) for coord in center], [str(side) for side in size] 

214 

215 fu.log(self.__class__.__name__ + ': No REMARK BOX CENTER line found in %s, exiting' % box_file_path, self.out_log) 

216 raise SystemExit(self.__class__.__name__ + ': No REMARK BOX CENTER line found in %s' % box_file_path) 

217 

218 def cmd_path(self, path): 

219 """ Renders a staged path the way gnina will see it 

220 

221 Inside a container every staged file sits in the mounted volume, so a 

222 bare name is enough once the command has moved there. Locally the staged 

223 paths are already usable as they stand, which also keeps them correct 

224 when the sandbox is disabled or already the working directory. 

225 """ 

226 if self.container_path: 

227 return str(PurePath(path).name) 

228 return str(path) 

229 

230 def build_cmd(self) -> list: 

231 """ Builds the gnina command line out of the staged files and the properties 

232 

233 Kept apart from :meth:`launch` so the command can be inspected without 

234 running gnina. Must be called after ``stage_files``. 

235 """ 

236 if self.container_path: 

237 working_dir = self.container_volume_path if self.container_volume_path else "/data" 

238 else: 

239 working_dir = self.stage_io_dict.get("unique_dir", ".") 

240 

241 cmd = ["cd", working_dir, ";", 

242 self.binary_path, 

243 "--receptor", self.cmd_path(self.stage_io_dict["in"]["input_receptor_path"]), 

244 "--ligand", self.cmd_path(self.stage_io_dict["in"]["input_ligand_path"]), 

245 "--out", self.cmd_path(self.stage_io_dict["out"]["output_sdf_path"])] 

246 

247 # the box file is read here and never handed to gnina, so it is taken 

248 # from io_dict and not from the staged copy 

249 if self.io_dict["in"].get("input_box_path"): 

250 box_path = self.io_dict["in"]["input_box_path"] 

251 center, size = self.calculate_box(box_path) 

252 fu.log('Docking box center %s and edge lengths %s, read from %s' % ( 

253 ' '.join('%.3f' % float(coord) for coord in center), 

254 ' '.join('%.3f' % float(side) for side in size), 

255 PurePath(box_path).name), self.out_log) 

256 cmd.extend(["--center_x", center[0], "--center_y", center[1], "--center_z", center[2], 

257 "--size_x", size[0], "--size_y", size[1], "--size_z", size[2]]) 

258 else: 

259 # gnina opens the reference structure, so this one must be the staged copy 

260 cmd.extend(["--autobox_ligand", self.cmd_path(self.stage_io_dict["in"]["input_autobox_path"])]) 

261 if self.autobox_add is not None: 

262 cmd.extend(["--autobox_add", str(self.autobox_add)]) 

263 if self.autobox_extend is not None: 

264 cmd.extend(["--autobox_extend", "1" if self.autobox_extend else "0"]) 

265 

266 # optional outputs are absent from stage_io_dict when they were not requested 

267 if self.stage_io_dict["out"].get("output_log_path"): 

268 cmd.extend(["--log", self.cmd_path(self.stage_io_dict["out"]["output_log_path"])]) 

269 

270 for flag, prop in self.VALUE_FLAGS: 

271 value = getattr(self, prop) 

272 if value is not None: 

273 cmd.extend([flag, str(value)]) 

274 

275 for flag, prop in self.SWITCH_FLAGS: 

276 if getattr(self, prop): 

277 cmd.append(flag) 

278 

279 return cmd 

280 

281 @launchlogger 

282 def launch(self) -> int: 

283 """Execute the :class:`GninaRun <gnina.gnina_run.GninaRun>` gnina.gnina_run.GninaRun object.""" 

284 

285 # check input/output paths and parameters 

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

287 

288 # Setup Biobb 

289 if self.check_restart(): 

290 return 0 

291 self.stage_files() 

292 

293 # create cmd 

294 self.cmd = self.build_cmd() 

295 

296 fu.log('Executing gnina', self.out_log, self.global_log) 

297 

298 # Run Biobb block 

299 self.run_biobb() 

300 

301 # Copy files to host 

302 self.copy_to_host() 

303 

304 # remove temporary folder(s) 

305 self.remove_tmp_files() 

306 

307 if self.return_code == 0: 

308 if self.io_dict["out"].get("output_summary_path"): 

309 process_output_gnina(self.io_dict["out"]["output_sdf_path"], 

310 self.io_dict["out"]["output_summary_path"], 

311 self.out_log, 

312 self.__class__.__name__) 

313 else: 

314 fu.log('gnina ended with return code %s, no summary was generated' % self.return_code, self.out_log, self.global_log) 

315 

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

317 

318 return self.return_code 

319 

320 

321def gnina_run(input_ligand_path: str, input_receptor_path: str, output_sdf_path: str, 

322 input_box_path: Optional[str] = None, input_autobox_path: Optional[str] = None, 

323 output_summary_path: Optional[str] = None, output_log_path: Optional[str] = None, 

324 properties: Optional[dict] = None, **kwargs) -> int: 

325 """Create the :class:`GninaRun <gnina.gnina_run.GninaRun>` class and 

326 execute the :meth:`launch() <gnina.gnina_run.GninaRun.launch>` method.""" 

327 return GninaRun(**dict(locals())).launch() 

328 

329 

330gnina_run.__doc__ = GninaRun.__doc__ 

331main = GninaRun.get_main(gnina_run, "Performs docking of a ligand to a receptor with CNN rescoring via the gnina software.") 

332 

333 

334if __name__ == '__main__': 

335 main()