import contextlib
import itertools
import logging
from typing import Optional
import weakref
import schrodinger
import schrodinger.utils.log as log
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui.gui_alignment import \
GuiCombinedChainProteinAlignment
from schrodinger.application.msv.gui.homology_modeling import constants
from schrodinger.application.msv.gui.homology_modeling import hm_models
from schrodinger.application.msv.gui.homology_modeling import \
homology_modeling_ui
from schrodinger.application.msv.gui.homology_modeling import steps
from schrodinger.application.msv.gui.homology_modeling.constants import Mode
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.application.ska import pairwise_align_ct
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.tasks import tasks
from schrodinger.ui.qt import mapperwidgets
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.utils import wrap_qt_tag
logger = log.get_output_logger("msv2_homology_panel")
if schrodinger.in_dev_env():
logger.setLevel(logging.DEBUG)
maestro = schrodinger.get_maestro()
dockwidget_stylesheet = """
QDockWidget::title {
color: #000;
background: #eee;
text-align: center;
}
"""
top_stylesheet = """
QWidget#homology_modeling {
background-color: white;
color: black;
}
"""
[docs]class HomologyPanel(widgetmixins.TaskPanelMixin,
widgetmixins.DockableMixinCollection,
QtWidgets.QDockWidget):
"""
:ivar copySeqsRequested: Signal emitted to request copying sequences to new
tabs for heteromultimer homology modeling.
:ivar importSeqRequested: Signal emitted to request importing sequence(s).
Emitted with whether the first new sequence should become the ref seq.
:ivar importTemplateRequested: Signal emitted to request importing template(s).
:ivar showBlastResultsRequested: Signal emitted to request showing BLAST
results.
:ivar taskStarted: Signal emitted with a task that was just started.
"""
copySeqsRequested = QtCore.pyqtSignal()
importSeqRequested = QtCore.pyqtSignal(bool)
importTemplateRequested = QtCore.pyqtSignal(bool)
showBlastResultsRequested = QtCore.pyqtSignal()
taskStarted = QtCore.pyqtSignal(tasks.AbstractTask)
model_class = gui_models.MsvGuiModel
ui_module = homology_modeling_ui
PRESETS_FEATURE_FLAG = False
PANEL_TASKS = (model_class._hm_launcher_task,)
[docs] def __init__(self, *args, msv_tab_widget, **kwargs):
self._msv_tab_widget = msv_tab_widget
super().__init__(*args, **kwargs)
[docs] def initSetOptions(self):
super().initSetOptions()
self.help_topic = "MSV_BUILD_HOMOLOGY_MODEL"
self._modified = True
self._msv_widget = None
self._dummy_model = self.model_class()
self._dummy_model.current_page.split_aln = None
self._homology_job_map = weakref.WeakValueDictionary()
self.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
self.setWindowTitle("Build Homology Model")
[docs] def initSetUp(self):
super().initSetUp()
for enum_item in Mode:
text = constants.MODE_TEXTS[enum_item]
self.ui.mode_combo.addItem(text, enum_item)
for enum_item, tooltip in constants.MODE_TOOLTIPS:
index = self.ui.mode_combo.findData(enum_item)
if index < 0:
continue
tooltip = wrap_qt_tag(tooltip)
self.ui.mode_combo.setItemData(index, tooltip,
QtCore.Qt.ToolTipRole)
sep_index = self.ui.mode_combo.findData(Mode.MANY_ONE)
self.ui.mode_combo.insertSeparator(sep_index)
self.steps_stacked_widget = mapperwidgets.EnumStackedWidget()
for mode in Mode:
workflow_cls = steps.MODE_WORKFLOWS[mode]
widget = workflow_cls()
widget.importSeqAsRefRequested.connect(self._importSeqChangeRef)
widget.findHomologsRequested.connect(self._findHomologs)
widget.importHomologsRequested.connect(self.importTemplateRequested)
widget.alignSeqsRequested.connect(self._alignSeqs)
widget.removeLigandConstraintsRequested.connect(
self._removeLigandConstraints)
widget.removeResidueConstraintsRequested.connect(
self._removeResidueConstraints)
widget.ligandDialogClosed.connect(self._stopLigandPicking)
if mode is Mode.MANY_ONE:
widget.setAsReferenceRequested.connect(
self._setTemplateAsReference)
widget.importSeqRequested.connect(self._importSeq)
elif mode is Mode.HETEROMULTIMER:
widget.copySeqsRequested.connect(self.copySeqsRequested)
elif mode is Mode.ONE_ONE:
widget.optimizeAlignmentRequested.connect(
self._optimizeAlignment)
if mode in (Mode.CHIMERA, Mode.HETEROMULTIMER, Mode.CONSENSUS):
widget.alignStructsRequested.connect(self._alignStructs)
setattr(self, mode.name, widget)
self.steps_stacked_widget.addWidget(widget)
self.steps_stacked_widget.setEnum(Mode)
# TaskPanelMixin setup
self.setStandardBaseName("homology_modeling")
self.getTaskManager().taskStarted.connect(self._onTaskStarted)
taskbar = self.getTaskBar()
self._start_btn = taskbar.start_btn
[docs] def initLayOut(self):
super().initLayOut()
self.ui.step_layout.addWidget(self.steps_stacked_widget)
self.setStyleSheet(dockwidget_stylesheet)
self.widget().setObjectName("homology_modeling")
self.widget().setStyleSheet(top_stylesheet)
[docs] def initFinalize(self):
super().initFinalize()
self._setCurrentMSVWidget()
[docs] def makeInitialModel(self):
"""
@overrides: MapperMixin
We use `None` as our initial model as a performance optimization.
MsvGui is responsible for setting the model on the panel
"""
return None
[docs] def setModel(self, model):
super().setModel(model)
if self.model is not self._dummy_model:
self._saved_model = self.model
[docs] def getSignalsAndSlots(self, model):
input_ = model.current_page.homology_modeling_input
return [
(model.current_pageReplaced, self._setCurrentMSVWidget),
(input_.target_template_pairsChanged, self._updateModelStatus),
(input_.readyChanged, self._updateModelStatus),
(input_.settings.prime_settings.num_output_structChanged, self._updateModelStatus),
(model.current_page.homology_modeling_inputChanged, self._setModified),
(model.current_page.options.pick_modeChanged, self._onPickModeChanged),
(input_.settings.ligand_dlg_model.constrain_ligandChanged, self._updateLigandPicking),
] # yapf: disable
[docs] def defineMappings(self):
M = self.model_class
input_ = M.current_page.homology_modeling_input
mappings = [
(self.ui.mode_combo, input_.mode),
(self.steps_stacked_widget, input_.mode),
(self._onModeChanged, input_.mode),
(self._onModeChanged, input_.heteromultimer_mode),
(self._updateEnabled, input_.ready),
(self._updateEnabled, M.heteromultimer_settings.ready),
] # yapf: disable
for idx in range(self.steps_stacked_widget.count()):
workflow_wdg = self.steps_stacked_widget.widget(idx)
if isinstance(workflow_wdg, steps.HeteromultimerWidget):
# Heteromultimer tab table needs access to M.pages
param = M
else:
param = M.current_page.homology_modeling_input
mappings.append((workflow_wdg, param))
return mappings
[docs] def showEvent(self, event):
self._enableModel()
super().showEvent(event)
[docs] def hideEvent(self, event):
self._disableModel()
super().hideEvent(event)
def _disableModel(self):
"""
Disconnect the real model from the panel and clear homology status
"""
# Clear homology markers from all alignments
for page in self.model.pages:
aln = page.aln
aln.resetHomologyCache()
self._clearChimericPicking()
self._stopLigandPicking()
self.setMSVWidget(None)
self.setModel(self._dummy_model)
# Disconnect all alignment signals from task input
for page in self._saved_model.pages:
page.homology_modeling_input._setAlignment(None)
def _enableModel(self):
"""
Set the real model on the panel and update homology status
"""
self.setModel(self._saved_model)
self._setCurrentMSVWidget()
self._showChimericHighlights()
self.model.current_page.homology_modeling_input.updateHomologyStatus()
self._updateHeteromultimerPages()
def _updateModelStatus(self):
input_ = self.model.current_page.homology_modeling_input
if (input_.mode is Mode.MANY_ONE and input_.ready):
num_queries = len(input_.target_template_pairs)
num_models = input_.settings.prime_settings.num_output_struct
text = f"{num_queries * num_models} models\nwill be created"
self.status_bar.showMessage(text, color=QtGui.QColor("#c7a049"))
else:
self.status_bar.clearMessage()
def _onModeChanged(self):
mode = self.model.current_page.homology_modeling_input.mode
text = "Generate Models" if mode is Mode.MANY_ONE else "Generate Model"
self._start_btn.setText(text)
self._updateChimericHighlights()
if not self.model.current_page.homology_modeling_input.usesLigands():
self._stopLigandPicking()
self._updateHeteromultimerPages()
def _updateHeteromultimerPages(self):
if self.mode is Mode.HETEROMULTIMER:
het_widget = getattr(self, Mode.HETEROMULTIMER.name)
het_widget.updateSelectedPages()
def _updateChimericHighlights(self):
"""
Show or hide chimeric highlights based on the homology mode
"""
if self.model.current_page.homology_modeling_input.isChimera():
self._showChimericHighlights()
else:
self._clearChimericPicking()
def _clearChimericPicking(self):
"""
Disable chimeric picking and hide chimeric region display
"""
for idx, page in enumerate(self.model.pages):
if page.options.pick_mode is PickMode.HMChimera:
page.options.pick_mode = None
widget = self._msv_tab_widget.widget(idx)
widget.view.setChimeraShown(False)
def _showChimericHighlights(self):
"""
Show chimeric region display if the mode is chimeric
"""
if not self.model.current_page.homology_modeling_input.isChimera():
return
for idx, page in enumerate(self.model.pages):
widget = self._msv_tab_widget.widget(idx)
widget.view.setChimeraShown(True)
def _stopLigandPicking(self):
for page in self.model.pages:
if page.options.pick_mode is PickMode.HMBindingSite:
page.options.pick_mode = None
def _onPickModeChanged(self):
"""
Hide chimeric highlights if picking is off and no regions are specified
"""
if self._msv_widget is None:
return
page = self._msv_widget.model
input_ = page.homology_modeling_input
if (page.options.pick_mode is not PickMode.HMChimera and
not input_.composite_regions):
self._msv_widget.view.setChimeraShown(False)
def _updateLigandPicking(self, do_constrain):
"""
Show/hide ligand constraint picks based on whether constraints are on.
If constraints are off, exit constraint picking mode.
"""
if not do_constrain:
self._stopLigandPicking()
for idx, page in enumerate(self.model.pages):
widget = self._msv_tab_widget.widget(idx)
widget.view.setLigandConstraintsShown(do_constrain)
@property
def mode(self):
return self.model.current_page.homology_modeling_input.mode
@property
def workflow(self):
return self.steps_stacked_widget.currentWidget()
def _setCurrentMSVWidget(self):
if self._msv_widget is not None:
# Heteromultimer mode uses multiple tabs, so users are likely
# to want another heteromultimer tab when they switch tabs
prev_mode = self._msv_widget.model.homology_modeling_input.mode
if prev_mode is Mode.HETEROMULTIMER:
self.model.current_page.homology_modeling_input.mode = prev_mode
self.setMSVWidget(self._msv_tab_widget.currentWidget())
def _getMSVWidgetByAlignment(self, aln):
for idx, page in enumerate(self.model.pages):
if page.aln == aln:
msv_widget = self._msv_tab_widget.widget(idx)
return msv_widget
def _setAlignmentOnTask(self, aln):
if self.model is None:
return
if isinstance(aln, GuiCombinedChainProteinAlignment):
response = self.question(
text="Homology modeling is not yet supported in combined chain "
"mode. Press OK to return to split chain mode or Cancel to "
"close the homology modeling panel.")
if response:
self.model.current_page.split_chain_view = True
else:
self.close()
return
self.model.current_page.homology_modeling_input._setAlignment(aln)
def _onTaskStarted(self, launcher_task):
task = launcher_task.subtask
task.job_idChanged.connect(self._onTaskJobIdChanged)
self.taskStarted.emit(task)
def _onTaskJobIdChanged(self, job_id):
# The Job ID isn't immediately available so we have to wait until the
# Job ID is set to store the associated page
task = self.sender()
msv_widget = self._getMSVWidgetByAlignment(task.aln)
if msv_widget:
self._homology_job_map[job_id] = msv_widget.model
[docs] def getHomologyJobPage(self, job_id: str) -> Optional[gui_models.PageModel]:
"""
Get the page associated with the given job ID, if any.
"""
return self._homology_job_map.get(job_id)
def _onTaskStatusChanged(self, status):
"""
Report the status of an individual task
@overrides: TaskPanelMixin
"""
super()._onTaskStatusChanged()
launcher_task = self.sender()
task = launcher_task.subtask
self._reportStatus(task, status)
if not maestro and status is task.DONE:
msv_widget = self._getMSVWidgetByAlignment(task.aln)
if msv_widget is None:
curr_aln = self.model.current_page.aln
msv_widget = self._getMSVWidgetByAlignment(curr_aln)
seqs = msv_widget.loadPdbs(task.getOutputStructureFiles())
if seqs:
aln = msv_widget.getAlignment()
aln.seq_selection_model.setSelectionState(seqs, False)
@QtCore.pyqtSlot()
def _setModified(self, *, modified=True):
self._modified = modified
self._delayedUpdateEnabled()
def _delayedUpdateEnabled(self):
# Enable doesn't always paint without timer
QtCore.QTimer.singleShot(0, self._updateEnabled)
def _updateEnabled(self):
"""
Update widgets based on whether the task is ready and none of taskman's
tasks are running
"""
if self._msv_widget is None:
return
hm_input = self.model.current_page.homology_modeling_input
is_startable = not self._msv_widget.isWorkspace()
if hm_input.mode is Mode.HETEROMULTIMER:
ready = self.model.heteromultimer_settings.ready
else:
ready = hm_input.ready
enable_run = is_startable and ready and self._modified
self.steps_stacked_widget.setEnabled(is_startable)
self._start_btn.setEnabled(enable_run)
if not is_startable:
tooltip = (
"Homology modeling is not available on the Workspace tab.\n"
"Copy sequences to a new tab or just switch tabs to proceed.")
elif not ready:
tooltip = "Required Homology Modeling steps are not complete"
elif not self._modified:
tooltip = ("Homology modeling was already started.\n"
"Change settings to generate another model.")
else:
tooltip = ""
self._start_btn.setToolTip(tooltip)
[docs] def defineTaskPreprocessors(self, model):
# Note: this sets the preprocessors on the dummy task; the
# preprocessors are manually set on the real task in `getTask`
return [
(model._hm_launcher_task, self._checkMode),
(model._hm_launcher_task, self._validate),
(model._hm_launcher_task, self._initSubtask),
]
@tasks.preprocessor(order=tasks.BEFORE_TASKDIR - 2)
def _checkMode(self):
if not maestro and self.mode is Mode.HETEROMULTIMER:
# TODO MSV-3239
msg = ("Heteromultimer homology modeling does not work "
"standalone, please open MSV in Maestro.")
return False, msg
@tasks.preprocessor(order=tasks.BEFORE_TASKDIR - 1)
def _validate(self):
"""
Check that homology modeling can be run.
"""
response = self._checkIfAligned()
if not response: # Canceled
# Explicitly return False; None will allow task to start
return False
min_quality = self.model.current_page.homology_modeling_input.getMinAlignmentQuality(
)
if min_quality in (constants.AlignmentQuality.WEAK,
constants.AlignmentQuality.LOW):
# TODO change the dialog to have the info icon - self.question is
# the only dialog that returns the response (can't use self.info).
# In PANEL-12506 we may expand the API to allow specifying the icon.
response = self.question(wrap_qt_tag(constants.LOW_PROCEED_TEXT),
"Low-Identity Template",
save_response_key="msv2_homology_low")
return bool(response)
@tasks.preprocessor(order=tasks.BEFORE_TASKDIR)
def _initSubtask(self):
subtask = hm_models.get_task_from_model(self.model)
subtask.aln = self._msv_widget.getAlignment()
task = self.getTask()
task.subtask = subtask
def _checkIfAligned(self):
"""
Guess whether the sequences have been aligned and prompt user
"""
input_ = self.model.current_page.homology_modeling_input
min_quality = input_.getMinAlignmentQuality()
is_low_and_ungapped = (min_quality is constants.AlignmentQuality.LOW and
not input_.hasGaps())
is_weak = min_quality is constants.AlignmentQuality.WEAK
if is_weak or is_low_and_ungapped:
if is_weak:
text = constants.WEAK_REALIGN_TEXT
response_key = "msv2_homology_weak"
else:
text = constants.LOW_REALIGN_TEXT
response_key = "msv2_homology_low_realign"
response = self.question(wrap_qt_tag(text),
"Sequences Are Not Aligned",
save_response_key=response_key,
yes_text="Align")
if response is True:
self._alignSeqs()
# Allow timer to run to update quality
QtWidgets.QApplication.instance().processEvents()
return response
return True
def _setTemplateAsReference(self):
"""
For many-one (batch) homology modeling, set the appropriate structured
sequence as the reference
"""
widget = self._msv_widget
if widget is None:
raise ValueError("Can't set reference seq without msv_widget")
if self.mode is not Mode.MANY_ONE:
raise ValueError("The template can only be set as reference in "
"batch homology modeling")
aln = widget.getAlignment()
new_ref = next(hm_models.get_possible_templates(aln), None)
if new_ref is None:
any_structured = any(
seq.hasStructure() for seq in itertools.islice(aln, 1, None))
any_selected = aln.seq_selection_model.hasSelection()
if any_structured and any_selected:
msg_text = ("None of the selected sequences have structure. "
"Select a structured sequence to set as Reference.")
else:
msg_text = "None of the sequences have structure."
self.warning(msg_text)
return
aln.setReferenceSeq(new_ref)
[docs] @contextlib.contextmanager
def updateNewSeqSelection(self):
"""
Update sequence selection for newly added sequences based on original
selection state.
If there were no non-reference sequences selected, the new sequences
should not be selected. If there were non-reference sequences selected,
both the new and original sequences should be selected.
"""
widget = self._msv_widget
if widget is None:
yield
else:
aln = widget.getAlignment()
sel_model = aln.seq_selection_model
orig_seqs = set(aln)
orig_sel = set(sel_model.getSelection())
select_new = bool(orig_sel - {aln.getReferenceSeq()})
yield
if select_new:
new_seqs = {seq for seq in aln if seq not in orig_seqs}
new_sel = orig_sel | new_seqs
else:
new_sel = orig_sel
sel_model.clearSelection()
sel_model.setSelectionState(new_sel, True)
def _findHomologs(self):
widget = self._msv_widget
if widget is None:
raise ValueError("Can't find homologs without msv_widget")
blast_settings = widget.model.blast_task.input.settings
og_structures = blast_settings.download_structures
og_align = blast_settings.align_after_download
og_multiple = blast_settings.allow_multiple_chains
blast_settings.download_structures = True
blast_settings.align_after_download = True
if self.mode is Mode.HOMOMULTIMER:
blast_settings.allow_multiple_chains = True
try:
if widget.hasBlastResults(for_ref_seq=True):
self.showBlastResultsRequested.emit()
else:
widget.openBlastSearchDialog()
finally:
blast_settings.download_structures = og_structures
blast_settings.align_after_download = og_align
blast_settings.allow_multiple_chains = og_multiple
def _importSeqChangeRef(self):
change_ref = True
self.importSeqRequested.emit(change_ref)
def _importSeq(self):
with self.updateNewSeqSelection():
change_ref = False
self.importSeqRequested.emit(change_ref)
def _alignSeqs(self):
"""
Align sequences for homology modeling
"""
widget = self._msv_widget
if widget is None:
raise ValueError("Can't align seqs without msv_widget")
input_ = self.model.current_page.homology_modeling_input
seqs_to_align = input_.getSeqsToAlign()
seq_sel_model = widget.getAlignment().seq_selection_model
with seq_sel_model.suspendSelection():
settings = widget.model.options.align_settings
orig_value = settings.align_only_selected_seqs
try:
settings.align_only_selected_seqs = True
seq_sel_model.setSelectionState(seqs_to_align, True)
widget.multipleAlignment()
finally:
settings.align_only_selected_seqs = orig_value
def _alignStructs(self):
"""
Align structures for homology modeling
"""
widget = self._msv_widget
if widget is None:
raise ValueError("Can't align seqs without msv_widget")
input_ = self.model.current_page.homology_modeling_input
seqs_to_disassociate = input_.getSeqsToStructureAlign()
widget._splitAllChains(seqs_to_disassociate, prompt=False)
# Disassociate creates new sequences, so we have to get the seqs again
seqs_to_align = input_.getSeqsToStructureAlign()
ref_st, *other_sts = (seq.getStructure() for seq in seqs_to_align)
query = (steps.get_seq_display_name(seqs_to_align[0]), ref_st)
templist = [(f"seq{idx + 1}", st) for idx, st in enumerate(other_sts)]
pairwise_align_ct(query, templist, save_props=True)
for seq, st in zip(seqs_to_align[1:], other_sts):
seq.setStructure(st)
# Force update of RMSD display
pairs = input_.target_template_pairs
input_.target_template_pairsChanged.emit(pairs)
def _optimizeAlignment(self, ligand_asl, residue_asl, radius):
widget = self._msv_widget
if widget is None:
raise ValueError("Can't align seqs without msv_widget")
input_ = self.model.current_page.homology_modeling_input
if not input_.target_template_pairs:
return
pair = input_.target_template_pairs[0]
if not pair.is_valid:
return
widget.optimizeAlignment(pair.target_seq,
pair.template_seq,
ligand_asl=ligand_asl,
residue_asl=residue_asl,
radius=radius)
def _removeLigandConstraints(self):
widget = self._msv_widget
if widget is None:
raise ValueError("Can't set reference seq without msv_widget")
widget.resetPick(PickMode.HMBindingSite)
def _removeResidueConstraints(self):
widget = self._msv_widget
if widget is None:
raise ValueError("Can't remove res constraints without msv_widget")
widget.resetPick(PickMode.HMProximity)
def _reportStatus(self, task, status):
logger.debug(f"Task is now {status.name}")
if status is task.FAILED:
logger.debug(task.failure_info)
def _runSlot(self):
"""
@overrides: TaskPanelMixin
"""
self.model.current_page.options.pick_mode = None
super()._runSlot()
self._setModified(modified=False)