import inflect
from schrodinger.application.msv.gui import gui_models
from schrodinger.application.msv.gui.dialogs import OptimizeAlignmentDialog
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_step_ui
from schrodinger.application.msv.gui.homology_modeling import \
homology_multiple_view_tabs_ui
from schrodinger.application.msv.gui.homology_modeling import ligand_dialog
from schrodinger.application.msv.gui.homology_modeling import settings_dialog
from schrodinger.application.msv.gui.homology_modeling import view_tab_table
from schrodinger.application.msv.gui.homology_modeling.constants import Mode
from schrodinger.application.msv.gui.homology_modeling.constants import \
StepAction
from schrodinger.application.msv.gui.homology_modeling.hm_models import \
get_seq_display_name
from schrodinger.application.msv.gui.menu import EnabledTargetSpec
from schrodinger.models import mappers
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt import mapperwidgets
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.mapperwidgets import EnumComboBox
from schrodinger.ui.qt.mapperwidgets import plptable
from schrodinger.ui.qt.utils import wrap_qt_tag
step_stylesheet = """
QWidget {
font-size: 12px;
}
QCheckBox#step_cb {
font-weight: bold;
}
QCheckBox#step_cb:!enabled {
color: #999;
}
QCheckBox#step_cb::indicator {
width: 24px;
height: 24px;
padding-right: 5px;
}
QLabel#detail_lbl {
color: #666;
font-size: 11px;
font-style: italic;
}
QLabel#detail_lbl:!enabled {
color: #aaa;
}
QToolButton#info_btn {
image: url(:/msv/icons/info_icon.png);
/* NOTE: image does not appear without border style */
border: 0px;
}
QToolButton#info_btn::hover{
image: url(:/msv/icons/info_icon-h.png);
}
QPushButton#action_btn {
border: 0px;
text-decoration: underline;
color: #235fa3;
}
QPushButton#action_btn:!enabled {
border: 0px;
text-decoration: none;
color: #ccc;
}
"""
workflow_additional_stylesheet = """
QLabel#instruction_lbl {
font-size: 11px;
}
QLabel#target_name_lbl, QLabel#identity_lbl {
color: #76a459;
font-style: italic;
}
QLabel#template_name_lbl {
color: #964f76;
font-style: italic;
}
QToolButton#optimize_btn {
padding: 0px;
width: 20px;
height: 20px;
image: url(:/msv/icons/optimize.png);
/* NOTE: image does not appear without border style */
border: 0px;
}
QToolButton#optimize_btn:!enabled {
image: url(:/msv/icons/optimize-d.png);
}
QToolButton#optimize_btn::hover {
image: url(:/msv/icons/optimize-h.png);
}
QToolButton#optimize_btn::pressed {
image: url(:/msv/icons/optimize-clicked.png);
}
QCheckBox#step_cb::indicator {
image: url(:/msv/icons/chevron-active.png);
}
QCheckBox#step_cb::indicator:!enabled {
image: url(:/msv/icons/chevron-disabled.png);
}
QCheckBox#step_cb::indicator[status="QUESTIONABLE"] {
image: url(:/msv/icons/warning-question.png);
}
QLabel#identity_lbl[status="QUESTIONABLE"] {
font-weight: bold;
color: #c7a049;
}
QCheckBox#step_cb::indicator[status="FIXABLE"] {
image: url(:/msv/icons/error-question.png);
}
QLabel#identity_lbl[status="FIXABLE"], QLabel#template_name_lbl[status="UNACCEPTABLE"] {
font-weight: bold;
color: #ab692c;
}
QCheckBox#step_cb::indicator[status="UNACCEPTABLE"] {
image: url(:/msv/icons/error-exclamation.png);
}
QCheckBox#step_cb::indicator[status="ACCEPTABLE"] {
image: url(:/msv/icons/check-proposed.png);
}
/* Put most specific selectors at the bottom */
QCheckBox#step_cb::indicator:checked[status="ACCEPTABLE"] {
image: url(:/msv/icons/check-approved.png);
}
QCheckBox#step_cb::indicator:checked!enabled[status="ACCEPTABLE"] {
image: url(:/msv/icons/check-approved-disabled.png);
}
"""
ligands_additional_stylesheet = """
QCheckBox#step_cb::indicator {
image: url(:/msv/icons/toggle-OFF-light.png);
}
QCheckBox#step_cb::indicator:checked {
image: url(:/msv/icons/toggle-ON-light-2.png);
}
QCheckBox#step_cb::indicator:hover{
image: url(:/msv/icons/toggle-hover-light.png);
}
QCheckBox#step_cb::indicator:!enabled {
image: url(:/msv/icons/toggle-disabled-light.png);
}
QLabel#num_ligs_lbl {
color: #666666;
font-style: italic;
}
QLabel#num_ligs_lbl[status="QUESTIONABLE"] {
font-weight: bold;
color: #c7a049;
}
"""
settings_additional_stylesheet = """
QCheckBox#step_cb::indicator {
image: url(:/msv/icons/settings-light.png);
}
QCheckBox#step_cb::indicator[highlight=true] {
image: url(:/msv/icons/settings-custom-light.png);
}
QCheckBox#step_cb::indicator:!enabled {
image: url(:/msv/icons/settings_disabled-light.png);
}
QCheckBox#step_cb::indicator:hover {
image: url(:/msv/icons/settings-hover-light.png);
}
"""
heteromultimer_stylesheet = """
QWidget {
font-size: 12px;
}
QLabel#create_details_lbl, QLabel#distribute_details_lbl {
color: #666;
font-size: 11px;
}
QLabel#distribute_details_lbl {
font-style: italic;
}
QCheckBox#distribute_chains_cb, QCheckBox#select_tabs_cb {
font-weight: bold;
}
QCheckBox#distribute_chains_cb::indicator, QCheckBox#select_tabs_cb::indicator {
image: url(:/msv/icons/chevron-active.png);
width: 24px;
height: 24px;
padding-right: 5px;
}
QToolButton#info_btn {
image: url(:/msv/icons/info_icon.png);
border: 0px;
}
QToolButton#info_btn::hover{
image: url(:/msv/icons/info_icon-h.png);
border: 0px;
}
"""
# Reduce the paragraph spacing.
tooltip_style = """
<style>
p {
margin-top: 5px;
margin-bottom: 5px;
}
</style>
"""
STRUCTALIGN_REF_PROP = 's_psp_StructAlign_Reference'
STRUCTALIGN_RMSD_PROP = 'r_psp_StructAlign_RMSD'
class _ActionMixin:
"""
Mixin for adding actions to an AbstractStep.
Child classes must define `ACTIONS` and may define `action_signals` to
define signals to emit on button click.
:cvar ACTIONS: Enum for action buttons
:vartype ACTIONS: tuple[constants.StepAction]
:ivar action_btns: Mapping of action enum member to action button
(automatically generated from `ACTIONS` - button text is enum value)
:vartype action_btns: dict[constants.StepAction, QtWidgets.QPushButton]
:ivar action_signals: Mapping between StepAction and signal to emit on
button click. Defaults to None, so buttons will not emit a signal.
:vartype action_signals: dict[constants.StepAction, QtCore.pyqtSignal]
:cvar ENABLE_ON_CHECK: Enabled state for actions when the checkbox is
checked. None does not affect the actions. Defaults to False (i.e.
disable actions when checkbox is checked, enable when unchecked)
:vartype ENABLE_ON_CHECK: bool or NoneType
"""
ACTIONS = NotImplemented
ENABLE_ON_CHECK = False
def initSetUp(self):
super().initSetUp()
self.action_signals = None
self.action_btns = {
enum_member: QtWidgets.QPushButton(enum_member.value)
for enum_member in self.ACTIONS
}
for btn in self.action_btns.values():
btn.setObjectName("action_btn")
self.ui.step_cb.toggled.connect(self._onCbToggled)
def initLayOut(self):
super().initLayOut()
for btn in self.action_btns.values():
self.ui.action_layout.addWidget(btn)
def initFinalize(self):
super().initFinalize()
if self.action_signals is not None:
for action, signal in self.action_signals.items():
btn = self.action_btns[action]
btn.clicked.connect(signal)
def _onCbToggled(self):
"""
If `ENABLE_ON_CHECK` is True, change the action button enabled state to
match the checkbox check state (i.e. enabled if checked).
If `ENABLE_ON_CHECK` is False, change the action button enabled state
to be the opposite of the checkbox check state (i.e. disabled if
checked).
"""
if self.ENABLE_ON_CHECK is None:
return
checked = self.ui.step_cb.isChecked()
enable = checked is self.ENABLE_ON_CHECK
for btn in self.action_btns.values():
btn.setEnabled(enable)
class _ToolTipMixin:
"""
Mixin to show tooltip when clicking `self.ui.info_btn`
"""
TOOLTIP = ""
def initSetUp(self):
super().initSetUp()
self.ui.info_btn.setToolTip(
"Click for more information about this step.")
self.ui.info_btn.clicked.connect(self.showToolTip)
def _getToolTipText(self):
"""
Get the tooltip text after applying the style.
"""
return f"{tooltip_style}{self.TOOLTIP}"
def showToolTip(self):
text = wrap_qt_tag(self._getToolTipText())
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), text, self.ui.info_btn)
[docs]class AbstractStep(_ToolTipMixin, mappers.MapperMixin, basewidgets.BaseWidget):
"""
Abstract class for a homology modeling step.
Subclasses must define `_onPairsChanged`, which is called with
target_template_pairs
:cvar TEXT: The text for the checkbox
:cvar DETAIL_TEXT: The detail text
:cvar EXTRA_STYLESHEET: Additional stylesheet to append
:cvar TOOLTIP: The text to show on clicking on info button
"""
model_class = hm_models.HomologyModelingInput
ui_module = homology_modeling_step_ui
TEXT = ""
DETAIL_TEXT = ""
EXTRA_STYLESHEET = ""
TOTAL_WIDTH = 300
[docs] def initSetUp(self):
super().initSetUp()
self.setStyleSheet(step_stylesheet + self.EXTRA_STYLESHEET)
self._font_metrics = QtGui.QFontMetrics(self.font())
self._text_width = (self.TOTAL_WIDTH -
self._font_metrics.horizontalAdvance(self.TEXT))
self._status_lbl = None
self.widgets_with_status = []
self.ui.step_cb.setText(self.TEXT)
self.ui.step_cb.setCheckable(False)
self.ui.detail_lbl.setText(self.DETAIL_TEXT)
[docs] def initLayOut(self):
super().initLayOut()
if self._status_lbl is not None:
self.ui.status_layout.addWidget(self._status_lbl)
self.widgets_with_status.append(self._status_lbl)
[docs] def defineMappings(self):
M = self.model_class
pairs_tgt = mappers.TargetSpec(setter=self._onPairsChanged)
return [
(pairs_tgt, M.target_template_pairs),
]
def _elideHtml(self, richtext):
"""
Elide text that may contain HTML based on the plaintext width. HTML
will be stripped if eliding to avoid partial HTML tags.
"""
plaintext = QtGui.QTextDocumentFragment.fromHtml(richtext).toPlainText()
elided_text = self._font_metrics.elidedText(plaintext,
QtCore.Qt.ElideRight,
self._text_width)
if len(elided_text) == len(plaintext):
# If text does not need to be elided, preserve HTML
return richtext
return elided_text
[docs] def connectToWorkflow(self, workflow):
"""
Connect this step to the correct workflow instance variables and signals.
May override for custom behavior.
:param workflow: The parent workflow to link the signals to
:type workflow: AbstractWorkflow
"""
[docs] def setStatusText(self, text):
"""
Set text to the status label
"""
if self._status_lbl is None:
return
tooltip = text
if text:
text = self._elideHtml(text)
self._status_lbl.setText(text)
self._status_lbl.setToolTip(tooltip)
[docs] def setStatus(self, status=None):
"""
:param status: Status to set on step or None to reset
:type status: constants.StepState or NoneType
"""
if status is not None:
status = status.name
for widget in self.widgets_with_status:
widget.setProperty("status", status)
qt_utils.update_widget_style(widget)
def _onPairsChanged(self, target_template_pairs):
"""
Must be reimplemented in subclasses to update the view based on the new
pairs
:param target_template_pairs: List of objects representing a
(target sequence, template sequence) pair
:type target_template_pairs: list[hm_models.TargetTemplatePair]
"""
raise NotImplementedError
[docs] def makeInitialModel(self):
"""
@overrides: MapperMixin
We just use `None` as our initial model as a performance optimization.
Steps never need their own model in isolation anyways.
"""
return None
[docs]class AbstractActionStep(_ActionMixin, AbstractStep):
"""
Class for steps where the corresponding action cannot be performed in MSV2
"""
[docs]class AbstractWorkflowStep(AbstractStep):
"""
Class for homology modeling steps that are part of the workflow
"""
EXTRA_STYLESHEET = workflow_additional_stylesheet
[docs] def initSetUp(self):
super().initSetUp()
self.widgets_with_status.append(self.ui.step_cb)
[docs] def setStatus(self, status=None):
super().setStatus(status)
self.ui.step_cb.setCheckable(status is constants.StepState.ACCEPTABLE)
def _getToolTipText(self):
"""
If the step is checked, show the completed info tooltip on click
"""
# overrides _ToolTipMixin
completed = self.ui.step_cb.isChecked()
if completed:
return f"{tooltip_style}{constants.COMPLETED_TOOLTIP}"
return super()._getToolTipText()
class _AbstractSequenceStep(_ActionMixin, AbstractWorkflowStep):
def initSetUp(self):
super().initSetUp()
self._status_lbl = QtWidgets.QLabel()
def _onPairsChanged(self, target_template_pairs):
"""
@overrides: AbstractStep
"""
self._updateSequenceName(target_template_pairs)
def _updateSequenceName(self, target_template_pairs):
"""
Updates the appropriate name label when the target_template_pairs change
"""
name = self._getSequenceName(target_template_pairs)
if name is None:
name = ""
if name == "":
status = None
else:
status = constants.StepState.ACCEPTABLE
self.setStatusText(name)
self.setStatus(status)
def _getSequenceName(self, target_template_pairs):
"""
Returns the name of the sequence(s)
"""
raise NotImplementedError()
[docs]class TargetSequenceStep(_AbstractSequenceStep):
TEXT = "Get the target sequence"
DETAIL_TEXT = "Must be first (Reference) sequence"
ACTIONS = (StepAction.IMPORT_SEQUENCE,)
TOOLTIP = constants.TARGET_SEQ_STEP_TT
importSeqAsRefRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self._status_lbl.setObjectName("target_name_lbl")
self.action_signals = {
StepAction.IMPORT_SEQUENCE: self.importSeqAsRefRequested
}
[docs] def connectToWorkflow(self, workflow):
# @overrides: AbstractStep
workflow.target_step = self
self.importSeqAsRefRequested.connect(workflow.importSeqAsRefRequested)
def _getSequenceName(self, pairs):
"""
Returns the first target seq name.
"""
if len(pairs) > 0:
target_seq = pairs[0].target_seq
if target_seq is not None:
return get_seq_display_name(target_seq)
[docs]class BatchTargetSequenceStep(TargetSequenceStep):
TEXT = "Get 1st target"
DETAIL_TEXT = "Set as Reference for finding template"
TOOLTIP = constants.BATCH_TARGET_SEQ_STEP_TT
[docs]class OtherTargetsStep(TargetSequenceStep):
TEXT = "Get other targets"
DETAIL_TEXT = "Targets must be below reference (all or selected)"
ACTIONS = (StepAction.IMPORT_SEQUENCES,)
TOOLTIP = constants.OTHER_TARGET_STEP_TT
importSeqRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self.action_signals = {
StepAction.IMPORT_SEQUENCES: self.importSeqRequested
}
def _getSequenceName(self, target_template_pairs):
"""
There are expected to be too many names to show so just returns the
number of sequences
"""
num_pairs = len(target_template_pairs)
if num_pairs > 1:
return f"({num_pairs} sequences)"
[docs]class TemplateStructureStep(_AbstractSequenceStep):
TEXT = "Specify a template structure"
DETAIL_TEXT = "Usually second sequence"
ACTIONS = (StepAction.FIND_HOMOLOGS, StepAction.IMPORT_HOMOLOGS)
TOOLTIP = constants.TEMP_ST_STEP_TT
findHomologsRequested = QtCore.pyqtSignal()
importHomologsRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self._status_lbl.setObjectName("template_name_lbl")
self.action_signals = {
StepAction.FIND_HOMOLOGS: self.findHomologsRequested,
StepAction.IMPORT_HOMOLOGS: self.importHomologsRequested
}
[docs] def initLayOut(self):
super().initLayOut()
layout = QtWidgets.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.action_btns[StepAction.IMPORT_HOMOLOGS])
layout.addWidget(QtWidgets.QLabel(' <i>or</i> '))
layout.addWidget(self.action_btns[StepAction.FIND_HOMOLOGS])
self.ui.action_layout.insertLayout(0, layout)
[docs] def connectToWorkflow(self, workflow):
# @overrides: AbstractStep
workflow.template_step = self
self.findHomologsRequested.connect(workflow.findHomologsRequested)
self.importHomologsRequested.connect(
lambda: workflow.importHomologsRequested.emit(False))
def _getSequenceName(self, pairs):
"""
Returns the first template seq name.
"""
if len(pairs) > 0:
template_seq = pairs[0].template_seq
if template_seq is not None:
return get_seq_display_name(template_seq)
[docs]class BatchTemplateStructureStep(TemplateStructureStep):
TEXT = "Specify template structure"
DETAIL_TEXT = "Once in tab, template must be set as <b>Reference</b> for modeling"
TOOLTIP = constants.BATCH_TEMP_ST_STEP_TT
setAsReferenceRequested = QtCore.pyqtSignal()
ACTIONS = (StepAction.FIND_HOMOLOGS, StepAction.IMPORT_HOMOLOGS,
StepAction.SET_AS_REFERENCE)
[docs] def initSetUp(self):
super().initSetUp()
self.action_signals = {
StepAction.FIND_HOMOLOGS: self.findHomologsRequested,
StepAction.IMPORT_HOMOLOGS: self.importHomologsRequested,
StepAction.SET_AS_REFERENCE: self.setAsReferenceRequested,
}
def _onPairsChanged(self, pairs):
super()._onPairsChanged(pairs)
status = None
num_pairs = len(pairs)
if num_pairs > 0:
pair = pairs[0]
if pair.template_seq is not None:
status = constants.StepState.ACCEPTABLE
elif num_pairs > 1:
status = constants.StepState.UNACCEPTABLE
self.setStatus(status)
ref_enabled = status is not constants.StepState.ACCEPTABLE
self.action_btns[StepAction.SET_AS_REFERENCE].setEnabled(ref_enabled)
def _getSequenceName(self, pairs):
name = super()._getSequenceName(pairs)
if name is None and len(pairs) > 1:
return "Ref lacks structure"
return name
[docs]class MultipleTemplateStructuresStep(TemplateStructureStep):
TEXT = "Specify template structures"
DETAIL_TEXT = "Must be second and later sequences"
TOOLTIP = constants.MULTI_TEMP_ST_STEP_TT
def _getSequenceName(self, target_template_pairs):
"""
Returns all of the template seq names.
"""
names = []
for pair in target_template_pairs:
template_seq = pair.template_seq
if template_seq is not None:
names.append(get_seq_display_name(template_seq))
return ", ".join(names)
[docs] def connectToWorkflow(self, workflow):
# @overrides: TemplateStructuresStep
workflow.template_step = self
self.findHomologsRequested.connect(workflow.findHomologsRequested)
self.importHomologsRequested.connect(
lambda: workflow.importHomologsRequested.emit(True))
[docs]class HomomultimerTemplateStructuresStep(MultipleTemplateStructuresStep):
TEXT = "Find template chains"
[docs]class BaseAlignSequencesStep(_ActionMixin, AbstractWorkflowStep):
TEXT = "Align sequences"
DETAIL_TEXT = "Run if not auto-aligned when template was imported"
ACTIONS = (StepAction.RUN_ALIGNMENT,)
TOOLTIP = constants.BASE_ALIGN_SEQ_STEP_TT
alignSeqsRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self._status_lbl = QtWidgets.QLabel()
self._status_lbl.setObjectName("identity_lbl")
self.action_signals = {
StepAction.RUN_ALIGNMENT: self.alignSeqsRequested
}
[docs] def connectToWorkflow(self, workflow):
# @overrides: AbstractStep
workflow.align_step = self
self.alignSeqsRequested.connect(workflow.alignSeqsRequested)
def _onPairsChanged(self, pairs):
# @overrides: AbstractStep
self._updateIdentity(pairs)
def _updateIdentity(self, pairs):
self._updateIdentityProperties(pairs)
text = self._getIdentityText(pairs)
self.setStatusText(text)
def _getIdentityText(self, pairs):
identities = []
for pair in pairs:
if pair.identity is None:
continue
id_text = f"{pair.identity:.0%}"
if pair.alignment_quality is not constants.AlignmentQuality.ACCEPTABLE:
id_text = f"<b>{id_text}</b>"
identities.append(id_text)
text = ", ".join(identities)
num_identities = len(identities)
if num_identities:
id_text = inflect.engine().plural("ID", num_identities)
text = f"{id_text} {text}"
return text
def _updateIdentityProperties(self, pairs):
min_quality = hm_models.get_min_alignment_quality(pairs)
if min_quality is constants.AlignmentQuality.WEAK:
status = constants.StepState.FIXABLE
elif min_quality is constants.AlignmentQuality.LOW:
status = constants.StepState.QUESTIONABLE
elif min_quality is constants.AlignmentQuality.ACCEPTABLE:
status = constants.StepState.ACCEPTABLE
else:
status = None
self.setStatus(status)
[docs]class AlignSequencesStep(BaseAlignSequencesStep):
"""
Sequence alignment step for simple (one-one) homology modeling
"""
optimizeAlignmentRequested = QtCore.pyqtSignal(str, str, float)
[docs] def initSetOptions(self):
super().initSetOptions()
self.TOOLTIP = self.TOOLTIP + constants.ALIGN_SEQ_STEPS_TT
[docs] def initSetUp(self):
super().initSetUp()
self._optimize_btn = QtWidgets.QToolButton()
self._optimize_btn.setObjectName("optimize_btn")
self._optimize_btn.setToolTip(
wrap_qt_tag("""
<span
style="color: #888888; font-weight: bold; font-style: italic;">
Optional step:</span><br/>
Click to optimize alignment at binding site after full sequence
alignment"""))
self._optimize_btn.setEnabled(False)
self._optimize_btn.setVisible(False)
self._optimize_dlg = OptimizeAlignmentDialog(self)
self._optimize_dlg.optimizeAlignmentRequested.connect(
self._onOptimizeAlignmentRequested)
self._optimize_btn.clicked.connect(self._runOptimizeDialog)
[docs] def initLayOut(self):
super().initLayOut()
self.ui.status_layout.insertWidget(0, self._optimize_btn)
def _onPairsChanged(self, pairs):
"""
@overrides: AbstractStep
"""
super()._onPairsChanged(pairs)
can_optimize = False
if pairs:
pair = pairs[0]
can_optimize = bool(
pair.is_valid and
pair.alignment_quality > constants.AlignmentQuality.WEAK)
self._optimize_btn.setEnabled(can_optimize)
self._optimize_btn.setVisible(can_optimize)
def _runOptimizeDialog(self):
template_seq = self.model.target_template_pairs[0].template_seq
sel_residues = self.model._aln.res_selection_model.getSelection()
self._optimize_dlg.setResidues(template_seq, sel_residues)
ligmols = self.model.settings.ligand_dlg_model.ligands
self._optimize_dlg.model.ligands = ligmols
self._optimize_dlg.resize(1, 1)
self._optimize_dlg.exec()
def _onOptimizeAlignmentRequested(self, ligand, residue_asl, radius):
if ligand is None:
ligand_asl = ""
else:
# Select ligand for inclusion in model
lig_model = self.model.settings.ligand_dlg_model
if ligand not in lig_model.selected_ligands:
for other_lig in lig_model.ligands:
if other_lig == ligand:
lig_model.selected_ligands.append(other_lig)
break
asls = (lig.asl for lig in ligand.source_ligands)
ligand_asl = " OR ".join(asls)
self.optimizeAlignmentRequested.emit(ligand_asl, residue_asl, radius)
[docs]class BatchAlignSequencesStep(BaseAlignSequencesStep):
DETAIL_TEXT = "Align all targets to template"
TOOLTIP = constants.BATCH_ALIGN_SEQ_STEP_TT
[docs]class AlignStructuresSequencesStep(BaseAlignSequencesStep):
TEXT = "Align structures && sequences" # Need to escape ampersand for btn
DETAIL_TEXT = "Align to first template then to target sequence"
ACTIONS = (StepAction.ALIGN_STRUCTURES, StepAction.ALIGN_SEQUENCES)
TOOLTIP = constants.ALIGN_ST_SEQ_STE_TT
alignStructsRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self.action_signals = {
StepAction.ALIGN_STRUCTURES: self.alignStructsRequested,
StepAction.ALIGN_SEQUENCES: self.alignSeqsRequested,
}
def _getIdentityText(self, pairs):
"""
@overrides: BaseAlignSequencesStep
"""
if not pairs:
return ""
text = super()._getIdentityText(pairs)
if text != "":
text = f"Seq {text}"
all_rmsds = []
templates = [
pair.template_seq for pair in pairs if pair.template_seq is not None
]
if not templates:
return
ref_seq, *other_seqs = templates
ref_name = get_seq_display_name(ref_seq)
for seq in other_seqs:
st = seq.getStructure()
if st.property.get(STRUCTALIGN_REF_PROP) == ref_name:
rmsd = st.property.get(STRUCTALIGN_RMSD_PROP)
if rmsd is not None:
all_rmsds.append(rmsd)
rmsd_text = ", ".join(f"{rmsd:.2f}" for rmsd in all_rmsds)
if rmsd_text:
rmsd_noun = inflect.engine().plural("RMSD", len(all_rmsds))
text = f"{rmsd_noun} {rmsd_text}; {text}"
return text
[docs]class HomomultimerAlignSequencesStep(BaseAlignSequencesStep):
TOOLTIP = constants.HOMOMULTIMER_ALIGN_SEQ_STEP_TT
[docs]class RegionsStep(_ActionMixin, AbstractWorkflowStep):
TEXT = "Define regions on templates"
DETAIL_TEXT = ("Second sequence is default template; select regions on "
"other sequences")
ACTIONS = (StepAction.PICK,)
TOOLTIP = constants.REGION_STEP_TT
[docs] def initSetUp(self):
super().initSetUp()
self.num_regions_lbl = QtWidgets.QLabel()
pick_btn = self.action_btns[StepAction.PICK]
pick_btn.setObjectName("pick_btn")
pick_btn.setCheckable(True)
self.pick_btn = pick_btn
[docs] def initLayOut(self):
super().initLayOut()
self.ui.status_layout.addWidget(self.num_regions_lbl)
[docs] def defineMappings(self):
mappings = super().defineMappings()
M = self.model_class
regions_tgt = mappers.TargetSpec(setter=self._onRegionsChanged)
mappings += [
(regions_tgt, M.composite_regions),
(self.pick_btn, M.pick_chimera),
] # yapf: disable
return mappings
def _onPairsChanged(self, pairs):
"""
@overrides: AbstractStep
Intentional no-op because regions view does not need to update when
pairs change
"""
pass
def _onRegionsChanged(self, regions):
num_regions = len(regions)
if num_regions > 0:
status = constants.StepState.ACCEPTABLE
else:
num_regions = "no"
status = None
alternate_text = inflect.engine().plural("alternate", num_regions)
text = f"({num_regions} {alternate_text})"
self.num_regions_lbl.setText(text)
self.setStatus(status)
def _onCbToggled(self):
"""
@overrides: _ActionMixin
Uncheck the pick_btn if it becomes disabled
"""
super()._onCbToggled()
if not self.pick_btn.isEnabled():
self.pick_btn.setChecked(False)
[docs]class OptionalMixin:
"""
Mixin to add non-bold text (optional) after the step name
"""
[docs] def initSetUp(self):
super().initSetUp()
self._optional_lbl = QtWidgets.QLabel(" (optional)")
[docs] def initLayOut(self):
super().initLayOut()
# layout item 0 is the step title
self.ui.cb_layout.insertWidget(1, self._optional_lbl)
[docs]class LigandsStep(OptionalMixin, AbstractActionStep):
TEXT = "Include ligands && cofactors"
DETAIL_TEXT = ("Check to preserve hets; optionally choose waters, "
"constrain proximity")
ACTIONS = (StepAction.CHOOSE_LIGANDS,)
EXTRA_STYLESHEET = ligands_additional_stylesheet
ENABLE_ON_CHECK = True
TOOLTIP = constants.LIGANDS_STEP_TT
removeLigandConstraintsRequested = QtCore.pyqtSignal()
ligandDialogClosed = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self._status_lbl = QtWidgets.QLabel()
self._status_lbl.setObjectName("num_ligs_lbl")
self._warning_lbl = QtWidgets.QLabel()
warning_pixmap = QtGui.QPixmap(':/msv/icons/error-exclamation.png')
warn_height = self._font_metrics.height() + 4
self._warning_lbl.setPixmap(warning_pixmap.scaledToHeight(warn_height))
self._warning_lbl.setVisible(False)
self.ui.step_cb.setCheckable(True)
self.ligand_dlg = ligand_dialog.LigandCofactorDialog(parent=self)
self.ligand_dlg.energyBasedModelingRequested.connect(
self._setEnergyBasedModeling)
self.ligand_dlg.removeConstraintsRequested.connect(
self.removeLigandConstraintsRequested)
self.ligand_pick_btn = self.ligand_dlg.ui.pick_constraints_btn
self.action_btns[StepAction.CHOOSE_LIGANDS].clicked.connect(
self._showLigandDialog)
[docs] def initLayOut(self):
super().initLayOut()
self.ui.status_layout.insertWidget(0, self._warning_lbl)
[docs] def defineMappings(self):
M = self.model_class
settings = M.settings
mappings = super().defineMappings()
mappings += [
(self.ui.step_cb, settings.include_hets),
(self.ligand_dlg, M.settings.ligand_dlg_model),
(self._updateNumLigands, settings.include_hets),
(self._updateNumLigands, M.settings.ligand_dlg_model.selected_ligands),
(self._updateNumLigands, M.settings.ligand_dlg_model.include_waters),
] # yapf: disable
return mappings
[docs] def connectToWorkflow(self, workflow):
# @overrides: AbstractStep
workflow.ligands_step = self
workflow.ligand_pick_btn = self.ligand_pick_btn
self.removeLigandConstraintsRequested.connect(
workflow.removeLigandConstraintsRequested)
self.ligandDialogClosed.connect(workflow.ligandDialogClosed)
def _onPairsChanged(self, target_template_pairs):
"""
@overrides: AbstractStep
"""
# Don't need to update view - ligand dialog model is updated by the task
def _updateNumLigands(self):
"""
Show the number of selected ligand molecules.
"""
if self.model.settings.include_hets:
include_waters = self.model.settings.ligand_dlg_model.include_waters
ligands = self.model.settings.ligand_dlg_model.selected_ligands
else:
include_waters = False
ligands = []
num_cofactors = 0
for ligmol in ligands:
if any(lig.cofactor for lig in ligmol.source_ligands):
num_cofactors += 1
num_ligands = len(ligands) - num_cofactors
status = None if num_ligands <= 1 else constants.StepState.QUESTIONABLE
self._warning_lbl.setVisible(status is constants.StepState.QUESTIONABLE)
text_parts = []
if num_ligands:
if num_cofactors and include_waters:
# Don't need to conditionally pluralize because min number is 2
lig_text = f"{num_ligands + num_cofactors} hets"
else:
lig_word = inflect.engine().plural("ligand", num_ligands)
lig_text = f"{num_ligands} {lig_word}"
text_parts.append(lig_text)
if num_cofactors and not (num_ligands and include_waters):
cofactor_word = inflect.engine().plural("cofactor", num_cofactors)
cofactor_text = f"{num_cofactors} {cofactor_word}"
text_parts.append(cofactor_text)
tt_parts = []
for ligmol in ligands:
tt_parts.append("-".join(
lig.pdbres.strip() for lig in ligmol.source_ligands))
tt_text = ", ".join(tt_parts)
if include_waters:
text_parts.append("waters")
water_tt = "all waters"
if len(ligands):
water_tt = " and " + water_tt
if len(ligands) > 1:
water_tt = "," + water_tt # Oxford comma
tt_text += water_tt
self.setStatusText(", ".join(text_parts))
# Override default status tooltip
self._status_lbl.setToolTip(tt_text)
self.setStatus(status)
def _showLigandDialog(self):
self.ligand_dlg.run(finished_callback=self.ligandDialogClosed.emit)
def _setEnergyBasedModeling(self):
prime_settings = self.model.settings.prime_settings
prime_settings.prime_method = constants.PrimeMethod.ENERGY
[docs]class MultipleTemplatesLigandStep(LigandsStep):
TOOLTIP = constants.MULTI_TEMP_LIGS_STEP_TT
[docs]class SettingsStep(OptionalMixin, AbstractActionStep):
model_class = hm_models.HomologyModelingSettings
TEXT = "Change model settings"
TOOLTIP = constants.SETTNGS_STEP_TT
ACTIONS = (StepAction.VIEW_SETTINGS,)
EXTRA_STYLESHEET = settings_additional_stylesheet
removeResidueConstraintsRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self.settings_dlg = settings_dialog.SettingsDialog(parent=self)
self.settings_dlg.removeResidueConstraintsRequested.connect(
self.removeResidueConstraintsRequested)
self.ui.step_cb.clicked.connect(self._showSettingsDialog)
self.action_btns[StepAction.VIEW_SETTINGS].clicked.connect(
self._showSettingsDialog)
[docs] def defineMappings(self):
"""
@overrides: AbstractStep
Intentionally skipping parent mappings because model class is different
"""
M = self.model_class
return [
(self.settings_dlg, M),
(mappers.TargetSpec(setter=self._updateDisplay), M),
]
[docs] def getSignalsAndSlots(self, model):
"""
@overrides: AbstractStep
Intentionally skipping parent slots because model class is different
"""
return []
[docs] def connectToWorkflow(self, workflow):
# @overrides: AbstractStep
workflow.settings_step = self
self.removeResidueConstraintsRequested.connect(
workflow.removeResidueConstraintsRequested)
def _updateDisplay(self, settings):
prime_settings = settings.prime_settings
self.ui.step_cb.setProperty('highlight', not prime_settings.isDefault())
qt_utils.update_widget_style(self.ui.step_cb)
summary_text = f"Currently: {prime_settings.getSummaryText()}"
self.ui.detail_lbl.setText(summary_text)
def _showSettingsDialog(self):
self.settings_dlg.run()
[docs]class AbstractWorkflow(mappers.MapperMixin, basewidgets.BaseWidget):
"""
Abstract widget containing a sequence of homology modeling `AbstractStep`.
Subclasses must override `STEP_CLASSES`.
Subclasses with custom step types should extend `_enableReachableSteps()`
to enable those steps when needed.
:cvar STEP_CLASSES: Classes of the steps
:vartype STEP_CLASSES: list[AbstractStep]
"""
model_class = hm_models.HomologyModelingInput
importSeqAsRefRequested = QtCore.pyqtSignal()
findHomologsRequested = QtCore.pyqtSignal()
importHomologsRequested = QtCore.pyqtSignal(bool)
alignSeqsRequested = QtCore.pyqtSignal()
removeLigandConstraintsRequested = QtCore.pyqtSignal()
removeResidueConstraintsRequested = QtCore.pyqtSignal()
ligandDialogClosed = QtCore.pyqtSignal()
STEP_CLASSES = []
SINGLE_TEMPLATE = False
INSTRUCTION_TEXT = (
'Choose links below as needed to prepare for model creation.\n'
'Click the "info" icon to learn more about a given step.')
[docs] def initSetUp(self):
super().initSetUp()
if self.INSTRUCTION_TEXT:
self._instruction_lbl = QtWidgets.QLabel(self.INSTRUCTION_TEXT)
self._instruction_lbl.setObjectName("instruction_lbl")
step_list = []
for cls in self.STEP_CLASSES:
step = cls()
step.connectToWorkflow(self)
step_list.append(step)
self._steps = step_list
[docs] def initLayOut(self):
super().initLayOut()
layout = self.main_layout
layout.setContentsMargins(6, 6, 6, 6)
if self.INSTRUCTION_TEXT:
layout.addWidget(self._instruction_lbl)
for step in self._steps:
layout.addWidget(step)
layout.addStretch()
[docs] def setModel(self, model):
super().setModel(model)
if model is not None:
self.setEnabled(True)
self.updateState()
[docs] def defineMappings(self):
M = self.model_class
mappings = []
for step in self._steps:
if isinstance(step, SettingsStep):
param = M.settings
else:
param = M
mappings.append((step, param))
return mappings
[docs] def getSignalsAndSlots(self, model):
return [
(model.target_template_pairsChanged, self.updateState),
]
[docs] def updateState(self):
"""
Updates the state of the workflow.
"""
self._disableUnreachableSteps()
self._enableReachableSteps(self.model.target_template_pairs)
def _disableUnreachableSteps(self):
"""
Disable all but the first step and any SettingsStep
"""
for idx, step in enumerate(self._steps):
enable = (idx == 0) or isinstance(step, SettingsStep)
step.setEnabled(enable)
def _enableReachableSteps(self, target_template_pairs):
"""
Enables the correct steps based on the target-template pairs.
:param target_template_pairs: List of objects representing a
(target sequence, template sequence) pair
:type target_template_pairs: list[hm_models.TargetTemplatePair]
"""
n_pairs = len(target_template_pairs)
if n_pairs == 0:
return
first_pair = target_template_pairs[0]
target_seq = first_pair.target_seq
template_seq = first_pair.template_seq
self.template_step.setEnabled(target_seq is not None)
if self.ligands_step is not None:
# Ligand settings handle both ligands and waters, so enable if
# either are present
self.ligands_step.setEnabled(template_seq is not None and
any(pair.ligands or pair.has_waters
for pair in target_template_pairs))
has_one_template = target_seq is not None and template_seq is not None
if self.SINGLE_TEMPLATE:
align_enabled = has_one_template
elif n_pairs > 1:
second_pair = target_template_pairs[1]
second_template = second_pair.template_seq
align_enabled = has_one_template and second_template is not None
else:
align_enabled = False
self.align_step.setEnabled(align_enabled)
[docs] def makeInitialModel(self):
"""
@overrides: MapperMixin
We just use `None` as our initial model as a performance optimization.
Workflows never need their own model in isolation anyways.
"""
return None
class _AbstractOneOneWorkflow(AbstractWorkflow):
"""
Base class for shared logic between OneOneWorkflow and
HeteromultimerOneOneWorkflow
"""
SINGLE_TEMPLATE = True
[docs]class OneOneWorkflow(_AbstractOneOneWorkflow):
STEP_CLASSES = [TargetSequenceStep,
TemplateStructureStep,
AlignSequencesStep,
LigandsStep,
SettingsStep] # yapf: disable
optimizeAlignmentRequested = QtCore.pyqtSignal(str, str, float)
[docs] def initSetUp(self):
super().initSetUp()
self.align_step.optimizeAlignmentRequested.connect(
self.optimizeAlignmentRequested)
[docs]class ManyOneWorkflow(AbstractWorkflow):
STEP_CLASSES = [BatchTargetSequenceStep,
BatchTemplateStructureStep,
OtherTargetsStep,
BatchAlignSequencesStep,
LigandsStep,
SettingsStep] # yapf: disable
SINGLE_TEMPLATE = True
setAsReferenceRequested = QtCore.pyqtSignal()
importSeqRequested = QtCore.pyqtSignal()
[docs] def initSetUp(self):
super().initSetUp()
self.target_step = self._steps[0]
self.other_targets_step = self._steps[2]
self.other_targets_step.importSeqRequested.connect(
self.importSeqRequested)
self.template_step.setAsReferenceRequested.connect(
self.setAsReferenceRequested)
def _enableReachableSteps(self, target_template_pairs):
"""
@overrides: AbstractWorkflow
"""
super()._enableReachableSteps(target_template_pairs)
n_pairs = len(target_template_pairs)
if n_pairs == 0:
return
first_pair = target_template_pairs[0]
target_seq = first_pair.target_seq
template_seq = first_pair.template_seq
self.other_targets_step.setEnabled(target_seq is not None)
self.align_step.setEnabled(template_seq is not None)
[docs]class ChimeraWorkflow(AbstractWorkflow):
STEP_CLASSES = [TargetSequenceStep,
MultipleTemplateStructuresStep,
AlignStructuresSequencesStep,
RegionsStep,
MultipleTemplatesLigandStep,
SettingsStep] # yapf: disable
[docs] def initSetUp(self):
super().initSetUp()
self.alignStructsRequested = self.align_step.alignStructsRequested
self.regions_step = self._steps[3]
self.chimera_pick_btn = self.regions_step.pick_btn
def _enableReachableSteps(self, target_template_pairs):
"""
@overrides: AbstractWorkflow
"""
super()._enableReachableSteps(target_template_pairs)
align_enabled = self.align_step.isEnabled()
self.regions_step.setEnabled(align_enabled)
[docs]class HomomultimerWorkflow(AbstractWorkflow):
STEP_CLASSES = [TargetSequenceStep,
HomomultimerTemplateStructuresStep,
HomomultimerAlignSequencesStep,
LigandsStep,
SettingsStep] # yapf: disable
[docs]class ConsensusWorkflow(AbstractWorkflow):
STEP_CLASSES = [TargetSequenceStep,
MultipleTemplateStructuresStep,
AlignStructuresSequencesStep] # yapf: disable
[docs] def initSetUp(self):
super().initSetUp()
self.alignStructsRequested = self.align_step.alignStructsRequested
self.ligands_step = None
[docs]class HeteromultimerOneOneWorkflow(_AbstractOneOneWorkflow):
STEP_CLASSES = [TargetSequenceStep,
TemplateStructureStep,
BaseAlignSequencesStep,
LigandsStep] # yapf: disable
INSTRUCTION_TEXT = None
[docs]class HeteromultimerChimeraWorkflow(ChimeraWorkflow):
STEP_CLASSES = [TargetSequenceStep,
MultipleTemplateStructuresStep,
AlignStructuresSequencesStep,
RegionsStep,
MultipleTemplatesLigandStep] # yapf: disable
INSTRUCTION_TEXT = None
MODE_WORKFLOWS = {
Mode.ONE_ONE: OneOneWorkflow,
Mode.MANY_ONE: ManyOneWorkflow,
Mode.CHIMERA: ChimeraWorkflow,
Mode.HOMOMULTIMER: HomomultimerWorkflow,
Mode.CONSENSUS: ConsensusWorkflow,
Mode.HETEROMULTIMER: HeteromultimerWidget
}