Skip to content

Commit 5c29ba1

Browse files
committed
Wire PyMOL GUI to read-only Python results adapter
1 parent 9ebd985 commit 5c29ba1

File tree

1 file changed

+136
-96
lines changed

1 file changed

+136
-96
lines changed

pymol_plugin/gui.py

Lines changed: 136 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,15 @@
33
Qt-based interface for docking result visualization and analysis.
44
"""
55

6-
import os
76
from pathlib import Path
87

98
try:
10-
from pymol.Qt import QtWidgets, QtCore
9+
from pymol.Qt import QtWidgets
1110
from pymol import cmd
1211
except 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

2617
class 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\trank\tn_poses\tbest_cf\tfree_energy\tenthalpy\tentropy\theat_capacity\ttemperature\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

Comments
 (0)