33Qt-based interface for docking result visualization and analysis.
44"""
55
6- import os
76from pathlib import Path
87
98try :
10- from pymol .Qt import QtWidgets , QtCore
9+ from pymol .Qt import QtWidgets
1110 from pymol import cmd
1211except ImportError :
1312 raise ImportError ("PyMOL Qt bindings not available" )
1413
15- from .visualization import (
16- load_binding_modes ,
17- show_pose_ensemble ,
18- color_by_boltzmann_weight ,
19- show_thermodynamics ,
20- export_to_nrgsuite ,
21- _loaded_modes ,
22- _temperature_K ,
23- )
14+ from . import results_adapter as ro_adapter
2415
2516
2617class FlexAIDSPanel (QtWidgets .QDialog ):
@@ -29,20 +20,18 @@ class FlexAIDSPanel(QtWidgets.QDialog):
2920 def __init__ (self , parent = None ):
3021 super ().__init__ (parent )
3122 self .setWindowTitle ("FlexAID∆S: Entropy-Driven Docking" )
32- self .setMinimumWidth (400 )
33- self .setMinimumHeight (500 )
23+ self .setMinimumWidth (420 )
24+ self .setMinimumHeight (520 )
3425
3526 self ._setup_ui ()
3627 self ._connect_signals ()
3728
38- # Data state — mode_name strings parallel to QListWidget rows
39- self ._mode_names : list = []
29+ self ._mode_ids : list [int ] = []
4030
4131 def _setup_ui (self ):
4232 """Construct GUI layout."""
4333 layout = QtWidgets .QVBoxLayout (self )
4434
45- # ─── File loading section ───
4635 file_group = QtWidgets .QGroupBox ("Load Docking Results" )
4736 file_layout = QtWidgets .QHBoxLayout ()
4837
@@ -59,7 +48,12 @@ def _setup_ui(self):
5948 file_group .setLayout (file_layout )
6049 layout .addWidget (file_group )
6150
62- # ─── Binding mode list ───
51+ adapter_info = QtWidgets .QLabel (
52+ "This panel now loads result directories through the read-only flexaidds Python API."
53+ )
54+ adapter_info .setWordWrap (True )
55+ layout .addWidget (adapter_info )
56+
6357 mode_group = QtWidgets .QGroupBox ("Binding Modes" )
6458 mode_layout = QtWidgets .QVBoxLayout ()
6559
@@ -70,7 +64,6 @@ def _setup_ui(self):
7064 mode_group .setLayout (mode_layout )
7165 layout .addWidget (mode_group )
7266
73- # ─── Thermodynamic properties display ───
7467 thermo_group = QtWidgets .QGroupBox ("Thermodynamics" )
7568 thermo_layout = QtWidgets .QFormLayout ()
7669
@@ -89,32 +82,38 @@ def _setup_ui(self):
8982 thermo_group .setLayout (thermo_layout )
9083 layout .addWidget (thermo_group )
9184
92- # ─── Visualization controls ───
9385 viz_group = QtWidgets .QGroupBox ("Visualization" )
9486 viz_layout = QtWidgets .QVBoxLayout ()
9587
9688 self .show_ensemble_btn = QtWidgets .QPushButton ("Show Pose Ensemble" )
9789 self .show_ensemble_btn .setEnabled (False )
9890
99- self .color_boltzmann_btn = QtWidgets .QPushButton ("Color by Boltzmann Weight" )
100- self .color_boltzmann_btn .setEnabled (False )
91+ self .color_cf_btn = QtWidgets .QPushButton ("Color by CF" )
92+ self .color_cf_btn .setEnabled (False )
93+
94+ self .color_free_energy_btn = QtWidgets .QPushButton ("Color by Free Energy" )
95+ self .color_free_energy_btn .setEnabled (False )
10196
10297 self .show_representative_btn = QtWidgets .QPushButton ("Show Representative Only" )
10398 self .show_representative_btn .setEnabled (False )
10499
100+ self .print_details_btn = QtWidgets .QPushButton ("Print Mode Details" )
101+ self .print_details_btn .setEnabled (False )
102+
105103 viz_layout .addWidget (self .show_ensemble_btn )
106- viz_layout .addWidget (self .color_boltzmann_btn )
104+ viz_layout .addWidget (self .color_cf_btn )
105+ viz_layout .addWidget (self .color_free_energy_btn )
107106 viz_layout .addWidget (self .show_representative_btn )
107+ viz_layout .addWidget (self .print_details_btn )
108108
109109 viz_group .setLayout (viz_layout )
110110 layout .addWidget (viz_group )
111111
112- # ─── NRGSuite integration ───
113- nrg_group = QtWidgets .QGroupBox ("NRGSuite Integration" )
112+ nrg_group = QtWidgets .QGroupBox ("Export" )
114113 nrg_layout = QtWidgets .QVBoxLayout ()
115114
116115 self .launch_nrgsuite_btn = QtWidgets .QPushButton ("Launch NRGSuite" )
117- self .export_to_nrg_btn = QtWidgets .QPushButton ("Export to NRGSuite Format " )
116+ self .export_to_nrg_btn = QtWidgets .QPushButton ("Export Mode Table " )
118117 self .export_to_nrg_btn .setEnabled (False )
119118
120119 nrg_layout .addWidget (self .launch_nrgsuite_btn )
@@ -123,7 +122,6 @@ def _setup_ui(self):
123122 nrg_group .setLayout (nrg_layout )
124123 layout .addWidget (nrg_group )
125124
126- # ─── Close button ───
127125 close_btn = QtWidgets .QPushButton ("Close" )
128126 close_btn .clicked .connect (self .close )
129127 layout .addWidget (close_btn )
@@ -137,12 +135,12 @@ def _connect_signals(self):
137135 self .load_btn .clicked .connect (self ._load_results )
138136 self .mode_list .itemSelectionChanged .connect (self ._on_mode_selected )
139137 self .show_ensemble_btn .clicked .connect (self ._show_pose_ensemble )
140- self .color_boltzmann_btn .clicked .connect (self ._color_by_boltzmann )
138+ self .color_cf_btn .clicked .connect (self ._color_by_cf )
139+ self .color_free_energy_btn .clicked .connect (self ._color_by_free_energy )
141140 self .show_representative_btn .clicked .connect (self ._show_representative )
141+ self .print_details_btn .clicked .connect (self ._print_mode_details )
142142 self .launch_nrgsuite_btn .clicked .connect (self ._launch_nrgsuite )
143- self .export_to_nrg_btn .clicked .connect (self ._export_to_nrgsuite )
144-
145- # ── slots ────────────────────────────────────────────────────────────────
143+ self .export_to_nrg_btn .clicked .connect (self ._export_mode_table )
146144
147145 def _browse_directory (self ):
148146 """Open file dialog to select FlexAID output directory."""
@@ -161,107 +159,137 @@ def _load_results(self):
161159 )
162160 return
163161
164- # Delegate to visualization module (parses PDBs, .cad, computes thermo)
165- load_binding_modes (str (output_dir ), temperature = _temperature_K )
162+ try :
163+ ro_adapter .load_docking_results (str (output_dir ))
164+ except Exception as exc :
165+ QtWidgets .QMessageBox .warning (
166+ self ,
167+ "Load Failed" ,
168+ f"Could not load results with the Python adapter:\n { exc } " ,
169+ )
170+ return
166171
167- if not _loaded_modes :
172+ result = ro_adapter ._loaded_result
173+ if result is None or not result .binding_modes :
168174 QtWidgets .QMessageBox .warning (
169- self , "No Results" ,
170- f "No FlexAID result files found in: \n { output_dir } \n \n "
171- "Expected: result_*.pdb files."
175+ self ,
176+ "No Results" ,
177+ f"No docking results could be parsed in: \n { output_dir } " ,
172178 )
173179 return
174180
175- # Populate list widget with ranked modes (sort by free energy)
176181 self .mode_list .clear ()
177- self ._mode_names .clear ()
182+ self ._mode_ids .clear ()
178183
179184 sorted_modes = sorted (
180- _loaded_modes .items (),
181- key = lambda kv : (
182- kv [1 ].free_energy if kv [1 ].free_energy is not None else float ("inf" )
185+ result .binding_modes ,
186+ key = lambda mode : (
187+ mode .free_energy if mode .free_energy is not None else float ("inf" ),
188+ mode .rank ,
189+ mode .mode_id ,
183190 ),
184191 )
185192
186- for mode_name , rec in sorted_modes :
193+ for mode in sorted_modes :
187194 dg_str = (
188- f"{ rec .free_energy :.2f} kcal/mol"
189- if rec .free_energy is not None
195+ f"{ mode .free_energy :.2f} kcal/mol"
196+ if mode .free_energy is not None
190197 else "N/A"
191198 )
192- label = f"{ mode_name } : ΔG = { dg_str } ({ rec . frequency } poses)"
199+ label = f"mode { mode . mode_id } : ΔG = { dg_str } ({ mode . n_poses } poses)"
193200 self .mode_list .addItem (label )
194- self ._mode_names .append (mode_name )
201+ self ._mode_ids .append (mode . mode_id )
195202
196- # Enable controls
197203 for btn in (
198204 self .show_ensemble_btn ,
199- self .color_boltzmann_btn ,
205+ self .color_cf_btn ,
206+ self .color_free_energy_btn ,
200207 self .show_representative_btn ,
208+ self .print_details_btn ,
201209 self .export_to_nrg_btn ,
202210 ):
203211 btn .setEnabled (True )
204212
205- # Auto-select first mode
206213 if self .mode_list .count ():
207214 self .mode_list .setCurrentRow (0 )
208215
209216 QtWidgets .QMessageBox .information (
210217 self ,
211218 "Loaded" ,
212- f"Loaded { len ( _loaded_modes ) } binding modes from { output_dir .name } ." ,
219+ f"Loaded { result . n_modes } binding modes from { output_dir .name } using the Python adapter ." ,
213220 )
214221
222+ def _selected_mode_id (self ) -> int | None :
223+ row = self .mode_list .currentRow ()
224+ if row < 0 or row >= len (self ._mode_ids ):
225+ QtWidgets .QMessageBox .warning (
226+ self , "No Mode Selected" , "Please select a binding mode first."
227+ )
228+ return None
229+ return self ._mode_ids [row ]
230+
231+ def _find_mode (self , mode_id : int ):
232+ result = ro_adapter ._loaded_result
233+ if result is None :
234+ return None
235+ for mode in result .binding_modes :
236+ if mode .mode_id == mode_id :
237+ return mode
238+ return None
239+
215240 def _on_mode_selected (self ):
216241 """Update thermodynamics panel when a binding mode is selected."""
217- row = self .mode_list . currentRow ()
218- if row < 0 or row >= len ( self . _mode_names ) :
242+ mode_id = self ._selected_mode_id ()
243+ if mode_id is None :
219244 return
220245
221- mode_name = self ._mode_names [row ]
222- rec = _loaded_modes .get (mode_name )
223- if rec is None :
246+ mode = self ._find_mode (mode_id )
247+ if mode is None :
224248 return
225249
226- T = _temperature_K
227- entropy_term = (rec .entropy * T ) if rec .entropy is not None else None
250+ temperature = mode .temperature
251+ if temperature is None and ro_adapter ._loaded_result is not None :
252+ temperature = ro_adapter ._loaded_result .temperature
253+ entropy_term = (mode .entropy * temperature ) if (mode .entropy is not None and temperature is not None ) else None
228254
229255 def _fmt (val , fmt = ".4f" ):
230256 return f"{ val :{fmt }} " if val is not None else "N/A"
231257
232- self .free_energy_label .setText (_fmt (rec .free_energy ))
233- self .enthalpy_label .setText (_fmt (rec .enthalpy ))
234- self .entropy_label .setText (_fmt (rec .entropy , ".6f" ))
258+ self .free_energy_label .setText (_fmt (mode .free_energy ))
259+ self .enthalpy_label .setText (_fmt (mode .enthalpy ))
260+ self .entropy_label .setText (_fmt (mode .entropy , ".6f" ))
235261 self .entropy_term_label .setText (_fmt (entropy_term ))
236- self .n_poses_label .setText (str (rec .frequency ))
237-
238- def _selected_mode_name (self ) -> str | None :
239- """Return the currently selected mode name, or None."""
240- row = self .mode_list .currentRow ()
241- if row < 0 or row >= len (self ._mode_names ):
242- QtWidgets .QMessageBox .warning (
243- self , "No Mode Selected" , "Please select a binding mode first."
244- )
245- return None
246- return self ._mode_names [row ]
262+ self .n_poses_label .setText (str (mode .n_poses ))
247263
248264 def _show_pose_ensemble (self ):
249265 """Render all poses in the selected binding mode."""
250- mode_name = self ._selected_mode_name ()
251- if mode_name :
252- show_pose_ensemble (mode_name , show_all = True )
253-
254- def _color_by_boltzmann (self ):
255- """Color poses by Boltzmann weight (blue = low, red = high)."""
256- mode_name = self ._selected_mode_name ()
257- if mode_name :
258- color_by_boltzmann_weight (mode_name )
266+ mode_id = self ._selected_mode_id ()
267+ if mode_id is not None :
268+ ro_adapter .show_binding_mode (mode_id , show_all = 1 )
269+
270+ def _color_by_cf (self ):
271+ """Color poses by CF score."""
272+ mode_id = self ._selected_mode_id ()
273+ if mode_id is not None :
274+ ro_adapter .color_mode_by_score (mode_id , metric = "cf" )
275+
276+ def _color_by_free_energy (self ):
277+ """Color poses by free energy."""
278+ mode_id = self ._selected_mode_id ()
279+ if mode_id is not None :
280+ ro_adapter .color_mode_by_score (mode_id , metric = "free_energy" )
259281
260282 def _show_representative (self ):
261- """Show only the representative pose (highest Boltzmann weight)."""
262- mode_name = self ._selected_mode_name ()
263- if mode_name :
264- show_pose_ensemble (mode_name , show_all = False )
283+ """Show only the representative pose."""
284+ mode_id = self ._selected_mode_id ()
285+ if mode_id is not None :
286+ ro_adapter .show_binding_mode (mode_id , show_all = 0 )
287+
288+ def _print_mode_details (self ):
289+ """Print mode thermodynamic details to the PyMOL console."""
290+ mode_id = self ._selected_mode_id ()
291+ if mode_id is not None :
292+ ro_adapter .show_mode_details (mode_id )
265293
266294 def _launch_nrgsuite (self ):
267295 """Launch NRGSuite plugin (if installed)."""
@@ -275,25 +303,37 @@ def _launch_nrgsuite(self):
275303 "Install from: https://github.com/NRGlab/NRGsuite" ,
276304 )
277305
278- def _export_to_nrgsuite (self ):
279- """Export binding modes to NRGSuite-compatible TSV format ."""
280- output_dir = self . file_path_edit . text (). strip ()
281- if not output_dir :
306+ def _export_mode_table (self ):
307+ """Export the current read-only mode table as TSV ."""
308+ result = ro_adapter . _loaded_result
309+ if result is None :
282310 QtWidgets .QMessageBox .warning (
283- self , "No Directory " , "Load a results directory first."
311+ self , "No Results " , "Load a results directory first."
284312 )
285313 return
286314
287- nrg_file , _ = QtWidgets .QFileDialog .getSaveFileName (
315+ output_dir = self .file_path_edit .text ().strip ()
316+ export_file , _ = QtWidgets .QFileDialog .getSaveFileName (
288317 self ,
289- "Save NRGSuite Export " ,
290- str (Path (output_dir ) / "nrgsuite_export.txt " ),
291- "Text Files (*.txt);;All Files (*)" ,
318+ "Save Mode Table " ,
319+ str (Path (output_dir ) / "flexaids_mode_table.tsv " ),
320+ "Tab-Separated Files (*.tsv);; Text Files (*.txt);;All Files (*)" ,
292321 )
293- if not nrg_file :
322+ if not export_file :
294323 return
295324
296- export_to_nrgsuite (output_dir , nrg_file )
325+ out_path = Path (export_file )
326+ with open (out_path , "w" , encoding = "utf-8" ) as fh :
327+ fh .write (
328+ "mode_id\t rank\t n_poses\t best_cf\t free_energy\t enthalpy\t entropy\t heat_capacity\t temperature\n "
329+ )
330+ for mode in sorted (result .binding_modes , key = lambda m : (m .rank , m .mode_id )):
331+ fh .write (
332+ f"{ mode .mode_id } \t { mode .rank } \t { mode .n_poses } \t { mode .best_cf } \t "
333+ f"{ mode .free_energy } \t { mode .enthalpy } \t { mode .entropy } \t "
334+ f"{ mode .heat_capacity } \t { mode .temperature or result .temperature } \n "
335+ )
336+
297337 QtWidgets .QMessageBox .information (
298- self , "Exported" , f"NRGSuite file written to:\n { nrg_file } "
338+ self , "Exported" , f"Mode table written to:\n { out_path } "
299339 )
0 commit comments