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
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 13:34 +0000
1#!/usr/bin/env python3
3"""Module containing the GninaSelectPose class and the command line interface."""
4from typing import Optional
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
10from biobb_vs.gnina.common import check_input_path, check_output_path, open_sdf, read_sdf_records, to_number
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.
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.
30 Examples:
31 This is a use example of how to use the building block from Python::
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)
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
50 """
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 }
59 def __init__(self, input_sdf_path, output_sdf_path,
60 properties=None, **kwargs) -> None:
61 properties = properties or {}
63 # Call parent class constructor
64 super().__init__(properties)
65 self.locals_var_dict = locals().copy()
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 }
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
79 # Check the properties
80 self.check_properties(properties)
81 self.check_arguments()
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__)
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)
92 def select_record(self, records):
93 """ Narrows the records down to the requested ligand, reorders them and picks one """
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)
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)
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)))
116 return records[self.pose - 1]
118 @launchlogger
119 def launch(self) -> int:
120 """Execute the :class:`GninaSelectPose <gnina.gnina_select_pose.GninaSelectPose>` gnina.gnina_select_pose.GninaSelectPose object."""
122 # check input/output paths and parameters
123 self.check_data_params(self.out_log, self.err_log)
125 # Setup Biobb
126 if self.check_restart():
127 return 0
128 self.stage_files()
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)
133 record = self.select_record(records)
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)
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'])
143 # Copy files to host
144 self.copy_to_host()
146 self.remove_tmp_files()
148 self.check_arguments(output_files_created=True, raise_exception=False)
150 return 0
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()
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.")
163if __name__ == '__main__':
164 main()