Source code for schrodinger.application.msv.gui.homology_modeling.homology_panel

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 getSettingsMenuActions(self, task): """ Hide reset action """ actions = super().getSettingsMenuActions(task) return [ act for act in actions if act is None or act[0] != widgetmixins.RESET ]
[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())
[docs] def setMSVWidget(self, msv_widget): """ Store the current MSV widget and update the task's alignment """ if self._msv_widget is not None: self._msv_widget.model.alnChanged.disconnect( self._setAlignmentOnTask) self._msv_widget = msv_widget if self._msv_widget is None: aln = None else: aln = self._msv_widget.getAlignment() self._msv_widget.model.alnChanged.connect(self._setAlignmentOnTask) self._setAlignmentOnTask(aln) if self.model is None: return self._setModified(modified=True) self._updateEnabled()
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)