import copy
import enum
import re
from collections import namedtuple
from functools import partial
from typing import List
from typing import Set
import inflect
import itertools
from schrodinger.application.msv import command
from schrodinger.application.msv.gui import color
from schrodinger.application.msv.gui import gui_alignment
from schrodinger.application.msv.gui import viewconstants
from schrodinger.application.msv.gui.homology_modeling import hm_models
from schrodinger.application.msv.gui.picking import PickMode
from schrodinger.infra import util
from schrodinger.models import diffy
from schrodinger.models import json
from schrodinger.models import mappers
from schrodinger.models import parameters
from schrodinger.protein import annotation
from schrodinger.protein.properties import SequenceProperty
from schrodinger.protein.tasks import blast
from schrodinger.Qt import QtCore
from schrodinger.ui.qt.appframework2 import settings
from schrodinger.utils import fileutils
from schrodinger.utils import scollections
RES_PROP_ANNOS = annotation.ProteinSequenceAnnotations.RES_PROPENSITY_ANNOTATIONS
ALN_ANNO_TYPES = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
SEQ_ANNO_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES
[docs]class PairwiseAlignSettingsModel(parameters.CompoundParam):
set_constraints: bool = False
lock_gaps: bool = False
superimpose_after: bool = False
sub_matrix: str = "BLOSUM62"
gap_open_penalty: float = 10.0
gap_extend_penalty: float = 1.0
prevent_ss_gaps: bool = False
penalize_end_gaps: bool = False
[docs] @json.adapter(version=48002)
def adapter48002(cls, json_dict):
# These were moved to PairwiseSSAlignSettingsModel
json_dict.pop('use_ss_prediction')
json_dict.pop('use_gpcr_aln')
return json_dict
[docs]class PairwiseSSAlignSettingsModel(parameters.CompoundParam):
set_constraints: bool = False
use_gpcr_aln: bool = False
[docs]class MultipleAlignSettingsModel(parameters.CompoundParam):
find_globally_conserved: bool = False
superimpose_after: bool = False
aln_algorithm: viewconstants.MultAlnAlgorithm = viewconstants.MultAlnAlgorithm.Muscle
gap_open_penalty: float = 10.0
gap_extend_penalty: float = 0.2
[docs]class ResidueNumberAlignSettingsModel(parameters.CompoundParam):
superimpose_after: bool = False
[docs]class ChainName(parameters.CompoundParam):
seq_chain: tuple
reference = parameters.StringParam()
[docs]class ProteinStructureAlignSettingsModel(parameters.CompoundParam):
force: bool = False
align_seqs: bool = False
seq_represents: viewconstants.StructAlnSequenceRepresents = \
viewconstants.StructAlnSequenceRepresents.SingleChain
# In combined chain mode, a sequence must represent the entire entry
seq_can_represent_chain: bool = True
# Whether seq_represents should be toggled back to SingleChain when
# seq_can_represent_chain is changed back to True
seq_represented_chain: bool = True
align_transforms: viewconstants.StructAlnTransform
force: bool = False
map_seqs: bool = False
ref_asl: str
other_asl: str
ref_asl_mode: viewconstants.StructAlnRefASLMode
other_define_asl: bool = False
chain_name = parameters.ParamListParam(item_class=ChainName)
map_seqs_enable: bool = False
available_ref_chains: List
[docs] def initConcrete(self):
self.ref_asl_modeChanged.connect(self._updateRefASL)
self.ref_aslChanged.connect(self._syncASL)
self.other_define_aslChanged.connect(self._syncASL)
self.seq_can_represent_chainChanged.connect(
self._onSeqCanRepresentChainChanged)
[docs] @classmethod
def fromJsonImplementation(cls, json_dict):
# Make sure that seq_represented_chain gets restored after
# seq_can_represent_chain and seq_represents, since changing those
# values can overwrite seq_represented_chain
seq_represented_chain = json_dict.pop("seq_represented_chain", True)
model = super().fromJsonImplementation(json_dict)
model.seq_represented_chain = seq_represented_chain
return model
[docs] def initializeValue(self):
self._updateRefASL()
@QtCore.pyqtSlot()
def _updateRefASL(self):
if self.ref_asl_mode is viewconstants.StructAlnRefASLMode.All:
self.ref_asl = "all"
@QtCore.pyqtSlot()
def _syncASL(self):
if not self.other_define_asl:
self.other_asl = self.ref_asl
def _onSeqCanRepresentChainChanged(self):
SeqRep = viewconstants.StructAlnSequenceRepresents
if self.seq_can_represent_chain:
if self.seq_represented_chain:
self.seq_represents = SeqRep.SingleChain
else:
self.seq_represented_chain = (
self.seq_represents == SeqRep.SingleChain)
if self.seq_represented_chain:
self.seq_represents = SeqRep.EntireEntry
[docs] def isAlignmentMapValid(self):
selected_chains = {row.reference for row in self.chain_name}
valid_aln_map = len(self.available_ref_chains) == len(selected_chains)
return valid_aln_map
[docs] def validateAlignmentMap(self):
"""
Validate the Alignment chain map,when required.
:return: Whether the map is valid or not
:rtype: bool
"""
validation_required = (
self.map_seqs and (self.align_transforms
== viewconstants.StructAlnTransform.Existing) and
(self.seq_represents
== viewconstants.StructAlnSequenceRepresents.SingleChain))
if validation_required:
valid_aln_map = self.isAlignmentMapValid()
return valid_aln_map
return True
[docs] def getUnusedChainWarningText(self):
"""
Generate a warning message about reference chains that are not used
in the alignment map.
:return: Warning message to display
:rtype: str
"""
selected_ref_chains = {row.reference for row in self.chain_name}
unused_ref_chains = set(self.available_ref_chains) - selected_ref_chains
if not selected_ref_chains:
return ''
inflect_engine = inflect.engine()
text = inflect_engine.plural("chain", len(unused_ref_chains))
warning_text = f"Invalid mapping: {','.join(unused_ref_chains)} {text} not in use"
return warning_text
[docs] def updateMapData(self, aln):
"""
Update the params that are associated with chain mapping data in the
Table and enable/disable the mapping options depending on input seqs.
:param aln :Protein Sequence alignment
:type aln : msv.gui.gui_alignment.GuiProteinAlignment
"""
input_seqs = aln.getSelectedSequences()
if not input_seqs:
input_seqs = aln
ref_seq = aln.getReferenceSeq()
self.chain_name.clear()
ref_chains = other_chains = []
if ref_seq is None or not ref_seq.hasStructure():
self.available_ref_chains.clear()
self.map_seqs = False
self._updateMapSeqsEnable(ref_chains, other_chains)
return
ref_eid = ref_seq.entry_id
ref_chains = {ref_seq.structure_chain}
for seq in input_seqs:
if not seq.hasStructure():
continue
if seq.entry_id == ref_eid:
ref_chains.add(seq.structure_chain)
else:
chain = seq.structure_chain
other_chains.append((seq.name, chain, seq.entry_id))
ref_chains = sorted(ref_chains)
for other_chain, ref_chain in zip(other_chains,
itertools.cycle(ref_chains)):
other_chain_name = other_chain[1]
if other_chain_name in ref_chains:
ref_chain = other_chain_name
_chain = ChainName(seq_chain=other_chain, reference=ref_chain)
self.chain_name.append(_chain)
self.available_ref_chains = ref_chains
self._updateMapSeqsEnable(ref_chains, other_chains)
def _updateMapSeqsEnable(self, ref_chains, other_chains):
"""
Disable/Enable the 'map_seqs_cb' depending on the input sequences.
The mapping options should be disabled when at least 2 chains from the
reference entry are not used in the map or when more than one chain
from non-reference sequence is used.
:param ref_chains: reference chains used in the map
:type ref_chains: list(str)
:param other_chains: non-reference chains used in the map.
:type other_chains: tuple(seq.name,seq.chain)
"""
enable = True
unique_other_entries = {chain[2] for chain in other_chains}
if (len(ref_chains) < 2 or
len(unique_other_entries) != len(other_chains) or
not other_chains or len(other_chains) < len(ref_chains)):
enable = False
self.map_seqs_enable = enable
self.map_seqs = False
[docs]class SuperimposeAlignSettingsModel(parameters.CompoundParam):
align_sel_res_only: bool = False
[docs]class BindingSiteAlignSettingsModel(parameters.CompoundParam):
binding_site_cutoff: annotation.BindingSiteDistance = \
annotation.BindingSiteDistance.d5
align_seqs: bool = False
align_sel_res_only: bool = False
mapping_dist: annotation.BindingSiteDistance = \
annotation.BindingSiteDistance.d5
previously_aligned: bool = False
[docs]class AlignSettingsModel(parameters.CompoundParam):
align_only_selected_seqs: bool = True
align_type: viewconstants.AlignType
seq_align_mode: viewconstants.SeqAlnMode
struct_align_mode: viewconstants.StructAlnMode
pairwise: PairwiseAlignSettingsModel
pairwise_ss: PairwiseSSAlignSettingsModel
multiple: MultipleAlignSettingsModel
residue_number: ResidueNumberAlignSettingsModel
protein_structure: ProteinStructureAlignSettingsModel
superimpose: SuperimposeAlignSettingsModel
binding_site: BindingSiteAlignSettingsModel
[docs] def getAlignMode(self):
if self.align_type is viewconstants.AlignType.Sequence:
return self.seq_align_mode
elif self.align_type is viewconstants.AlignType.Structure:
return self.struct_align_mode
[docs]class OptionsModel(parameters.CompoundParam):
##################
# Custom signals
##################
all_visible_annotationsChanged = QtCore.pyqtSignal()
##################
# Params
##################
pick_mode: PickMode = None
seq_filter: str
seq_filter_enabled: bool
auto_align: bool
align_settings: AlignSettingsModel
blast_local_only: bool = True
# View Style Dialog Params
compute_for_columns: viewconstants.ColumnMode
font_size: int = 14
identity_display: viewconstants.IdentityDisplayMode
include_gaps: bool = False
res_format: viewconstants.ResidueFormat
show_conservation_col: bool = False
show_identity_col: bool = True
show_score_col: bool = False
show_similarity_col: bool = False
wrap_sequences: bool = True
# Color Dialog Params
average_in_cols: bool = False
colors_enabled: bool = True
seq_color_scheme: color.AbstractRowColorScheme = \
color.SideChainChemistryScheme()
custom_color_scheme: color.AbstractRowColorScheme = None
color_by_aln: viewconstants.ColorByAln
weight_by_quality: bool = False
ws_color_sync: bool = False
ws_color_all_atoms: bool = False
# Quick Annotations Dialog Params
annotations_enabled: bool = False
antibody_cdr: bool = False
antibody_cdr_scheme: annotation.AntibodyCDRScheme = annotation.DEFAULT_ANTIBODY_SCHEME
kinase_features: bool = False
kinase_features_enabled: bool = False
binding_site_distance: annotation.BindingSiteDistance = \
annotation.BindingSiteDistance.d5
group_by: viewconstants.GroupBy
residue_propensity_enabled: bool = False
residue_propensity_annotations: Set[SEQ_ANNO_TYPES]
sequence_annotations: Set[SEQ_ANNO_TYPES]
alignment_annotations: Set[ALN_ANNO_TYPES]
predicted_annotations: Set[SEQ_ANNO_TYPES]
annotation_spacer_enabled: bool = True
sequence_properties: List[SequenceProperty]
show_hm_ligand_constraints: bool = False
show_hm_proximity_constraints: bool = False
@property
def all_visible_annotations(self):
# The ruler should always be enabled, so we just add it in here.
visible = {ALN_ANNO_TYPES.indices}
if self.annotations_enabled:
visible.update(self.alignment_annotations,
self.sequence_annotations,
self.predicted_annotations)
if self.residue_propensity_enabled:
visible.update(self.residue_propensity_annotations)
if self.kinase_features_enabled:
visible.add(SEQ_ANNO_TYPES.kinase_features)
if self.pick_mode is PickMode.Pairwise:
visible.add(SEQ_ANNO_TYPES.pairwise_constraints)
if (self.show_hm_ligand_constraints or
self.pick_mode is PickMode.HMBindingSite):
visible.add(SEQ_ANNO_TYPES.binding_sites)
if (self.show_hm_proximity_constraints or
self.pick_mode is PickMode.HMProximity):
visible.add(SEQ_ANNO_TYPES.proximity_constraints)
return frozenset(visible)
##################
# Methods
##################
[docs] def initConcrete(self):
"""
@overrides: parameters.CompoundParam
"""
self._restore_annotations_enabled = None
self.residue_propensity_enabledChanged.connect(
self.all_visible_annotationsChanged)
self.sequence_annotationsChanged.connect(
self.all_visible_annotationsChanged)
self.alignment_annotationsChanged.connect(
self.all_visible_annotationsChanged)
self.annotations_enabledChanged.connect(
self.all_visible_annotationsChanged)
self.predicted_annotationsChanged.connect(
self.all_visible_annotationsChanged)
self.show_hm_ligand_constraintsChanged.connect(
self.all_visible_annotationsChanged)
self.show_hm_proximity_constraintsChanged.connect(
self.all_visible_annotationsChanged)
self.kinase_features_enabledChanged.connect(
self.all_visible_annotationsChanged)
self.annotations_enabledChanged.connect(
self._onAnnotationsEnabledChanged)
self.binding_site_distanceChanged.connect(self._enableBindingSiteAnno)
self.residue_propensity_annotationsChanged.connect(
self._onResiduePropensityAnnotationsChanged)
self.pick_modeChanged.connect(self._onPickModeChanged)
self.seq_filter_enabledChanged.connect(self._updateSeqFilter)
[docs] @classmethod
def getJsonBlacklist(cls):
"""
@overrides: parameters.CompoundParam
"""
return [
# Pick mode and seq_filter are state that shouldn't be retained
# across sessions
cls.pick_mode,
cls.seq_filter,
cls.seq_filter_enabled,
# Server settings are stored using preferences
cls.blast_local_only,
]
[docs] def toJsonImplementation(self):
# See parent class for method documentation
json_dict = super().toJsonImplementation()
# If the custom color scheme is the currently selected color scheme,
# don't bother to store it twice
if self.seq_color_scheme.custom:
assert self.seq_color_scheme is self.custom_color_scheme
json_dict["custom_color_scheme"] = None
return json_dict
[docs] @classmethod
def fromJsonImplementation(cls, json_dict):
opt_model = super().fromJsonImplementation(json_dict)
# If the custom color scheme was the currently selected color scheme,
# make sure that seq_color_scheme and custom_color_scheme point to the
# same object
if opt_model.seq_color_scheme.custom:
opt_model.custom_color_scheme = opt_model.seq_color_scheme
return opt_model
[docs] @json.adapter(version=48003)
def adapter48003(cls, json_dict):
color_scheme_name = json_dict["seq_color_scheme"]
json_dict["seq_color_scheme"] = {
"name": color_scheme_name,
"custom": False
}
json_dict["custom_color_scheme"] = None
return json_dict
[docs] @json.adapter(version=48007)
def adapter48007(cls, json_dict):
json_dict["domains"] = False
return json_dict
[docs] @json.adapter(version=48009)
def adapter48009(cls, json_dict):
json_dict.pop("domains")
return json_dict
[docs] @json.adapter(version=49001)
def adapter49001(cls, json_dict):
json_dict.pop('consensus_freq')
json_dict.pop('rescode')
return json_dict
@QtCore.pyqtSlot()
def _enableBindingSiteAnno(self):
self.sequence_annotations.add(SEQ_ANNO_TYPES.binding_sites)
@QtCore.pyqtSlot(object)
def _onResiduePropensityAnnotationsChanged(self, res_prop_annos):
if self.residue_propensity_enabled:
self.all_visible_annotationsChanged.emit()
@QtCore.pyqtSlot(object)
def _onPickModeChanged(self, pick_mode):
if (self.align_settings.pairwise.set_constraints and
pick_mode is not PickMode.Pairwise):
self.align_settings.pairwise.set_constraints = False
if pick_mode is not None:
# Disabling set_constraints will turn off picking, so we need to set
# pick_mode again
# This assignment will call this slot but not this branch
self.pick_mode = pick_mode
return
if pick_mode:
prev_annotations_enabled = self.annotations_enabled
self.annotations_enabled = False
# Store `_restore_annotations_enabled` after changing
# `annotations_enabled` because changing `annotations_enabled` clears
# `_restore_annotations_enabled`
self._restore_annotations_enabled = prev_annotations_enabled
elif self._restore_annotations_enabled is not None:
self.annotations_enabled = self._restore_annotations_enabled
self.all_visible_annotationsChanged.emit()
self.annotation_spacer_enabled = pick_mode is not PickMode.Pairwise
@QtCore.pyqtSlot(object)
@QtCore.pyqtSlot(bool)
def _onAnnotationsEnabledChanged(self, _):
self._restore_annotations_enabled = None
@QtCore.pyqtSlot(object)
@QtCore.pyqtSlot(bool)
def _updateSeqFilter(self, seq_filter_enabled):
"""
When disabling seq filtering, automatically clear the query text
"""
if not seq_filter_enabled:
self.seq_filter = ""
[docs]class AlignmentSignals(gui_alignment.AlignmentSignals):
@property
def aln(self):
return self.parent().aln
[docs]class PageModel(parameters.CompoundParam):
alnChanged = QtCore.pyqtSignal(object)
split_chain_viewChanged = QtCore.pyqtSignal(object)
_COMBINED_ALN_JSON_KEY = "combined_aln_data"
split_aln: gui_alignment.GuiProteinAlignment
options: OptionsModel
_split_chain_view: bool = True
title: str = "View"
is_workspace: bool = False
menu_statuses: MenuEnabledModel
blast_task: blast.BlastTask
homology_modeling_input: hm_models.HomologyModelingInput
_is_null_page: bool = False
undo_stack = parameters.NonParamAttribute()
aln_signals = parameters.NonParamAttribute()
[docs] def __init__(self, *args, split_chain_view=None, **kwargs):
"""
Any of the params listed above can be provided as arguments to
`__init__`. (`_split_chain_view` may be given as `split_chain_view`.)
Additional arguments:
:param undo_stack: The undo stack. (Toggling between split-chain view
and combined-chain view is undoable.)
:type undo_stack: schrodinger.application.msv.command.UndoStack
"""
if split_chain_view is not None:
kwargs["_split_chain_view"] = split_chain_view
super().__init__(*args, **kwargs)
[docs] def initConcrete(self, undo_stack=None):
self._combined_aln = None
# This class maintains a separate AlignmentSignals object that always
# mimics the AlignmentSignals object of the current alignment (i.e. the
# split-chain or combined-chain alignment depending on the current
# setting of split_chain_view). That way, clients can connect to
# signals from aln_signals and not have to worry about disconnecting and
# reconnecting when split_chain_view changes.
self.aln_signals = AlignmentSignals(self)
self._aln_connected_to_aln_signals = None
self.setUndoStack(undo_stack)
self.split_alnChanged.connect(self._updateAlnSignals)
self.split_alnChanged.connect(self.alnChanged)
self._split_chain_viewChanged.connect(self._onSplitChainViewChanged)
self._split_chain_viewChanged.connect(self.split_chain_viewChanged)
if not self.split_chain_view:
self._combined_aln = \
gui_alignment.GuiCombinedChainProteinAlignment(
self.split_aln)
self._updateAlnSignals()
self.menu_statuses.can_sort_by_chain = self.split_chain_view
if self.is_workspace:
self.blast_task.input.settings.download_structures = True
aln_signals = self.aln_signals
structure_alignment_signals = [
aln_signals.sequencesRemoved,
aln_signals.sequencesInserted,
aln_signals.sequenceNameChanged,
aln_signals.sequencesReordered,
aln_signals.seqSelectionChanged,
aln_signals.alignmentCleared,
]
for signal in structure_alignment_signals:
signal.connect(self._updateChainMapData)
def _updateChainMapData(self):
aln = self.split_aln
self.options.align_settings.protein_structure.updateMapData(aln)
[docs] @classmethod
def configureParam(cls):
"""
@overrides: parameters.CompoundParam
"""
hm_settings = cls.homology_modeling_input.settings
cls.setReference(cls.options.show_hm_ligand_constraints,
hm_settings.ligand_dlg_model.constrain_ligand)
cls.setReference(cls.options.show_hm_proximity_constraints,
hm_settings.set_constraints)
[docs] @classmethod
def getJsonBlacklist(cls):
"""
@overrides: parameters.CompoundParam
"""
return [cls.homology_modeling_input]
def __deepcopy__(self, memo):
dup = super().__deepcopy__(memo)
if not self.split_chain_view:
# deepcopy will automatically set dup.split_aln and
# dup._combined_aln._split_undoable_aln to the same object
dup._combined_aln = copy.deepcopy(self._combined_aln, memo)
# The deepcopy modifies private params directly, so we need to make sure
# that aln_signals is connected to the correct alignment
dup._updateAlnSignals()
return dup
[docs] def isNullPage(self):
return self._is_null_page
[docs] def getShownAnnIndexes(self, seq, ann):
if ann not in self.options.all_visible_annotations:
return None
# TODO MSV-3293 allow hiding annotation rows from specific sequences
# TODO MSV-3294 allow deleting specific multi-value annotation rows
return tuple(range(seq.getNumAnnValues(ann)))
[docs] def toJsonImplementation(self):
json_dict = super().toJsonImplementation()
if not self.split_chain_view:
combined_aln_json_data = \
self._combined_aln.jsonDataWithoutSplitAln()
json_dict[self._COMBINED_ALN_JSON_KEY] = combined_aln_json_data
return json_dict
[docs] @classmethod
def fromJsonImplementation(cls, json_dict):
combined_aln_json_data = json_dict.pop(cls._COMBINED_ALN_JSON_KEY, None)
page_model = super().fromJsonImplementation(json_dict)
if not page_model.split_chain_view:
GCCPA = gui_alignment.GuiCombinedChainProteinAlignment
page_model._combined_aln = GCCPA.fromJsonAndSplitAln(
page_model.split_aln, combined_aln_json_data)
# super().fromJsonImplementation modifies private params directly, so we
# need to make sure that aln_signals is connected to the correct
# alignment
page_model._updateAlnSignals()
return page_model
@property
def aln(self):
"""
The split-chain or combined-chain alignment, based on the current
`OptionsModel.split_chain_view` setting. Note that this value should
always be set using a split-chain alignment.
Assigning a new split-chain alignment to a page that is already in
`MsvGuiModel.pages` will not work reliably. To add a page with a
pre-existing new alignment, use `MsvGuiModel.addViewPage(aln=new_aln)`.
:rtype: gui_alignment.GuiProteinAlignment or
gui_alignment.GuiCombinedChainProteinAlignment
"""
if self.split_chain_view:
return self.split_aln
else:
return self._combined_aln
@aln.setter
def aln(self, val):
if not isinstance(val, gui_alignment.GuiProteinAlignment):
raise TypeError(f"Invalid type for aln: {type(val)}")
self.split_aln = val
self.split_aln.setSeqFilterEnabled(self.options.seq_filter_enabled)
self.split_aln.setSeqFilterQuery(self.options.seq_filter)
if not self.split_chain_view:
self._combined_aln = \
gui_alignment.GuiCombinedChainProteinAlignment(
self.split_aln)
self._updateAlnSignals()
self.alnChanged.emit(self.aln)
[docs] def isSplitChainViewDefault(self):
"""
Check whether split chain view is set to its default.
:return: Whether split_chain_view is default
:rtype: bool
"""
Cls = type(self)
default = Cls._split_chain_view.defaultValue()
return self._split_chain_view == default
@property
def split_chain_view(self):
"""
Whether the current view is split-chain (True) or combined-chain
(False). Note that toggling this value is undoable and will clear all
residue anchors.
:rtype: bool
"""
return self._split_chain_view
@split_chain_view.setter
def split_chain_view(self, value):
if value == self._split_chain_view:
return
aln = self.aln
clear_text = []
clear_funcs = []
if aln.getAnchoredResidues():
clear_text.append("Residue Anchors")
clear_funcs.append(aln.clearAnchors)
if aln.hasAlnSets():
clear_text.append("Alignment Sets")
clear_funcs.append(lambda: aln.removeSeqsFromAlnSet(aln))
if clear_text:
desc = f"Toggle Split Chain View and Clear {' and '.join(clear_text)}"
with command.compress_command(self.undo_stack, desc):
for func in clear_funcs:
func()
self._setSplitChainViewCommand(value)
else:
self._setSplitChainViewCommand(value)
@command.do_command
def _setSplitChainViewCommand(self, value):
# If we're undoing enabling split_chain_view, we need to restore the
# same combined-chain instance. Otherwise, further undos won't work
# since they'll get undone on the wrong instance.
if value:
cur_combined_align = self._combined_aln
else:
cur_combined_align = \
gui_alignment.GuiCombinedChainProteinAlignment(
self.split_aln)
def enable():
self._combined_aln = None
self._split_chain_view = True
struc_aln_settings = self.options.align_settings.protein_structure
struc_aln_settings.seq_can_represent_chain = True
self.alnChanged.emit(self.aln)
def disable():
self._combined_aln = cur_combined_align
self._split_chain_view = False
struc_aln_settings = self.options.align_settings.protein_structure
struc_aln_settings.seq_can_represent_chain = False
self.alnChanged.emit(self.aln)
desc = "Toggle Split Chain"
if value:
return enable, disable, desc
else:
return disable, enable, desc
@QtCore.pyqtSlot(object)
@QtCore.pyqtSlot(bool)
def _onSplitChainViewChanged(self, split_chain_view):
"""
Respond to changes in split chain view, since some items aren't
appropriate or implemented yet for combined chain view.
This includes aligning methods that involve use of the structure when in
combined-chain mode, as those aligning methods don't yet work with
combined-chain mode. (See MSV-2362.)
:param split_chain_view: Whether split-chain view is enabled.
:type split_chain_view: bool
"""
self.menu_statuses.can_sort_by_chain = split_chain_view
self.menu_statuses.can_select_antibody_chain = split_chain_view
self.menu_statuses.can_set_constraints = split_chain_view
self.menu_statuses.can_renumber_residues = split_chain_view
if not split_chain_view:
self.options.align_settings.pairwise.set_constraints = False
self._updateAlnSignals()
[docs] def setUndoStack(self, undo_stack):
self.undo_stack = undo_stack
self.split_aln.setUndoStack(undo_stack)
[docs] def regenerateCombinedChainAlignment(self):
"""
If `split_chain_view` is True, do nothing. If False, recreate the
combined-chain alignment using the split-chain alignment. This method
must be called whenever the split-chain alignment is modified directly
while in combined-chain mode. If `split_chain_view` is False, this
method will delete all residue anchors and the action cannot be undone.
:note: This method should not typically be necessary. Any modifications
should be made to the combined-chain alignment, which will
automatically update the split-chain alignment as required. This
method should only be called when updates must be made directly to
the split-chain alignment.
"""
if not self._split_chain_view:
self._combined_aln = \
gui_alignment.GuiCombinedChainProteinAlignment(
self.split_aln)
self._updateAlnSignals()
self.alnChanged.emit(self._combined_aln)
def _updateAlnSignals(self):
"""
Connect the current alignment's `AlignmentSignals` object to this
model's `AlignmentSignals` object and disconnect the previous alignment.
"""
if self.aln is self._aln_connected_to_aln_signals:
return
if self._aln_connected_to_aln_signals is not None:
aln = self._aln_connected_to_aln_signals
for signal, slot in zip(aln.signals.allSignals(),
self.aln_signals.allSignals()):
signal.disconnect(slot)
if self.aln is not None:
# self.aln can be None if this method gets triggered while the
# instance is in an intermediate state (e.g. in the middle of
# being populated during a deepcopy)
for signal, slot in zip(self.aln.signals.allSignals(),
self.aln_signals.allSignals()):
signal.connect(slot)
self._aln_connected_to_aln_signals = self.aln
[docs]class NullPage(PageModel):
_is_null_page: bool = True
[docs]class HeteromultimerSettings(parameters.CompoundParam):
"""
Settings for heteromultimer homology modeling
"""
selected_pages: List[PageModel]
settings: hm_models.HomologyModelingSettings
ready: bool
[docs] def initConcrete(self):
super().initConcrete()
self.selected_pagesChanged.connect(self._updateReady)
def _updateReady(self):
self.ready = len(self.selected_pages) >= 2 and all(
page.homology_modeling_input.ready for page in self.selected_pages)
[docs]class MsvGuiModel(parameters.CompoundParam):
"""
The model for the entire MSV2.
"""
pages: List[PageModel]
current_page: PageModel = NullPage()
light_mode: bool = False
blast_local_only: bool = True
sequence_local_only: bool = True
pdb_local_only: bool = True
auto_align: bool = False
edit_mode: bool = False
auto_save: viewconstants.Autosave = viewconstants.Autosave.OnlyAfterEdit
align_settings: AlignSettingsModel
heteromultimer_settings: HeteromultimerSettings
custom_color_scheme: color.AbstractRowColorScheme = None
undo_stack = parameters.NonParamAttribute()
_hm_launcher_task: hm_models.HMLauncherTask
_emittingMutated = util.flag_context_manager("_emitting_mutated")
[docs] def initConcrete(self):
"""
@overrides: parameters.CompoundParam
"""
super().initConcrete()
self.undo_stack = None
self._page_option_mapper = None
self._emitting_mutated = False
self.pages.mutated.connect(self.onPagesMutated)
self._updateUserSettings()
for param in _USER_SETTINGS.keys():
signal = param.getParamSignal(self)
slot = partial(self._storeUserSetting, param)
signal.connect(slot)
self.current_pageReplaced.connect(self._onCurrentPageReplaced)
[docs] @json.adapter(version=49003)
def adapter49003(cls, json_dict):
# Switched from storing entire task to just storing input
hm_task = json_dict.pop('homology_modeling_task')
json_dict['homology_modeling_input'] = hm_task['input']
return json_dict
[docs] @json.adapter(version=50002)
def adapter50002(cls, json_dict):
# Moved homology modeling input from MsvGuiModel to PageModel
json_dict.pop('homology_modeling_input')
return json_dict
[docs] @classmethod
def getJsonBlacklist(cls):
"""
@overrides: parameters.CompoundParam
"""
blacklist = list(_USER_SETTINGS.keys())
return blacklist + [
cls.heteromultimer_settings,
cls.align_settings,
cls._hm_launcher_task,
cls.edit_mode,
]
[docs] @util.skip_if("_emitting_mutated")
@QtCore.pyqtSlot(object, object)
def onPagesMutated(self, new_pages, old_pages):
"""
Synchronize settings that should be global with all pages
"""
new_current = self._getNewCurrentPage(new_pages, old_pages)
if new_current is not None:
with self._emittingMutated():
# Re-emit the mutated signal because replacing `current_page`
# will disconnect any slots of mutated in getSignalsAndSlots
self.pages.mutated.emit(new_pages, old_pages)
self.current_page = new_current
mapper = mappers.TargetParamMapper()
for page in self.pages:
for guimodel_param, page_param in _GLOBAL_PARAMS:
auto_tgt = mappers.ParamTargetSpec(page, page_param)
mapper.addMapping(auto_tgt, guimodel_param)
if self._page_option_mapper is not None:
self._page_option_mapper.setModel(None)
self._page_option_mapper = mapper
self._page_option_mapper.setModel(self)
aln = self.current_page.split_aln
self.current_page.options.align_settings.protein_structure.updateMapData(
aln)
def _getNewCurrentPage(self, new_pages, old_pages):
if not self.pages:
return NullPage()
if self.current_page.isNullPage():
return self.pages[-1]
added, removed, moved = diffy.get_diff(new_pages, old_pages)
if added:
first_added_page, _ = sorted(added, key=lambda x: x[1])[0]
return first_added_page
elif removed:
for removed_page, removed_idx in removed:
if self.current_page == removed_page:
break
else:
# Current page was not removed, don't change
return None
# Use the same index or the last page
new_idx = min(removed_idx, len(self.pages) - 1)
new_page = self.pages[new_idx]
return new_page
return None
[docs] def appendSavedPages(self, new_pages):
"""
Load pages that were saved to file
"""
# Set the global values from the first new page on the model
page = new_pages[0]
blacklist = scollections.IdSet(self.getJsonBlacklist())
for guimodel_param, page_param in _GLOBAL_PARAMS:
if guimodel_param in blacklist:
continue
page_value = page_param.getParamValue(page)
guimodel_param.setParamValue(self, page_value)
self.pages.extend(new_pages)
[docs] def setUndoStack(self, undo_stack):
"""
Set the undo stack to use. (Toggling between split-chain view and
combined-chain view is undoable.)
:param undo_stack: The undo stack.
:type undo_stack: schrodinger.application.msv.command.UndoStack
"""
self.undo_stack = undo_stack
for page in self.pages:
page.setUndoStack(undo_stack)
[docs] def addViewPage(self, *, aln=None):
"""
Add a view page, i.e. a page that doesn't represent the workspace.
:param aln: An alignment to use for the page
:type aln: gui_alignment.GuiProteinAlignment
:return: The newly created view page
:rtype: PageModel
"""
page_model = PageModel(undo_stack=self.undo_stack)
if aln is not None:
page_model.aln = aln
num_view_tabs = sum(1 for page in self.pages if not page.is_workspace)
page_model.title = f"View {num_view_tabs+1}"
self.pages.append(page_model)
return page_model
[docs] def addWorkspacePage(self, aln):
"""
Add a page representing the workspace. If a workspace page is required,
this method must be called before any view pages are added.
:param aln: The workspace alignment
:type aln: gui_alignment.GuiProteinAlignment
:return: The newly created workspace page
:rtype: PageModel
"""
ws_pm = PageModel(is_workspace=True,
title='Workspace',
undo_stack=self.undo_stack)
ws_pm.aln = aln
ws_pm.menu_statuses.can_delete_tab = False
self.pages.append(ws_pm)
return ws_pm
[docs] def hasWorkspacePage(self):
return self.pages and self.pages[0].is_workspace
[docs] def getWorkspacePage(self):
"""
Return the current workspace page if one exists.
:rtype: PageModel
:raises RuntimeError: If no workspace page exists.
"""
if not self.hasWorkspacePage():
raise RuntimeError("No workspace page present.")
return self.pages[0]
[docs] def getViewPages(self):
if self.hasWorkspacePage():
return self.pages[1:]
else:
return self.pages[:]
[docs] def reset(self, *args, **kwargs):
if args or kwargs:
super().reset(*args, **kwargs)
else:
self.resetPages()
if self.undo_stack is not None:
self.undo_stack.clear()
[docs] def resetPages(self):
pages = self.pages
while pages and not pages[-1].is_workspace:
pages.pop()
self.addViewPage()
self.current_page = self.pages[0]
[docs] def duplicatePage(self, index):
def update_suffix(match):
if match.group(1) is None:
return " Copy"
cur_count = match.group(2)
if cur_count is None:
new_count = 2
else:
new_count = int(cur_count) + 1
return " Copy %i" % new_count
copied_page_model = copy.deepcopy(self.pages[index])
copied_page_model.options.pick_mode = None
original_title = copied_page_model.title
title = re.sub(r"( Copy(?: (\d*))?)?$", update_suffix, original_title)
copied_page_model.title = title
copied_page_model.is_workspace = False
self.pages.append(copied_page_model)
[docs] @classmethod
def fromJsonImplementation(cls, json_dict):
current_page_idx = json_dict.pop('current_page_idx')
ret = super().fromJsonImplementation(json_dict)
if current_page_idx is None:
ret.current_page = NullPage()
else:
ret.current_page = ret.pages[current_page_idx]
return ret
[docs] def toJsonImplementation(self):
"""
Save just the index of the `current_page`. We turn it back into a proper
`PageModel` when we deserialize.
"""
ret = super().toJsonImplementation()
ret['pages'] = [p for p in ret['pages'] if not p.is_workspace]
non_ws_pages = [p for p in self.pages if not p.is_workspace]
if self.current_page.is_workspace:
idx = None
else:
idx = non_ws_pages.index(self.current_page)
ret['current_page_idx'] = idx
ret.pop('current_page')
return ret
[docs] def getAlignmentOfSequence(self, seq):
"""
Returns the alignment that contains `seq` or `None` if none of the
pages' alignments own `seq`. The split-chain alignment will be returned
regardless of the tab's current split chain view setting. If you need
access to the combined-chain alignment, use `getPageInfoForSequence`
instead.
:param seq: The split-chain sequence to find the owner of.
:type seq: sequence.Sequence
:rtype: alignment.ProteinAlignment or None
"""
for page in self.pages:
if seq in page.split_aln:
return page.split_aln
# Sequence not owned by any pages alignment so return None
return None
[docs] def getPageInfoForSequence(self, seq):
"""
Returns information about the page that contains `seq` or `None` if none
of the pages alignments own `seq`.
:param seq: The split-chain sequence to find the owner of.
:type seq: sequence.Sequence
:rtype: SequencePageInfo or None
"""
for page in self.pages:
if seq in page.split_aln:
return SequencePageInfo(seq, page)
# Sequence not owned by any pages alignment so return None
return None
def _storeUserSetting(self, param, value):
"""
When the given param changes to a not-none value, store its value in
the user's persistent settings
"""
if value is None:
# Preferences cannot store None
return
pref_key = _USER_SETTINGS[param]
settings.set_persistent_value(pref_key, value)
def _updateUserSettings(self):
"""
Update the model with the user's persistent settings
"""
for param, pref_key in _USER_SETTINGS.items():
value = settings.get_persistent_value(pref_key, None)
if value is not None:
# Only update for non-missing values
param.setParamValue(self, value)
def _onCurrentPageReplaced(self):
aln = self.current_page.split_aln
self.current_page.options.align_settings.protein_structure.updateMapData(
aln)
# MsvGuiModel subparams that are mapped to each page
_GLOBAL_PARAMS = [
(MsvGuiModel.auto_align, PageModel.options.auto_align),
(MsvGuiModel.align_settings, PageModel.options.align_settings),
(MsvGuiModel.blast_local_only, PageModel.options.blast_local_only),
(MsvGuiModel.custom_color_scheme, PageModel.options.custom_color_scheme),
] # yapf: disable
# MsvGuiModel subparams that are stored as user preferences
_USER_SETTINGS = {
MsvGuiModel.blast_local_only: 'MSV2_BLAST_LOCAL_ONLY',
MsvGuiModel.sequence_local_only: 'MSV2_SEQUENCE_LOCAL_ONLY',
MsvGuiModel.pdb_local_only: 'MSV2_PDB_LOCAL_ONLY',
} # yapf: disable
[docs]class SequencePageInfo(
namedtuple("SequencePageInfo", ("aln", "split_aln", "seq", "split_seq",
"split_chain_view", "chain_offset"))):
"""
Information about the page that a given sequence is on.
:ivar aln: The current alignment for the page. Can be either split-chain or
combined-chain.
:vartype aln: gui_alignment.GuiProteinAlignment or
gui_alignment.GuiCombinedChainProteinAlignment
:ivar split_aln: The split-chain alignment for the page.
:vartype split_aln: gui_alignment.GuiProteinAlignment
:ivar seq: The relevant sequence in `aln`. Can be either a split-chain or
combined-chain sequence.
:vartype seq: sequence.ProteinSequence or
sequence.CombinedChainProteinSequence or sequence.SequenceProxy
:ivar split_seq: The split-chain sequence.
:vartype seq: sequence.ProteinSequence or sequence.SequenceProxy
:ivar split_chain_view: The current split-chain view setting for the page.
Will be True for split-chain and False for combined-chain.
:vartype split_chain_view: bool
:ivar chain_offset: In combined-chain mode, gives the index of the first
residue of `split_seq` in `seq`. In split-chain mode, is 0.
:vartype chain_offset: int
"""
def __new__(cls, split_seq, page):
"""
:param split_seq: The split-chain sequence
:type split_seq: sequence.Sequence
:param page: The page that the sequence is on
:type page: PageModel
"""
if page.split_chain_view:
seq = split_seq
chain_offset = 0
else:
seq = page.aln.combinedSeqForSplitSeq(split_seq)
chain_offset = seq.offsetForChain(split_seq)
return super().__new__(cls, page.aln, page.split_aln, seq, split_seq,
page.split_chain_view, chain_offset)
[docs]class ExportImageModel(parameters.CompoundParam):
"""
Model for the export image dialog. The values of the enums are what gets
shown as text in the file dialog's combo boxes
"""
[docs] class ExportTypes(enum.Enum):
ENTIRE_ALN = "Entire alignment region"
VISIBLE_ALN = "Visible alignment region"
SEQ_LOGO = "Sequence Logo only"
[docs] class Dpi(enum.IntEnum):
SEVENTY_TWO = 72
ONE_HUNDRED_FIFTY = 150
THREE_HUNDRED = 300
SIX_HUNDRED = 600
export_type: ExportTypes
dpi: Dpi = Dpi.ONE_HUNDRED_FIFTY
format: Format
[docs]class ExportSequenceModel(parameters.CompoundParam):
"""
Model for the export sequence dialog.
"""
[docs] class Sequences(enum.Enum):
DISPLAYED = "Displayed Sequences"
ALL = "All Sequences"
SELECTED = "Selected Sequences"
[docs] class Residues(enum.Enum):
ALL = "Full Alignment"
SELECTED = "Selected Blocks"
SplitFileBy = enum.IntEnum('SplitFileBy', 'Chain Structure')
format: Format
which_sequences: Sequences
which_residues: Residues
preserve_indices: bool = False
include_ss_anno: bool = False
include_similarity: bool = False
enable_non_format_options: bool = True
export_descriptors: bool = False
enable_export_descriptors: bool = False
create_multiple_files: bool = False
split_file_by: SplitFileBy
enable_create_multiple_file: bool = True
[docs] def initConcrete(self):
self.formatChanged.connect(self.onFormatChanged)
self.enable_non_format_optionsChanged.connect(self.onEnableChanged)
self._prev_include_ss_anno = self.include_ss_anno
self._prev_include_similarity = self.include_similarity
[docs] def onEnableChanged(self):
if self.enable_non_format_options is True:
self.include_ss_anno = self._prev_include_ss_anno
self.include_similarity = self._prev_include_similarity
else:
self._prev_include_ss_anno = self.include_ss_anno
self._prev_include_similarity = self.include_similarity
self.include_ss_anno = False
self.include_similarity = False