# the GUI of new BFEE
import html
import os
import pathlib
import re
import shutil
import sys
import webbrowser
import numpy as np
import requests
# use appdirs to manage persistent configuration
from appdirs import user_config_dir
from PySide6 import QtCore
from PySide6.QtGui import (
QAction,
QActionGroup,
QColor,
QFont,
QIcon,
QPalette,
QTextCursor,
)
from PySide6.QtWidgets import (
QApplication,
QButtonGroup,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QMainWindow,
QMessageBox,
QProgressDialog,
QPushButton,
QRadioButton,
QSpacerItem,
QSplitter,
QTabWidget,
QTextBrowser,
QToolBar,
QVBoxLayout,
QWidget,
)
import BFEE2.inputGenerator as inputGenerator
import BFEE2.postTreatment as postTreatment
import BFEE2.templates_gromacs.BFEEGromacs as BFEEGromacs
from BFEE2.commonTools import commonSlots, fileParser, ploter
from BFEE2.skills import AISkillExecutor
from BFEE2.templates_readme import rags
from BFEE2.third_party import py_bar
try:
import importlib.resources as pkg_resources
except ImportError:
# Try backported to PY<37 `importlib_resources`.
import importlib_resources as pkg_resources
import BFEE2.version
from BFEE2 import doc
__PROGRAM_NAME__ = f"BFEEstimator v{BFEE2.version.__VERSION__}"
__NAMD_VERSION__ = f"v{BFEE2.version.__NAMD_VERSION__}"
__GMX_VERSION__ = f"{BFEE2.version.__GMX_VERSION__}"
__DEFAULT_OPENROUTER_BASE_URL__ = "https://openrouter.ai/api/v1"
[docs]
class mainSettings(QWidget):
"""settings in the menubar
set pathes of third-party softwares (VMD, gmx and tleap)
"""
def __init__(self):
super().__init__()
self.config_dir = user_config_dir("BFEE2", "chinfo")
# test if config directory exists
if not os.path.exists(self.config_dir):
# create it if not exists
os.makedirs(self.config_dir)
self._initUI()
self._initSingalsSlots()
self.setWindowTitle("Settings")
self.setWindowTitle("Settings")
self.setWindowIcon(QIcon("BFEE2/icon/icon.png"))
self.currentTheme = "dark" # Default theme
self._readConfig()
# self.setGeometry(0,0,0,0)
# self.show()
def _initUI(self):
"""settings GUI"""
self.mainLayout = QVBoxLayout()
# third party softward
self.thirdPartySoftware = QGroupBox("Third party software:")
self.thirdPartySoftwareGridLayout = QGridLayout()
# vmd
self.vmdLabel = QLabel("VMD:")
self.vmdLineEdit = QLineEdit()
self.vmdButton = QPushButton("Browse")
self.thirdPartySoftwareGridLayout.addWidget(self.vmdLabel, 0, 0)
self.thirdPartySoftwareGridLayout.addWidget(self.vmdLineEdit, 0, 1)
self.thirdPartySoftwareGridLayout.addWidget(self.vmdButton, 0, 2)
# online AI assistant
self.onlineAIService = QGroupBox("Online OpenAI-Compatible AI Service:")
self.onlineAIServiceGridLayout = QGridLayout()
# OpenAI-compatible API
self.openAICompatibleAPIAddressLabel = QLabel(
"API Address:"
)
self.openAICompatibleAPIAddressLineEdit = QLineEdit(
__DEFAULT_OPENROUTER_BASE_URL__
)
self.openAICompatibleKeyLabel = QLabel("API Key:")
self.openAICompatibleKeyLineEdit = QLineEdit("")
self.openAICompatibleModelLabel = QLabel("Model:")
self.openAICompatibleModelLineEdit = QLineEdit("")
self.openAICompatibleTemperatureLabel = QLabel("Temperature:")
self.openAICompatibleTemperatureLineEdit = QLineEdit("")
self.openAICompatibleTopPLabel = QLabel("Top P:")
self.openAICompatibleTopPLineEdit = QLineEdit("")
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleAPIAddressLabel, 0, 0
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleAPIAddressLineEdit, 0, 1
)
self.onlineAIServiceGridLayout.addWidget(self.openAICompatibleKeyLabel, 1, 0)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleKeyLineEdit, 1, 1
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleModelLabel, 2, 0
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleModelLineEdit, 2, 1
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleTemperatureLabel, 3, 0
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleTemperatureLineEdit, 3, 1
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleTopPLabel, 4, 0
)
self.onlineAIServiceGridLayout.addWidget(
self.openAICompatibleTopPLineEdit, 4, 1
)
# OK and Cancel
self.settingsButtonLayout = QHBoxLayout()
self.settingsOKButton = QPushButton("OK")
self.settingsButtonLayout.addWidget(QSplitter())
self.settingsButtonLayout.addWidget(self.settingsOKButton)
self.thirdPartySoftware.setLayout(self.thirdPartySoftwareGridLayout)
self.onlineAIService.setLayout(self.onlineAIServiceGridLayout)
self.mainLayout.addWidget(self.thirdPartySoftware)
self.mainLayout.addWidget(self.onlineAIService)
self.mainLayout.addLayout(self.settingsButtonLayout)
self.setLayout(self.mainLayout)
def _initSingalsSlots(self):
"""initialize singals and slots"""
self.vmdButton.clicked.connect(
commonSlots.openFileDialog("exe", self.vmdLineEdit)
)
# self.gromacsButton.clicked.connect(commonSlots.openFileDialog('exe', self.gromacsLineEdit))
# self.tleapButton.clicked.connect(commonSlots.openFileDialog('exe', self.tleapLineEdit))
self.settingsOKButton.clicked.connect(self._OKSlot())
def _readConfig(self):
"""read the config saving paths for third-party softwares"""
if not os.path.exists(f"{self.config_dir}/3rdSoft.ini"):
return
with open(f"{self.config_dir}/3rdSoft.ini", "r") as cFile:
lines = [line.strip() for line in cFile.readlines()]
if not lines:
return
self.vmdLineEdit.setText(lines[0])
if len(lines) >= 7:
self.openAICompatibleAPIAddressLineEdit.setText(lines[1])
self.openAICompatibleKeyLineEdit.setText(lines[2])
self.openAICompatibleModelLineEdit.setText(lines[3])
self.openAICompatibleTemperatureLineEdit.setText(lines[4])
self.openAICompatibleTopPLineEdit.setText(lines[5])
saved_theme = lines[6]
else:
# Legacy OpenRouter-only config: VMD, key, model, temperature, top_p, theme.
if len(lines) > 1:
self.openAICompatibleAPIAddressLineEdit.setText(
__DEFAULT_OPENROUTER_BASE_URL__
)
self.openAICompatibleKeyLineEdit.setText(lines[1])
if len(lines) > 2:
self.openAICompatibleModelLineEdit.setText(lines[2])
if len(lines) > 3:
self.openAICompatibleTemperatureLineEdit.setText(lines[3])
if len(lines) > 4:
self.openAICompatibleTopPLineEdit.setText(lines[4])
saved_theme = lines[5] if len(lines) > 5 else ""
# Read theme
if saved_theme:
self.currentTheme = saved_theme
def _writeConfig(self):
"""write the config saving paths for third-party softwares"""
with open(f"{self.config_dir}/3rdSoft.ini", "w") as cFile:
cFile.write(self.vmdLineEdit.text() + "\n")
cFile.write(self.openAICompatibleAPIAddressLineEdit.text() + "\n")
cFile.write(self.openAICompatibleKeyLineEdit.text() + "\n")
cFile.write(self.openAICompatibleModelLineEdit.text() + "\n")
cFile.write(self.openAICompatibleTemperatureLineEdit.text() + "\n")
cFile.write(self.openAICompatibleTopPLineEdit.text() + "\n")
cFile.write(self.currentTheme + "\n")
def _OKSlot(self):
"""the slot corresponding the OK button"""
def f():
self._writeConfig()
self.close()
return f
[docs]
class geometricAdvancedSettings(QWidget):
"""advanced settings for the geometrical route
set pulling direction, non-standard solvent
and number of stratification windows
"""
def __init__(self):
super().__init__()
self._initUI()
self._initSingalsSlots()
self.setWindowTitle("Advanced settings for the geometrical route")
self.setWindowIcon(QIcon("BFEE2/icon/icon.png"))
self.setGeometry(0, 0, 0, 0)
# self.show()
def _initUI(self):
"""initialize UI for the geometrical advanced settings"""
self.mainLayout = QVBoxLayout()
# user-defined pulling direction
self.userDefinedDirection = QGroupBox("User-defined separation direction")
self.userDefinedDirectionLayout = QHBoxLayout()
self.userDefinedDirectionLabel = QLabel("Reference:")
self.userDefinedDirectionLineEdit = QLineEdit()
self.userDefinedDirectionLayout.addWidget(self.userDefinedDirectionLabel)
self.userDefinedDirectionLayout.addWidget(self.userDefinedDirectionLineEdit)
self.userDefinedDirection.setLayout(self.userDefinedDirectionLayout)
# non-standard solvent
self.nonStandardSolvent = QGroupBox("User-provided large box")
self.nonStandardSolventLayout = QGridLayout()
self.nonStandardSolventPsfLabel = QLabel("psf/parm file:")
self.nonStandardSolventPsfLineEdit = QLineEdit()
self.nonStandardSolventPsfButton = QPushButton("Browse")
self.nonStandardSolventPdbLabel = QLabel("pdb/rst7 file:")
self.nonStandardSolventPdbLineEdit = QLineEdit()
self.nonStandardSolventPdbButton = QPushButton("Browse")
self.nonStandardSolventLayout.addWidget(self.nonStandardSolventPsfLabel, 0, 0)
self.nonStandardSolventLayout.addWidget(
self.nonStandardSolventPsfLineEdit, 0, 1
)
self.nonStandardSolventLayout.addWidget(self.nonStandardSolventPsfButton, 0, 2)
self.nonStandardSolventLayout.addWidget(self.nonStandardSolventPdbLabel, 1, 0)
self.nonStandardSolventLayout.addWidget(
self.nonStandardSolventPdbLineEdit, 1, 1
)
self.nonStandardSolventLayout.addWidget(self.nonStandardSolventPdbButton, 1, 2)
self.nonStandardSolvent.setLayout(self.nonStandardSolventLayout)
# stratification
self.stratification = QGroupBox("Stratification (number of strata)")
self.stratificationLayout = QGridLayout()
self.stratificationRMSDBoundLabel = QLabel("RMSD(Bound):")
self.stratificationRMSDBoundLineEdit = QLineEdit("3")
self.stratificationTheta = QLabel("Theta:")
self.stratificationThetaLineEdit = QLineEdit("1")
self.stratificationPhiLabel = QLabel("Phi:")
self.stratificationPhiLineEdit = QLineEdit("1")
self.stratificationPsiLabel = QLabel("Psi:")
self.stratificationPsiLineEdit = QLineEdit("1")
self.stratificationthetaLabel = QLabel("theta:")
self.stratificationthetaLineEdit = QLineEdit("1")
self.stratificationphiLabel = QLabel("phi:")
self.stratificationphiLineEdit = QLineEdit("1")
self.stratificationRLabel = QLabel("r:")
self.stratificationRLineEdit = QLineEdit("5")
self.stratificationRMSDUnboundLabel = QLabel("RMSD(Unbound):")
self.stratificationRMSDUnboundLineEdit = QLineEdit("3")
self.stratificationLayout.addWidget(self.stratificationRMSDBoundLabel, 0, 0)
self.stratificationLayout.addWidget(self.stratificationRMSDBoundLineEdit, 0, 1)
self.stratificationLayout.addWidget(self.stratificationTheta, 0, 2)
self.stratificationLayout.addWidget(self.stratificationThetaLineEdit, 0, 3)
self.stratificationLayout.addWidget(self.stratificationPhiLabel, 0, 4)
self.stratificationLayout.addWidget(self.stratificationPhiLineEdit, 0, 5)
self.stratificationLayout.addWidget(self.stratificationPsiLabel, 0, 6)
self.stratificationLayout.addWidget(self.stratificationPsiLineEdit, 0, 7)
self.stratificationLayout.addWidget(self.stratificationthetaLabel, 1, 0)
self.stratificationLayout.addWidget(self.stratificationthetaLineEdit, 1, 1)
self.stratificationLayout.addWidget(self.stratificationphiLabel, 1, 2)
self.stratificationLayout.addWidget(self.stratificationphiLineEdit, 1, 3)
self.stratificationLayout.addWidget(self.stratificationRLabel, 1, 4)
self.stratificationLayout.addWidget(self.stratificationRLineEdit, 1, 5)
self.stratificationLayout.addWidget(self.stratificationRMSDUnboundLabel, 1, 6)
self.stratificationLayout.addWidget(
self.stratificationRMSDUnboundLineEdit, 1, 7
)
self.stratification.setLayout(self.stratificationLayout)
# compatibility
self.compatibility = QGroupBox("Compatibility")
self.compatibilityLayout = QGridLayout()
self.pinDownProCheckbox = QCheckBox("Pinning down the protein")
self.pinDownProCheckbox.setChecked(True)
self.useOldCvCheckbox = QCheckBox("Use quaternion-based CVs")
self.useOldCvCheckbox.setChecked(False)
self.reflectingBoundaryCheckbox = QCheckBox("Use reflecting boundary")
self.reflectingBoundaryCheckbox.setChecked(True)
self.useCUDASOAIntegrator = QCheckBox("Use CUDASOA integrator")
self.useCUDASOAIntegrator.setChecked(True)
self.compatibilityLayout.addWidget(self.pinDownProCheckbox, 0, 0)
self.compatibilityLayout.addWidget(self.useOldCvCheckbox, 0, 1)
self.compatibilityLayout.addWidget(self.reflectingBoundaryCheckbox, 1, 0)
self.compatibilityLayout.addWidget(self.useCUDASOAIntegrator, 1, 1)
self.compatibility.setLayout(self.compatibilityLayout)
# force field settings
self.FFSettings = QGroupBox("Force field settings")
self.FFSettingsLayout = QHBoxLayout()
self.OPLSMixingRuleCheckbox = QCheckBox("OPLS mixing rules")
self.OPLSMixingRuleCheckbox.setChecked(False)
self.timestepLayout = QHBoxLayout()
self.timestepLabel = QLabel(" Timestep:")
self.timestepLineEdit = QLineEdit("2.0")
self.timestepLayout.addWidget(self.timestepLabel)
self.timestepLayout.addWidget(self.timestepLineEdit)
self.FFSettingsLayout.addWidget(self.OPLSMixingRuleCheckbox)
self.FFSettingsLayout.addLayout(self.timestepLayout)
self.FFSettings.setLayout(self.FFSettingsLayout)
# strategy settings
self.strategy = QGroupBox("Strategy settings")
self.strategyLayout = QHBoxLayout()
self.considerRMSDCVCheckbox = QCheckBox(
"Take into account RMSD CV (flexible ligand)"
)
self.considerRMSDCVCheckbox.setChecked(True)
self.useGaWTMCheckbox = QCheckBox("Use GaWTM-eABF")
self.useGaWTMCheckbox.setChecked(False)
self.strategyLayout.addWidget(self.considerRMSDCVCheckbox)
self.strategyLayout.addWidget(self.useGaWTMCheckbox)
self.strategy.setLayout(self.strategyLayout)
# membrane protein
self.modeling = QGroupBox("Modeling (avaiable for CHARMM FF)")
self.modelingLayout = QVBoxLayout()
self.memProCheckbox = QCheckBox("Membrane protein")
self.memProCheckbox.setChecked(False)
self.neutralizeLigOnlyLayout = QHBoxLayout()
self.neutralizeLigOnlyLabel = QLabel("Auto-neutralize ligand-only system by:")
self.neutralizeLigOnlyCombobox = QComboBox()
self.neutralizeLigOnlyCombobox.addItem("NaCl")
self.neutralizeLigOnlyCombobox.addItem("KCl")
self.neutralizeLigOnlyCombobox.addItem("CaCl2")
self.neutralizeLigOnlyCombobox.addItem("None")
self.neutralizeLigOnlyLayout.addWidget(self.neutralizeLigOnlyLabel)
self.neutralizeLigOnlyLayout.addWidget(self.neutralizeLigOnlyCombobox)
self.modelingLayout.addWidget(self.memProCheckbox)
self.modelingLayout.addLayout(self.neutralizeLigOnlyLayout)
self.modeling.setLayout(self.modelingLayout)
# parallel runs for error estimation
self.parallelRuns = QGroupBox("Parallel runs")
self.parallelRunsLayout = QHBoxLayout()
self.parallelRunsLabel = QLabel("Number of parallel runs: ")
self.parallelRunsLineEdit = QLineEdit("1")
self.parallelRunsLayout.addWidget(self.parallelRunsLabel)
self.parallelRunsLayout.addWidget(self.parallelRunsLineEdit)
self.parallelRuns.setLayout(self.parallelRunsLayout)
self.geometricAdvancedSettingsButtonLayout = QHBoxLayout()
self.geometricAdvancedSettingsOKButton = QPushButton("OK")
# self.geometricAdvancedSettingsCancelButton = QPushButton('Cancel')
self.geometricAdvancedSettingsButtonLayout.addWidget(QSplitter())
self.geometricAdvancedSettingsButtonLayout.addWidget(
self.geometricAdvancedSettingsOKButton
)
# self.geometricAdvancedSettingsButtonLayout.addWidget(self.geometricAdvancedSettingsCancelButton)
self.mainLayout.addWidget(self.userDefinedDirection)
self.mainLayout.addWidget(self.nonStandardSolvent)
self.mainLayout.addWidget(self.stratification)
self.mainLayout.addWidget(self.compatibility)
self.mainLayout.addWidget(self.FFSettings)
self.mainLayout.addWidget(self.strategy)
self.mainLayout.addWidget(self.modeling)
self.mainLayout.addWidget(self.parallelRuns)
self.mainLayout.addLayout(self.geometricAdvancedSettingsButtonLayout)
self.setLayout(self.mainLayout)
# below are slow functions
def _toggleGaWTMBox(self, checked):
"""when enable GaWTM, restraint simulations and considering RMSD are not needed, and
re-equilbration and double-wide simulations are necessary.
"""
if checked:
self.useCUDASOAIntegrator.setEnabled(False)
self.useCUDASOAIntegrator.setChecked(False)
else:
self.useCUDASOAIntegrator.setEnabled(True)
self.useCUDASOAIntegrator.setChecked(True)
def _toggleRMSDLineEdits(self, checked):
"""Enable or disable RMSD(Bound) and RMSD(Unbound) LineEdits based on checkbox state."""
self.stratificationRMSDBoundLineEdit.setEnabled(checked)
self.stratificationRMSDUnboundLineEdit.setEnabled(checked)
def _initSingalsSlots(self):
"""initialize (connect) signial and slots for geometrical advanced settings"""
self.nonStandardSolventPsfButton.clicked.connect(
commonSlots.openFileDialog(
"psf/parm/top", self.nonStandardSolventPsfLineEdit
)
)
self.nonStandardSolventPdbButton.clicked.connect(
commonSlots.openFileDialog("pdb/gro", self.nonStandardSolventPdbLineEdit)
)
self.geometricAdvancedSettingsOKButton.clicked.connect(self.close)
self.useGaWTMCheckbox.toggled.connect(self._toggleGaWTMBox)
self.considerRMSDCVCheckbox.toggled.connect(self._toggleRMSDLineEdits)
[docs]
class alchemicalAdvancedSettings(QWidget):
"""advanced settings for the alchemical route
set the number of stratification windows
"""
def __init__(self):
super().__init__()
self._initUI()
self._initSingalsSlots()
self.setWindowTitle("Advanced settings for the alchemical route")
self.setWindowIcon(QIcon("BFEE2/icon/icon.png"))
self.setGeometry(0, 0, 0, 0)
# self.show()
def _initUI(self):
"""initialize UI for the alchemical advanced settings"""
self.mainLayout = QVBoxLayout()
# stratification windows
self.stratification = QGroupBox("Stratification (number of strata)")
self.stratificationLayout = QGridLayout()
self.boundLigandLabel = QLabel("Ligand/Bound state:")
self.boundLigandLineEdit = QLineEdit("200")
self.unboundLigandLabel = QLabel("Ligand/Unbound state:")
self.unboundLigandLineEdit = QLineEdit("100")
self.boundRestraintsLabel = QLabel("Restraints/Bound state:")
self.boundRestraintsLineEdit = QLineEdit("200")
self.unboundRestraintsLabel = QLabel("Restraints/Unbound state:")
self.unboundRestraintsLineEdit = QLineEdit("100")
self.stratificationLayout.addWidget(self.boundLigandLabel, 0, 0)
self.stratificationLayout.addWidget(self.boundLigandLineEdit, 0, 1)
self.stratificationLayout.addWidget(self.unboundLigandLabel, 0, 2)
self.stratificationLayout.addWidget(self.unboundLigandLineEdit, 0, 3)
self.stratificationLayout.addWidget(self.boundRestraintsLabel, 1, 0)
self.stratificationLayout.addWidget(self.boundRestraintsLineEdit, 1, 1)
self.stratificationLayout.addWidget(self.unboundRestraintsLabel, 1, 2)
self.stratificationLayout.addWidget(self.unboundRestraintsLineEdit, 1, 3)
self.stratification.setLayout(self.stratificationLayout)
# double-wide simulation
self.doubleWide = QGroupBox("Double-wide sampling simulation")
self.doubleWideLayout = QGridLayout()
self.doubleWideCheckbox = QCheckBox(
"Generate input files for double-wide sampling"
)
self.doubleWideCheckbox.setChecked(True)
self.doubleWideLayout.addWidget(self.doubleWideCheckbox)
self.doubleWide.setLayout(self.doubleWideLayout)
# minimize before sampling in each window
self.minBeforeSample = QGroupBox("Minimization before sampling")
self.minBeforeSampleLayout = QVBoxLayout()
self.minBeforeSampleCheckbox = QCheckBox(
"Minimize before sampling in each window"
)
self.minBeforeSampleCheckbox.setChecked(False)
self.minBeforeSampleLayout.addWidget(self.minBeforeSampleCheckbox)
self.minBeforeSample.setLayout(self.minBeforeSampleLayout)
# compatibility
self.compatibility = QGroupBox("Compatibility")
self.compatibilityLayout = QGridLayout()
self.pinDownProCheckbox = QCheckBox("Pinning down the protein")
self.pinDownProCheckbox.setChecked(True)
self.useOldCvCheckbox = QCheckBox("Use quaternion-based CVs")
self.useOldCvCheckbox.setChecked(False)
self.useCUDASOAIntegrator = QCheckBox("Use CUDASOA integrator")
self.useCUDASOAIntegrator.setChecked(True)
self.compatibilityLayout.addWidget(self.pinDownProCheckbox, 0, 0)
self.compatibilityLayout.addWidget(self.useOldCvCheckbox, 0, 1)
self.compatibilityLayout.addWidget(self.useCUDASOAIntegrator, 1, 0)
self.compatibility.setLayout(self.compatibilityLayout)
# force field settings
self.FFSettings = QGroupBox("Force field settings")
self.FFSettingsLayout = QHBoxLayout()
self.OPLSMixingRuleCheckbox = QCheckBox("OPLS mixing rules")
self.OPLSMixingRuleCheckbox.setChecked(False)
self.timestepLayout = QHBoxLayout()
self.timestepLabel = QLabel(" Timestep:")
self.timestepLineEdit = QLineEdit("2.0")
self.timestepLayout.addWidget(self.timestepLabel)
self.timestepLayout.addWidget(self.timestepLineEdit)
self.FFSettingsLayout.addWidget(self.OPLSMixingRuleCheckbox)
self.FFSettingsLayout.addLayout(self.timestepLayout)
self.FFSettings.setLayout(self.FFSettingsLayout)
# strategy settings
self.strategy = QGroupBox("Strategy settings")
self.strategyLayout = QGridLayout()
self.considerRMSDCVCheckbox = QCheckBox(
"Take into account RMSD CV (flexible ligand)"
)
self.considerRMSDCVCheckbox.setChecked(True)
self.reEqCheckbox = QCheckBox("Re-equilibration after histogram")
self.reEqCheckbox.setChecked(True)
self.LDDMCheckbox = QCheckBox("Use LDDM strategy")
self.LDDMCheckbox.setChecked(False)
self.useWTMLambdaABFCheckbox = QCheckBox("Use WTM-lambdaABF")
self.useWTMLambdaABFCheckbox.setChecked(False)
self.strategyLayout.addWidget(self.considerRMSDCVCheckbox, 0, 0)
self.strategyLayout.addWidget(self.reEqCheckbox, 0, 1)
self.strategyLayout.addWidget(self.LDDMCheckbox, 1, 0)
self.strategyLayout.addWidget(self.useWTMLambdaABFCheckbox, 1, 1)
self.strategy.setLayout(self.strategyLayout)
# membrane protein
self.modeling = QGroupBox("Modeling (avaiable for CHARMM FF)")
self.modelingLayout = QVBoxLayout()
self.memProCheckbox = QCheckBox("Membrane protein")
self.memProCheckbox.setChecked(False)
self.neutralizeLigOnlyLayout = QHBoxLayout()
self.neutralizeLigOnlyLabel = QLabel("Auto-neutralize ligand-only system by:")
self.neutralizeLigOnlyCombobox = QComboBox()
self.neutralizeLigOnlyCombobox.addItem("NaCl")
self.neutralizeLigOnlyCombobox.addItem("KCl")
self.neutralizeLigOnlyCombobox.addItem("CaCl2")
self.neutralizeLigOnlyCombobox.addItem("None")
self.neutralizeLigOnlyLayout.addWidget(self.neutralizeLigOnlyLabel)
self.neutralizeLigOnlyLayout.addWidget(self.neutralizeLigOnlyCombobox)
self.modelingLayout.addWidget(self.memProCheckbox)
self.modelingLayout.addLayout(self.neutralizeLigOnlyLayout)
self.modeling.setLayout(self.modelingLayout)
self.alchemicalAdvancedSettingsButtonLayout = QHBoxLayout()
self.alchemicalAdvancedSettingsOKButton = QPushButton("OK")
# self.alchemicalAdvancedSettingsCancelButton = QPushButton('Cancel')
self.alchemicalAdvancedSettingsButtonLayout.addWidget(QSplitter())
self.alchemicalAdvancedSettingsButtonLayout.addWidget(
self.alchemicalAdvancedSettingsOKButton
)
# self.alchemicalAdvancedSettingsButtonLayout.addWidget(self.alchemicalAdvancedSettingsCancelButton)
self.mainLayout.addWidget(self.stratification)
self.mainLayout.addWidget(self.doubleWide)
self.mainLayout.addWidget(self.compatibility)
self.mainLayout.addWidget(self.FFSettings)
self.mainLayout.addWidget(self.strategy)
self.mainLayout.addWidget(self.minBeforeSample)
self.mainLayout.addWidget(self.modeling)
self.mainLayout.addLayout(self.alchemicalAdvancedSettingsButtonLayout)
self.setLayout(self.mainLayout)
# below are slow functions
def _toggleLDDMBox(self, checked):
"""when enable LDDM, restraint simulations and considering RMSD are not needed, and
re-equilbration and double-wide simulations are necessary.
"""
if checked:
self.boundRestraintsLineEdit.setEnabled(False)
self.unboundRestraintsLineEdit.setEnabled(False)
self.considerRMSDCVCheckbox.setChecked(False)
self.considerRMSDCVCheckbox.setEnabled(False)
self.reEqCheckbox.setChecked(True)
self.reEqCheckbox.setEnabled(False)
self.doubleWideCheckbox.setChecked(True)
self.doubleWideCheckbox.setEnabled(False)
self.minBeforeSampleCheckbox.setChecked(False)
self.minBeforeSampleCheckbox.setEnabled(False)
self.useWTMLambdaABFCheckbox.setChecked(False)
self.useWTMLambdaABFCheckbox.setEnabled(False)
else:
self.boundRestraintsLineEdit.setEnabled(True)
self.unboundRestraintsLineEdit.setEnabled(True)
self.considerRMSDCVCheckbox.setEnabled(True)
self.reEqCheckbox.setEnabled(True)
self.doubleWideCheckbox.setEnabled(True)
self.minBeforeSampleCheckbox.setEnabled(True)
self.useWTMLambdaABFCheckbox.setEnabled(True)
def _toggleRMSDLineEdit(self, checked):
"""Enable or disable Restraints/Unbound State LineEdit based on RMSD CV checkbox state."""
self.unboundRestraintsLineEdit.setEnabled(checked)
def _initSingalsSlots(self):
"""initialize (connect) signals and slots for the alchemical advanced settings"""
self.LDDMCheckbox.toggled.connect(self._toggleLDDMBox)
self.considerRMSDCVCheckbox.toggled.connect(self._toggleRMSDLineEdit)
self.alchemicalAdvancedSettingsOKButton.clicked.connect(self.close)
[docs]
class AIAssistantDialog(QWidget):
"""AI Assistant dialog for interactive help with OpenAI-compatible API"""
THEME_COLORS = {
"dark": {
"surface": "#1e1e1e",
"panel": "#2b2b2b",
"border": "#555555",
"text": "#f0f0f0",
"accent": "#409EFF",
},
"light": {
"surface": "#ffffff",
"panel": "#f5f5f7",
"border": "#d2d2d7",
"text": "#1d1d1f",
"accent": "#0071e3",
},
"ocean": {
"surface": "#1e293b",
"panel": "#0f172a",
"border": "#334155",
"text": "#f1f5f9",
"accent": "#22d3ee",
},
"forest": {
"surface": "#21252b",
"panel": "#282c34",
"border": "#3e4451",
"text": "#abb2bf",
"accent": "#98c379",
},
"candy": {
"surface": "#2c2c2c",
"panel": "#212121",
"border": "#424242",
"text": "#ffffff",
"accent": "#ff007a",
},
}
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.skillExecutor = AISkillExecutor(self)
self._hasShownWelcomeMessage = False
self._themeName = self._resolveThemeName()
self._conversationEntries = []
self._initUI()
self._initSignalsSlots()
self.setWindowTitle("AI Assistant")
self.setGeometry(100, 100, 800, 600)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.Dialog | QtCore.Qt.Tool)
self.messageHistory = ""
[docs]
def showEvent(self, event):
"""Display a welcome message the first time the dialog is opened."""
super().showEvent(event)
if not self._hasShownWelcomeMessage:
self._appendConversationText(
"AI: I am the BFEE3 AI assistant. I can explain the algorithms in BFEE3 and where they are best applied, help you configure options for generating input files, and describe what each option does."
)
self._hasShownWelcomeMessage = True
def _initUI(self):
"""Initialize the UI for AI Assistant dialog"""
self.mainLayout = QVBoxLayout()
# Conversation display area
self.conversationDisplay = QTextBrowser()
self.conversationDisplay.setOpenExternalLinks(True)
self.conversationDisplay.document().setDocumentMargin(8)
self.applyTheme(self._themeName)
# Input area
self.inputLayout = QHBoxLayout()
self.inputLineEdit = QLineEdit()
self.inputLineEdit.setPlaceholderText("Type your message here...")
self.sendButton = QPushButton("Send")
self.inputLayout.addWidget(self.inputLineEdit)
self.inputLayout.addWidget(self.sendButton)
# Clear conversation button
self.clearButton = QPushButton("Clear Conversation")
# Layout assembly
self.mainLayout.addWidget(
QLabel("AI Assistant - Ask about and automate BFEE settings:")
)
self.mainLayout.addWidget(self.conversationDisplay)
self.mainLayout.addLayout(self.inputLayout)
self.mainLayout.addWidget(self.clearButton)
self.setLayout(self.mainLayout)
def _initSignalsSlots(self):
"""Initialize signals and slots"""
self.sendButton.clicked.connect(self._sendMessage)
self.inputLineEdit.returnPressed.connect(self._sendMessage)
self.clearButton.clicked.connect(self._clearMessageHistory)
def _sendMessage(self):
"""Send message to OpenAI-compatible API and display response"""
user_message = self.inputLineEdit.text().strip()
if not user_message:
return
# Add user message to display
self._appendConversationText(f"You: {user_message}")
self.messageHistory += f"You: {user_message}\n"
self.inputLineEdit.clear()
# Get API settings from parent's mainSettings
if self.parent:
api_base_url = (
self.parent.mainSettings.openAICompatibleAPIAddressLineEdit.text()
.strip()
)
api_key = (
self.parent.mainSettings.openAICompatibleKeyLineEdit.text().strip()
)
model = (
self.parent.mainSettings.openAICompatibleModelLineEdit.text().strip()
)
# optional parameters
temperature_text = (
self.parent.mainSettings.openAICompatibleTemperatureLineEdit.text()
.strip()
)
top_p_text = (
self.parent.mainSettings.openAICompatibleTopPLineEdit.text().strip()
)
else:
self._appendConversationText("AI: Error - Cannot access API settings")
self.messageHistory += f"AI: Error - Cannot access API settings\n"
return
if not api_base_url or not api_key or not model:
settings_message = (
"AI: Please configure OpenAI-compatible API address, key, "
"and model in Settings first"
)
self._appendConversationText(settings_message)
self.messageHistory += f"{settings_message}\n"
return
if not api_base_url.lower().startswith(("http://", "https://")):
self._appendConversationText(
"AI: Please configure a valid OpenAI-compatible API address in Settings"
)
self.messageHistory += (
f"AI: Please configure a valid OpenAI-compatible API address in Settings\n"
)
return
# Show a modal waiting dialog in front of the AI window
wait_dlg = QProgressDialog("Receiving server response...", None, 0, 0, self)
wait_dlg.setWindowTitle("Please wait")
wait_dlg.setCancelButton(None) # non-interactive
wait_dlg.setWindowModality(QtCore.Qt.WindowModal) # block AI window
wait_dlg.setAutoClose(False)
wait_dlg.setAutoReset(False)
wait_dlg.setMinimumDuration(0)
# Remove close/help buttons
wait_dlg.setWindowFlag(QtCore.Qt.WindowCloseButtonHint, False)
wait_dlg.setWindowFlag(QtCore.Qt.WindowContextHelpButtonHint, False)
wait_dlg.show()
QApplication.processEvents()
# Prepare API request
try:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
data = {
"model": model,
"messages": [
{"role": "system", "content": rags.systemPrompt},
{"role": "user", "content": self.messageHistory},
],
}
if temperature_text:
try:
data["temperature"] = float(temperature_text)
except ValueError:
pass
if top_p_text:
try:
data["top_p"] = float(top_p_text)
except ValueError:
pass
response = requests.post(
f"{api_base_url.rstrip('/')}/chat/completions",
headers=headers,
json=data,
timeout=30,
)
if response.status_code == 200:
result = response.json()
ai_response = result["choices"][0]["message"]["content"]
self._appendConversationText(f"AI: {ai_response}")
self.messageHistory += f"AI: {ai_response}\n"
self.skillExecutor.execute(ai_response)
else:
self._appendConversationText(
f"AI: Error - API request failed: {response.status_code}"
)
self.messageHistory += (
f"AI: Error - API request failed: {response.status_code}\n"
)
except Exception as e:
self._appendConversationText(f"AI: Error - {str(e)}")
self.messageHistory += f"AI: Error - {str(e)}\n"
finally:
wait_dlg.close()
def _appendConversationText(self, text: str):
"""Append a chat message using a bubble-style layout."""
self._conversationEntries.append(text)
self._insertConversationText(text)
def _insertConversationText(self, text: str):
"""Render one message entry in the conversation area."""
message_type, speaker, content = self._classifyConversationText(text)
bubble_html = self._buildConversationBubble(
message_type, speaker, self._formatConversationBody(content)
)
cursor = self.conversationDisplay.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertHtml(bubble_html)
cursor.insertBlock()
self.conversationDisplay.setTextCursor(cursor)
scrollbar = self.conversationDisplay.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def _resolveThemeName(self):
"""Return the currently selected app theme."""
if self.parent and hasattr(self.parent, "mainSettings"):
return self.parent.mainSettings.currentTheme
return "system"
[docs]
def applyTheme(self, theme_name=None):
"""Apply the current app theme to the conversation display."""
self._themeName = theme_name or self._resolveThemeName()
colors = self._getConversationThemeColors()
self.conversationDisplay.setStyleSheet(
f"""
QTextBrowser {{
background-color: {self._colorToCss(colors["surface"])};
color: {self._colorToCss(colors["text"])};
border: 1px solid {self._colorToCss(colors["border"])};
border-radius: 10px;
padding: 6px;
}}
QTextBrowser a {{
color: {self._colorToCss(colors["accent"])};
}}
"""
)
self._rerenderConversation()
def _rerenderConversation(self):
"""Rebuild all bubbles so existing messages follow the current theme."""
if not hasattr(self, "conversationDisplay"):
return
scrollbar = self.conversationDisplay.verticalScrollBar()
keep_at_bottom = scrollbar.value() >= max(0, scrollbar.maximum() - 4)
self.conversationDisplay.clear()
for text in self._conversationEntries:
self._insertConversationText(text)
if keep_at_bottom:
scrollbar.setValue(scrollbar.maximum())
def _getConversationThemeColors(self):
"""Get the base colors for the chat UI."""
theme_colors = self.THEME_COLORS.get(self._themeName)
if theme_colors:
return {name: QColor(value) for name, value in theme_colors.items()}
palette = self.conversationDisplay.palette()
return {
"surface": palette.color(QPalette.Base),
"panel": palette.color(QPalette.Window),
"border": palette.color(QPalette.Mid),
"text": palette.color(QPalette.Text),
"accent": palette.color(QPalette.Highlight),
}
def _buildBubbleStyles(self):
"""Derive bubble colors from the active theme."""
colors = self._getConversationThemeColors()
user_background = self._blendColors(colors["panel"], colors["accent"], 0.82)
user_text = self._idealForeground(user_background)
ai_background = self._blendColors(colors["surface"], colors["panel"], 0.45)
exec_background = self._blendColors(colors["surface"], colors["accent"], 0.14)
return {
"user": {
"align": "right",
"width": "78%",
"background": self._colorToCss(user_background),
"border": self._colorToCss(
self._blendColors(colors["border"], colors["accent"], 0.72)
),
"text": self._colorToCss(user_text),
"label": self._colorToCss(
self._blendColors(user_text, user_background, 0.28)
),
},
"ai": {
"align": "left",
"width": "78%",
"background": self._colorToCss(ai_background),
"border": self._colorToCss(
self._blendColors(colors["border"], colors["surface"], 0.22)
),
"text": self._colorToCss(colors["text"]),
"label": self._colorToCss(
self._blendColors(colors["text"], ai_background, 0.34)
),
},
"exec": {
"align": "center",
"width": "62%",
"background": self._colorToCss(exec_background),
"border": self._colorToCss(
self._blendColors(colors["border"], colors["accent"], 0.26)
),
"text": self._colorToCss(colors["text"]),
"label": self._colorToCss(
self._blendColors(colors["text"], exec_background, 0.34)
),
},
}
def _blendColors(self, base: QColor, overlay: QColor, alpha: float):
"""Blend two colors and return a new QColor."""
alpha = max(0.0, min(alpha, 1.0))
return QColor(
round(base.red() * (1.0 - alpha) + overlay.red() * alpha),
round(base.green() * (1.0 - alpha) + overlay.green() * alpha),
round(base.blue() * (1.0 - alpha) + overlay.blue() * alpha),
)
def _idealForeground(self, background: QColor):
"""Pick a readable foreground color for a given background."""
luminance = (
0.299 * background.red()
+ 0.587 * background.green()
+ 0.114 * background.blue()
)
return QColor("#111111") if luminance > 170 else QColor("#ffffff")
def _colorToCss(self, color: QColor):
"""Convert QColor to a CSS hex string."""
return color.name()
def _classifyConversationText(self, text: str):
"""Map plain text messages into display roles for chat bubbles."""
if text.startswith("You: "):
return ("user", "You", text[5:])
if text.startswith("AI: "):
return ("ai", "AI Assistant", text[4:])
return ("exec", "BFEE2", text)
def _formatConversationBody(self, text: str):
"""Escape user content and turn plain URLs into clickable links."""
escaped_text = html.escape(text)
escaped_text = re.sub(
r"(https?://[^\s<]+)",
r'<a href="\1" style="color: inherit; text-decoration: underline;">\1</a>',
escaped_text,
)
return escaped_text.replace("\n", "<br>")
def _buildConversationBubble(self, message_type: str, speaker: str, body_html: str):
"""Render one message bubble aligned by speaker."""
bubble_styles = self._buildBubbleStyles()
style = bubble_styles[message_type]
return f"""
<table width="100%" cellspacing="0" cellpadding="0" style="margin: 0 0 12px 0;">
<tr>
<td align="{style["align"]}">
<table width="{style["width"]}" cellspacing="0" cellpadding="0"
style="background-color: {style["background"]};
border: 1px solid {style["border"]};
border-radius: 14px;">
<tr>
<td style="padding: 10px 12px;">
<div style="font-size: 10pt; font-weight: 600; color: {style["label"]}; margin-bottom: 4px;">
{html.escape(speaker)}
</div>
<div style="font-size: 10pt; line-height: 1.45; color: {style["text"]}; white-space: pre-wrap;">
{body_html}
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
"""
def _clearMessageHistory(self):
"""Clear the message history"""
self.conversationDisplay.clear()
self._conversationEntries = []
self.messageHistory = ""
def _appendExecMessage(self, message: str):
"""Append execution status message to the AI dialog."""
self._appendConversationText(message)
self.messageHistory += message + "\n"
def _execute_skill_json(self, ai_text: str):
"""Parse and execute whitelisted skills from a JSON payload."""
self.skillExecutor.execute(ai_text)
def _execute_suggested_calls(self, ai_text: str):
"""
Backward-compatible wrapper. The old dynamic function-call execution path
has been replaced by JSON skill dispatch with a strict whitelist.
"""
self._execute_skill_json(ai_text)
[docs]
class mainUI(QMainWindow):
"""the main window UI
include the preTreatment, postTreatment and QuickPlot tab
the preTreatment tab include NAMD and Gromacs tab
the postTreatment tab include geometric and alchemical tab
"""
def __init__(self):
super().__init__()
self._initActions()
self._initNAMDTab()
self._initGromacsTab()
self._initPreTreatmentTab()
self._initQuickPlotTab()
self._initGeometricTab()
self._initAlchemicalTab()
self._toggleAlchemicalMethod() # Set initial state for alchemical method selection
self._initLDDMTab()
self._initPostTreatmentTab()
self._initSingalsSlots()
self._initMainUI()
# other dialogs
self.mainSettings = mainSettings()
self.alchemicalAdvancedSettings = alchemicalAdvancedSettings()
self.geometricAdvancedSettings = geometricAdvancedSettings()
self.aiAssistantDialog = AIAssistantDialog(self)
self.aiAssistantDialog.hide()
# Apply saved theme
self._applyTheme(self.mainSettings.currentTheme, save_config=False)
self.setGeometry(0, 0, 0, 0)
self.setWindowTitle(__PROGRAM_NAME__)
self.setWindowIcon(QIcon("BFEE2/icon/icon.png"))
self.show()
self._showMDEngineVersionWarning()
def _applyTheme(self, theme_name, save_config=True):
"""Apply the selected theme to the application."""
app = QApplication.instance()
style_file = ""
# update state
if theme_name == "dark":
self.modernDarkAction.setChecked(True)
style_file = "modern_dark.qss"
elif theme_name == "light":
self.modernLightAction.setChecked(True)
style_file = "modern_light.qss"
elif theme_name == "ocean":
self.oceanBlueAction.setChecked(True)
style_file = "ocean_blue.qss"
elif theme_name == "forest":
self.forestNightAction.setChecked(True)
style_file = "forest_night.qss"
elif theme_name == "candy":
self.candyPopAction.setChecked(True)
style_file = "candy_pop.qss"
elif theme_name == "system":
self.systemStyleAction.setChecked(True)
# apply style
if style_file:
try:
style_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "styles", style_file
)
if os.path.exists(style_path):
with open(style_path, "r") as f:
app.setStyleSheet(f.read())
else:
print(f"Style file not found: {style_path}")
except Exception as e:
print(f"Error loading style: {e}")
else:
app.setStyleSheet("")
if hasattr(self, "aiAssistantDialog"):
self.aiAssistantDialog.applyTheme(theme_name)
# save to config
if save_config:
self.mainSettings.currentTheme = theme_name
self.mainSettings._writeConfig()
def _showMDEngineVersionWarning(self):
"""show a message box ask for the latest NAMD version"""
msgBox = QMessageBox(self)
msgBox.setIcon(QMessageBox.Warning)
msgBox.setWindowTitle("Warning")
msgBox.setTextFormat(QtCore.Qt.RichText)
msgBox.setText(
f"\
{__PROGRAM_NAME__} is fully compatible with <b>NAMD {__NAMD_VERSION__}</b> or <b>GROMACS {__GMX_VERSION__}</b>. <br>\
Please use the same or a later version of NAMD or GROMACS if you have any problem.<br>\
<br>\
<b>We highly recommend you start from Quick Settings menu.</b><br>"
)
msgBox.exec_()
def _initActions(self):
"""initialize actions for menubar"""
# settings
self.settingsAction = QAction("&Settings", self)
self.settingsAction.setStatusTip("Setting third-party service")
self.settingsAction.triggered.connect(self._mainSettings())
# exit
self.exitAction = QAction("&Exit", self)
self.exitAction.setStatusTip("Exit application")
self.exitAction.triggered.connect(QApplication.quit)
# theme
self.themeActionGroup = QActionGroup(self)
self.systemStyleAction = QAction("System Style", self)
self.systemStyleAction.setCheckable(True)
self.systemStyleAction.triggered.connect(lambda: self._applyTheme("system"))
self.themeActionGroup.addAction(self.systemStyleAction)
self.modernDarkAction = QAction("Modern Dark", self)
self.modernDarkAction.setCheckable(True)
# self.modernDarkAction.setChecked(True) # Handled by _applyTheme on init
self.modernDarkAction.triggered.connect(lambda: self._applyTheme("dark"))
self.themeActionGroup.addAction(self.modernDarkAction)
self.modernLightAction = QAction("Modern Light", self)
self.modernLightAction.setCheckable(True)
self.modernLightAction.triggered.connect(lambda: self._applyTheme("light"))
self.themeActionGroup.addAction(self.modernLightAction)
self.oceanBlueAction = QAction("Ocean Blue", self)
self.oceanBlueAction.setCheckable(True)
self.oceanBlueAction.triggered.connect(lambda: self._applyTheme("ocean"))
self.themeActionGroup.addAction(self.oceanBlueAction)
self.forestNightAction = QAction("Forest Night", self)
self.forestNightAction.setCheckable(True)
self.forestNightAction.triggered.connect(lambda: self._applyTheme("forest"))
self.themeActionGroup.addAction(self.forestNightAction)
self.candyPopAction = QAction("Candy Pop", self)
self.candyPopAction.setCheckable(True)
self.candyPopAction.triggered.connect(lambda: self._applyTheme("candy"))
self.themeActionGroup.addAction(self.candyPopAction)
# quick settings
# geometrical protein protein
self.quickGeometricProteinProteinAction = QAction(
"Protein-Protein / Geometrical"
)
self.quickGeometricProteinProteinAction.setStatusTip(
"Change settings for geometrical protein-protein binding free-energy calculations"
)
self.quickGeometricProteinProteinAction.triggered.connect(
self._quickSetProteinProteinGeometric
)
# geometrical protein ligand
self.quickGeometricProteinLigandAction = QAction("Protein-Ligand / Geometrical")
self.quickGeometricProteinLigandAction.setStatusTip(
"Change settings for geometrical protein-ligand binding free-energy calculations"
)
self.quickGeometricProteinLigandAction.triggered.connect(
self._quickSetProteinLigandGeometric
)
# alchemical protein ligand
self.quickAlchemicalProteinLigandAction = QAction("Protein-Ligand / Alchemical")
self.quickAlchemicalProteinLigandAction.setStatusTip(
"Change settings for alchemical protein-ligand binding free-energy calculations"
)
self.quickAlchemicalProteinLigandAction.triggered.connect(
self._quickSetProteinLigandAlchemical
)
# LDDM
self.quickLDDMProteinLigandAction = QAction("Protein-Ligand / LDDM")
self.quickLDDMProteinLigandAction.setStatusTip(
"Change settings for protein-ligand binding free-energy calculations using the LDDM strategy"
)
self.quickLDDMProteinLigandAction.triggered.connect(
self._quickSetProteinLigandLDDM
)
# LDDM
self.quickAISettingAction = QAction("AI-Assisted Setting")
self.quickAISettingAction.setStatusTip("Set up simulations using AI assistance")
self.quickAISettingAction.triggered.connect(self._quickSetAI)
# help
self.helpAction = QAction("&Help (Deprecated)", self)
self.helpAction.setStatusTip("Open user manual")
self.helpAction.triggered.connect(self._openDocFile)
# python API
self.pythonAPIAction = QAction("&Python API Docs", self)
self.pythonAPIAction.setStatusTip("Open Python API Documentation")
self.pythonAPIAction.triggered.connect(self._openPythonAPIFile)
# about
self.aboutAction = QAction("&About", self)
self.aboutAction.setStatusTip("About BFEEstimator")
self.aboutAction.triggered.connect(self._showAboutBox())
def _initMainUI(self):
"""initialize main window"""
# status bar
# self.statusBar()
# menu bar
menubar = self.menuBar()
menubar.setNativeMenuBar(False)
self.fileMenu = menubar.addMenu("&File")
self.fileMenu.addAction(self.settingsAction)
self.themeMenu = self.fileMenu.addMenu("Theme")
self.themeMenu.addAction(self.systemStyleAction)
self.themeMenu.addAction(self.modernDarkAction)
self.themeMenu.addAction(self.modernLightAction)
self.themeMenu.addAction(self.oceanBlueAction)
self.themeMenu.addAction(self.forestNightAction)
self.themeMenu.addAction(self.candyPopAction)
self.fileMenu.addSeparator()
self.fileMenu.addAction(self.exitAction)
self.quickSettingsMenu = menubar.addMenu("&Quick Settings")
self.quickSettingsMenu.addAction(self.quickGeometricProteinProteinAction)
self.quickSettingsMenu.addAction(self.quickGeometricProteinLigandAction)
self.quickSettingsMenu.addAction(self.quickAlchemicalProteinLigandAction)
self.quickSettingsMenu.addAction(self.quickLDDMProteinLigandAction)
self.quickSettingsMenu.addAction(self.quickAISettingAction)
self.helpMenu = menubar.addMenu("&Help")
# User manual is temporarily hidden - to be restored later
# self.helpMenu.addAction(self.helpAction)
self.helpMenu.addAction(self.pythonAPIAction)
self.helpMenu.addSeparator()
self.helpMenu.addAction(self.aboutAction)
# main layout
self.mainLayout = QVBoxLayout()
# title
self.title = QLabel("Binding Free Energy Estimator")
titleFont = QFont()
titleFont.setBold(True)
self.title.setFont(titleFont)
self.titleBox = QGroupBox()
self.titleBoxLayout = QVBoxLayout()
self.titleBoxLayout.addWidget(self.title, alignment=QtCore.Qt.AlignCenter)
self.titleBox.setLayout(self.titleBoxLayout)
# tabs
self.mainTabs = QTabWidget()
# self.preTreatmentTab = QWidget()
# self.postTreatmentTab = QWidget()
# self.quickPlot = QWidget()
self.mainTabs.addTab(self.preTreatmentTab, "Pre-treatment")
self.mainTabs.addTab(self.postTreatmentTab, "Post-treatment")
self.mainTabs.addTab(self.quickPlot, "Quick-Plot")
# main layout
# self.mainLayout.addWidget(self.titleBox)
self.mainLayout.addWidget(self.mainTabs)
self.mainWidgit = QWidget()
self.mainWidgit.setLayout(self.mainLayout)
self.setCentralWidget(self.mainWidgit)
def _initPreTreatmentTab(self):
"""initialize pre-treatment tab"""
self.preTreatmentTab = QWidget()
# pre-treatment tabs
# NAMD and gromacs
self.preTreatmentMainTabs = QTabWidget()
self.preTreatmentMainTabs.addTab(
self.NAMDTab, "NAMD/Gromacs(CHARMM/Amber files)"
)
self.preTreatmentMainTabs.addTab(self.GromacsTab, "Gromacs(Gromacs files)")
self.preTreatmentMainLayout = QVBoxLayout()
self.preTreatmentMainLayout.addWidget(self.preTreatmentMainTabs)
# other parameters
self.otherParameters = QGroupBox("Other parameters")
self.otherParametersLayout = QVBoxLayout()
# temperature, selection protein and ligand layout
self.otherParametersChildLayout = QGridLayout()
# temperature
self.temperatureLabel = QLabel("Temperature: ")
self.temperatureLineEdit = QLineEdit("300")
self.otherParametersChildLayout.addWidget(self.temperatureLabel, 0, 0)
self.otherParametersChildLayout.addWidget(self.temperatureLineEdit, 0, 1)
# select protein
self.selectProteinLabel = QLabel("Select protein: ")
self.selectProteinLineEdit = QLineEdit("segid SH3D")
self.otherParametersChildLayout.addWidget(self.selectProteinLabel, 1, 0)
self.otherParametersChildLayout.addWidget(self.selectProteinLineEdit, 1, 1)
# select ligand
self.selectLigandLabel = QLabel("Select ligand: ")
self.selectLigandLineEdit = QLineEdit("segid PPRO")
self.otherParametersChildLayout.addWidget(self.selectLigandLabel, 2, 0)
self.otherParametersChildLayout.addWidget(self.selectLigandLineEdit, 2, 1)
# select strategy
self.selectStrategyLayout = QHBoxLayout()
self.selectStrategyLabel = QLabel("Select MD engine and strategy: ")
self.selectStrategyCombobox = QComboBox()
self.selectStrategyCombobox.addItem("Geometrical")
self.selectStrategyCombobox.addItem("Alchemical")
self.selectStrategyAdvancedButton = QPushButton("Advanced settings")
self.selectMDEngineCombobox = QComboBox()
self.selectMDEngineCombobox.addItem("NAMD")
self.selectMDEngineCombobox.addItem("Gromacs")
self.selectStrategyChildLayout = QHBoxLayout()
self.selectStrategyChildLayout.addWidget(self.selectMDEngineCombobox)
self.selectStrategyChildLayout.addWidget(self.selectStrategyCombobox)
self.selectStrategyChildLayout.addWidget(self.selectStrategyAdvancedButton)
self.selectStrategyLayout.addWidget(self.selectStrategyLabel)
self.selectStrategyLayout.addLayout(self.selectStrategyChildLayout)
# generate input button
self.generateInputButton = QPushButton("Generate Inputs")
self.otherParametersLayout.addLayout(self.otherParametersChildLayout)
self.otherParametersLayout.addLayout(self.selectStrategyLayout)
self.otherParameters.setLayout(self.otherParametersLayout)
self.preTreatmentMainLayout.addWidget(self.otherParameters)
self.preTreatmentMainLayout.addWidget(self.generateInputButton)
self.preTreatmentTab.setLayout(self.preTreatmentMainLayout)
def _initNAMDTab(self):
"""initialize NAMD Tab in pre-treatment Tab"""
self.NAMDTab = QWidget()
self.NAMDTabMainLayout = QVBoxLayout()
# inputs for the complex
self.inputsForComplex = QGroupBox("Inputs for complex")
self.inputsForComplexLayout = QGridLayout()
# psf/parm
self.psfLabel = QLabel("psf/parm file:")
self.psfLineEdit = QLineEdit()
self.psfButton = QPushButton("Browse")
self.inputsForComplexLayout.addWidget(self.psfLabel, 0, 0)
self.inputsForComplexLayout.addWidget(self.psfLineEdit, 0, 1)
self.inputsForComplexLayout.addWidget(self.psfButton, 0, 2)
# coor
self.coorLabel = QLabel("pdb/rst file:")
self.coorLineEdit = QLineEdit()
self.coorButton = QPushButton("Browse")
self.inputsForComplexLayout.addWidget(self.coorLabel, 1, 0)
self.inputsForComplexLayout.addWidget(self.coorLineEdit, 1, 1)
self.inputsForComplexLayout.addWidget(self.coorButton, 1, 2)
# force fields
self.forceFields = QGroupBox("Force fields")
self.forceFieldsLayout = QVBoxLayout()
# force field type
self.forceFieldTypeLayout = QHBoxLayout()
self.forceFieldTypeLabel = QLabel("Force field type:")
self.forceFieldCombobox = QComboBox()
self.forceFieldCombobox.addItem("CHARMM")
self.forceFieldCombobox.addItem("Amber")
self.forceFieldTypeLayout.addWidget(self.forceFieldTypeLabel)
self.forceFieldTypeLayout.addWidget(self.forceFieldCombobox)
# CHARMM force field files
self.forceFieldFilesLayout = QVBoxLayout()
self.forceFieldFilesLabel = QLabel("Force field files:")
self.forceFieldFilesBox = QListWidget()
self.forceFieldFilesChildLayout = QHBoxLayout()
self.forceFieldAddButton = QPushButton("Add")
self.forceFieldClearButton = QPushButton("Clear")
self.forceFieldFilesChildLayout.addWidget(self.forceFieldAddButton)
self.forceFieldFilesChildLayout.addWidget(self.forceFieldClearButton)
self.forceFieldFilesLayout.addWidget(self.forceFieldFilesLabel)
self.forceFieldFilesLayout.addWidget(self.forceFieldFilesBox)
self.forceFieldFilesLayout.addLayout(self.forceFieldFilesChildLayout)
self.forceFieldsLayout.addLayout(self.forceFieldTypeLayout)
self.forceFieldsLayout.addLayout(self.forceFieldFilesLayout)
self.inputsForComplex.setLayout(self.inputsForComplexLayout)
self.forceFields.setLayout(self.forceFieldsLayout)
self.NAMDTabMainLayout.addWidget(self.inputsForComplex)
self.NAMDTabMainLayout.addWidget(self.forceFields)
self.NAMDTab.setLayout(self.NAMDTabMainLayout)
def _initGromacsTab(self):
"""initialize GMX Tab in pre-treatment Tab"""
self.GromacsTab = QWidget()
self.GromacsTabMainLayout = QVBoxLayout()
# inputs for the complex
self.GromacsInputsForComplex = QGroupBox("Inputs for complex")
self.GromacsInputsForComplexLayout = QGridLayout()
# top
self.topLabel = QLabel("Topology file: ")
self.topLineEdit = QLineEdit()
self.topButton = QPushButton("Browse")
self.GromacsInputsForComplexLayout.addWidget(self.topLabel, 0, 0)
self.GromacsInputsForComplexLayout.addWidget(self.topLineEdit, 0, 1)
self.GromacsInputsForComplexLayout.addWidget(self.topButton, 0, 2)
# gro
self.gromacsPdbLabel = QLabel("Structure file: ")
self.gromacsPdbLineEdit = QLineEdit()
self.gromacsPdbButton = QPushButton("Browse")
self.GromacsInputsForComplexLayout.addWidget(self.gromacsPdbLabel, 1, 0)
self.GromacsInputsForComplexLayout.addWidget(self.gromacsPdbLineEdit, 1, 1)
self.GromacsInputsForComplexLayout.addWidget(self.gromacsPdbButton, 1, 2)
# structure file format
self.gromacsStructureFileFormatLayout = QHBoxLayout()
self.gromacsStructureFileFormatLabel = QLabel("Structure file format:")
self.gromacsStructureFileFormatCombobox = QComboBox()
self.gromacsStructureFileFormatCombobox.addItem("pdb")
self.gromacsStructureFileFormatCombobox.addItem("xpdb")
self.gromacsStructureFileFormatCombobox.setToolTip(
'Select "<b>xpdb</b>" if your PDB file has more than 9,999 number of residues'
)
self.gromacsStructureFileFormatLayout.addWidget(
self.gromacsStructureFileFormatLabel
)
self.gromacsStructureFileFormatLayout.addWidget(
self.gromacsStructureFileFormatCombobox
)
self.GromacsInputsForComplexLayout.addLayout(
self.gromacsStructureFileFormatLayout, 2, 0, 1, 3
)
self.GromacsInputsForComplexLayout.addWidget(QSplitter(), 3, 1)
self.GromacsInputsForComplex.setLayout(self.GromacsInputsForComplexLayout)
# inputs for the ligand-only system
self.GromacsInputsLigandOnly = QGroupBox("Inputs for ligand-only system")
self.GromacsInputsLigandOnlyLayout = QGridLayout()
# top
self.gromacsLigandOnlyTopLabel = QLabel("Topology file: ")
self.gromacsLigandOnlyTopLineEdit = QLineEdit()
self.gromacsLigandOnlyTopButton = QPushButton("Browse")
self.GromacsInputsLigandOnlyLayout.addWidget(
self.gromacsLigandOnlyTopLabel, 0, 0
)
self.GromacsInputsLigandOnlyLayout.addWidget(
self.gromacsLigandOnlyTopLineEdit, 0, 1
)
self.GromacsInputsLigandOnlyLayout.addWidget(
self.gromacsLigandOnlyTopButton, 0, 2
)
# gro
self.gromacsLigandOnlyPdbLabel = QLabel("Structure file: ")
self.gromacsLigandOnlyPdbLineEdit = QLineEdit()
self.gromacsLigandOnlyPdbButton = QPushButton("Browse")
self.GromacsInputsLigandOnlyLayout.addWidget(
self.gromacsLigandOnlyPdbLabel, 1, 0
)
self.GromacsInputsLigandOnlyLayout.addWidget(
self.gromacsLigandOnlyPdbLineEdit, 1, 1
)
self.GromacsInputsLigandOnlyLayout.addWidget(
self.gromacsLigandOnlyPdbButton, 1, 2
)
# structure file format
self.gromacsLigandOnlyStructureFileFormatLayout = QHBoxLayout()
self.gromacsLigandOnlyStructureFileFormatLabel = QLabel(
"Structure file format:"
)
self.gromacsLigandOnlyStructureFileFormatCombobox = QComboBox()
self.gromacsLigandOnlyStructureFileFormatCombobox.addItem("pdb")
self.gromacsLigandOnlyStructureFileFormatCombobox.addItem("xpdb")
self.gromacsLigandOnlyStructureFileFormatCombobox.setToolTip(
'Select "<b>xpdb</b>" if your PDB file has more than 9,999 number of residues'
)
self.gromacsLigandOnlyStructureFileFormatLayout.addWidget(
self.gromacsLigandOnlyStructureFileFormatLabel
)
self.gromacsLigandOnlyStructureFileFormatLayout.addWidget(
self.gromacsLigandOnlyStructureFileFormatCombobox
)
self.GromacsInputsLigandOnlyLayout.addLayout(
self.gromacsLigandOnlyStructureFileFormatLayout, 2, 0, 1, 3
)
self.GromacsInputsLigandOnlyLayout.addWidget(QSplitter(), 3, 1)
self.GromacsInputsLigandOnly.setLayout(self.GromacsInputsLigandOnlyLayout)
self.GromacsTabMainLayout.addWidget(self.GromacsInputsForComplex)
self.GromacsTabMainLayout.addWidget(self.GromacsInputsLigandOnly)
self.GromacsTab.setLayout(self.GromacsTabMainLayout)
def _initPostTreatmentTab(self):
"""initialize pre-treatment tab"""
self.postTreatmentTab = QWidget()
# post-treatment tabs
# Geometrical and alchemical
self.postTreatmentMainTabs = QTabWidget()
self.postTreatmentMainTabs.addTab(self.geometricTab, "Geometrical")
self.postTreatmentMainTabs.addTab(self.alchemicalTab, "Alchemical")
self.postTreatmentMainTabs.addTab(self.LDDMTab, "LDDM")
self.postTreatmentMainLayout = QVBoxLayout()
self.postTreatmentMainLayout.addWidget(self.postTreatmentMainTabs)
self.calculateButton = QPushButton("Calculate binding free energy")
self.postTreatmentMainLayout.addWidget(self.calculateButton)
self.postTreatmentTab.setLayout(self.postTreatmentMainLayout)
def _initGeometricTab(self):
"""initialize geometrical tab of post-treatment"""
self.geometricTab = QWidget()
self.geometricTabLayout = QVBoxLayout()
# ligand flexibility
self.ligandFlexibilityLayout = QHBoxLayout()
self.geometricFlexibleLigandRadioButton = QRadioButton("Flexible ligand")
self.geometricRigidLigandRadioButton = QRadioButton(
"Rigid ligand/Protein-Protein"
)
self.geometricFlexibleLigandRadioButton.setChecked(True)
self.ligandFlexibilityButtonGroup = QButtonGroup(self)
self.ligandFlexibilityButtonGroup.addButton(
self.geometricFlexibleLigandRadioButton
)
self.ligandFlexibilityButtonGroup.addButton(
self.geometricRigidLigandRadioButton
)
self.ligandFlexibilityLayout.addStretch(1)
self.ligandFlexibilityLayout.addWidget(self.geometricFlexibleLigandRadioButton)
self.ligandFlexibilityLayout.addWidget(self.geometricRigidLigandRadioButton)
# separator
self.ligandFlexibilitySeparator = QLabel("|")
self.ligandFlexibilityLayout.addWidget(self.ligandFlexibilitySeparator)
# PMF type radio buttons
self.geometricPlainPmfRadioButton = QRadioButton("Plain PMFs")
self.geometricHistoryPmfRadioButton = QRadioButton(
"History PMFs (error estimation)"
)
self.geometricPlainPmfRadioButton.setChecked(True)
self.geometricPmfTypeButtonGroup = QButtonGroup(self)
self.geometricPmfTypeButtonGroup.addButton(self.geometricPlainPmfRadioButton)
self.geometricPmfTypeButtonGroup.addButton(self.geometricHistoryPmfRadioButton)
self.ligandFlexibilityLayout.addWidget(self.geometricPlainPmfRadioButton)
self.ligandFlexibilityLayout.addWidget(self.geometricHistoryPmfRadioButton)
self.ligandFlexibilityLayout.addStretch(1)
self.geometricFlexibleLigandRadioButton.toggled.connect(
self._toggleGeometricLigandFlexibility
)
self.geometricPlainPmfRadioButton.toggled.connect(self._toggleGeometricPmfType)
self.geometricTabLayout.addLayout(self.ligandFlexibilityLayout)
# pmf inputs
self.pmfInputs = QGroupBox("Merged PMF inputs (.czar.pmf/.UI.pmf):")
self.pmfInputsLayout = QVBoxLayout()
# bound stats
self.boundStateLabel = QLabel("Bound state:")
self.boundStateLayout = QGridLayout()
# RMSD
self.rmsdBoundLabel = QLabel("RMSD: ")
self.rmsdBoundLineEdit = QLineEdit()
self.rmsdBoundButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.rmsdBoundLabel, 0, 0)
self.boundStateLayout.addWidget(self.rmsdBoundLineEdit, 0, 1)
self.boundStateLayout.addWidget(self.rmsdBoundButton, 0, 2)
# Theta
self.ThetaLabel = QLabel("Theta:")
self.ThetaLineEdit = QLineEdit()
self.ThetaButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.ThetaLabel, 1, 0)
self.boundStateLayout.addWidget(self.ThetaLineEdit, 1, 1)
self.boundStateLayout.addWidget(self.ThetaButton, 1, 2)
# Phi
self.PhiLabel = QLabel("Phi: ")
self.PhiLineEdit = QLineEdit()
self.PhiButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.PhiLabel, 2, 0)
self.boundStateLayout.addWidget(self.PhiLineEdit, 2, 1)
self.boundStateLayout.addWidget(self.PhiButton, 2, 2)
# Psi
self.PsiLabel = QLabel("Psi: ")
self.PsiLineEdit = QLineEdit()
self.PsiButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.PsiLabel, 3, 0)
self.boundStateLayout.addWidget(self.PsiLineEdit, 3, 1)
self.boundStateLayout.addWidget(self.PsiButton, 3, 2)
# theta
self.thetaLayout = QHBoxLayout()
self.thetaLabel = QLabel("theta:")
self.thetaLineEdit = QLineEdit()
self.thetaButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.thetaLabel, 4, 0)
self.boundStateLayout.addWidget(self.thetaLineEdit, 4, 1)
self.boundStateLayout.addWidget(self.thetaButton, 4, 2)
# phi
self.phiLayout = QHBoxLayout()
self.phiLabel = QLabel("phi: ")
self.phiLineEdit = QLineEdit()
self.phiButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.phiLabel, 5, 0)
self.boundStateLayout.addWidget(self.phiLineEdit, 5, 1)
self.boundStateLayout.addWidget(self.phiButton, 5, 2)
# r
self.rLabel = QLabel("r: ")
self.rLineEdit = QLineEdit()
self.rButton = QPushButton("Browse")
self.boundStateLayout.addWidget(self.rLabel, 6, 0)
self.boundStateLayout.addWidget(self.rLineEdit, 6, 1)
self.boundStateLayout.addWidget(self.rButton, 6, 2)
self.unboundStateLabel = QLabel("Unbound state:")
# RMSD unbound
self.rmsdUnboundLayout = QHBoxLayout()
self.rmsdUnboundLabel = QLabel("RMSD: ")
self.rmsdUnboundLineEdit = QLineEdit()
self.rmsdUnboundButton = QPushButton("Browse")
self.rmsdUnboundLayout.addWidget(self.rmsdUnboundLabel)
self.rmsdUnboundLayout.addWidget(self.rmsdUnboundLineEdit)
self.rmsdUnboundLayout.addWidget(self.rmsdUnboundButton)
self.pmfInputsLayout.addWidget(self.boundStateLabel)
self.pmfInputsLayout.addLayout(self.boundStateLayout)
self.pmfInputsLayout.addWidget(self.unboundStateLabel)
self.pmfInputsLayout.addLayout(self.rmsdUnboundLayout)
self.pmfInputs.setLayout(self.pmfInputsLayout)
# force constants
self.forceConstants = QGroupBox("Force constants (in Colvars unit):")
self.forceConstantsLayout = QGridLayout()
self.fcBoundStateLabel = QLabel("Bound state:")
# all widgets
self.fcRMSDLabel = QLabel("RMSD:")
self.fcRMSDLineEdit = QLineEdit("10")
self.fcThetaLabel = QLabel("Theta:")
self.fcThetaLineEdit = QLineEdit("0.1")
self.fcPhiLabel = QLabel("Phi:")
self.fcPhiLineEdit = QLineEdit("0.1")
self.fcPsiLabel = QLabel("Psi:")
self.fcPsiLineEdit = QLineEdit("0.1")
self.fcthetaLabel = QLabel("theta:")
self.fcthetaLineEdit = QLineEdit("0.1")
self.fcphiLabel = QLabel("phi:")
self.fcphiLineEdit = QLineEdit("0.1")
self.forceConstantsLayout.addWidget(self.fcRMSDLabel, 0, 0)
self.forceConstantsLayout.addWidget(self.fcRMSDLineEdit, 0, 1)
self.forceConstantsLayout.addWidget(self.fcThetaLabel, 0, 2)
self.forceConstantsLayout.addWidget(self.fcThetaLineEdit, 0, 3)
self.forceConstantsLayout.addWidget(self.fcPhiLabel, 0, 4)
self.forceConstantsLayout.addWidget(self.fcPhiLineEdit, 0, 5)
self.forceConstantsLayout.addWidget(self.fcPsiLabel, 1, 0)
self.forceConstantsLayout.addWidget(self.fcPsiLineEdit, 1, 1)
self.forceConstantsLayout.addWidget(self.fcthetaLabel, 1, 2)
self.forceConstantsLayout.addWidget(self.fcthetaLineEdit, 1, 3)
self.forceConstantsLayout.addWidget(self.fcphiLabel, 1, 4)
self.forceConstantsLayout.addWidget(self.fcphiLineEdit, 1, 5)
self.forceConstants.setLayout(self.forceConstantsLayout)
# other parameters
self.postOtherParams = QGroupBox("Other parameters:")
self.postOtherParamsLayout = QHBoxLayout()
self.postTemperatureLabel = QLabel("temperature:")
self.postTemperatureLineEdit = QLineEdit("300")
self.postRstarLabel = QLabel(" r*:")
self.postRstarLineEdit = QLineEdit("30")
self.postPMFTypeLabel = QLabel(" PMF type:")
self.postPMFTypeBox = QComboBox()
self.postPMFTypeBox.addItem("NAMD")
self.postPMFTypeBox.addItem("Gromacs")
self.postOtherParamsLayout.addWidget(self.postTemperatureLabel)
self.postOtherParamsLayout.addWidget(self.postTemperatureLineEdit)
self.postOtherParamsLayout.addWidget(self.postRstarLabel)
self.postOtherParamsLayout.addWidget(self.postRstarLineEdit)
self.postOtherParamsLayout.addWidget(self.postPMFTypeLabel)
self.postOtherParamsLayout.addWidget(self.postPMFTypeBox)
self.postOtherParams.setLayout(self.postOtherParamsLayout)
self.fcBoundStateLabel = QLabel("Bound state:")
self.geometricTabLayout.addWidget(self.pmfInputs)
self.geometricTabLayout.addWidget(self.forceConstants)
self.geometricTabLayout.addWidget(self.postOtherParams)
self.geometricTab.setLayout(self.geometricTabLayout)
def _initAlchemicalTab(self):
"""initialize alchemical tab of post-treatment"""
self.alchemicalTab = QWidget()
self.alchemicalTabLayout = QVBoxLayout()
# FEP method and ligand flexibility radio buttons (all in one row)
self.alchemicalMethodLayout = QHBoxLayout()
# FEP method selection
self.alchemicalBidirectionalFEPRadio = QRadioButton("Bidirectional FEP")
self.alchemicalDoubleWideFEPRadio = QRadioButton("Double-wide FEP")
self.alchemicalWTMABFRadio = QRadioButton("WTM-λABF")
self.alchemicalBidirectionalFEPRadio.setChecked(True) # Default selection
self.alchemicalMethodButtonGroup = QButtonGroup(self)
self.alchemicalMethodButtonGroup.addButton(self.alchemicalBidirectionalFEPRadio)
self.alchemicalMethodButtonGroup.addButton(self.alchemicalDoubleWideFEPRadio)
self.alchemicalMethodButtonGroup.addButton(self.alchemicalWTMABFRadio)
# Ligand flexibility selection
self.alchemicalFlexibleLigandRadio = QRadioButton("Flexible ligand")
self.alchemicalRigidLigandRadio = QRadioButton("Rigid ligand")
self.alchemicalFlexibleLigandRadio.setChecked(True) # Default selection
self.alchemicalLigandFlexibilityButtonGroup = QButtonGroup(self)
self.alchemicalLigandFlexibilityButtonGroup.addButton(
self.alchemicalFlexibleLigandRadio
)
self.alchemicalLigandFlexibilityButtonGroup.addButton(
self.alchemicalRigidLigandRadio
)
# Add all to layout with separator
self.alchemicalMethodLayout.addStretch(1)
self.alchemicalMethodLayout.addWidget(self.alchemicalBidirectionalFEPRadio)
self.alchemicalMethodLayout.addWidget(self.alchemicalDoubleWideFEPRadio)
self.alchemicalMethodLayout.addWidget(self.alchemicalWTMABFRadio)
self.alchemicalMethodLayout.addWidget(QLabel(" | ")) # Separator
self.alchemicalMethodLayout.addWidget(self.alchemicalFlexibleLigandRadio)
self.alchemicalMethodLayout.addWidget(self.alchemicalRigidLigandRadio)
self.alchemicalMethodLayout.addStretch(1)
# Connect radio buttons to state change handlers
self.alchemicalBidirectionalFEPRadio.toggled.connect(
self._toggleAlchemicalMethod
)
self.alchemicalDoubleWideFEPRadio.toggled.connect(self._toggleAlchemicalMethod)
self.alchemicalWTMABFRadio.toggled.connect(self._toggleAlchemicalMethod)
self.alchemicalFlexibleLigandRadio.toggled.connect(
self._toggleAlchemicalLigandFlexibility
)
self.alchemicalTabLayout.addLayout(self.alchemicalMethodLayout)
self.restraintInputs = QGroupBox(
"Inputs for alchemical simulations (.fepout/.hist.pmf/.log):"
)
self.restraintInputsLayout = QVBoxLayout()
# bound state
self.alchemicalBoundStateLabel = QLabel("Atoms/Bound state (.fepout/.pmf):")
self.alchemicalBoundStateLayout = QGridLayout()
self.alchemicalForwardLabel1 = QLabel("Forward:")
self.alchemicalForwardLineEdit1 = QLineEdit()
self.alchemicalForwardButton1 = QPushButton("Browse")
self.alchemicalBoundStateLayout.addWidget(self.alchemicalForwardLabel1, 0, 0)
self.alchemicalBoundStateLayout.addWidget(self.alchemicalForwardLineEdit1, 0, 1)
self.alchemicalBoundStateLayout.addWidget(self.alchemicalForwardButton1, 0, 2)
self.alchemicalBackwardLabel1 = QLabel("Backward:")
self.alchemicalBackwardLineEdit1 = QLineEdit()
self.alchemicalBackwardButton1 = QPushButton("Browse")
self.alchemicalBoundStateLayout.addWidget(self.alchemicalBackwardLabel1, 1, 0)
self.alchemicalBoundStateLayout.addWidget(
self.alchemicalBackwardLineEdit1, 1, 1
)
self.alchemicalBoundStateLayout.addWidget(self.alchemicalBackwardButton1, 1, 2)
self.alchemicalBoundStateLabel2 = QLabel("Restraints/Bound state (.log):")
self.alchemicalBoundStateLayout2 = QGridLayout()
self.alchemicalForwardLabel2 = QLabel("Forward:")
self.alchemicalForwardLineEdit2 = QLineEdit()
self.alchemicalForwardButton2 = QPushButton("Browse")
self.alchemicalBoundStateLayout2.addWidget(self.alchemicalForwardLabel2, 0, 0)
self.alchemicalBoundStateLayout2.addWidget(
self.alchemicalForwardLineEdit2, 0, 1
)
self.alchemicalBoundStateLayout2.addWidget(self.alchemicalForwardButton2, 0, 2)
self.alchemicalBackwardLabel2 = QLabel("Backward:")
self.alchemicalBackwardLineEdit2 = QLineEdit()
self.alchemicalBackwardButton2 = QPushButton("Browse")
self.alchemicalBoundStateLayout2.addWidget(self.alchemicalBackwardLabel2, 1, 0)
self.alchemicalBoundStateLayout2.addWidget(
self.alchemicalBackwardLineEdit2, 1, 1
)
self.alchemicalBoundStateLayout2.addWidget(self.alchemicalBackwardButton2, 1, 2)
# unbound state
self.alchemicalUnboundStateLabel = QLabel("Atoms/Unbound state (.fepout/.pmf):")
self.alchemicalUnboundStateLayout = QGridLayout()
self.alchemicalForwardLabel3 = QLabel("Forward:")
self.alchemicalForwardLineEdit3 = QLineEdit()
self.alchemicalForwardButton3 = QPushButton("Browse")
self.alchemicalUnboundStateLayout.addWidget(self.alchemicalForwardLabel3, 0, 0)
self.alchemicalUnboundStateLayout.addWidget(
self.alchemicalForwardLineEdit3, 0, 1
)
self.alchemicalUnboundStateLayout.addWidget(self.alchemicalForwardButton3, 0, 2)
self.alchemicalBackwardLabel3 = QLabel("Backward:")
self.alchemicalBackwardLineEdit3 = QLineEdit()
self.alchemicalBackwardButton3 = QPushButton("Browse")
self.alchemicalUnboundStateLayout.addWidget(self.alchemicalBackwardLabel3, 1, 0)
self.alchemicalUnboundStateLayout.addWidget(
self.alchemicalBackwardLineEdit3, 1, 1
)
self.alchemicalUnboundStateLayout.addWidget(
self.alchemicalBackwardButton3, 1, 2
)
self.alchemicalUnboundStateLabel2 = QLabel("Restraints/Unbound state (.log):")
self.alchemicalUnboundStateLayout2 = QGridLayout()
self.alchemicalForwardLabel4 = QLabel("Forward:")
self.alchemicalForwardLineEdit4 = QLineEdit()
self.alchemicalForwardButton4 = QPushButton("Browse")
self.alchemicalUnboundStateLayout2.addWidget(self.alchemicalForwardLabel4, 0, 0)
self.alchemicalUnboundStateLayout2.addWidget(
self.alchemicalForwardLineEdit4, 0, 1
)
self.alchemicalUnboundStateLayout2.addWidget(
self.alchemicalForwardButton4, 0, 2
)
self.alchemicalBackwardLabel4 = QLabel("Backward:")
self.alchemicalBackwardLineEdit4 = QLineEdit()
self.alchemicalBackwardButton4 = QPushButton("Browse")
self.alchemicalUnboundStateLayout2.addWidget(
self.alchemicalBackwardLabel4, 1, 0
)
self.alchemicalUnboundStateLayout2.addWidget(
self.alchemicalBackwardLineEdit4, 1, 1
)
self.alchemicalUnboundStateLayout2.addWidget(
self.alchemicalBackwardButton4, 1, 2
)
self.restraintInputsLayout.addWidget(self.alchemicalBoundStateLabel)
self.restraintInputsLayout.addLayout(self.alchemicalBoundStateLayout)
self.restraintInputsLayout.addWidget(self.alchemicalBoundStateLabel2)
self.restraintInputsLayout.addLayout(self.alchemicalBoundStateLayout2)
self.restraintInputsLayout.addWidget(self.alchemicalUnboundStateLabel)
self.restraintInputsLayout.addLayout(self.alchemicalUnboundStateLayout)
self.restraintInputsLayout.addWidget(self.alchemicalUnboundStateLabel2)
self.restraintInputsLayout.addLayout(self.alchemicalUnboundStateLayout2)
self.restraintInputs.setLayout(self.restraintInputsLayout)
# alchemical force constants
self.alchemicalForceConstants = QGroupBox("Force constants (in Colvars unit):")
# all widgets
self.alchemicalFCLayout = QHBoxLayout()
self.alchemicalfcThetaLabel = QLabel("Theta:")
self.alchemicalfcThetaLineEdit = QLineEdit("0.1")
self.alchemicalfcPhiLabel = QLabel(" Phi: ")
self.alchemicalfcPhiLineEdit = QLineEdit("0.1")
self.alchemicalfcPsiLabel = QLabel("Psi: ")
self.alchemicalfcPsiLineEdit = QLineEdit("0.1")
self.alchemicalfcthetaLabel = QLabel("theta:")
self.alchemicalfcthetaLineEdit = QLineEdit("0.1")
self.alchemicalfcphiLabel = QLabel(" phi: ")
self.alchemicalfcphiLineEdit = QLineEdit("0.1")
self.alchemicalfcRLabel = QLabel("r: ")
self.alchemicalfcRLineEdit = QLineEdit("10")
self.alchemicalFCLayout.addWidget(self.alchemicalfcThetaLabel)
self.alchemicalFCLayout.addWidget(self.alchemicalfcThetaLineEdit)
self.alchemicalFCLayout.addWidget(self.alchemicalfcPhiLabel)
self.alchemicalFCLayout.addWidget(self.alchemicalfcPhiLineEdit)
self.alchemicalFCLayout.addWidget(self.alchemicalfcPsiLabel)
self.alchemicalFCLayout.addWidget(self.alchemicalfcPsiLineEdit)
self.alchemicalFCLayout.addWidget(self.alchemicalfcthetaLabel)
self.alchemicalFCLayout.addWidget(self.alchemicalfcthetaLineEdit)
self.alchemicalFCLayout.addWidget(self.alchemicalfcphiLabel)
self.alchemicalFCLayout.addWidget(self.alchemicalfcphiLineEdit)
self.alchemicalFCLayout.addWidget(self.alchemicalfcRLabel)
self.alchemicalFCLayout.addWidget(self.alchemicalfcRLineEdit)
self.alchemicalForceConstants.setLayout(self.alchemicalFCLayout)
# alchemical restraint centers
self.alchemicalRestraintCenters = QGroupBox(
"Restraint centers (in Colvars unit), temperature and others:"
)
# all widgets
self.alchemicalRCLayout = QHBoxLayout()
self.alchemicalRCThetaLabel = QLabel("Theta:")
self.alchemicalRCThetaLineEdit = QLineEdit("0")
self.alchemicalRCthetaLabel = QLabel("theta:")
self.alchemicalRCthetaLineEdit = QLineEdit("90")
self.alchemicalRCRLabel = QLabel("r: ")
self.alchemicalRCRLineEdit = QLineEdit("8")
self.alchemicalPostTemperatureLabel = QLabel("temperature:")
self.alchemicalPostTemperatureLineEdit = QLineEdit("300")
self.alchemicalPostTypeLabel = QLabel("Post-treatment type:")
self.alchemicalPostTypeBox = QComboBox()
self.alchemicalPostTypeBox.addItem("FEP")
self.alchemicalPostTypeBox.addItem("BAR")
self.alchemicalPostTypeBox.addItem("PMF")
self.alchemicalPostTypeBox.setCurrentIndex(1)
self.alchemicalRCLayout.addWidget(self.alchemicalRCThetaLabel)
self.alchemicalRCLayout.addWidget(self.alchemicalRCThetaLineEdit)
self.alchemicalRCLayout.addWidget(self.alchemicalRCthetaLabel)
self.alchemicalRCLayout.addWidget(self.alchemicalRCthetaLineEdit)
self.alchemicalRCLayout.addWidget(self.alchemicalRCRLabel)
self.alchemicalRCLayout.addWidget(self.alchemicalRCRLineEdit)
self.alchemicalRCLayout.addWidget(self.alchemicalPostTemperatureLabel)
self.alchemicalRCLayout.addWidget(self.alchemicalPostTemperatureLineEdit)
self.alchemicalRCLayout.addWidget(self.alchemicalPostTypeLabel)
self.alchemicalRCLayout.addWidget(self.alchemicalPostTypeBox)
self.alchemicalRestraintCenters.setLayout(self.alchemicalRCLayout)
self.alchemicalTabLayout.addWidget(self.restraintInputs)
self.alchemicalTabLayout.addWidget(self.alchemicalForceConstants)
self.alchemicalTabLayout.addWidget(self.alchemicalRestraintCenters)
self.alchemicalTab.setLayout(self.alchemicalTabLayout)
def _initLDDMTab(self):
"""initialize LDDM tab of post-treatment"""
self.LDDMTab = QWidget()
self.LDDMTabLayout = QVBoxLayout()
self.LDDMSimulationInputs = QGroupBox("Inputs for LDDM simulations:")
self.LDDMSimulationInputsLayout = QVBoxLayout()
# step 1
self.LDDMStep1Label = QLabel("Step 1:")
self.LDDMStep1Layout = QGridLayout()
self.LDDMStep1ColvarsLabel = QLabel("colvars.in.tmp file:")
self.LDDMStep1ColvarsLineEdit = QLineEdit()
self.LDDMStep1ColvarsButton = QPushButton("Browse")
self.LDDMStep1Layout.addWidget(self.LDDMStep1ColvarsLabel, 0, 0)
self.LDDMStep1Layout.addWidget(self.LDDMStep1ColvarsLineEdit, 0, 1)
self.LDDMStep1Layout.addWidget(self.LDDMStep1ColvarsButton, 0, 2)
self.LDDMStep1ColvarsTrajLabel = QLabel("colvars.traj file:")
self.LDDMStep1ColvarsTrajLineEdit = QLineEdit()
self.LDDMStep1ColvarsTrajButton = QPushButton("Browse")
self.LDDMStep1Layout.addWidget(self.LDDMStep1ColvarsTrajLabel, 1, 0)
self.LDDMStep1Layout.addWidget(self.LDDMStep1ColvarsTrajLineEdit, 1, 1)
self.LDDMStep1Layout.addWidget(self.LDDMStep1ColvarsTrajButton, 1, 2)
self.LDDMStep1FepoutLabel = QLabel("fepout file:")
self.LDDMStep1FepoutLineEdit = QLineEdit()
self.LDDMStep1FepoutButton = QPushButton("Browse")
self.LDDMStep1Layout.addWidget(self.LDDMStep1FepoutLabel, 2, 0)
self.LDDMStep1Layout.addWidget(self.LDDMStep1FepoutLineEdit, 2, 1)
self.LDDMStep1Layout.addWidget(self.LDDMStep1FepoutButton, 2, 2)
self.LDDMStep3Label = QLabel("Step 3:")
self.LDDMStep3Layout = QGridLayout()
self.LDDMStep3FepoutLabel = QLabel("fepout file: ")
self.LDDMStep3FepoutLineEdit = QLineEdit()
self.LDDMStep3FepoutButton = QPushButton("Browse")
self.LDDMStep3Layout.addWidget(self.LDDMStep3FepoutLabel, 0, 0)
self.LDDMStep3Layout.addWidget(self.LDDMStep3FepoutLineEdit, 0, 1)
self.LDDMStep3Layout.addWidget(self.LDDMStep3FepoutButton, 0, 2)
self.LDDMSimulationInputsLayout.addWidget(self.LDDMStep1Label)
self.LDDMSimulationInputsLayout.addLayout(self.LDDMStep1Layout)
self.LDDMSimulationInputsLayout.addWidget(self.LDDMStep3Label)
self.LDDMSimulationInputsLayout.addLayout(self.LDDMStep3Layout)
self.LDDMSimulationInputs.setLayout(self.LDDMSimulationInputsLayout)
# other parameter
self.LDDMParameters = QGroupBox("Other parameters:")
# all widgets
self.LDDMParametersLayout = QGridLayout()
self.LDDMStep1StepsPerWindowLabel = QLabel("Steps per window (Step 1):")
self.LDDMStep1StepsPerWindowLineEdit = QLineEdit("500000")
self.LDDMStep1EquilStepsPerWindowLabel = QLabel(
"Equilbration per window (Step 1): "
)
self.LDDMStep1EquilStepsPerWindowLineEdit = QLineEdit("100000")
self.LDDMStep1WindowsLabel = QLabel("Windows (Step 1): ")
self.LDDMStep1WindowsLineEdit = QLineEdit("200")
self.LDDMPostTemperatureLabel = QLabel("temperature:")
self.LDDMPostTemperatureLineEdit = QLineEdit("300")
self.LDDMPostTypeLabel = QLabel("Post-treatment type:")
self.LDDMPostTypeBox = QComboBox()
self.LDDMPostTypeBox.addItem("FEP")
self.LDDMPostTypeBox.addItem("BAR")
self.LDDMPostTypeBox.setCurrentIndex(1)
self.LDDMParametersLayout.addWidget(self.LDDMStep1StepsPerWindowLabel, 0, 0)
self.LDDMParametersLayout.addWidget(self.LDDMStep1StepsPerWindowLineEdit, 0, 1)
self.LDDMParametersLayout.addWidget(self.LDDMStep1WindowsLabel, 0, 2)
self.LDDMParametersLayout.addWidget(self.LDDMStep1WindowsLineEdit, 0, 3)
self.LDDMParametersLayout.addWidget(
self.LDDMStep1EquilStepsPerWindowLabel, 1, 0
)
self.LDDMParametersLayout.addWidget(
self.LDDMStep1EquilStepsPerWindowLineEdit, 1, 1
)
self.LDDMParametersLayout.addWidget(self.LDDMPostTemperatureLabel, 1, 2)
self.LDDMParametersLayout.addWidget(self.LDDMPostTemperatureLineEdit, 1, 3)
self.LDDMParametersLayout.addWidget(self.LDDMPostTypeLabel, 2, 0)
self.LDDMParametersLayout.addWidget(self.LDDMPostTypeBox, 2, 1)
self.LDDMParameters.setLayout(self.LDDMParametersLayout)
self.LDDMTabLayout.addWidget(self.LDDMSimulationInputs)
self.LDDMTabLayout.addWidget(self.LDDMParameters)
self.LDDMTab.setLayout(self.LDDMTabLayout)
def _initQuickPlotTab(self):
"""initialize quick-plot tab"""
self.quickPlot = QWidget()
self.quickPlotLayout = QVBoxLayout()
# merge/plot a (stratified) pmf
self.mergePmf = QGroupBox("Merge stratified (WTM-eABF/GaWTM-eABF) PMFs:")
self.mergePmfLayout = QVBoxLayout()
# Radio buttons for PMF type selection
self.mergePmfRadioLayout = QHBoxLayout()
self.mergePmfPlainRadio = QRadioButton("Plain PMFs")
self.mergePmfHistoryRadio = QRadioButton("History PMFs")
self.mergePmfPlainRadio.setChecked(True) # Default selection
self.mergePmfRadioButtonGroup = QButtonGroup(self)
self.mergePmfRadioButtonGroup.addButton(self.mergePmfPlainRadio)
self.mergePmfRadioButtonGroup.addButton(self.mergePmfHistoryRadio)
self.mergePmfRadioLayout.addStretch(1)
self.mergePmfRadioLayout.addWidget(self.mergePmfPlainRadio)
self.mergePmfRadioLayout.addWidget(self.mergePmfHistoryRadio)
self.mergePmfRadioLayout.addStretch(1)
# Connect radio buttons to state change handler
self.mergePmfPlainRadio.toggled.connect(self._changeMergePmfTypeState)
self.mergePmfHistoryRadio.toggled.connect(self._changeMergePmfTypeState)
self.mergePmfLabel = QLabel(
"PMF files (.czar.pmf, with optional .reweightamd1.cumulant.pmf):"
)
self.mergePmfBox = QListWidget()
self.mergePmfChildLayout = QHBoxLayout()
self.mergePmfAddButton = QPushButton("Add")
self.mergePmfClearButton = QPushButton("Clear")
self.mergePmfPlotButton = QPushButton("Plot")
self.mergePmfSaveButton = QPushButton("Save")
self.mergePmfChildLayout.addWidget(self.mergePmfAddButton)
self.mergePmfChildLayout.addWidget(self.mergePmfClearButton)
self.mergePmfChildLayout.addWidget(self.mergePmfPlotButton)
self.mergePmfChildLayout.addWidget(self.mergePmfSaveButton)
self.mergePmfLayout.addLayout(self.mergePmfRadioLayout)
self.mergePmfLayout.addWidget(self.mergePmfLabel)
self.mergePmfLayout.addWidget(self.mergePmfBox)
self.mergePmfLayout.addLayout(self.mergePmfChildLayout)
self.mergePmf.setLayout(self.mergePmfLayout)
# calculate pmf RMSD convergence
self.plotPmfConvergence = QGroupBox("Calculate PMF RMSD convergence:")
self.plotPmfConvergenceLayout = QVBoxLayout()
self.plotPmfConvergenceLabel = QLabel("History file:")
self.plotPmfConvergenceBox = QLineEdit()
self.plotPmfConvergenceChildLayout = QHBoxLayout()
self.plotPmfConvergenceBrowseButton = QPushButton("Browse")
self.plotPmfConvergencePlotButton = QPushButton("Plot")
self.plotPmfConvergenceSaveButton = QPushButton("Save")
self.plotPmfConvergenceChildLayout.addWidget(
self.plotPmfConvergenceBrowseButton
)
self.plotPmfConvergenceChildLayout.addWidget(self.plotPmfConvergencePlotButton)
self.plotPmfConvergenceChildLayout.addWidget(self.plotPmfConvergenceSaveButton)
self.plotPmfConvergenceLayout.addWidget(self.plotPmfConvergenceLabel)
self.plotPmfConvergenceLayout.addWidget(self.plotPmfConvergenceBox)
self.plotPmfConvergenceLayout.addLayout(self.plotPmfConvergenceChildLayout)
self.plotPmfConvergence.setLayout(self.plotPmfConvergenceLayout)
# plot hysteresis between forward and backward simulations
self.plotHysteresis = QGroupBox(
"Plot hysteresis between bidirectional simulations:"
)
self.plotHysteresisLayout = QVBoxLayout()
# Radio buttons for file type selection
self.plotHysteresisRadioLayout = QHBoxLayout()
self.plotHysteresisRadioBidirectionalFepout = QRadioButton(
"Bidirectional fepout"
)
self.plotHysteresisRadioBidirectionalLog = QRadioButton("Bidirectional log")
self.plotHysteresisRadioDoubleWideFepout = QRadioButton("Double-wide fepout")
self.plotHysteresisRadioBidirectionalFepout.setChecked(
True
) # Default selection
self.plotHysteresisRadioButtonGroup = QButtonGroup(self)
self.plotHysteresisRadioButtonGroup.addButton(
self.plotHysteresisRadioBidirectionalFepout
)
self.plotHysteresisRadioButtonGroup.addButton(
self.plotHysteresisRadioBidirectionalLog
)
self.plotHysteresisRadioButtonGroup.addButton(
self.plotHysteresisRadioDoubleWideFepout
)
self.plotHysteresisRadioLayout.addStretch(1)
self.plotHysteresisRadioLayout.addWidget(
self.plotHysteresisRadioBidirectionalFepout
)
self.plotHysteresisRadioLayout.addWidget(
self.plotHysteresisRadioBidirectionalLog
)
self.plotHysteresisRadioLayout.addWidget(
self.plotHysteresisRadioDoubleWideFepout
)
self.plotHysteresisRadioLayout.addStretch(1)
# Connect radio buttons to state change handler
self.plotHysteresisRadioBidirectionalFepout.toggled.connect(
self._changeHysteresisInputState
)
self.plotHysteresisRadioBidirectionalLog.toggled.connect(
self._changeHysteresisInputState
)
self.plotHysteresisRadioDoubleWideFepout.toggled.connect(
self._changeHysteresisInputState
)
self.plotHysteresisForwardLayout = QHBoxLayout()
self.plotHysteresisForwardLabel = QLabel("Forward (fepout):")
self.plotHysteresisForwardLineEdit = QLineEdit()
self.plotHysteresisForwardButton = QPushButton("Browse")
self.plotHysteresisForwardLayout.addWidget(self.plotHysteresisForwardLabel)
self.plotHysteresisForwardLayout.addWidget(self.plotHysteresisForwardLineEdit)
self.plotHysteresisForwardLayout.addWidget(self.plotHysteresisForwardButton)
self.plotHysteresisBackwardLayout = QHBoxLayout()
self.plotHysteresisBackwardLabel = QLabel("Backward (fepout):")
self.plotHysteresisBackwardLineEdit = QLineEdit()
self.plotHysteresisBackwardButton = QPushButton("Browse")
self.plotHysteresisBackwardLayout.addWidget(self.plotHysteresisBackwardLabel)
self.plotHysteresisBackwardLayout.addWidget(self.plotHysteresisBackwardLineEdit)
self.plotHysteresisBackwardLayout.addWidget(self.plotHysteresisBackwardButton)
self.plotHysteresisButtonLayout = QHBoxLayout()
self.plotHysteresisPlotButton = QPushButton("Plot")
self.plotHysteresisSaveButton = QPushButton("Save")
self.plotHysteresisButtonLayout.addWidget(self.plotHysteresisPlotButton)
self.plotHysteresisButtonLayout.addWidget(self.plotHysteresisSaveButton)
self.plotHysteresisLayout.addLayout(self.plotHysteresisRadioLayout)
self.plotHysteresisLayout.addLayout(self.plotHysteresisForwardLayout)
self.plotHysteresisLayout.addLayout(self.plotHysteresisBackwardLayout)
self.isRigidLigandCheckbox = QCheckBox("Rigid ligand (ligand RMSD restraints):")
self.isRigidLigandCheckbox.setChecked(False)
self.isRigidLigandCheckbox.setEnabled(
False
) # Disabled by default (Bidirectional fepout)
self.plotHysteresisLayout.addWidget(self.isRigidLigandCheckbox)
self.plotHysteresisLayout.addLayout(self.plotHysteresisButtonLayout)
self.plotHysteresis.setLayout(self.plotHysteresisLayout)
self.quickPlotLayout.addWidget(self.mergePmf)
self.quickPlotLayout.addWidget(self.plotPmfConvergence)
self.quickPlotLayout.addWidget(self.plotHysteresis)
self.quickPlot.setLayout(self.quickPlotLayout)
# slots are defined below
# otherwise they are defined in slots.py
def _mainSettings(self):
"""call main settings"""
def f():
self.mainSettings.show()
return f
def _advancedSettings(self, comboBox):
"""call advanced settings in pre-treatment
the returned function is depended on the comboBox(strategy type)
"""
def f():
if comboBox.currentText() == "Geometrical":
self.geometricAdvancedSettings.show()
elif comboBox.currentText() == "Alchemical":
self.alchemicalAdvancedSettings.show()
return f
def _changeFFButtonState(self):
"""enable/disable the add and clear button of force field section"""
if self.forceFieldCombobox.currentText() == "CHARMM":
self.forceFieldAddButton.setEnabled(True)
self.forceFieldClearButton.setEnabled(True)
self.forceFieldFilesBox.setEnabled(True)
elif self.forceFieldCombobox.currentText() == "Amber":
self.forceFieldAddButton.setEnabled(False)
self.forceFieldClearButton.setEnabled(False)
self.forceFieldFilesBox.setEnabled(False)
def _changeStrategySettingState(self):
"""enable/disable the alchemical route and some advanced settings based on the MD engine"""
if self.selectMDEngineCombobox.currentText() == "NAMD":
self.selectStrategyCombobox.setEnabled(True)
self.geometricAdvancedSettings.useOldCvCheckbox.setEnabled(True)
self.geometricAdvancedSettings.OPLSMixingRuleCheckbox.setEnabled(True)
self.geometricAdvancedSettings.useGaWTMCheckbox.setEnabled(True)
elif self.selectMDEngineCombobox.currentText() == "Gromacs":
index = self.selectStrategyCombobox.findText(
"Geometrical", QtCore.Qt.MatchFixedString
)
if index >= 0:
self.selectStrategyCombobox.setCurrentIndex(index)
self.selectStrategyCombobox.setEnabled(False)
self.geometricAdvancedSettings.useOldCvCheckbox.setChecked(False)
self.geometricAdvancedSettings.useOldCvCheckbox.setEnabled(False)
self.geometricAdvancedSettings.OPLSMixingRuleCheckbox.setChecked(False)
self.geometricAdvancedSettings.OPLSMixingRuleCheckbox.setEnabled(False)
self.geometricAdvancedSettings.useGaWTMCheckbox.setEnabled(False)
self.geometricAdvancedSettings.useGaWTMCheckbox.setChecked(False)
def _changeStrategySettingStateForOldGromacs(self):
"""enable/disable a lot of options for the old Gromacs tab"""
if self.preTreatmentMainTabs.currentWidget() == self.NAMDTab:
self.selectStrategyAdvancedButton.setEnabled(True)
self.selectMDEngineCombobox.setEnabled(True)
elif self.preTreatmentMainTabs.currentWidget() == self.GromacsTab:
index = self.selectMDEngineCombobox.findText(
"Gromacs", QtCore.Qt.MatchFixedString
)
if index >= 0:
self.selectMDEngineCombobox.setCurrentIndex(index)
index = self.selectStrategyCombobox.findText(
"Geometrical", QtCore.Qt.MatchFixedString
)
if index >= 0:
self.selectStrategyCombobox.setCurrentIndex(index)
self.selectMDEngineCombobox.setEnabled(False)
self.selectStrategyCombobox.setEnabled(False)
self.selectStrategyAdvancedButton.setEnabled(False)
def _changeHysteresisInputState(self):
"""enable/disable and update labels for hysteresis input fields based on radio button selection"""
if self.plotHysteresisRadioBidirectionalFepout.isChecked():
# Bidirectional fepout: both rows enabled with fepout labels
self.plotHysteresisForwardLabel.setText("Forward (fepout):")
self.plotHysteresisBackwardLabel.setText("Backward (fepout):")
self.plotHysteresisForwardLineEdit.setEnabled(True)
self.plotHysteresisForwardButton.setEnabled(True)
self.plotHysteresisBackwardLineEdit.setEnabled(True)
self.plotHysteresisBackwardButton.setEnabled(True)
self.isRigidLigandCheckbox.setEnabled(False)
elif self.plotHysteresisRadioBidirectionalLog.isChecked():
# Bidirectional log: both rows enabled with log labels
self.plotHysteresisForwardLabel.setText("Forward (log):")
self.plotHysteresisBackwardLabel.setText("Backward (log):")
self.plotHysteresisForwardLineEdit.setEnabled(True)
self.plotHysteresisForwardButton.setEnabled(True)
self.plotHysteresisBackwardLineEdit.setEnabled(True)
self.plotHysteresisBackwardButton.setEnabled(True)
self.isRigidLigandCheckbox.setEnabled(True)
elif self.plotHysteresisRadioDoubleWideFepout.isChecked():
# Double-wide fepout: only first row enabled, second row disabled
self.plotHysteresisForwardLabel.setText("Double-wide (fepout):")
self.plotHysteresisBackwardLabel.setText("Backward (fepout):")
self.plotHysteresisForwardLineEdit.setEnabled(True)
self.plotHysteresisForwardButton.setEnabled(True)
self.plotHysteresisBackwardLineEdit.setEnabled(False)
self.plotHysteresisBackwardButton.setEnabled(False)
self.isRigidLigandCheckbox.setEnabled(False)
def _changeMergePmfTypeState(self):
"""enable/disable and update labels for merge PMF based on radio button selection"""
if self.mergePmfPlainRadio.isChecked():
# Plain PMFs: Plot enabled, normal label
self.mergePmfLabel.setText(
"PMF files (.czar.pmf, with optional .reweightamd1.cumulant.pmf):"
)
self.mergePmfPlotButton.setEnabled(True)
elif self.mergePmfHistoryRadio.isChecked():
# History PMFs: Plot disabled, history label
self.mergePmfLabel.setText(
"History PMF files [.hist.czar.pmf, with optional .reweightamd1.cumulant(.hist).pmf]:"
)
self.mergePmfPlotButton.setEnabled(False)
def _toggleGeometricLigandFlexibility(self):
"""enable/disable widgets based on ligand flexibility selection in geometric tab"""
enable = self.geometricFlexibleLigandRadioButton.isChecked()
# bound RMSD
if not enable:
self.rmsdBoundLineEdit.clear()
self.rmsdBoundLineEdit.setEnabled(enable)
self.rmsdBoundButton.setEnabled(enable)
# unbound RMSD
if not enable:
self.rmsdUnboundLineEdit.clear()
self.rmsdUnboundLineEdit.setEnabled(enable)
self.rmsdUnboundButton.setEnabled(enable)
# force constant RMSD
self.fcRMSDLineEdit.setEnabled(enable)
def _toggleGeometricPmfType(self):
"""enable/disable and update labels for geometric PMF type based on radio button selection"""
if self.geometricPlainPmfRadioButton.isChecked():
# Plain PMFs: normal label
self.pmfInputs.setTitle("Merged PMF inputs (.czar.pmf/.UI.pmf):")
elif self.geometricHistoryPmfRadioButton.isChecked():
# History PMFs: history label
self.pmfInputs.setTitle("Merged history PMFs (.hist.czar.pmf/.hist.pmf):")
def _toggleAlchemicalMethod(self):
"""enable/disable widgets and update labels based on alchemical method selection"""
if self.alchemicalBidirectionalFEPRadio.isChecked():
# Bidirectional FEP: all enabled, labels "Forward:", file extension ".fepout"
self.alchemicalBoundStateLabel.setText("Atom/Bound state (.fepout):")
self.alchemicalUnboundStateLabel.setText("Atom/Unbound state (.fepout):")
self.alchemicalForwardLabel1.setText("Forward:")
self.alchemicalForwardLabel3.setText("Forward:")
# Enable backward row
self.alchemicalBackwardLineEdit1.setEnabled(True)
self.alchemicalBackwardButton1.setEnabled(True)
self.alchemicalBackwardLineEdit3.setEnabled(True)
self.alchemicalBackwardButton3.setEnabled(True)
# Post-treatment type: disable PMF, default to BAR
self.alchemicalPostTypeBox.setCurrentText("BAR")
# Enable FEP and BAR, disable PMF
for i in range(self.alchemicalPostTypeBox.count()):
item = self.alchemicalPostTypeBox.model().item(i)
if self.alchemicalPostTypeBox.itemText(i) == "PMF":
item.setEnabled(False)
else:
item.setEnabled(True)
elif self.alchemicalDoubleWideFEPRadio.isChecked():
# Double-wide FEP: Forward label becomes "Double-wide:", backward disabled
self.alchemicalBoundStateLabel.setText("Atom/Bound state (.fepout):")
self.alchemicalUnboundStateLabel.setText("Atom/Unbound state (.fepout):")
self.alchemicalForwardLabel1.setText("Double-wide:")
self.alchemicalForwardLabel3.setText("Double-wide:")
# Disable backward row
self.alchemicalBackwardLineEdit1.setEnabled(False)
self.alchemicalBackwardButton1.setEnabled(False)
self.alchemicalBackwardLineEdit3.setEnabled(False)
self.alchemicalBackwardButton3.setEnabled(False)
# Post-treatment type: disable PMF, default to BAR
self.alchemicalPostTypeBox.setCurrentText("BAR")
# Enable FEP and BAR, disable PMF
for i in range(self.alchemicalPostTypeBox.count()):
item = self.alchemicalPostTypeBox.model().item(i)
if self.alchemicalPostTypeBox.itemText(i) == "PMF":
item.setEnabled(False)
else:
item.setEnabled(True)
elif self.alchemicalWTMABFRadio.isChecked():
# WTM-λABF: Forward label becomes "Full window:", file extension ".pmf", backward disabled
self.alchemicalBoundStateLabel.setText("Atom/Bound state (.hist.pmf):")
self.alchemicalUnboundStateLabel.setText("Atom/Unbound state (.hist.pmf):")
self.alchemicalForwardLabel1.setText("Full window:")
self.alchemicalForwardLabel3.setText("Full window:")
# Disable backward row
self.alchemicalBackwardLineEdit1.setEnabled(False)
self.alchemicalBackwardButton1.setEnabled(False)
self.alchemicalBackwardLineEdit3.setEnabled(False)
self.alchemicalBackwardButton3.setEnabled(False)
# Post-treatment type: disable FEP and BAR, default to PMF
self.alchemicalPostTypeBox.setCurrentText("PMF")
# Enable PMF, disable FEP and BAR
for i in range(self.alchemicalPostTypeBox.count()):
item = self.alchemicalPostTypeBox.model().item(i)
if self.alchemicalPostTypeBox.itemText(i) == "PMF":
item.setEnabled(True)
else:
item.setEnabled(False)
def _toggleAlchemicalLigandFlexibility(self):
"""enable/disable Restraints/Unbound state (.log) inputs based on ligand flexibility selection"""
enable = self.alchemicalFlexibleLigandRadio.isChecked()
# Restraints/Unbound state (.log) - Forward row
if not enable:
self.alchemicalForwardLineEdit4.clear()
self.alchemicalForwardLineEdit4.setEnabled(enable)
self.alchemicalForwardButton4.setEnabled(enable)
# Restraints/Unbound state (.log) - Backward row
if not enable:
self.alchemicalBackwardLineEdit4.clear()
self.alchemicalBackwardLineEdit4.setEnabled(enable)
self.alchemicalBackwardButton4.setEnabled(enable)
def _openDocFile(self):
"""open Documentation file"""
webbrowser.open("https://www.nature.com/articles/s41596-021-00676-1")
def _openPythonAPIFile(self):
"""open Python API Documentation file"""
webbrowser.open("https://fhh2626.github.io/BFEE2APIDocs/")
def _showAboutBox(self):
"""the about message box
Returns:
function obj: a slot function that shows that about message box
"""
def f():
QMessageBox.about(
self,
"About",
f"<center><b>{__PROGRAM_NAME__}</b></center><br>"
+ r"""
Binding free energy estimator (BFEE) is a python-based software
that automates absolute binding free energy calculations through
either the alchemical or geometrical route by molecular dynamics
simulations.<br>
<b>Authors:</b><br>
Haohao Fu (<a href="mailto:fhh2626@gmail.com">fhh2626@gmail.com</a>)<br>
Haochuan Chen (<a href="mailto:summersnow9403@gmail.com">summersnow9403@gmail.com</a>)<br>
<b>License:</b><br>
BFEE is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.<br>
<b>References:</b><br>
When possible, please consider mentioning <b>Fu et al. Nat. Protoc. 2022, 17 (4), 1114–1141</b> when BFEE is used in your project.<br>
<b>Additional references:</b><br>
<b>WTM-λABF:</b> Zhou et al. J. Phys. Chem. Lett. 2025, 16, 4419-4427 and Zhou et al. Acc. Chem. Res. 2026, 59, 90-102<br>
<b>The "LDDM" route:</b> Bian et al. Nat. Comput. Sci. 2025, 5, 621-626<br>
<b>BFEE2:</b> Fu et al. J. Chem. Inf. Model. 2021, 61, 2116-2123<br>
<b>BFEE2 for protein-protein binding:</b> Fu et al. J. Chem. Inf. Model. 2023, 63, 2512-2519<br>
<b>Alchemical and geometrical routes:</b> Gumbart et al. J. Chem. Theory Comput. 2013, 9, 794-802<br>
<b>WTM-eABF:</b> Fu et al. Acc. Chem. Res. 2019, 52, 3254-3264 and Fu et al. J. Phys. Chem. Lett. 2018, 9, 4738-4745<br>
<b>NAMD3:</b> Phillips et al. J. Chem. Phys. 2020, 153, 044130<br>
<b>Gromacs:</b> Abraham et al. SoftwareX 2015, 1, 19-25<br>
<b>Collective variables for restraints:</b> Fu et al. J. Chem. Theory Comput. 2017, 13, 5173-5178<br>
<b>Colvars module:</b> Fiorin et al. Mol. Phys. 2013, 111, 3345-3362<br>
<b>"Mother" of all restraint-based binding free-energy calculations:</b> Hermans et al. Isr. J. Chem. 1986, 27, 225-227<br>
""",
)
return f
def _showGeometricResults(self, unit):
"""calculate binding from the geometrical route,
parameters in the Geometrical tab will be read.
Show a QMessageBox for the result
Inputs:
unit (string): 'namd' or 'gromacs'"""
pTreat = postTreatment.postTreatment(
float(self.postTemperatureLineEdit.text()), unit, "geometric"
)
pmfs = [
self.rmsdBoundLineEdit.text(),
self.ThetaLineEdit.text(),
self.PhiLineEdit.text(),
self.PsiLineEdit.text(),
self.thetaLineEdit.text(),
self.phiLineEdit.text(),
self.rLineEdit.text(),
self.rmsdUnboundLineEdit.text(),
]
try:
parameters = [
float(self.fcRMSDLineEdit.text()),
float(self.fcThetaLineEdit.text()),
float(self.fcPhiLineEdit.text()),
float(self.fcPsiLineEdit.text()),
float(self.fcthetaLineEdit.text()),
float(self.fcphiLineEdit.text()),
float(self.postRstarLineEdit.text()),
float(self.fcRMSDLineEdit.text()),
]
except:
QMessageBox.warning(self, "Error", f"Force constant or r* input error!")
return
# check inputs
for index, item in enumerate(pmfs):
if (index != 0) and (index != 7) and (not os.path.exists(item)):
QMessageBox.warning(self, "Error", f"file {item} does not exist!")
return
if (index == 7) and (not os.path.exists(item)):
# QMessageBox.warning(self, 'Warning', f'Supposing dealing with a rigid ligand!')
pass
# Check if History PMFs mode is selected
use_history_pmf = self.geometricHistoryPmfRadioButton.isChecked()
# calculate free energies
try:
if use_history_pmf:
result, errors = pTreat.geometricBindingFreeEnergyHistPMF(
pmfs, parameters
)
else:
result = pTreat.geometricBindingFreeEnergy(pmfs, parameters)
except postTreatment.RStarTooLargeError:
QMessageBox.warning(
self,
"Error",
f"\
r_star cannot be larger than r_max of step 7!\n",
)
return
except Exception as e:
print(e)
QMessageBox.warning(
self,
"Error",
f"\
Unknown error! The error message is: \n\
{e}\n",
)
return
if use_history_pmf:
# Display results with error bars
QMessageBox.about(
self,
"Result",
f"\
Results:\n\
ΔG(site,c) = {result[0]:.2f} ± {errors[0]:.2f} kcal/mol\n\
ΔG(site,eulerTheta) = {result[1]:.2f} ± {errors[1]:.2f} kcal/mol\n\
ΔG(site,eulerPhi) = {result[2]:.2f} ± {errors[2]:.2f} kcal/mol\n\
ΔG(site,eulerPsi) = {result[3]:.2f} ± {errors[3]:.2f} kcal/mol\n\
ΔG(site,polarTheta) = {result[4]:.2f} ± {errors[4]:.2f} kcal/mol\n\
ΔG(site,polarPhi) = {result[5]:.2f} ± {errors[5]:.2f} kcal/mol\n\
(1/beta)*ln(S*I*C0) = {result[6]:.2f} ± {errors[6]:.2f} kcal/mol\n\
ΔG(bulk,c) = {result[7]:.2f} ± {errors[7]:.2f} kcal/mol\n\
ΔG(bulk,o) = {result[8]:.2f} kcal/mol\n\
\n\
Standard Binding Free Energy:\n\
ΔG(total) = {result[9]:.2f} ± {errors[9]:.2f} kcal/mol\n",
)
else:
# Display results without error bars (original behavior)
QMessageBox.about(
self,
"Result",
f"\
Results:\n\
ΔG(site,c) = {result[0]:.2f} kcal/mol\n\
ΔG(site,eulerTheta) = {result[1]:.2f} kcal/mol\n\
ΔG(site,eulerPhi) = {result[2]:.2f} kcal/mol\n\
ΔG(site,eulerPsi) = {result[3]:.2f} kcal/mol\n\
ΔG(site,polarTheta) = {result[4]:.2f} kcal/mol\n\
ΔG(site,polarPhi) = {result[5]:.2f} kcal/mol\n\
(1/beta)*ln(S*I*C0) = {result[6]:.2f} kcal/mol\n\
ΔG(bulk,c) = {result[7]:.2f} kcal/mol\n\
ΔG(bulk,o) = {result[8]:.2f} kcal/mol\n\
\n\
Standard Binding Free Energy:\n\
ΔG(total) = {result[9]:.2f} kcal/mol\n",
)
def _showAlchemicalResults(self, unit):
"""calculate binding from the alchemical route,
parameters in the alchemical tab will be read.
Show a QMessageBox for the result
Args:
unit (str): 'namd' or 'gromacs'
"""
pTreat = postTreatment.postTreatment(
float(self.alchemicalPostTemperatureLineEdit.text()), unit, "alchemical"
)
# alchemical outputs
files = [
self.alchemicalForwardLineEdit1.text(),
self.alchemicalBackwardLineEdit1.text(),
self.alchemicalForwardLineEdit2.text(),
self.alchemicalBackwardLineEdit2.text(),
self.alchemicalForwardLineEdit3.text(),
self.alchemicalBackwardLineEdit3.text(),
self.alchemicalForwardLineEdit4.text(),
self.alchemicalBackwardLineEdit4.text(),
]
try:
parameters = [
float(self.alchemicalRCThetaLineEdit.text()),
float(self.alchemicalRCthetaLineEdit.text()),
float(self.alchemicalRCRLineEdit.text()),
float(self.alchemicalfcThetaLineEdit.text()),
float(self.alchemicalfcPhiLineEdit.text()),
float(self.alchemicalfcPsiLineEdit.text()),
float(self.alchemicalfcthetaLineEdit.text()),
float(self.alchemicalfcphiLineEdit.text()),
float(self.alchemicalfcRLineEdit.text()),
]
temperature = float(self.alchemicalPostTemperatureLineEdit.text())
except:
QMessageBox.warning(
self,
"Error",
f"Force constant or restraint center or temperature input error!",
)
return
if self.alchemicalPostTypeBox.currentText() == "FEP":
jobType = "fep"
elif self.alchemicalPostTypeBox.currentText() == "BAR":
jobType = "bar"
elif self.alchemicalPostTypeBox.currentText() == "PMF":
jobType = "pmf"
# check inputs
rigid_ligand = False
if jobType != "pmf":
for index, item in enumerate(
[
self.alchemicalBackwardLineEdit1.text(),
self.alchemicalBackwardLineEdit2.text(),
self.alchemicalBackwardLineEdit3.text(),
self.alchemicalBackwardLineEdit4.text(),
]
):
if (not os.path.exists(item)) and (index != 3):
QMessageBox.warning(
self,
"Error",
f"backward file {item} does not exist and is not empty!",
)
return
for index, item in enumerate(
[
self.alchemicalForwardLineEdit1.text(),
self.alchemicalForwardLineEdit2.text(),
self.alchemicalForwardLineEdit3.text(),
self.alchemicalForwardLineEdit4.text(),
]
):
if (not os.path.exists(item)) and (index != 3):
QMessageBox.warning(self, "Error", f"file {item} does not exist!")
return
elif (not os.path.exists(item)) and (index == 3):
# QMessageBox.warning(self, 'Warning', f'Supposing dealing with a rigid ligand!')
rigid_ligand = True
# calculate free energies
result, errors = pTreat.alchemicalBindingFreeEnergy(
files, parameters, temperature, jobType, rigid_ligand
)
QMessageBox.about(
self,
"Result",
f"\
Results:\n\
ΔG(site,couple) = {result[0]:.2f} ± {errors[0]:.2f} kcal/mol\n\
ΔG(site,c+o+a+r) = {result[1]:.2f} ± {errors[1]:.2f} kcal/mol\n\
ΔG(bulk,decouple) = {result[2]:.2f} ± {errors[2]:.2f} kcal/mol\n\
ΔG(bulk,c) = {result[3]:.2f} ± {errors[3]:.2f} kcal/mol\n\
ΔG(bulk,o+a+r) = {result[4]:.2f} kcal/mol\n\
\n\
Standard Binding Free Energy:\n\
ΔG(total) = {result[5]:.2f} ± {errors[5]:.2f} kcal/mol\n",
)
def _showLDDMResults(self, unit):
"""calculate binding from the LDDM route,
parameters in the LDDM tab will be read.
Show a QMessageBox for the result
Args:
unit (str): 'namd' or 'gromacs'
"""
pTreat = postTreatment.postTreatment(
float(self.LDDMPostTemperatureLineEdit.text()), unit, "LDDM"
)
# alchemical outputs
try:
temperature = float(self.LDDMPostTemperatureLineEdit.text())
except:
QMessageBox.warning(self, "Error", f"temperature input error!")
return
if self.LDDMPostTypeBox.currentText() == "FEP":
jobType = "fep"
elif self.LDDMPostTypeBox.currentText() == "BAR":
jobType = "bar"
# check inputs
rigid_ligand = False
for index, item in enumerate(
[
self.LDDMStep1ColvarsLineEdit.text(),
self.LDDMStep1ColvarsTrajLineEdit.text(),
self.LDDMStep1FepoutLineEdit.text(),
self.LDDMStep3FepoutLineEdit.text(),
]
):
if (not os.path.exists(item)) and (index != 3):
QMessageBox.warning(
self, "Error", f"{item} does not exist and is not empty!"
)
return
try:
int(self.LDDMStep1StepsPerWindowLineEdit.text())
int(self.LDDMStep1EquilStepsPerWindowLineEdit.text())
int(self.LDDMStep1WindowsLineEdit.text())
except:
QMessageBox.warning(self, "Error", f"Invalid value in LDDM parameters!")
return
# calculate free energies
try:
step1_result, step3_result = pTreat.LDDMBindingFreeEnergy(
self.LDDMStep1ColvarsLineEdit.text(),
self.LDDMStep1ColvarsTrajLineEdit.text(),
self.LDDMStep1FepoutLineEdit.text(),
int(self.LDDMStep1StepsPerWindowLineEdit.text()),
int(self.LDDMStep1EquilStepsPerWindowLineEdit.text()),
int(self.LDDMStep1WindowsLineEdit.text()) + 1,
self.LDDMStep3FepoutLineEdit.text(),
temperature=temperature,
jobType=jobType,
)
except Exception as e:
print(e)
QMessageBox.warning(
self,
"Error",
f"\
LDDM result calculation failed! The error message is: \n\
{e}\n",
)
QMessageBox.about(
self,
"Result",
f"\
Results:\n\
ΔG(Step 1) = {step1_result[0]:.2f} ± {step1_result[1]:.2f} kcal/mol\n\
ΔG(Step 3) = {step3_result[0]:.2f} ± {step3_result[1]:.2f} kcal/mol\n\
\n\
Standard Binding Free Energy:\n\
ΔG(total) = {step1_result[0] + step3_result[0]:.2f} ± {np.sqrt(step1_result[1] ** 2 + step3_result[1] ** 2):.2f} kcal/mol\n",
)
def _showFinalResults(self):
"""calculate binding free energy and show the final results
Returns:
function obj: a slot function that calculates binding free energy and show the final results
"""
def f():
if self.postPMFTypeBox.currentText() == "NAMD":
unit = "namd"
elif self.postPMFTypeBox.currentText() == "Gromacs":
unit = "gromacs"
if self.postTreatmentMainTabs.currentIndex() == 0:
jobType = "geometric"
elif self.postTreatmentMainTabs.currentIndex() == 1:
jobType = "alchemical"
elif self.postTreatmentMainTabs.currentIndex() == 2:
jobType = "LDDM"
if jobType == "geometric":
self._showGeometricResults(unit)
elif jobType == "alchemical":
self._showAlchemicalResults(unit)
elif jobType == "LDDM":
self._showLDDMResults(unit)
return f
def _generateInputFiles(self):
"""generate input files for binding free energy simulation
Returens:
function obj: a slot function that generates input files for binding free energy simulation
"""
def f():
path = QFileDialog.getExistingDirectory(None, "Select a directory")
# cancel
if path == "":
return
iGenerator = inputGenerator.inputGenerator()
# third-party softwares and user-provided solvation boxes
for item in [
self.mainSettings.vmdLineEdit.text(),
# self.mainSettings.gromacsLineEdit.text(),
# self.mainSettings.tleapLineEdit.text(),
self.geometricAdvancedSettings.nonStandardSolventPdbLineEdit.text(),
self.geometricAdvancedSettings.nonStandardSolventPsfLineEdit.text(),
]:
if (not os.path.exists(item)) and item != "":
QMessageBox.warning(self, "Error", f"file {item} does not exist!")
return
if self.preTreatmentMainTabs.currentIndex() == 0:
# force fields
forceFieldFiles = [
self.forceFieldFilesBox.item(i).text()
for i in range(self.forceFieldFilesBox.count())
]
# NAMD files
for item in [
self.psfLineEdit.text(),
self.coorLineEdit.text(),
] + forceFieldFiles:
if not os.path.exists(item):
QMessageBox.warning(
self, "Error", f"file {item} does not exist!"
)
return
# check inputs
try:
float(self.temperatureLineEdit.text())
stratification = [
int(
self.geometricAdvancedSettings.stratificationRMSDBoundLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationThetaLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationPhiLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationPsiLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationthetaLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationphiLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationRLineEdit.text()
),
int(
self.geometricAdvancedSettings.stratificationRMSDUnboundLineEdit.text()
),
]
alchemicalStratification = [
int(self.alchemicalAdvancedSettings.boundLigandLineEdit.text()),
int(
self.alchemicalAdvancedSettings.boundRestraintsLineEdit.text()
),
int(
self.alchemicalAdvancedSettings.unboundLigandLineEdit.text()
),
int(
self.alchemicalAdvancedSettings.unboundRestraintsLineEdit.text()
),
]
except:
QMessageBox.warning(
self, "Error", f"Force constant or r* input error!"
)
return
# job type
if self.forceFieldCombobox.currentText() == "CHARMM":
forceFieldType = "charmm"
elif self.forceFieldCombobox.currentText() == "Amber":
forceFieldType = "amber"
# make sure there are CHARMM FF files
if forceFieldFiles == [] and forceFieldType == "charmm":
QMessageBox.warning(
self,
"Error",
f"\
CHARMM force field files must be specified!",
)
return
if self.selectStrategyCombobox.currentText() == "Geometrical":
if self.selectMDEngineCombobox.currentText().lower() == "gromacs":
QMessageBox.warning(
self,
"Warning",
f"Please pay attention if you use the new Gromacs interface: \n\
1. Occationally, the atom number in complex.ndx do not correct. If so, just modify it manually; \n\
2. Make sure in complex.gro, the center of box is approximately at (x/2, y/2, z/2) rather than (0, 0, 0); \n\
3. Gromacs patched by the latest (master branch) version of Colvars is needed.",
)
# for the amber force field, files of large box must be provided
if forceFieldType == "amber":
if (
self.geometricAdvancedSettings.nonStandardSolventPdbLineEdit.text()
== ""
or self.geometricAdvancedSettings.nonStandardSolventPsfLineEdit.text()
== ""
):
QMessageBox.warning(
self,
"Error",
f'\
Coordinate and topology files of large box must be \
provided in "Advanced Settings"when using the Amber \
force fields!',
)
return
if self.geometricAdvancedSettings.useGaWTMCheckbox.isChecked():
if self.geometricAdvancedSettings.useCUDASOAIntegrator.isChecked():
QMessageBox.warning(
self,
"Error",
f"\
GaWTM-eABF is not compatible with CUDASOAIntegrator in NAMD! \n",
)
return
QMessageBox.warning(
self,
"Warning",
f"\
The feature of using GaWTM-eABF as the workhorse engine is \
experimental! Please always use the latest devel version of NAMD!\n",
)
try:
iGenerator.generateNAMDGeometricFiles(
path,
self.psfLineEdit.text(),
self.coorLineEdit.text(),
forceFieldType,
forceFieldFiles,
float(self.temperatureLineEdit.text()),
self.selectProteinLineEdit.text(),
self.selectLigandLineEdit.text(),
self.geometricAdvancedSettings.userDefinedDirectionLineEdit.text(),
self.geometricAdvancedSettings.nonStandardSolventPsfLineEdit.text(),
self.geometricAdvancedSettings.nonStandardSolventPdbLineEdit.text(),
stratification,
self.geometricAdvancedSettings.memProCheckbox.isChecked(),
self.geometricAdvancedSettings.neutralizeLigOnlyCombobox.currentText(),
self.geometricAdvancedSettings.pinDownProCheckbox.isChecked(),
self.geometricAdvancedSettings.useOldCvCheckbox.isChecked(),
int(
self.geometricAdvancedSettings.parallelRunsLineEdit.text()
),
self.mainSettings.vmdLineEdit.text(),
self.geometricAdvancedSettings.reflectingBoundaryCheckbox.isChecked(),
self.selectMDEngineCombobox.currentText().lower(),
self.geometricAdvancedSettings.OPLSMixingRuleCheckbox.isChecked(),
self.geometricAdvancedSettings.considerRMSDCVCheckbox.isChecked(),
self.geometricAdvancedSettings.useGaWTMCheckbox.isChecked(),
self.geometricAdvancedSettings.useCUDASOAIntegrator.isChecked(),
float(
self.geometricAdvancedSettings.timestepLineEdit.text()
),
)
except fileParser.SelectionError:
QMessageBox.warning(
self,
"Error",
f"\
Selection corresponding to nothing!\n\
Check your selection again!",
)
if os.path.exists(f"{path}/BFEE"):
shutil.rmtree(f"{path}/BFEE")
return
except inputGenerator.DirectoryExistError:
QMessageBox.warning(
self,
"Error",
f"\
./BFEE* directory already exists!",
)
return
except inputGenerator.FileTypeUnknownError:
QMessageBox.warning(
self,
"Error",
f"\
Unkwn input file types! The following file types are supported:\n\
psf, parm, prm7, parm7, prmtop, pdb, coor, rst, rst7, inpcrd ",
)
return
except PermissionError:
QMessageBox.warning(
self,
"Error",
f"\
Cannot read input files due to the permission reason!\n\
Restart the program or check the authority of the files!",
)
return
except Exception as e:
print(e)
QMessageBox.warning(
self,
"Error",
f"\
Unknown error! The error message is: \n\
{e}\n",
)
return
elif self.selectStrategyCombobox.currentText() == "Alchemical":
try:
iGenerator.generateNAMDAlchemicalFiles(
path,
self.psfLineEdit.text(),
self.coorLineEdit.text(),
forceFieldType,
forceFieldFiles,
float(self.temperatureLineEdit.text()),
self.selectProteinLineEdit.text(),
self.selectLigandLineEdit.text(),
alchemicalStratification,
self.alchemicalAdvancedSettings.doubleWideCheckbox.isChecked(),
self.alchemicalAdvancedSettings.minBeforeSampleCheckbox.isChecked(),
self.alchemicalAdvancedSettings.memProCheckbox.isChecked(),
self.alchemicalAdvancedSettings.neutralizeLigOnlyCombobox.currentText(),
self.alchemicalAdvancedSettings.pinDownProCheckbox.isChecked(),
self.alchemicalAdvancedSettings.useOldCvCheckbox.isChecked(),
self.mainSettings.vmdLineEdit.text(),
self.alchemicalAdvancedSettings.OPLSMixingRuleCheckbox.isChecked(),
self.alchemicalAdvancedSettings.considerRMSDCVCheckbox.isChecked(),
self.alchemicalAdvancedSettings.useCUDASOAIntegrator.isChecked(),
float(
self.alchemicalAdvancedSettings.timestepLineEdit.text()
),
self.alchemicalAdvancedSettings.reEqCheckbox.isChecked(),
self.alchemicalAdvancedSettings.LDDMCheckbox.isChecked(),
self.alchemicalAdvancedSettings.useWTMLambdaABFCheckbox.isChecked(),
)
except PermissionError:
QMessageBox.warning(
self,
"Error",
f"\
Cannot read input files due to the permission reason!\n\
Restart the program or check the authority of the files!",
)
return
except fileParser.SelectionError:
QMessageBox.warning(
self,
"Error",
f"\
Selection corresponding to nothing!\n\
Check your selection again!",
)
if os.path.exists(f"{path}/BFEE"):
shutil.rmtree(f"{path}/BFEE")
return
except inputGenerator.DirectoryExistError:
QMessageBox.warning(
self,
"Error",
f"\
./BFEE directory already exists!",
)
return
except inputGenerator.FileTypeUnknownError:
QMessageBox.warning(
self,
"Error",
f"\
Unkwn input file types! The following file types are supported:\n\
psf, parm, prm7, parm7, prmtop, pdb, coor, rst, rst7, inpcrd ",
)
return
except Exception as e:
print(e)
QMessageBox.warning(
self,
"Error",
f"\
Unknown error! The error message is: \n\
{e}\n",
)
return
# gromacs
if self.preTreatmentMainTabs.currentIndex() == 1:
QMessageBox.warning(
self,
"Warning",
(
"<ol>\
<li>Any setting in \"Advanced settings\" is not supported\
when using Gromacs-formatted files as inputs!</li>\
<li>C-rescale pressure coupling (pcoupl) is used for all simulations, \
GROMACS version >= 2021 with Colvars module is required. \
You may need to download it from the \
<a href='https://github.com/Colvars/colvars/'>Colvars website</a>.</li></ol>"
),
)
for item in [
self.topLineEdit.text(),
self.gromacsPdbLineEdit.text(),
self.gromacsLigandOnlyPdbLineEdit.text(),
self.gromacsLigandOnlyTopLineEdit.text(),
]:
if not os.path.exists(item):
QMessageBox.warning(
self, "Error", f"file {item} does not exist!"
)
return
if self.selectStrategyCombobox.currentText() == "Geometrical":
try:
iGenerator.generateGromacsGeometricFiles(
path=path,
topFile=self.topLineEdit.text(),
pdbFile=self.gromacsPdbLineEdit.text(),
pdbFileFormat=self.gromacsStructureFileFormatCombobox.currentText(),
ligandOnlyTopFile=self.gromacsLigandOnlyTopLineEdit.text(),
ligandOnlyPdbFile=self.gromacsLigandOnlyPdbLineEdit.text(),
ligandOnlyPdbFileFormat=self.gromacsLigandOnlyStructureFileFormatCombobox.currentText(),
selectionPro=self.selectProteinLineEdit.text(),
selectionLig=self.selectLigandLineEdit.text(),
temperature=float(self.temperatureLineEdit.text()),
)
except inputGenerator.DirectoryExistError:
QMessageBox.warning(
self,
"Error",
f"\
./BFEE directory already exists!",
)
return
except BFEEGromacs.SelectionError:
QMessageBox.warning(
self,
"Error",
f"\
Selection corresponding to nothing!\n\
Check your selection again!",
)
if os.path.exists(f"{path}/BFEE"):
shutil.rmtree(f"{path}/BFEE")
return
except Exception as e:
print(e)
QMessageBox.warning(
self,
"Error",
f"\
Unknown error!",
)
return
elif self.selectStrategyCombobox.currentText() == "Alchemical":
QMessageBox.warning(
self,
"Error",
f"Alchemical route is not supported using Gromacs!",
)
return
QMessageBox.information(
self,
"Input generation",
f"Input files have been generated successfully!",
)
del iGenerator
return f
def _getMergedPMF(self):
"""Read PMF files from mergePmfBox and return the merged PMF.
For GaWTM simulations, the user should add both .czar.pmf files and their
corresponding .reweightamd1.cumulant.pmf correction files. The function will
automatically pair them by matching base names (e.g., 001.czar.pmf with
001.reweightamd1.cumulant.pmf).
Returns:
numpy.ndarray or None: The merged PMF array, or None if an error occurred.
"""
if self.mergePmfBox.count() == 0:
QMessageBox.warning(self, "Warning", f"Warning, no PMF selected!")
return None
pmfFiles = [
self.mergePmfBox.item(i).text() for i in range(self.mergePmfBox.count())
]
if not ploter.isGaWTM(pmfFiles):
# No GaWTM correction files found, read PMFs directly
pmfs = [ploter.readPMF(item) for item in pmfFiles]
else:
# GaWTM simulation: pair czar.pmf files with their corrections
(
paired,
unpaired_czar,
orphan_corrections,
other_files,
wrong_correction_files,
) = ploter.pairGaWTMFiles(pmfFiles)
# Check for wrong correction file type first
if wrong_correction_files:
fileNames = [pathlib.Path(f).name for f in wrong_correction_files]
QMessageBox.warning(
self,
"Error",
f"The following correction files are not precise enough:\n"
f"{chr(10).join(fileNames)}\n\n"
f"Please use *.reweightamd1.cumulant.pmf instead of *.reweightamd1.reweight.pmf!",
)
return None
# Only show error if there's a mismatch (some czar.pmf missing correction or orphan corrections)
# Don't show error if:
# - No correction files at all (handled by isGaWTM returning False)
# - All czar.pmf files have corresponding corrections (perfect pairing)
has_mismatch = (unpaired_czar and paired) or orphan_corrections
if has_mismatch:
error_messages = []
# Error about unpaired czar.pmf files (missing correction)
if unpaired_czar:
fileNames = [pathlib.Path(f).name for f in unpaired_czar]
error_messages.append(
f"The following .czar.pmf files do not have corresponding correction files:\n"
f"{chr(10).join(fileNames)}"
)
# Error about orphan correction files (missing czar.pmf)
if orphan_corrections:
fileNames = [pathlib.Path(f).name for f in orphan_corrections]
error_messages.append(
f"The following correction files do not have corresponding .czar.pmf files:\n"
f"{chr(10).join(fileNames)}"
)
QMessageBox.warning(
self,
"Error",
"\n\n".join(error_messages) + "\n\nPlease add the missing files!",
)
return None
pmfs = []
# Process paired files (with correction)
for pmfFile, correctionFile in paired:
try:
pmfs.append(ploter.correctGaWTM(pmfFile, correctionFile))
except ploter.NoCorrectionFileError as e:
QMessageBox.warning(self, "Error", str(e))
return None
# Process other PMF files (non-GaWTM format)
for pmfFile in other_files:
pmfs.append(ploter.readPMF(pmfFile))
return ploter.mergePMF(pmfs)
def _getMergedHistPMF(self):
"""Read History PMF files from mergePmfBox and return the merged History PMF.
For GaWTM simulations, the user should add both .hist.czar.pmf files and their
corresponding correction files (.reweightamd1.cumulant.hist.pmf or
.reweightamd1.cumulant.pmf). The function will automatically pair them.
Returns:
list[np.array] or None: The merged History PMF (list of frames), or None if error.
"""
if self.mergePmfBox.count() == 0:
QMessageBox.warning(self, "Warning", f"Warning, no History PMF selected!")
return None
pmfFiles = [
self.mergePmfBox.item(i).text() for i in range(self.mergePmfBox.count())
]
if not ploter.isGaWTMHist(pmfFiles):
# No GaWTM correction files found, read History PMFs directly
all_hist_pmfs = [ploter.readHistPMF(item) for item in pmfFiles]
else:
# GaWTM simulation: pair hist.czar.pmf files with their corrections
(
paired,
unpaired_czar,
orphan_corrections,
other_files,
wrong_correction_files,
) = ploter.pairGaWTMHistFiles(pmfFiles)
# Check for wrong correction file type first
if wrong_correction_files:
fileNames = [pathlib.Path(f).name for f in wrong_correction_files]
QMessageBox.warning(
self,
"Error",
f"The following files have wrong correction file type:\n"
f"{chr(10).join(fileNames)}\n\n"
f"Please use *.reweightamd1.cumulant.pmf instead of *.reweightamd1.reweight.pmf!",
)
return None
# Only show error if there's a mismatch
has_mismatch = (unpaired_czar and paired) or orphan_corrections
if has_mismatch:
error_messages = []
if unpaired_czar:
fileNames = [pathlib.Path(f).name for f in unpaired_czar]
error_messages.append(
f"The following .hist.czar.pmf files do not have corresponding correction files:\n"
f"{chr(10).join(fileNames)}"
)
if orphan_corrections:
fileNames = [pathlib.Path(f).name for f in orphan_corrections]
error_messages.append(
f"The following correction files do not have corresponding .hist.czar.pmf files:\n"
f"{chr(10).join(fileNames)}"
)
QMessageBox.warning(
self,
"Error",
"\n\n".join(error_messages) + "\n\nPlease add the missing files!",
)
return None
all_hist_pmfs = []
# Process paired files (with correction)
for pmfFile, correctionFile, is_hist_correction in paired:
try:
all_hist_pmfs.append(
ploter.correctGaWTMHist(
pmfFile, correctionFile, is_hist_correction
)
)
except ploter.NoCorrectionFileError as e:
QMessageBox.warning(self, "Error", str(e))
return None
# Process other History PMF files (non-GaWTM format)
for pmfFile in other_files:
all_hist_pmfs.append(ploter.readHistPMF(pmfFile))
return ploter.mergeHistPMF(all_hist_pmfs)
def _savePMFs(self):
"""Save merged PMFs to a file.
Returns:
function obj: a slot function that saves the merged PMFs
"""
def f():
if self.mergePmfHistoryRadio.isChecked():
# History PMF mode
mergedHistPMF = self._getMergedHistPMF()
if mergedHistPMF is None:
return
path, _ = QFileDialog.getSaveFileName(
None,
"Set the name of merged History PMF",
"",
"History PMF files (*.hist.pmf);;All Files (*)",
)
if not path:
return
ploter.writeHistPMF(path, mergedHistPMF)
QMessageBox.information(
self,
"Save History PMFs",
f"History PMF saved successfully!\n{len(mergedHistPMF)} frames written.",
)
else:
# Plain PMF mode
mergedPMF = self._getMergedPMF()
if mergedPMF is None:
return
path, _ = QFileDialog.getSaveFileName(
None,
"Set the name of merged PMF",
"",
"PMF files (*.pmf);;All Files (*)",
)
if not path:
return
ploter.writePMF(path, mergedPMF)
QMessageBox.information(self, "Save PMFs", f"PMF saved successfully!")
return f
def _plotPMFs(self):
"""Plot merged PMFs.
Returns:
function obj: a slot function that plots the merged PMFs
"""
def f():
# History PMF mode does not support plotting (button should be disabled)
# but check anyway for safety
if self.mergePmfHistoryRadio.isChecked():
QMessageBox.warning(
self,
"Warning",
"Plotting is not available for History PMFs. Please use Save instead.",
)
return
mergedPMF = self._getMergedPMF()
if mergedPMF is None:
return
ploter.plotPMF(mergedPMF)
return f
def _plotRMSDConvergence(self):
"""plot time evolution of PMF rmsd with respect to zero array
Returns:
function obj: a slot function that plots time evolution of PMF rmsd with respect to zero array
"""
def f():
path = self.plotPmfConvergenceBox.text()
if not os.path.exists(path):
QMessageBox.warning(self, "Error", f"file {path} does not exist!")
return
rmsds = ploter.parseHistFile(path)
ploter.plotConvergence(rmsds)
return f
def _saveRMSDConvergence(self):
"""save PMF RMSD convergence data to a file
Returns:
function obj: a slot function that saves PMF RMSD convergence data to a file
"""
def f():
path = self.plotPmfConvergenceBox.text()
if not os.path.exists(path):
QMessageBox.warning(self, "Error", f"file {path} does not exist!")
return
savePath, _ = QFileDialog.getSaveFileName(
None,
"Save PMF RMSD convergence data",
"",
"Data files (*.dat *.txt);;All Files (*)",
)
if not savePath:
return
rmsds = ploter.parseHistFile(path)
ploter.saveConvergence(rmsds, savePath)
QMessageBox.information(self, "Save", f"Data saved successfully!")
return f
def _getHysteresisProfiles(self):
"""Parse input files and return forward and backward hysteresis profiles.
Returns:
tuple: (forwardProfile, backwardProfile) as np.arrays, or (None, None) on error
"""
forwardFilePath = self.plotHysteresisForwardLineEdit.text()
backwardFilePath = self.plotHysteresisBackwardLineEdit.text()
# Check forward file exists
if not os.path.exists(forwardFilePath):
QMessageBox.warning(
self, "Error", f"file {forwardFilePath} does not exist!"
)
return None, None
forwardPostfix = os.path.splitext(forwardFilePath)[-1]
# Validate file type
if forwardPostfix not in [".fepout", ".log"]:
QMessageBox.warning(
self,
"Error",
"File type not correct! Only .fepout and .log files are supported.",
)
return None, None
# Check backward file if provided
hasBackwardFile = backwardFilePath.strip() != "" and os.path.exists(
backwardFilePath
)
if backwardFilePath.strip() != "" and not hasBackwardFile:
QMessageBox.warning(
self, "Error", f"file {backwardFilePath} does not exist!"
)
return None, None
if hasBackwardFile:
backwardPostfix = os.path.splitext(backwardFilePath)[-1]
if forwardPostfix != backwardPostfix:
QMessageBox.warning(
self,
"Error",
"File types of forward and backward simulations are not the same!",
)
return None, None
try:
if forwardPostfix == ".fepout":
# Use py_bar.NAMDParser to handle both double-wide and separate files
backward = backwardFilePath if hasBackwardFile else ""
try:
parser = py_bar.NAMDParser(forwardFilePath, backward)
windows, deltaU_data = parser.get_data()
except RuntimeError as e:
if "Forward and backward data do not match" in str(e):
QMessageBox.warning(
self,
"Error",
"Cannot parse fepout file!\n\n"
"If you only provide one file, please make sure it is a double-wide format.\n"
"Otherwise, please provide both forward and backward files.",
)
return None, None
raise
# Get temperature for free energy calculation
try:
temperature = float(self.alchemicalPostTemperatureLineEdit.text())
except ValueError:
temperature = 300.0
# Calculate free energy profiles using FEPAnalyzer
analyzer = py_bar.FEPAnalyzer(windows, deltaU_data, temperature)
window_boundaries, free_energies, errors = analyzer.FEP_free_energy()
# Build forward and backward profiles from bidirectional FEP data
# Extract lambda values and cumulative free energies
lambdas = [w[0] for w in window_boundaries]
lambdas.append(window_boundaries[-1][1]) # Add final lambda
# Calculate forward cumulative sum
forward_cumsum = [0.0]
backward_cumsum = [0.0]
for i, (dU_forward, dU_backward) in enumerate(deltaU_data):
# Forward: exponential average of forward deltaU
beta = 1.0 / (py_bar.BOLTZMANN * temperature)
dG_forward = (
-py_bar.BOLTZMANN
* temperature
* np.log(np.mean(np.exp(-dU_forward * beta)))
)
dG_backward = (
-py_bar.BOLTZMANN
* temperature
* np.log(np.mean(np.exp(-dU_backward * beta)))
)
forward_cumsum.append(forward_cumsum[-1] + dG_forward)
backward_cumsum.append(backward_cumsum[-1] - dG_backward)
forwardProfile = np.column_stack([lambdas, forward_cumsum])
backwardProfile = np.column_stack([lambdas, backward_cumsum])
elif forwardPostfix == ".log":
# For .log files, require both forward and backward files
if not hasBackwardFile:
QMessageBox.warning(
self,
"Error",
"For .log files, both forward and backward files are required.",
)
return None, None
pTreat = postTreatment.postTreatment(
float(self.alchemicalPostTemperatureLineEdit.text()),
"namd",
"alchemical",
)
forwardProfile = np.transpose(
pTreat._tiLogFile(
forwardFilePath, self.isRigidLigandCheckbox.isChecked()
)
)
backwardProfile = np.transpose(
pTreat._tiLogFile(
backwardFilePath, self.isRigidLigandCheckbox.isChecked()
)
)
return forwardProfile, backwardProfile
except Exception as e:
QMessageBox.warning(
self, "Error", f"Failed to parse hysteresis data:\n{str(e)}"
)
return None, None
def _plotHysteresis(self):
"""plot hysteresis between forward and backward alchemical transformations
For .fepout files: uses py_bar.NAMDParser to handle both double-wide and
separate forward/backward files.
For .log files: uses the original TI log parsing method.
Returns:
function obj: a slot function that plot hysteresis between forward and backward alchemical transformations
"""
def f():
forwardProfile, backwardProfile = self._getHysteresisProfiles()
if forwardProfile is None:
return
ploter.plotHysteresis(forwardProfile, backwardProfile)
return f
def _saveHysteresis(self):
"""save hysteresis data to a file
Returns:
function obj: a slot function that saves hysteresis data to a file
"""
def f():
forwardProfile, backwardProfile = self._getHysteresisProfiles()
if forwardProfile is None:
return
savePath, _ = QFileDialog.getSaveFileName(
None,
"Save hysteresis data",
"",
"Data files (*.dat *.txt);;All Files (*)",
)
if not savePath:
return
ploter.saveHysteresis(forwardProfile, backwardProfile, savePath)
QMessageBox.information(self, "Save", f"Data saved successfully!")
return f
def _showLigandTypeDialog(self):
"""Show a dialog asking the user to select between Flexible ligand and Rigid ligand.
Returns:
str or None: 'flexible', 'rigid', or None if cancelled
"""
dialog = QDialog(self)
dialog.setWindowTitle("Select Ligand Type")
layout = QVBoxLayout(dialog)
# Add explanatory label
label = QLabel("Please select the ligand type:")
layout.addWidget(label)
# Create radio buttons
flexibleRadio = QRadioButton("Flexible ligand")
rigidRadio = QRadioButton("Rigid ligand")
flexibleRadio.setChecked(True) # Default to flexible
# Add radio buttons to layout
layout.addWidget(flexibleRadio)
layout.addWidget(rigidRadio)
# Add button box
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttonBox.accepted.connect(dialog.accept)
buttonBox.rejected.connect(dialog.reject)
layout.addWidget(buttonBox)
# Show dialog and get result
if dialog.exec() == QDialog.Accepted:
if flexibleRadio.isChecked():
return "flexible"
else:
return "rigid"
return None
def _quickSetProteinProteinGeometric(self):
"""quick setting for protein-protein binding free energy calculations through the geometrical route"""
self.selectMDEngineCombobox.setCurrentText("NAMD")
self.selectStrategyCombobox.setCurrentText("Geometrical")
self.geometricAdvancedSettings.stratificationRLineEdit.setText("5")
self.geometricAdvancedSettings.useCUDASOAIntegrator.setChecked(False)
self.geometricAdvancedSettings.considerRMSDCVCheckbox.setChecked(False)
self.geometricAdvancedSettings.useGaWTMCheckbox.setChecked(True)
QMessageBox.information(
self,
"Settings",
f"Changed settings for protein-protein binding free-energy calculations \
through the geometrical route!",
)
def _quickSetProteinLigandGeometric(self):
"""quick setting for protein-ligand binding free energy calculations through the geometrical route"""
# Ask user to select ligand type
ligandType = self._showLigandTypeDialog()
if ligandType is None:
return # User cancelled
self.selectMDEngineCombobox.setCurrentText("NAMD")
self.selectStrategyCombobox.setCurrentText("Geometrical")
self.geometricAdvancedSettings.stratificationRMSDBoundLineEdit.setText("3")
self.geometricAdvancedSettings.stratificationRMSDUnboundLineEdit.setText("3")
self.geometricAdvancedSettings.stratificationRLineEdit.setText("5")
self.geometricAdvancedSettings.useCUDASOAIntegrator.setChecked(True)
self.geometricAdvancedSettings.useGaWTMCheckbox.setChecked(False)
# Set RMSD CV checkbox based on ligand type for both Advanced Settings
if ligandType == "flexible":
self.geometricAdvancedSettings.considerRMSDCVCheckbox.setChecked(True)
self.alchemicalAdvancedSettings.considerRMSDCVCheckbox.setChecked(True)
QMessageBox.information(
self,
"Settings",
f"Changed settings for protein-ligand (flexible) binding free-energy calculations \
through the geometrical route!",
)
else: # rigid
self.geometricAdvancedSettings.considerRMSDCVCheckbox.setChecked(False)
self.alchemicalAdvancedSettings.considerRMSDCVCheckbox.setChecked(False)
QMessageBox.information(
self,
"Settings",
f"Changed settings for protein-ligand (rigid) binding free-energy calculations \
through the geometrical route!",
)
def _quickSetProteinLigandAlchemical(self):
"""quick setting for protein-ligand binding free energy calculations through the alchemical route"""
# Ask user to select ligand type
ligandType = self._showLigandTypeDialog()
if ligandType is None:
return # User cancelled
self.selectMDEngineCombobox.setCurrentText("NAMD")
self.selectStrategyCombobox.setCurrentText("Alchemical")
self.alchemicalAdvancedSettings.boundLigandLineEdit.setText("200")
self.alchemicalAdvancedSettings.boundRestraintsLineEdit.setText("200")
self.alchemicalAdvancedSettings.unboundLigandLineEdit.setText("100")
self.alchemicalAdvancedSettings.unboundRestraintsLineEdit.setText("100")
self.alchemicalAdvancedSettings.doubleWideCheckbox.setChecked(True)
self.alchemicalAdvancedSettings.useCUDASOAIntegrator.setChecked(True)
self.alchemicalAdvancedSettings.reEqCheckbox.setChecked(True)
self.alchemicalAdvancedSettings.LDDMCheckbox.setChecked(False)
self.alchemicalAdvancedSettings.useWTMLambdaABFCheckbox.setChecked(False)
# Set RMSD CV checkbox based on ligand type for both Advanced Settings
if ligandType == "flexible":
self.geometricAdvancedSettings.considerRMSDCVCheckbox.setChecked(True)
self.alchemicalAdvancedSettings.considerRMSDCVCheckbox.setChecked(True)
QMessageBox.information(
self,
"Settings",
f"Changed settings for protein-ligand (flexible) binding free-energy calculations \
through the alchemical route!",
)
else: # rigid
self.geometricAdvancedSettings.considerRMSDCVCheckbox.setChecked(False)
self.alchemicalAdvancedSettings.considerRMSDCVCheckbox.setChecked(False)
QMessageBox.information(
self,
"Settings",
f"Changed settings for protein-ligand (rigid) binding free-energy calculations \
through the alchemical route!",
)
def _quickSetProteinLigandLDDM(self):
"""quick setting for protein-ligand binding free energy calculations through the alchemical route"""
self.selectMDEngineCombobox.setCurrentText("NAMD")
self.selectStrategyCombobox.setCurrentText("Alchemical")
self.alchemicalAdvancedSettings.pinDownProCheckbox.setChecked(True)
self.alchemicalAdvancedSettings.useCUDASOAIntegrator.setChecked(True)
self.alchemicalAdvancedSettings.LDDMCheckbox.setChecked(True)
self.alchemicalAdvancedSettings.boundLigandLineEdit.setText("200")
self.alchemicalAdvancedSettings.unboundLigandLineEdit.setText("100")
self.alchemicalAdvancedSettings.timestepLineEdit.setText("2.0")
QMessageBox.information(
self,
"Settings",
f"Changed settings for protein-ligand binding free-energy calculations \
through the LDDM!",
)
def _quickSetAI(self):
"""AI-assisted setting for binding free energy calculations"""
if (
self.mainSettings.openAICompatibleAPIAddressLineEdit.text().strip() == ""
or self.mainSettings.openAICompatibleKeyLineEdit.text().strip() == ""
or self.mainSettings.openAICompatibleModelLineEdit.text().strip() == ""
):
QMessageBox.warning(
self,
"Warning",
"Please set the OpenAI-compatible API address, key, and model before using the AI assistance.",
)
return
self.aiAssistantDialog.show()
def _initSingalsSlots(self):
"""initialize (connect) singals and slots"""
# pre-treatment tab
self.selectStrategyAdvancedButton.clicked.connect(
self._advancedSettings(self.selectStrategyCombobox)
)
self.preTreatmentMainTabs.currentChanged.connect(
self._changeStrategySettingStateForOldGromacs
)
# NAMD tab
self.psfButton.clicked.connect(
commonSlots.openFileDialog("psf/parm", self.psfLineEdit)
)
self.coorButton.clicked.connect(
commonSlots.openFileDialog("pdb/rst", self.coorLineEdit)
)
# force field selection
self.forceFieldCombobox.currentTextChanged.connect(self._changeFFButtonState)
self.forceFieldAddButton.clicked.connect(
commonSlots.openFilesDialog("prm", self.forceFieldFilesBox)
)
self.forceFieldClearButton.clicked.connect(self.forceFieldFilesBox.clear)
# MD engine
self.selectMDEngineCombobox.currentTextChanged.connect(
self._changeStrategySettingState
)
# gromacs tab
self.gromacsPdbButton.clicked.connect(
commonSlots.openFileDialog("pdb", self.gromacsPdbLineEdit)
)
self.topButton.clicked.connect(
commonSlots.openFileDialog("top", self.topLineEdit)
)
self.gromacsLigandOnlyPdbButton.clicked.connect(
commonSlots.openFileDialog("pdb", self.gromacsLigandOnlyPdbLineEdit)
)
self.gromacsLigandOnlyTopButton.clicked.connect(
commonSlots.openFileDialog("top", self.gromacsLigandOnlyTopLineEdit)
)
# geometric tab
self.rmsdBoundButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.rmsdBoundLineEdit)
)
self.rmsdUnboundButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.rmsdUnboundLineEdit)
)
self.ThetaButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.ThetaLineEdit)
)
self.PhiButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.PhiLineEdit)
)
self.PsiButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.PsiLineEdit)
)
self.thetaButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.thetaLineEdit)
)
self.phiButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.phiLineEdit)
)
self.rButton.clicked.connect(
commonSlots.openFileDialog("czar.pmf/UI.pmf", self.rLineEdit)
)
# alchemical tab
self.alchemicalForwardButton1.clicked.connect(
commonSlots.openFileDialog("log", self.alchemicalForwardLineEdit1)
)
self.alchemicalBackwardButton1.clicked.connect(
commonSlots.openFileDialog("log", self.alchemicalBackwardLineEdit1)
)
self.alchemicalForwardButton2.clicked.connect(
commonSlots.openFileDialog("log", self.alchemicalForwardLineEdit2)
)
self.alchemicalBackwardButton2.clicked.connect(
commonSlots.openFileDialog("log", self.alchemicalBackwardLineEdit2)
)
self.alchemicalForwardButton3.clicked.connect(
commonSlots.openFileDialog("fepout", self.alchemicalForwardLineEdit3)
)
self.alchemicalBackwardButton3.clicked.connect(
commonSlots.openFileDialog("fepout", self.alchemicalBackwardLineEdit3)
)
self.alchemicalForwardButton4.clicked.connect(
commonSlots.openFileDialog("fepout", self.alchemicalForwardLineEdit4)
)
self.alchemicalBackwardButton4.clicked.connect(
commonSlots.openFileDialog("fepout", self.alchemicalBackwardLineEdit4)
)
# LDDM tab
self.LDDMStep1ColvarsButton.clicked.connect(
commonSlots.openFileDialog("in.tmp", self.LDDMStep1ColvarsLineEdit)
)
self.LDDMStep1ColvarsTrajButton.clicked.connect(
commonSlots.openFileDialog(
"colvars.traj", self.LDDMStep1ColvarsTrajLineEdit
)
)
self.LDDMStep1FepoutButton.clicked.connect(
commonSlots.openFileDialog("fepout", self.LDDMStep1FepoutLineEdit)
)
self.LDDMStep3FepoutButton.clicked.connect(
commonSlots.openFileDialog("in.tmp", self.LDDMStep3FepoutLineEdit)
)
# generate input files
self.generateInputButton.clicked.connect(self._generateInputFiles())
# calculate binding free energy
self.calculateButton.clicked.connect(self._showFinalResults())
# quick-plot tab
self.mergePmfAddButton.clicked.connect(
commonSlots.openFilesDialog("pmf", self.mergePmfBox)
)
self.mergePmfClearButton.clicked.connect(self.mergePmfBox.clear)
self.mergePmfPlotButton.clicked.connect(self._plotPMFs())
self.mergePmfSaveButton.clicked.connect(self._savePMFs())
self.plotPmfConvergenceBrowseButton.clicked.connect(
commonSlots.openFileDialog("pmf", self.plotPmfConvergenceBox)
)
self.plotPmfConvergencePlotButton.clicked.connect(self._plotRMSDConvergence())
self.plotPmfConvergenceSaveButton.clicked.connect(self._saveRMSDConvergence())
self.plotHysteresisForwardButton.clicked.connect(
commonSlots.openFileDialog("fepout/log", self.plotHysteresisForwardLineEdit)
)
self.plotHysteresisBackwardButton.clicked.connect(
commonSlots.openFileDialog(
"fepout/log", self.plotHysteresisBackwardLineEdit
)
)
self.plotHysteresisPlotButton.clicked.connect(self._plotHysteresis())
self.plotHysteresisSaveButton.clicked.connect(self._saveHysteresis())