Coverage for biobb_vs/gnina/gnina_select_pose.py: 94%

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

4from typing import Optional 

5 

6from biobb_common.generic.biobb_object import BiobbObject 

7from biobb_common.tools import file_utils as fu 

8from biobb_common.tools.file_utils import launchlogger 

9 

10from biobb_vs.gnina.common import check_input_path, check_output_path, open_sdf, read_sdf_records, to_number 

11 

12 

13class GninaSelectPose(BiobbObject): 

14 """ 

15 | biobb_vs GninaSelectPose 

16 | Selects a single pose in the output of the gnina_run building block. 

17 | Extracts one pose out of the multi record SDF file written by the gnina_run building block, copying the record verbatim. 

18 

19 Args: 

20 input_sdf_path (str): Path to the SDF file with the docked poses written by the gnina_run building block. File type: input. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/data/gnina/gnina_poses.sdf>`_. Accepted formats: sdf (edam:format_3814), gz (edam:format_3989). 

21 output_sdf_path (str): Path to the output SDF file with the selected pose. File type: output. `Sample file <https://github.com/bioexcel/biobb_vs/raw/master/biobb_vs/test/reference/gnina/ref_output_pose.sdf>`_. Accepted formats: sdf (edam:format_3814), gz (edam:format_3989). 

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

23 * **pose** (*int*) - (1) [1~10000|1] Rank of the pose to extract, counted over the poses left after ligand has been applied and sort_by has been honoured. 

24 * **ligand** (*int*) - (None) [1~1000000|1] Index of the ligand whose poses are considered, following the order of the ligands in the file gnina docked. All poses in the file are considered when unset. 

25 * **sort_by** (*str*) - (None) Score to reorder the poses by before one is picked. The poses are taken in the order gnina wrote them when unset, which is already gnina's own ranking. Note that this reorders only the poses present in the file, so it is not equivalent to the pose_sort_order property of gnina_run, which reorders the whole internal pool before the redundancy filter and the num_modes cutoff discard poses. Values: CNNscore (network pose score, highest first), CNNaffinity (network predicted affinity, highest first), minimizedAffinity (empirical affinity in kcal/mol, lowest first). 

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

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

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

29 

30 Examples: 

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

32 

33 from biobb_vs.gnina.gnina_select_pose import gnina_select_pose 

34 prop = { 

35 'pose': 1, 

36 'sort_by': 'CNNaffinity' 

37 } 

38 gnina_select_pose(input_sdf_path='/path/to/myPoses.sdf', 

39 output_sdf_path='/path/to/myBestPose.sdf', 

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 

52 # score to sort by, mapped to whether the highest value is the best one 

53 SORT_ORDERS = { 

54 'CNNscore': True, 

55 'CNNaffinity': True, 

56 'minimizedAffinity': False 

57 } 

58 

59 def __init__(self, input_sdf_path, output_sdf_path, 

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

61 properties = properties or {} 

62 

63 # Call parent class constructor 

64 super().__init__(properties) 

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

66 

67 # Input/Output files 

68 self.io_dict = { 

69 "in": {"input_sdf_path": input_sdf_path}, 

70 "out": {"output_sdf_path": output_sdf_path} 

71 } 

72 

73 # Properties specific for BB 

74 self.pose = properties.get('pose', 1) 

75 self.ligand = properties.get('ligand', None) 

76 self.sort_by = properties.get('sort_by', None) 

77 self.properties = properties 

78 

79 # Check the properties 

80 self.check_properties(properties) 

81 self.check_arguments() 

82 

83 def check_data_params(self, out_log, err_log): 

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

85 self.io_dict["in"]["input_sdf_path"] = check_input_path(self.io_dict["in"]["input_sdf_path"], "input_sdf_path", out_log, self.__class__.__name__) 

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

87 

88 if self.sort_by is not None and self.sort_by not in self.SORT_ORDERS: 

89 fu.log(self.__class__.__name__ + ': Unknown sort_by %s, use one of %s, exiting' % (self.sort_by, ', '.join(self.SORT_ORDERS)), out_log) 

90 raise SystemExit(self.__class__.__name__ + ': Unknown sort_by %s' % self.sort_by) 

91 

92 def select_record(self, records): 

93 """ Narrows the records down to the requested ligand, reorders them and picks one """ 

94 

95 if self.ligand is not None: 

96 records = [record for record in records if record['ligand_index'] == self.ligand] 

97 if not records: 

98 fu.log(self.__class__.__name__ + ': No poses found for ligand %s, exiting' % self.ligand, self.out_log) 

99 raise SystemExit(self.__class__.__name__ + ': No poses found for ligand %s' % self.ligand) 

100 fu.log('%d pose(s) found for ligand %s' % (len(records), self.ligand), self.out_log) 

101 

102 if self.sort_by: 

103 missing = [record for record in records if self.sort_by not in record['data']] 

104 if missing: 

105 fu.log(self.__class__.__name__ + ': %s is missing from some poses, gnina only writes it for certain cnn_scoring values, exiting' % self.sort_by, self.out_log) 

106 raise SystemExit(self.__class__.__name__ + ': %s is missing from some poses' % self.sort_by) 

107 records = sorted(records, 

108 key=lambda record: to_number(record['data'][self.sort_by]), 

109 reverse=self.SORT_ORDERS[self.sort_by]) 

110 fu.log('Poses reordered by %s' % self.sort_by, self.out_log) 

111 

112 if self.pose < 1 or self.pose > len(records): 

113 fu.log(self.__class__.__name__ + ': pose %s is out of range, only %d pose(s) to choose from, exiting' % (self.pose, len(records)), self.out_log) 

114 raise SystemExit(self.__class__.__name__ + ': pose %s is out of range, only %d pose(s) to choose from' % (self.pose, len(records))) 

115 

116 return records[self.pose - 1] 

117 

118 @launchlogger 

119 def launch(self) -> int: 

120 """Execute the :class:`GninaSelectPose <gnina.gnina_select_pose.GninaSelectPose>` gnina.gnina_select_pose.GninaSelectPose object.""" 

121 

122 # check input/output paths and parameters 

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

124 

125 # Setup Biobb 

126 if self.check_restart(): 

127 return 0 

128 self.stage_files() 

129 

130 records = read_sdf_records(self.io_dict["in"]["input_sdf_path"]) 

131 fu.log('%d pose(s) read from %s' % (len(records), self.io_dict["in"]["input_sdf_path"]), self.out_log) 

132 

133 record = self.select_record(records) 

134 

135 fu.log('Saving pose %s of ligand %s (%s) to %s file' % ( 

136 record['pose'], record['ligand_index'], record['name'], 

137 self.io_dict["out"]["output_sdf_path"]), self.out_log) 

138 

139 # the record is copied as it stands, scores included 

140 with open_sdf(self.io_dict["out"]["output_sdf_path"], 'wt') as output_file: 

141 output_file.write(record['text']) 

142 

143 # Copy files to host 

144 self.copy_to_host() 

145 

146 self.remove_tmp_files() 

147 

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

149 

150 return 0 

151 

152 

153def gnina_select_pose(input_sdf_path: str, output_sdf_path: str, properties: Optional[dict] = None, **kwargs) -> int: 

154 """Create the :class:`GninaSelectPose <gnina.gnina_select_pose.GninaSelectPose>` class and 

155 execute the :meth:`launch() <gnina.gnina_select_pose.GninaSelectPose.launch>` method.""" 

156 return GninaSelectPose(**dict(locals())).launch() 

157 

158 

159gnina_select_pose.__doc__ = GninaSelectPose.__doc__ 

160main = GninaSelectPose.get_main(gnina_select_pose, "Selects a single pose in the output of the gnina_run building block.") 

161 

162 

163if __name__ == '__main__': 

164 main()