Source code for schrodinger.application.msv.gui.gui_models

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] @classmethod def configureParam(cls): """ @overrides: parameters.CompoundParam """ super().configureParam() cls.setReference(cls.pairwise.set_constraints, cls.pairwise_ss.set_constraints)
[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 configureParam(cls): """ @overrides: parameters.CompoundParam """ super().configureParam() cls.setReference(cls.binding_site_distance, cls.align_settings.binding_site.binding_site_cutoff)
[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] @classmethod def configureParam(cls): """ @overrides: parameters.CompoundParam """ super().configureParam() cls.setReference(cls.current_page.homology_modeling_input.settings, cls.heteromultimer_settings.settings)
[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 Format(enum.Enum): PNG = "PNG Image (*.png)" PDF = "PDF Image (*.pdf)"
[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"
[docs] class Format(enum.Enum): FASTA = fileutils.get_name_filter( {'FASTA': [fileutils.SeqFormat.fasta]})[0] ALN = fileutils.get_name_filter( {'CLUSTAL': [fileutils.SeqFormat.clustal]})[0] CSV = fileutils.get_name_filter( {'Comma Separated Values': [fileutils.SeqFormat.csv]})[0] # TODO : MSV-3807 Move this hardcoded value to a common file. SEQD = "Sequence + Data (*.seqd)"
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
[docs] def onFormatChanged(self): self.enable_non_format_options = self.format not in (self.Format.ALN, self.Format.CSV) self.enable_export_descriptors = self.format in (self.Format.CSV, self.Format.SEQD) self.enable_create_multiple_file = self.format is not self.Format.SEQD