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

import collections
import contextlib
import copy
import itertools
import types
import typing
import weakref
from functools import partial

import inflect

from schrodinger import in_dev_env
from schrodinger import structure
from schrodinger.application.msv import command
from schrodinger.application.msv import utils as msv_utils
from schrodinger.models import json
from schrodinger.protein import alignment
from schrodinger.protein import annotation
from schrodinger.protein import residue
from schrodinger.protein import sequence
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui

_ResidueOutline = collections.namedtuple("_ResidueOutline",
                                         ["start_idx", "end_idx", "color"])


[docs]class StandingSelectionError(RuntimeError): pass
[docs]class AbstractAlignmentSelectionModel(QtCore.QObject): """ A class that manages selection of elements in an undoable alignment. An element can be either a residue or a sequence. Because of limitations with Qt's selection models, we store selection status in our own domain objects instead. This class has an undo stack because selection is undoable in the MSV. :ivar _selection: The current selection state :vartype _selection: set :ivar _old_selection: The selection state from last time selectionChanged was emitted :vartype _old_selection: set :cvar selectionChanged: A signal emitted to notify listeners that selection has changed, with the set of elements that have been selected and the set of elements that have been deselected. Note that this is called on a single shot timer so that if selection is modified multiple times successively (eg by a "clear then select"), only one signal is emitted. :vartype selectionChanged: `QtCore.pyqtSignal` emitting `set()` and `set()` """ selectionChanged = QtCore.pyqtSignal(set, set)
[docs] def __init__(self, aln): """ :param aln: The alignment whose selection state we're tracking :type aln: ProteinAlignment """ super().__init__() self.aln = aln self.undo_stack = None self._selection = set() self._old_selection = set() # We use _emit_selection_changed_timer to avoid unnecessarily emitting # self.selectionChanged self._emit_selection_changed_timer = QtCore.QTimer(self) self._emit_selection_changed_timer.setSingleShot(True) self._emit_selection_changed_timer.setInterval(0) self._emit_selection_changed_timer.timeout.connect( self._emitSelectionChanged) self.aln.signals.sequencesAboutToBeRemoved.connect( self.onSequencesAboutToBeRemoved)
[docs] def onSequencesAboutToBeRemoved(self, start, end): """ When sequences are about to be removed, deselect all the elements contained by those sequences. :param start: Start index of sequences about to be removed. :type start: int :param end: End index of sequences about to be removed. :type end: int """ raise NotImplementedError()
def __deepcopy__(self, memo): """ Do not allow copying of selection models """ raise RuntimeError("{} should not be copied".format(self.__class__))
[docs] def getSelectionIndices(self): """ Return a list of selected element indices. Child classes should reimplement. :return: List of selection element indices :rtype: list """ raise NotImplementedError()
[docs] def setUndoStack(self, undo_stack): """ :param undo_stack: The undo stack to push commands onto :type undo_stack: schrodinger.application.msv.command.UndoStack """ # The undo stack is currently not used but will be necessary when # we implement proper selection undo behavior (MSV-1535). self.undo_stack = undo_stack
[docs] def setSelectionState(self, items, selected): """ Set the selection state of the provided items, ignoring `None` :type items: iterable :param selected: Whether to select or deselect the items :type selected: bool """ if selected: self._selection.update(items) else: self._selection.difference_update(items) self._selection.discard(None) self._emit_selection_changed_timer.start()
[docs] def clearSelection(self): """ Unselect all elements. """ self._selection.clear() self._emit_selection_changed_timer.start()
[docs] def forceSelectionUpdate(self): """ Force the selectionChanged signal to emit immediately rather than waiting for the timer to expire. """ self._emit_selection_changed_timer.stop() self._emitSelectionChanged()
def _emitSelectionChanged(self): """ Emit a selectionChanged with the elements whose selection state has changed since the last time selectionChanged was emitted. Note that selectionChanged might not necessarily be emitted after every single single call that modifies selection. """ newly_selected = set(self._selection.difference(self._old_selection)) newly_deselected = set(self._old_selection.difference(self._selection)) if newly_selected or newly_deselected: self.selectionChanged.emit(newly_selected, newly_deselected) self._old_selection = self._selection.copy()
[docs] def isSelected(self, ele): """ :param ele: The alignment element to determine the selection state of :type res: object :return: whether ele is selected :rtype: bool """ return ele in self._selection
[docs] def getSelection(self): """ :return: A set of currently selected elements :rtype: set """ return set(self._selection)
[docs] def hasSelection(self): """ Whether any items are currently selected. :rtype: bool """ return bool(self._selection)
[docs] @contextlib.contextmanager def suspendSelection(self): """ Inside the context, the selection model is deselected. :param sel_model: The selection model to temporarily deselect :type sel_model: AbstractAlignmentSelectionModel """ raise NotImplementedError
[docs]class ResidueSelectionModel(AbstractAlignmentSelectionModel): # Setting SELECTION_SANITY_CHECK to True will cause setSelectionState and # setCurrentSelectionState to raise ValueError if you attempt to select a # residue that's not in the alignment. This check can slow down selection, # particularly when selecting all residues in the alignment, so we only # apply it for developers. SELECTION_SANITY_CHECK = in_dev_env() _CLEAR_DESC = "Clear Selected Residues"
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._standing_selection = None self.aln.signals.residuesAboutToBeRemoved.connect( self.onResiduesAboutToBeRemoved)
def _checkResidues(self, residues): """ Make sure that all residues are from the associated alignment. :param residues: The residues to check :type residues: Iter(residue.Residue) :raise ValueError: If any residues are not from the correct alignment. """ for res in residues: if res.sequence is None or res.sequence not in self.aln: raise ValueError(f"Can't select {res}. Residues must be " "contained in a sequence to be selectable.") def _checkIfCanSelect(self, residues, selected): if self._standing_selection is not None: raise StandingSelectionError("Cannot set selection state while a" " current selection is present.") if self.SELECTION_SANITY_CHECK and selected: # We may be deselecting residues that have just been removed from # the alignment, so only check residues to be selected. self._checkResidues(residues)
[docs] def selectAll(self, *, _undoable=True): """ Convenience method to select all residues. Skips sanity check for speed. :param bool _undoable: Whether to create an undoable command. Should only be passed by a command implementation. """ all_elements = itertools.chain(*self.aln) orig_sanity = self.SELECTION_SANITY_CHECK self.SELECTION_SANITY_CHECK = False try: self.setSelectionState(all_elements, True, _undoable=_undoable) finally: self.SELECTION_SANITY_CHECK = orig_sanity
[docs] def setSelectionState(self, residues, selected, *, _undoable=True): """ See parent class for additional method documentation. :param bool _undoable: Whether to create an undoable command. Should only be passed by a command implementation or a non-undoable action that takes responsibility for restoring selection. """ residues = set(residues) # in case residues is a generator self._checkIfCanSelect(residues, selected) method = (self._undoableSetSelectionState if _undoable else super().setSelectionState) method(residues, selected)
@staticmethod def _mergeSelectionCommands(d1, d2): """ Command to merge time-adjacent selection commands. """ # Rather than attempt to parse the strings and come up with a combined # description, we throw up our hands and go with this generic message return "Change Residue Selection" @command.do_command(command_id=command.CommandType.SelectResidues, command_class=command.TimeBasedCommand) def _undoableSetSelectionState(self, residues, selected): residues, desc = self._getSelectionResiduesAndDesc(residues, selected) set_selection = super().setSelectionState def redo(): set_selection(residues, selected) def undo(): set_selection(residues, not selected) return redo, undo, desc, self._mergeSelectionCommands def _getSelectionResiduesAndDesc(self, residues, selected): """ Preprocess residues and create undo description for selecting residues. :param residues: Elements to select or deselect. Note: must be a set (not a list or generator expression) :type residues: set(residue.AbstractSequenceElement) :param bool selected: Whether to select or deselect the elements. """ if selected: residues = residues - self._selection else: residues = residues & self._selection select_txt = "Select" if selected else "Deselect" num_res = len(residues) res_txt = inflect.engine().plural("residue", num_res) desc = f"{select_txt} {num_res} {res_txt}" return residues, desc
[docs] def setCurrentSelectionState(self, residues, selected, *, _undoable=True): """ Set residues as selected or deselected in the "current" selection. Note that "current" here means "the portion of the selection that's in the process of being updated," i.e., the selection that's from the mouse click (or click and drag) that we're currently in the middle of. This is equivalent to passing the `QItemSelectionModel::Current | QItemSelectionModel::Clear` flags to `QItemSelectionModel::select`. .. note:: `setSelectionState` must not be called until the current selection has finished. (See `finishCurrentSelection`.) :param residues: The residues to set the current selection to. Any previous current selection will be completely replaced. :type residues: Iter(residue.Residue) :param selected: Whether the specified residues should be selected or deselected. :type selected: bool :param bool _undoable: Whether to create an undoable command. Should only be passed by a command implementation. """ if self._standing_selection is None: # If we're starting a new current selection, save the existing # selection as _standing_selection. The current selection is always # added to or removed from _standing_selection to get _selection. self._standing_selection = self._selection residues = set(residues) if self.SELECTION_SANITY_CHECK: self._checkResidues(residues) new_residues, desc = self._getCurrentSelectionResiduesAndDesc( residues, selected) if new_residues == self._selection: # No selection change is actually occuring, so don't bother to add # anything to the undo stack return if _undoable and self.undo_stack.in_macro is False: self.undo_stack.beginMacro(desc) method = (self._undoableSetCurrentSelectionState if _undoable else self._setCurrentSelectionState) method(new_residues)
def _setCurrentSelectionState(self, new_residues): self._selection = new_residues self._emit_selection_changed_timer.start() @command.do_command(command_id=command.CommandType.SelectResidues, command_class=command.TimeBasedCommand) def _undoableSetCurrentSelectionState(self, new_residues): def redo(): self._setCurrentSelectionState(new_residues) restore = set(self._standing_selection) def undo(): self._setCurrentSelectionState(restore) desc = "Set Current Residue Selection State" return redo, undo, desc, self._mergeSelectionCommands def _getCurrentSelectionResiduesAndDesc(self, residues, selected): if selected: residues = self._standing_selection | residues else: residues = self._standing_selection - residues select = "Select" if selected else "Deselect" desc = f"{select} Residues" return residues, desc
[docs] def finishCurrentSelection(self, *, _undoable=True): """ Finish the "current" selection and permanently merge it into the main selection. If there's no "current" selection, then this method is a no-op, but is still safe to call. See `setCurrentSelectionState` for additional information about the "current" selection. :param bool _undoable: Whether to create an undoable command. Should only be passed by a command implementation. """ self._standing_selection = None if _undoable and self.undo_stack.in_macro is True: self.undo_stack.endMacro()
[docs] def clearSelection(self, *, _undoable=True): """ Deselect all elements. :param bool _undoable: Whether to create an undoable command. Should only be passed by a command implementation. """ if _undoable: self._undoableClearSelection() else: super().clearSelection()
@command.do_command(command_id=command.CommandType.SelectResidues, command_class=command.TimeBasedCommand) def _undoableClearSelection(self): residues = set(self._selection) redo = super().clearSelection set_selection = super().setSelectionState undo = lambda: set_selection( {elem for elem in residues if elem.sequence is not None}, True) return redo, undo, self._CLEAR_DESC, self._mergeSelectionCommands
[docs] def onSequencesAboutToBeRemoved(self, start, end): """ When sequences are about to be removed, deselect all the residues in those sequences. :param start: Start index of sequences about to be removed. :type start: int :param end: End index of sequences about to be removed. :type end: int """ residues = list(itertools.chain(*self.aln[start:end + 1])) self.setSelectionState(residues, False, _undoable=False) # emit selectionChanged while the sequences are still part of the # alignment self._emitSelectionChanged()
[docs] def onResiduesAboutToBeRemoved(self, residues): """ When residues are about to be removed, deselect those residues. :param residues: A list of residues that were removed. :type residues: list(residue.AbstractSequenceElement) """ self.setSelectionState(residues, False, _undoable=False)
[docs] def getSelectionIndices(self, sort=True): """ Return a list of selected residue indices within the alignment. :return: A list of (sequence index, residue index) tuples for all selected residues. :rtype: list(tuple(int, int)) """ return self.aln.getResidueIndices(self._selection, sort=sort)
[docs] def isSingleBlockSingleSeqSelected(self): """ Determine whether exactly one single block of residues/gaps (i.e. contiguous residues/gaps) in a single sequence is selected. :rtype: bool """ if not self._selection: return False sel_seq = {res.sequence for res in self._selection} if len(sel_seq) != 1: return False sel_seq = sel_seq.pop() found_selection = False selection_ended = False for res in sel_seq: if res in self._selection: if not found_selection: # This is the start of the first selected block that we've # found found_selection = True elif selection_ended: # This is the second selected block that we've found return False elif found_selection: # This is the end of the first selected block selection_ended = True return True
def _getSelectionBlockForIndex(self, sel_idx, sel_indices): """ Given a selected index (in the form of a (sequence index, residue index) tuple), return a set of other selected indices within the same selection block (i.e. contiguous selcted residues/gaps). :param sel_idx: Selected index to return the block of :type sel_idx: tuple(int, int) :param sel_indices: All selected indices :type sel_indices: set(tuple(int, int)) :return: Set of indices in the same selection block as the specified index. :rtype: set(tuple(int, int)) """ # tuples of all visited residues visited = {sel_idx} # tuples of all residues that were newly visited in the current # iteration current = {sel_idx} # Use graph traversal to find all other selected indices "reachable" # from sel_idx. (A selected index is reachable iff there exists a path # from sel_idx to it such that all indices along the path are selected.) while current: prev = current current = set() for seq_i, res_i in prev: for delta_seq, delta_res in ((-1, 0), (1, 0), (0, -1), (0, 1)): node = (seq_i + delta_seq, res_i + delta_res) if node in sel_indices and node not in visited: visited.add(node) current.add(node) return visited
[docs] def isSingleBlockSelected(self): """ Determine whether exactly one single block of residues/gaps (i.e. contiguous residues/gaps) is selected. :rtype: bool """ sel_indices = self.getSelectionIndices(sort=False) if not sel_indices: return False first_sel_index = sel_indices[0] # speed up inclusion checks for _getSelectionBlockForIndex by converting # sel_indices to a set sel_indices = set(sel_indices) visited = self._getSelectionBlockForIndex(first_sel_index, sel_indices) return len(visited) == len(sel_indices)
[docs] def numBlocksSelected(self): """ Return the number of blocks of residues/gaps (i.e. contiguous residues/gaps) that are selected :return: Number of selected blocks :rtype: int """ sel_indices = set(self.getSelectionIndices(sort=False)) unseen_indices = sel_indices.copy() num_blocks = 0 while unseen_indices: cur_idx = unseen_indices.pop() cur_block = self._getSelectionBlockForIndex(cur_idx, sel_indices) num_blocks += 1 unseen_indices.difference_update(cur_block) return num_blocks
[docs] def anyStructuredResiduesSelected(self): """ Determine if any structured residues (i.e. residues in a structure- linked sequence that aren't SEQRES only) are selected. :rtype: bool """ return residue.any_structured_residues(self._selection)
@command.do_command def _undoableSuspendSelection(self): """ Undoable suspend selection which suspends the current residue selection and clears the selection. """ orig_sel = set(self.getSelection()) redo = lambda: self.clearSelection(_undoable=False) def undo(): valid_res = (res for res in orig_sel if res.sequence is not None) self.setSelectionState(valid_res, True, _undoable=False) return redo, undo, 'Suspend current residue selection.' @command.do_command def _undoableUnsuspendSelection(self, sel_to_restore): """ Undoable unsuspend selection which clears the current selection and restores the selection to the given selection. Note: Invalid residues will not be selected. :param sel_to_restore: Selection to restore. :type sel_to_restore: Iterable. """ orig_sel = set(self.getSelection()) def get_valid_residues(residues): return (res for res in residues if res.sequence is not None) def redo(): self.clearSelection(_undoable=False) res_to_select = get_valid_residues(sel_to_restore) self.setSelectionState(res_to_select, True, _undoable=False) def undo(): self.clearSelection(_undoable=False) res_to_select = get_valid_residues(orig_sel) self.setSelectionState(res_to_select, True, _undoable=False) return redo, undo, 'Restore previous residue selection.'
[docs] @contextlib.contextmanager def suspendSelection(self): """ Suspend the selection in the context and restore it upon exit. Note that this is undoable, any selected elements that were removed from their sequence in the context when restored will be reselected. """ orig_sel = set(self.getSelection()) self._undoableSuspendSelection() yield self._undoableUnsuspendSelection(orig_sel)
[docs]class SequenceSelectionModel(AbstractAlignmentSelectionModel):
[docs] def setSelectionState(self, sequences, selected): """ Set the selection state of the specified sequences. Hidden sequences will not be selected. """ if selected: sequences = list(sequences) # in case `sequences` is a generator for seq in sequences: if seq not in self.aln: raise ValueError( f"Can't select '{seq.name}{seq.chain}'. Sequences must be " "contained in the alignment to be selectable.") if self.aln.anyHidden(): sequences = set(self.aln.getShownSeqs()).intersection(sequences) super().setSelectionState(sequences, selected)
[docs] def onSequencesAboutToBeRemoved(self, start, end): """ Respond to a `sequencesAboutToBeRemoved` signal by deselecting any sequences that are about to be removed. :param start: Start index of sequences about to be removed. :type start: int :param end: End index of sequences about to be removed. :type end: int """ self.setSelectionState(self.aln[start:end + 1], False) # emit selectionChanged while the sequences are still part of the # alignment self._emitSelectionChanged()
[docs] def getSelectionIndices(self): """ Return a list of the selected sequence indices within the alignment :return: Selected sequence indices :rtype: list(int) """ return [self.aln.index(s) for s in self._selection]
[docs] @contextlib.contextmanager def suspendSelection(self): """ Suspend the selection in the context and restore it upon exit. """ orig_sel = set(self.getSelection()) self.clearSelection() yield self.clearSelection() self.setSelectionState(orig_sel, True)
[docs]class AnnotationSelectionModel(AbstractAlignmentSelectionModel): """ Class that tracks the selection state of sequence annotation as AnnotationRowInfo namedtuples. """
[docs] def onSequencesAboutToBeRemoved(self, start, end): removed_seqs = set(self.aln[start:end + 1]) to_remove = set() for ann_id in self.getSelection(): seq = ann_id.seq if seq is not None and seq in removed_seqs: to_remove.add(ann_id) self.setSelectionState(to_remove, False) self._emitSelectionChanged()
class _PairwiseConstraints: """ Data class to handle setting pairwise constraints. """ def __init__(self): self._resetAttributes() def _resetAttributes(self): self._ref_residue = None self._other_residue = None self._pairwise_constraints = {} @property def indices(self): """ :return: Tuples of residue indices of constraints :rtype: list[tuple(int, int)] """ return [(r1.idx_in_seq, r2.idx_in_seq) for r1, r2 in self._pairwise_constraints.items()] @property def ref_residue(self): """ Most recently picked ref residue """ return self._ref_residue @property def other_residue(self): """ Most recently picked non-ref residue """ return self._other_residue def hasConstraints(self): return bool(self._pairwise_constraints) def getPairs(self): """ :return: Pairs of reference residue, non-reference residue :rtype: list(tuple(residue.Residue, residue.Residue)) """ return list(self._pairwise_constraints.items()) def reset(self): self._resetAttributes() def setRefConstraint(self, res): """ Create or break constraint for the given reference residue """ half_ref = self._ref_residue half_other = self._other_residue if half_other is not None: if self._pairwise_constraints.get(res) == half_other: self._pairwise_constraints.pop(res) else: for ref_res, other_res in self._pairwise_constraints.items(): if other_res == half_other: self._pairwise_constraints.pop(ref_res) break self._pairwise_constraints[res] = half_other self._other_residue = None elif half_ref == res: self._ref_residue = None else: self._ref_residue = res def setOtherConstraint(self, res): """ Create or break constraint for the given non-reference residue """ half_other = self._other_residue half_ref = self._ref_residue if half_ref is not None: if self._pairwise_constraints.get(half_ref) == res: self._pairwise_constraints.pop(half_ref) else: self._pairwise_constraints[half_ref] = res self._ref_residue = None elif half_other == res: self._other_residue = None else: self._other_residue = res class _HMProximityConstraints(QtCore.QObject): """ Data class to handle setting residue (proximity) constraints for homology modeling. """ constraintsChanged = QtCore.pyqtSignal() def __init__(self): super().__init__() self._resetAttributes() def reset(self): do_signal = self.hasConstraints() or self.picked_residue self._resetAttributes() if do_signal: self.constraintsChanged.emit() def _resetAttributes(self): self._picked_residue = None self._constraints = {} self._reverse_constraints = {} @property def indexes(self): """ :return: Tuples of residue indexes of constraints :rtype: list[tuple(int, int)] """ return [(r1.idx_in_seq, r2.idx_in_seq) for r1, r2 in self._constraints.items()] @property def picked_residue(self): """ Most recently picked residue """ return self._picked_residue def hasConstraints(self): return bool(self._constraints) def getPairs(self): """ :return: Pairs of constrained residues :rtype: list(tuple(residue.Residue, residue.Residue)) """ return list(self._constraints.items()) def setConstraint(self, res): """ Create or break constraint for the given reference residue. If this is the first picked residue, it will be stored as a "half constraint". If this is the second picked residue, a constraint will be formed. If the picked residue is already part of a constraint, the constraint will be broken. """ self._setConstraint(res) self.constraintsChanged.emit() def _setConstraint(self, res): half_pick = self._picked_residue if half_pick is not None: if half_pick != res: # If the residues are different, set a constraint self._constraints[half_pick] = res self._reverse_constraints[res] = half_pick self._picked_residue = None else: # If res is in the constraint dict, remove the constraint from both other = self._constraints.pop(res, None) if other is not None: del self._reverse_constraints[other] return # If res is in the reverse dict, remove the constraint from both other = self._reverse_constraints.pop(res, None) if other is not None: del self._constraints[other] return # Otherwise, pick the res self._picked_residue = res def _onResiduesAboutToBeRemoved(self, residues): """ Remove constraints for residues that are about to be removed. """ do_signal = False for res in residues: val = self._constraints.pop(res, None) if val is not None: del self._reverse_constraints[val] do_signal = True if do_signal: self.constraintsChanged.emit() def _onSequencesAboutToBeRemoved(self, sequences): """ Remove constraints for residues in sequences that are about to be removed. """ sequences = set(sequences) res_to_pop = set() for res in self._constraints.keys(): if res.sequence in sequences: res_to_pop.add(res) for res in res_to_pop: other_res = self._constraints.pop(res) del self._reverse_constraints[other_res] if res_to_pop: self.constraintsChanged.emit() class _HMLigandConstraints(QtCore.QObject): """ Data class to handle setting ligand constraints for homology modeling """ constraintsChanged = QtCore.pyqtSignal() def __init__(self): super().__init__() self._constraints = {} def reset(self): do_signal = self.hasConstraints() self._constraints = {} if do_signal: self.constraintsChanged.emit() @property def constraints(self): for res, lig_list in self._constraints.items(): for lig in lig_list: yield (res, lig) def hasConstraints(self): return bool(self._constraints) def isConstraint(self, res, lig): res_constraints = self._constraints.get(res, set()) return lig in res_constraints def handlePick(self, res, lig): """ Pick or unpick the given residue ligand pair """ res_constraints = self._constraints.setdefault(res, set()) if lig in res_constraints: res_constraints.remove(lig) else: res_constraints.add(lig) self.constraintsChanged.emit() def _onResiduesAboutToBeRemoved(self, residues): """ Remove ligand constraints for residues that are about to be removed. """ do_signal = False for res in residues: val = self._constraints.pop(res, None) if val is not None: do_signal = True if do_signal: self.constraintsChanged.emit() def _onSequencesAboutToBeRemoved(self, sequences): """ Remove ligand constraints for residues in sequences that are about to be removed. """ sequences = set(sequences) res_to_pop = set() for res in self._constraints.keys(): if res.sequence in sequences: res_to_pop.add(res) for res in res_to_pop: self._constraints.pop(res) if res_to_pop: self.constraintsChanged.emit() class _ResidueOutlines(QtCore.QObject): """ Class to encapsulate residue outlines :ivar resOutlineStatusChanged: Signal emitted when residue outline changes. Emitted with whether any residues are outlined. """ resOutlineStatusChanged = QtCore.pyqtSignal(bool) def __init__(self): super().__init__() self._outlines = {} self._res_outlines_by_seq = None self.resOutlineStatusChanged.connect(self.invalidateOutlines) def getOutlineMap(self): """ Get the read-only map of outlines """ return types.MappingProxyType(self._outlines) def getResOutlinesForSeq(self, seq): """ Get the residue outline blocks for the given sequence """ if self._res_outlines_by_seq is None: self._cacheResOutlinesBySeq() return self._res_outlines_by_seq.get(seq, ()) def setResOutlines(self, color_map): """ Set a new mapping between residues and outline RGB. For undoability, should only be called from within a command redo or undo method. """ self._outlines = color_map self.resOutlineStatusChanged.emit(bool(color_map)) def _getResOutlineColorCmd(self, residues, color): """ Return command implementation for setting residue outline color. Should be called from a method wrapped in `command.do_command`. """ redo, undo = self._getOutlineCommands(residues, color) if color: color_name = QtGui.QColor(*color).name() desc = f"Outline %s in {color_name}" else: desc = "Remove Outline from %s" n_res = len(residues) residue_text = inflect.engine().plural("Residue", n_res) desc = desc % f"{n_res} Selected {residue_text}" return redo, undo, desc @QtCore.pyqtSlot() def invalidateOutlines(self): self._res_outlines_by_seq = None def _cacheResOutlinesBySeq(self): """ Create a mapping between sequences and outline blocks """ outlined_res_by_seq = collections.defaultdict(list) for res, color_ in self._outlines.items(): seq = res.sequence res_idx_in_seq = seq.index(res) outlined_res_by_seq[seq].append((res_idx_in_seq, color_)) res_outlines_by_seq = { seq: self._createOutlinesBySeq(residue_infos) for seq, residue_infos in outlined_res_by_seq.items() } self._res_outlines_by_seq = res_outlines_by_seq def _createOutlinesBySeq(self, residue_infos): """ Group outlined residues into contiguous blocks by color :param residue_infos: Tuples of res_idx, color :type residue_infos: list[tuple(int, tuple)] """ outlines = [] residue_infos.sort() prev_res_iter, res_iter = itertools.tee(residue_infos) outline_start = next(res_iter, None) for prev_res_info, res_info in itertools.zip_longest( prev_res_iter, res_iter): prev_res_idx, prev_color = prev_res_info if res_info is None: # Last iteration; always need to store store = True else: res_idx, res_color = res_info together = (res_color == prev_color and res_idx - prev_res_idx == 1) # Store if the current res info doesn't go with the previous res store = not together if store: outline_info = _ResidueOutline(outline_start[0], prev_res_idx, prev_color) outlines.append(outline_info) # The current res is the start of the next outline block outline_start = res_info return outlines def _getOutlineColorMap(self, copy=True): """ :param copy: Whether to copy the map :type copy: bool :return: Mapping mapping between residue object and RGB tuple :rtype: dict """ color_map = self._outlines if copy: color_map = color_map.copy() return color_map def _getOutlineCommands(self, residues, color): """ Create the redo and undo commands to outline the specified residues. :param color: RGB tuple to outline the residues or empty tuple to clear :type color: tuple """ orig_color_map = self._getOutlineColorMap() new_color_map = self._getOutlineColorMap(copy=False) for res in residues: if color: new_color_map[res] = color else: new_color_map.pop(res, None) redo = partial(self.setResOutlines, new_color_map) undo = partial(self.setResOutlines, orig_color_map) return redo, undo
[docs]class AlignmentSignals(alignment.AlignmentSignals): """ Signals that can be emitted by the GUI alignments. :cvar seqExpansionChanged: Signal emitted when the expansion state (i.e. are the annotations expanded or collapsed) for a sequence changes. Emitted with: - The sequence that changed - Whether the sequence is now expanded :cvar resHighlightStatusChanged: Signal emitted when residue highlighting changes. Emitted with whether any residues are highlighted. :ivar resOutlineStatusChanged: Signal emitted when residue outline changes. Emitted with whether any residues are outlined. :ivar homologyLigandConstraintsChanged: Signal emitted when homology modeling ligand constraints change. :ivar homologyProximityConstraintsChanged: Signal emitted when homology modeling residue constraints change. :cvar homologyCompositeResiduesChanged: Signal emitted when composite residues change. :cvar homologyStatusChanged: Signal emitted when homology status changes. Emitted with the sequence that changed. Signal not emitted for sequences that are removed from the alignment. :cvar resSelectionChanged: Signal emitted when the residue selection in the alignment changes. Emitted with: - set of newly selected residues - set of newly deselected residues :cvar seqSelectionChanged: Signal emitted when the sequence selection in the alignment changes. Emitted with: - set of newly selected sequences - set of newly deselected sequences :cvar syncWsResSelection: Signal emitted when a new sequence has been added to the alignment. In response to this signal, the MSV Widget is responsible for selecting all residues that correspond to selected workspace residues. Emitted with the iterable of sequences to select residues in. """ seqExpansionChanged = QtCore.pyqtSignal(sequence.AbstractSequence, bool) resHighlightStatusChanged = QtCore.pyqtSignal(bool) resOutlineStatusChanged = QtCore.pyqtSignal(bool) homologyCompositeResiduesChanged = QtCore.pyqtSignal() homologyLigandConstraintsChanged = QtCore.pyqtSignal() homologyProximityConstraintsChanged = QtCore.pyqtSignal() homologyStatusChanged = QtCore.pyqtSignal(sequence.AbstractSequence) pairwiseConstraintsChanged = QtCore.pyqtSignal() hiddenSeqsChanged = QtCore.pyqtSignal(bool) resSelectionChanged = QtCore.pyqtSignal(set, set) seqSelectionChanged = QtCore.pyqtSignal(set, set) syncWsResSelection = QtCore.pyqtSignal(object)
class _ProteinAlignment(QtCore.QObject, metaclass=msv_utils.QtDocstringWrapperMetaClass, wraps=alignment.ProteinAlignment): """ A ProteinAlignment class that presents the same interface as a regular ProteinAlignment but optionally accomplishes mutating operations via a command stack. If no command stack is set on the object, commands are executed but cannot be undone. Undoable protein alignments have an `AlignmentSelectionModel` because they are intended for use in GUIs, whereas normal non-undoable alignments don't have a selection model because their use cases (aligning sequences) don't require a concept of selection. :cvar _CLEAR_DESC: The undo stack description for clearing the alignment. :vartype _CLEAR_DESC: str """ _CLEAR_DESC = "Remove All Sequences" _MOVE_SELECTED_SEQS_DESC = "Move Selected Sequences" _EXPAND_SELECTION_TO_FULL_CHAIN = "Expand to Full Chain" def __init__(self, sequences=None, aln=None, is_workspace=False): """ :param sequences: An optional iterable of sequences :type sequences: list :param aln: An alignment to wrap this instance around. :type aln: alignment.ProteinAlignment :param is_workspace: Whether this alignment will only include sequences that are currently included in the workspace. This should only be set to True for an alignment created by the structure model. Note that this argument has absolutely no effect on the behavior of this alignment object. Instead, it is the responsibility of the structure model to make sure that the alignment is kept up to date. :type is_workspace: bool """ if aln is not None and sequences is not None: err_msg = ( 'Either aln or sequences can be passed into the constructor ' 'not both.') raise ValueError(err_msg) super().__init__() self.signals = AlignmentSignals(self) self.undo_stack = None self._aln = None self._is_workspace = is_workspace self._residue_highlights = {} self._residue_outlines = _ResidueOutlines() self._residue_outlines.resOutlineStatusChanged.connect( self.signals.resOutlineStatusChanged) if aln is None: aln = alignment.ProteinAlignment(sequences) self._setInnerAlignment(aln) self._initSelectionModels() self.res_selection_model.selectionChanged.connect( self.signals.resSelectionChanged) self.seq_selection_model.selectionChanged.connect( self.signals.seqSelectionChanged) self._initHomologyCache() self._initHomologyCompositeResidues() self._initPairwiseConstraints() self._initHMLigandConstraints() self._initHMProximityConstraints() self._expanded_seqs = set() self._hidden_seqs = weakref.WeakSet() self._hidden_seq_selected_residues = weakref.WeakKeyDictionary() self._any_hidden = False self._seq_shown_state_cache = None self._name_query = "" self._filter_enabled = False def _initSelectionModels(self): """ Instantiate the residue and sequence selection models. """ self.res_selection_model = ResidueSelectionModel(self) self.seq_selection_model = SequenceSelectionModel(self) self.ann_selection_model = AnnotationSelectionModel(self) def setSeqExpanded(self, seq, expanded=True): if expanded == (seq in self._expanded_seqs): return if expanded: self._expanded_seqs.add(seq) else: self._expanded_seqs.discard(seq) self.signals.seqExpansionChanged.emit(seq, expanded) def isSeqExpanded(self, seq): return seq in self._expanded_seqs def _setInnerAlignment(self, aln): if self._aln is not None: for signal, name in self._aln.signals.allSignalsAndNames(): signal.disconnect(getattr(self.signals, name)) self._aln = aln for signal, name in self._aln.signals.allSignalsAndNames(): signal.connect(getattr(self.signals, name)) self.signals.sequencesAboutToBeRemoved.connect(self._initHomologyCache) self.signals.sequencesAboutToBeRemoved.connect( self._removeConstraintsForRemovedSequences) self.signals.residuesAboutToBeRemoved.connect( self._removeConstraintsForRemovedResidues) for signal in (self.signals.sequencesAboutToBeRemoved, self.signals.sequencesAboutToBeInserted, self.signals.sequencesAboutToBeReordered): signal.connect(self.resetHomologyCompositeResidues) for signal in (self.signals.sequenceResiduesChanged, self.signals.residuesAdded, self.signals.residuesAboutToBeRemoved): signal.connect(self._residue_outlines.invalidateOutlines) for signal in (self.signals.sequencesInserted, self.signals.sequencesRemoved, self.signals.sequencesReordered, self.signals.alignmentCleared): signal.connect(self._emitHiddenSeqsChangedOnSeqChange) def __deepcopy__(self, memo): """ Copy the alignment. Note that the isWorkspace() status is intentionally not copied. Since the sequence model (and not the alignment itself) is responsible for keeping the workspace alignment up to date with the Maestro workspace, a copy of it is not going to be kept up to date and therefore does not qualify as a workspace alignment. """ copied_inner_aln = copy.deepcopy(self._aln, memo) aln = self.__class__(aln=copied_inner_aln) def get_new_residues(residues): res_idxs = self.getResidueIndices(residues, sort=False) return [aln[sidx][ridx] for sidx, ridx in res_idxs] new_highlight_res = get_new_residues(self._residue_highlights.keys()) new_highlights = dict( zip(new_highlight_res, self._residue_highlights.values())) aln._residue_highlights = new_highlights og_outline_map = self._residue_outlines.getOutlineMap() new_outline_res = get_new_residues(og_outline_map.keys()) new_outline_map = dict(zip(new_outline_res, og_outline_map.values())) aln._residue_outlines.setResOutlines(new_outline_map) res_sel_model = aln.res_selection_model sel_residues = get_new_residues(self.res_selection_model.getSelection()) if sel_residues: res_sel_model.setSelectionState(sel_residues, selected=True, _undoable=False) seq_sel_model = aln.seq_selection_model sel_seq_idxs = self.seq_selection_model.getSelectionIndices() sel_sequences = [aln._aln[sidx] for sidx in sel_seq_idxs] if sel_sequences: seq_sel_model.setSelectionState(sel_sequences, selected=True) for idx, seq in enumerate(self): aln.setSeqExpanded(aln[idx], expanded=self.isSeqExpanded(seq)) hidden_seqs = [ aln[idx] for idx, shown in enumerate(self.getSeqShownStates()) if not shown ] aln._showHideSeqCommand(to_hide=hidden_seqs) aln.setUndoStack(self.undo_stack) return aln def __repr__(self): """ :rtype: str :return: A str representation of the alignment """ return self._aln._getRepr(type(self).__qualname__) def setUndoStack(self, undo_stack): """ :param undo_stack: The undo stack on which to push commands :type undo_stack: schrodinger.application.msv.command.UndoStack Set the undo stack on the object """ self.undo_stack = undo_stack self.res_selection_model.setUndoStack(undo_stack) self.seq_selection_model.setUndoStack(undo_stack) @contextlib.contextmanager def suspendAnchors(self): """ "Undoable" suspendAnchors. This is necessary for when we use suspendAnchors in a macro in order to preserve inter-command state. For example, if we do:: with compress_command(undo_aln.undo_stack): with undo_aln.suspendAnchors(): undo_aln.removeAllGaps() undo_aln.addGapsByIndices(idxs) Without an undoable suspendAnchors the above code will error on undo since the anchors won't be resuspended. This is implemented by creating a command for both entering and exiting the context. If we're not in a macro, this method just delegates to the wrapped alignment. """ if not getattr(self.undo_stack, 'in_macro', False): with self._aln.suspendAnchors(): yield else: self._suspendAnchors() yield self._unsuspendAnchors() @command.do_command def _suspendAnchors(self): redo = self._aln._suspendAnchors undo = self._aln._unsuspendAnchors return redo, undo, 'Suspend Anchors' @command.do_command def _unsuspendAnchors(self): redo = self._aln._unsuspendAnchors undo = self._aln._suspendAnchors return redo, undo, 'Unsuspend Anchors' def getSelectedSequences(self): """ Return a list of the currently selected sequences in alignment order. :return: List of currently selected sequences :rtype: list[sequence.Sequence] """ selected_seqs_set = self.seq_selection_model.getSelection() return [seq for seq in self if seq in selected_seqs_set] @command.do_command def reorderSequences(self, seq_indices): """ Reorder the sequences in the alignment using the specified list of indices :param seq_indices: A list with the new indices for sequences :type: list of int :raises ValueError: In the event that the list of indices does not match the length of the alignment """ if len(seq_indices) != len(self): msg = ("The number of elements in seq_indices should match the " "number of sequences in the alignment") raise ValueError(msg) redo, undo = self._getReorderSequencesFuncs(seq_indices) desc = "Reorder Sequences" return redo, undo, desc def _getReorderSequencesFuncs(self, seq_indices): """ Generate redo and undo commands for reordering the sequences in the alignment using the specified list of indices. :param seq_indices: A list with the new indices for sequences :type: list of int :return: Redo and undo commands :rtype: tuple(function, function) """ inverted_indices = self._getUndoSequenceOrdering(seq_indices) redo = partial(self._aln.reorderSequences, seq_indices) undo = partial(self._aln.reorderSequences, inverted_indices) return redo, undo @command.do_command def sortByProperty(self, seq_prop, reverse=False): sort_indices = self._aln._getSortByPropertyIndices(seq_prop, reverse=reverse) redo, undo = self._getReorderSequencesFuncs(sort_indices) desc = "Sort Sequences by Property" return redo, undo, desc @command.do_command def sort(self, *, key, reverse=False): sort_indices = self._aln._getSortIndices(key, reverse=reverse) redo, undo = self._getReorderSequencesFuncs(sort_indices) desc = "Sort Sequences" return redo, undo, desc @command.do_command def moveSelectedSequences(self, after_seq): """ Move all selected sequences in the alignment. :param dest_seq: The sequence to place the selected sequences after. :type dest_seq: sequence.Sequence """ selected = self.seq_selection_model.getSelection() move_indices = self._getMoveSequenceAfterIndices(after_seq, selected) inverted_indices = self._getUndoSequenceOrdering(move_indices) redo = partial(self._aln.reorderSequences, move_indices) undo = partial(self._aln.reorderSequences, inverted_indices) return redo, undo, self._MOVE_SELECTED_SEQS_DESC @staticmethod def _getUndoSequenceOrdering(seq_indices): """ Given a new ordering for sequences in an alignment, return an ordering that will restore the original order of sequences. Given a an alignment [a, b, c, d, e] an ordering of [3, 1, 4, 2, 0] will rearrange the sequences into [d, b, e, c, a]. We need an ordering of [4, 1, 3, 0, 2] to restore the original arrangement of [a, b, c, d, e]. This method is used in undo operations. :param seq_indices: A list with the new indices for sequences :type: list of int :rtype: list of int :return: An ordering list that will restore the original arrangement of sequences in the alignment """ new_indices = [None] * len(seq_indices) for current_index, new_index in enumerate(seq_indices): new_indices[new_index] = current_index return new_indices @command.do_command def setReferenceSeq(self, seq): # See alignment.ProteinAlignment for documentation self._assertCanSetReferenceSeq() redo_ordering = self._getReferenceSeqReordering(seq) undo_ordering = self._getUndoSequenceOrdering(redo_ordering) redo = partial(self._aln.reorderSequences, redo_ordering) undo = partial(self._aln.reorderSequences, undo_ordering) return redo, undo, self._setReferenceSeqDesc(seq) def _setReferenceSeqDesc(self, seq): """ Generate an undo stack description for setting the reference sequence. :param seq: The new reference sequence :type seq: sequence.ProteinSequence :return: The requested description :rtype: str """ return "Set %s as Reference Sequence" % seq.fullname def addSeq(self, seq, index=None, replace_selection=False): """ Add a single sequence to the alignment :param seq: The sequence to add. :type seq: sequence.ProteinSequence :param index: The index at which to insert; if None, the sequence is appended. :type index: int :param replace_selection: Whether to select the newly added sequences and deselect all other sequences. If False, selection will not be changed. :type replace_selection: bool """ self.addSeqs([seq], index, replace_selection) @command.do_command def addSeqs(self, seqs, index=None, replace_selection=False): """ Add multiple sequences to the alignment :param seqs: Sequences to add. :type seqs: list[sequence.ProteinSequence] :param index: The index at which to insert; if None, seqs are appended. :type index: int :param replace_selection: Whether to select the newly added sequences and deselect all other sequences. If False, selection will not be changed. :type replace_selection: bool """ self._assertCanAddSeqs(index) redo = partial(self._addSeqs, seqs, index, replace_selection) def undo(): self._aln.removeSeqs(seqs) self._expanded_seqs -= set(seqs) return redo, undo, self._addSeqsDesc(seqs) def _addSeqs(self, seqs, index, replace_selection): # See addSeqs above for method documentation self._expanded_seqs |= set(seqs) self._aln.addSeqs(seqs, index) self.signals.syncWsResSelection.emit(seqs) if replace_selection: self.seq_selection_model.clearSelection() self.seq_selection_model.setSelectionState(seqs, True) def _addSeqsDesc(self, seqs): """ Generate an undo stack description for adding the given sequences. :param seqs: Sequences to add :type seqs: list[sequence.ProteinSequence] :return: The requested description :rtype: str """ return f"Add {len(seqs)} Sequences" def removeSeq(self, seq): # See alignment.BaseAlignment for documentation self.removeSeqs([seq]) def removeSeqs(self, seqs): # See alignment.BaseAlignment for documentation if self[0] in seqs: self._assertCanRemoveRef() self._removeSeqs(seqs) @command.do_command def _removeSeqs(self, seqs): # See alignment.BaseAlignment.removeSeqs for documentation idx_map = {self.index(seq): seq for seq in seqs} old_anchors = self._aln.getAnchoredResidues() old_aln_sets, old_set_id, old_no_set = self._getCurrentAlnSets(seqs) old_highlight_color_map = self._getHighlightColorMap() old_outline_map = self.getOutlineMap().copy() invalidated_known_bonds, invalidated_pred_bonds = \ self._getInvalidatedBonds(seqs) expanded_seqs = self._expanded_seqs.intersection(seqs) def redo(): new_highlight_color_map = self._getHighlightColorMap(copy=False) new_highlight_color_map = { res: value for res, value in new_highlight_color_map.items() if res.sequence not in seqs } self._setResHighlights(new_highlight_color_map) new_outline_map = self.getOutlineMap() new_outline_map = { res: value for res, value in new_outline_map.items() if res.sequence not in seqs } self._residue_outlines.setResOutlines(new_outline_map) self._aln.removeSeqs(seqs) def undo(): for index in sorted(idx_map): self._aln.addSeq(idx_map[index], index) self.signals.syncWsResSelection.emit(seqs) self._aln.anchorResidues(old_anchors) self._restoreAlnSets(old_aln_sets, old_set_id) self._setResHighlights(old_highlight_color_map) self._residue_outlines.setResOutlines(old_outline_map) self._aln._restoreInvalidatedBonds(invalidated_known_bonds, invalidated_pred_bonds) # make sure we haven't inadvertantly collapsed any sequences when we # removed bonds self._expanded_seqs.update(expanded_seqs) return redo, undo, self._removeSeqsDesc(seqs) def _removeSeqsDesc(self, seqs): """ Generate an undo stack description for removing the given sequences. :param seqs: Sequences to remove :type seqs: list[sequence.ProteinSequence] :return: The requested description :rtype: str """ seqs = list(seqs) num_seqs = len(seqs) plural_sequences = inflect.engine().plural("Sequence", num_seqs) if num_seqs > 3: seqs_txt = f"{seqs[0].fullname}...{seqs[-1].fullname}" else: seqs_txt = ", ".join(seq.fullname for seq in seqs) return f"Remove {num_seqs} {plural_sequences} ({seqs_txt})" @command.do_command def renameSeq(self, seq, new_name): """ Changes the name for a sequence :param seq: The sequence to change the name of :type seq: schrodinger.protein.sequence.ProteinSequence :param new_name: The new name for the sequence :type new_name: str """ old_name = seq.name redo = partial(self._renameSeq, seq, new_name) undo = partial(self._renameSeq, seq, old_name) return redo, undo, self._renameSeqDesc(seq) def _renameSeq(self, seq, name): seq.name = name def _renameSeqDesc(self, seq): """ Generate an undo stack description for renaming the given sequence. :param seqs: Sequence to rename :type seqs: sequence.ProteinSequence :return: The requested description :rtype: str """ return f'Rename Sequence {seq.fullname}' @command.do_command def changeSeqChain(self, seq, new_chain): """ Changes the chain for a sequence :param seq: The sequence to change the name of :type seq: schrodinger.protein.sequence.ProteinSequence :param new_chain: The new chain name for the sequence :type new_chain: str """ old_chain = seq.chain redo = lambda: setattr(seq, "chain", new_chain) undo = lambda: setattr(seq, "chain", old_chain) return redo, undo, self._renameSeqDesc(seq) @command.do_command def clear(self): # See alignment.ProteinAlignment for documentation seqs = list(self._aln) def undo(): self._aln.addSeqs(seqs) redo = partial(self._aln.clear) return redo, undo, self._CLEAR_DESC @command.do_command def removeElements(self, elements): # See alignment.ProteinAlignment for documentation self._aln._assertCanRemove(elements) undo = self._createRemoveElementsUndo(elements) redo = self._createRemoveElementsRedo(elements) desc = f"Remove {len(elements)} Sequence Elements" return redo, undo, desc def _createRemoveElementsRedo(self, elements): residues = [elem for elem in elements if elem.is_res] gap_idxs = [] for elem in elements: if elem.is_gap: seq_i = self._aln.index(elem.sequence) res_i = elem.idx_in_seq gap_idxs.append((seq_i, res_i)) def redo(): new_highlight_color_map = self._getHighlightColorMap() for res in residues: new_highlight_color_map.pop(res, None) self._setResHighlights(new_highlight_color_map) new_outline_map = self.getOutlineMap().copy() for elem in elements: new_outline_map.pop(elem, None) self._residue_outlines.setResOutlines(new_outline_map) gaps = [] for seq_i, res_i in gap_idxs: gaps.append(self._aln[seq_i][res_i]) assert all(elem.is_gap for elem in gaps) self._aln.removeElements(residues + gaps) return redo def _createRemoveElementsUndo(self, elements): """ Helper method to create the undo function for `removeElements` """ orig_indices = self.getResidueIndices(elements) elems_to_restore = collections.defaultdict(list) for s_idx, r_idx in orig_indices: seq = self._aln[s_idx] elems_to_restore[seq].append((r_idx, seq[r_idx])) has_anchors = len(self.getAnchoredResidues()) > 0 if has_anchors: gaps_to_remove = dict() for seq, ele_info in elems_to_restore.items(): eles = [t[1] for t in ele_info] gaps = self._getAnchorConservingGapIdxs(seq, eles) gaps_to_remove[seq] = gaps selected = self.res_selection_model.getSelection() sel_to_remove = selected.intersection(elements) og_highlight_color_map = self._getHighlightColorMap() old_outline_map = self.getOutlineMap().copy() disulfides_to_restore = set() pred_disulfides_to_restore = set() for ele in elements: if ele.is_gap: continue if ele.disulfide_bond is not None: disulfides_to_restore.add(ele.disulfide_bond) if ele.pred_disulfide_bond is not None: pred_disulfides_to_restore.add(ele.pred_disulfide_bond) def undo(): with self.suspendAnchors(): for seq, res_list in elems_to_restore.items(): if has_anchors: gap_idxs = gaps_to_remove[seq] gaps = [seq[g_idx] for g_idx in gap_idxs] self._aln.removeElements(gaps) for r_idx, res in res_list: self._aln.addElements(seq, r_idx, [res]) self.res_selection_model.setSelectionState(sel_to_remove, True, _undoable=False) self._setResHighlights(og_highlight_color_map) self._residue_outlines.setResOutlines(old_outline_map) self._aln._restoreInvalidatedBonds(disulfides_to_restore, pred_disulfides_to_restore) return undo @command.do_command def mutateResidues(self, seq_i, start, end, elements, *, select=False): """ See `alignment.ProteinAlignment.mutateResidues` for additional method documentation. Note that the `select` argument is specific to this class and isn't present in the `alignment.ProteinAlignment` method. :param select: Whether to select the mutated residues. Note that this argument applies on undo as well, so it should only be True if the residues to be mutated are selected when this method is called. Also note that this argument is keyword-only. :type select: bool """ self._assertCanMutateResidues(seq_i, start, end, elements) seq = self[seq_i] to_remove = seq[start:end] net_lost_res = to_remove[len(elements):] new_gap_idxs = self._getAnchorConservingGapIdxs(seq, net_lost_res) num_mutated = end - start def redo(): self.res_selection_model.setSelectionState(to_remove, False, _undoable=False) self._aln.mutateResidues(seq_i, start, end, elements) if select: last_mutated_idx = start + len(elements) mutated_elems = seq[start:last_mutated_idx] to_select = set(mutated_elems) self.res_selection_model.setSelectionState(to_select, True, _undoable=False) def undo(): mutated_elems = seq[start:start + len(elements)] to_restore = to_remove with self.suspendAnchors(): if len(new_gap_idxs): new_gaps = [seq[idx] for idx in new_gap_idxs] self._aln.removeElements(new_gaps) self._aln.removeElements(mutated_elems) self._aln.addElements(seq, start, to_restore) if select: to_select = set(to_restore) self.res_selection_model.setSelectionState(to_select, True, _undoable=False) desc = f"Mutate {num_mutated} Residues" return redo, undo, desc @command.do_command def replaceResiduesWithGaps(self, residues): # See alignment.ProteinAlignment for documentation self._assertCanRemove(residues) def redo(): sel_model = self.res_selection_model cur_selection = sel_model.getSelection() for (seq_i, res_i), res in zip(self.getResidueIndices(residues), residues): gap = residue.Gap() self._aln.mutateResidues(seq_i, res_i, res_i + 1, [gap]) if res in cur_selection: sel_model.setSelectionState([gap], True, _undoable=False) undo_list = [] for (seq_i, res_i) in self.getResidueIndices(residues): undo_list.append((seq_i, res_i, self[seq_i][res_i])) def undo(): sel_model = self.res_selection_model cur_selection = sel_model.getSelection() for seq_i, res_i, res in undo_list: gap = self._aln[seq_i][res_i] self._aln.mutateResidues(seq_i, res_i, res_i + 1, [res]) if gap in cur_selection: sel_model.setSelectionState([res], True, _undoable=False) num_residues = len(residues) residues_text = inflect.engine().plural("Residue", num_residues) desc = f"Replace {num_residues} {residues_text} with Gaps" return redo, undo, desc @command.do_command def addElements(self, seq, res_i, elements, *, select=False): """ See `alignment.ProteinAlignment.addElements` for additional method documentation. Note that the `select` argument is specific to this class and isn't present in the `alignment.ProteinAlignment` method. :param select: Whether to select the added residues. Note that this argument is keyword-only. :type select: bool """ self._assertCanInsert(seq, [res_i]) def redo(): self._aln.addElements(seq, res_i, elements) if select: new_res = seq[res_i:res_i + len(elements)] self.res_selection_model.setSelectionState(new_res, True, _undoable=False) def undo(): to_remove = seq[res_i:res_i + len(elements)] self._aln.removeElements(to_remove) num_elements = len(elements) element_text = inflect.engine().plural("Element", num_elements) desc = f"Add {num_elements} Sequence {element_text}" return redo, undo, desc @command.do_command def addDisulfideBond(self, res1, res2, known=True): redo = partial(self._aln.addDisulfideBond, res1, res2, known=known) # We use a lambda for undo so `res1.disulfide_bond` isn't evaluated # until undo is actually called. if known: undo = lambda: self._aln.removeDisulfideBond(res1.disulfide_bond) else: undo = lambda: self._aln.removeDisulfideBond(res1. pred_disulfide_bond) desc = "Add Disulfide Bond" return redo, undo, desc @command.do_command def removeDisulfideBond(self, bond): res1, res2 = bond known = bond == res1.disulfide_bond # We use a lambda for redo so `res1.disulfide_bond` isn't evaluated # until redo is actually called. if known: redo = lambda: self._aln.removeDisulfideBond(res1.disulfide_bond) else: redo = lambda: self._aln.removeDisulfideBond(res1. pred_disulfide_bond) undo = partial(self._aln.addDisulfideBond, res1, res2, known=known) desc = "Remove Disulfide Bond" return redo, undo, desc def _getRestoreGapsMethod(self): """ Utility method for creating an undo method that reverts a change to gaps. Note that the undo method restores the same gap *instances* to avoid interfering with other undo steps. :return: A method that restores the state of gaps to when the method was created. :rtype: function """ original_gaps = self.getGaps() original_gaps_idxs = self._getElementIndexes(original_gaps) selection = self.res_selection_model.getSelection() sel_gaps = [elem for elem in selection if elem.is_gap] sel_gap_idxs = self.getResidueIndices(sel_gaps) def undo(): with self.suspendAnchors(): self._aln.removeAllGaps() for seq, gaps_by_seq in zip(self, original_gaps_idxs): for gap_index, gap in gaps_by_seq: seq.insertElements(gap_index, [gap]) gaps_to_select = [ self._aln[res_i][seq_i] for (res_i, seq_i) in sel_gap_idxs ] self.res_selection_model.setSelectionState(gaps_to_select, True, _undoable=False) return undo @command.do_command def addGapsByIndices(self, gap_indices): # See alignment.ProteinAlignment for documentation # validate gap_indices separately so an exception is not buried in a # command object self._validateGapIndices(gap_indices) num_gaps = sum(len(indices) for indices in gap_indices) redo = partial(self._aln.addGapsByIndices, gap_indices) desc = f"Add {num_gaps} Gaps to the Sequences" return redo, self._getRestoreGapsMethod(), desc @command.do_command(command_id=command.CommandType.AddGaps, command_class=command.TimeBasedCommand) def addGapsBeforeIndices(self, gap_indices): # See alignment.ProteinAlignment for documentation # validate gap_indices separately so an exception is not buried in a # command object self._validateGapBeforeIndices(gap_indices) num_gaps = sum(len(indices) for indices in gap_indices) redo = partial(self._aln.addGapsBeforeIndices, gap_indices) desc = f"Add {num_gaps} Gaps to the Sequences" def merge_desc(desc1, desc2): bad_prefix = desc1[:4] != desc2[:4] or desc1[:4] != "Add " bad_suffix = desc1[-22:] != desc2[-22:] or desc1[ -22:] != " gaps to the sequences" if bad_prefix or bad_suffix: # Check that descriptions are "Add _ gaps to the sequences" # and return the first description if not. return desc1 # Add the two numbers of gaps together. num_gaps = int(desc1[4:-22]) + int(desc2[4:-22]) return "Add %i Gaps to the Sequences" % num_gaps return redo, self._getRestoreGapsMethod(), desc, merge_desc @command.do_command def padAlignment(self): # See alignment.ProteinAlignment for documentation redo = self._aln.padAlignment desc = "Pad Alignment" return redo, self._getRestoreGapsMethod(), desc @command.do_command def removeTerminalGaps(self): # See alignment.ProteinAlignment for documentation terminal_gaps = self.getTerminalGaps() def redo(): to_remove_by_seq = self.getTerminalGaps() to_remove = list(itertools.chain(*to_remove_by_seq)) self._aln.removeElements(to_remove) def undo(): for seq, gaps in zip(self, terminal_gaps): if gaps: seq.extend(gaps) desc = "Remove Terminal Gaps from Sequences" return redo, undo, desc def _getElementIndexes(self, elements): """ Returns the indices (in the alignment) of the specified residues :param elements: Residues and gaps to get indices for, formatted as [list of residues/gaps for sequence 0, list of residues/gaps for sequence 1,...] :type elements: list[list[residue.AbstractSequenceElement]] :rtype: List of the requested indices and elements, formatted as [list of (index, element) tuples for sequence 0, list of (index, element) tuples for sequence 1, ...]. Tuples are given in the order they appear in the sequence. :return: list[list[tuple(int, residue.AbstractSequenceElement)]] """ indexes = [] for eles in elements: cur_indexes = [(ele.idx_in_seq, ele) for ele in eles] cur_indexes.sort() indexes.append(cur_indexes) return indexes @command.do_command def removeAllGaps(self): # See alignment.ProteinAlignment for documentation desc = "Remove All Gaps From Sequences" redo = partial(self._aln.removeAllGaps) return redo, self._getRestoreGapsMethod(), desc @command.do_command def insertSubalignment(self, aln, start): # See parent class for documentation self._aln._assertRectangular(aln) end = start + aln.num_columns redo = partial(self._aln.insertSubalignment, aln, start) undo = partial(self._aln.removeSubalignment, start, end) desc = f"Insert Section in Sequences (at Position {start + 1})" return redo, undo, desc def _getUndoSubalignment(self, start, end): """ Return a new alignment containing new sequences with identical residues The returned alignment must only be used for undo operations. """ AlignmentClass = self._aln.__class__ if len(self._aln) == 0: return AlignmentClass() SequenceClass = self._aln[0].__class__ seqs = [] for orig_seq in self._aln: new_seq = SequenceClass() # Bypass sequence __init__ to avoid re-parenting residues new_seq._sequence = orig_seq[start:end] seqs.append(new_seq) return AlignmentClass(seqs) @command.do_command def removeSubalignment(self, start, end): # See alignment.ProteinAlignment for documentation self._aln._assertCanRemoveSubalignment(start, end) subalignment = self._getUndoSubalignment(start, end) redo = partial(self._aln.removeSubalignment, start, end) def undo(): with self.suspendAnchors(): self._aln._insertSubalignment(subalignment, start, require_rectangular=False) desc = f"Remove Section ({start + 1}-{end}) of Sequences" return redo, undo, desc @command.do_command def replaceSubalignment(self, aln, start, end): # See alignment.ProteinAlignment for documentation self._aln._assertRectangular(aln) original_subaln = self._getUndoSubalignment(start, end) undo_end = start + aln.num_columns redo = partial(self._aln.replaceSubalignment, aln, start, end) undo = partial(self._aln.replaceSubalignment, original_subaln, start, undo_end) desc = "Replace Section ({start + 1}-{end}) in Sequences" return redo, undo, desc @command.do_command def minimizeAlignment(self): # See alignment.ProteinAlignment for documentation def redo(): with self.suspendAnchors(): self._aln.minimizeAlignment() gap_only_columns = self.getGapOnlyColumns() def undo(): with self.suspendAnchors(): self._aln.addGapsByIndices(gap_only_columns) desc = "Remove Gap-Only Columns" return redo, undo, desc @command.do_command def anchorResidues(self, residues): self._aln._assertCanAnchor(residues) old_anchors = self._aln.getAnchoredResidues() redo = partial(self._aln.anchorResidues, residues) def undo(): self._aln.clearAnchors() self._aln.anchorResidues(old_anchors) desc = f'Anchor {len(residues)} Residues' return redo, undo, desc @command.do_command def removeAnchors(self, residues): old_anchors = self._aln.getAnchoredResidues() redo = partial(self._aln.removeAnchors, residues) undo = partial(self._aln.anchorResidues, old_anchors) desc = f'Unanchor {len(residues)} Residues' return redo, undo, desc @command.do_command def clearAnchors(self): old_anchors = self._aln.getAnchoredResidues() redo = self._aln.clearAnchors undo = partial(self._aln.anchorResidues, old_anchors) desc = 'Clear Residue Anchors' return redo, undo, desc def _anchorSelectionValid(self): """ Helper method for determining whether anchoring the selection is valid. Anchoring is valid if there exists at least one selected residue for which it's valid. :return: Whether anchoring the selection is valid. :rtype: bool """ ref_seq = self.getReferenceSeq() ref_len = len(ref_seq) sel_residues = self.res_selection_model.getSelection() cols_with_ref_selected = set() cols_with_non_ref_selected = set() for res in sel_residues: is_gap = res.is_gap col_idx = res.idx_in_seq if res.sequence is ref_seq: if not is_gap: cols_with_ref_selected.add(col_idx) continue elif (not is_gap and col_idx < ref_len and not ref_seq[col_idx].is_gap): return True cols_with_non_ref_selected.add(col_idx) cols_with_only_ref_selected = (cols_with_ref_selected - cols_with_non_ref_selected) for col in cols_with_only_ref_selected: # we didn't add any columns with reference gaps to # cols_with_ref_selected, so we don't need to check that here if any(col_idx < len(seq) and not seq[col_idx].is_gap for seq in itertools.islice(self, 1, None)): return True return False def anchorResidueValid(self, res): """ Helper method returning whether anchoring the given residue is valid. Anchoring is valid if the given residue isn't a gap and isn't aligned to a reference gap. If the given residue is a reference residue, this method will return whether at least one residue aligned to it can be anchored. :param res: The given residue. :type res: residue.Residue :return: Whether the given residue can be anchored. :rtype: bool """ ref_seq = self.getReferenceSeq() res_idx = res.idx_in_seq if res in ref_seq: col = self._aln.getColumn(res_idx) return not (len(col) == 1 or res.is_gap or all(r is None or r.is_gap for r in col)) else: return not (res.is_gap or res_idx >= len(ref_seq) or ref_seq[res_idx].is_gap) @command.do_command def setSelectedResColor(self, color): """ Set the selected residues to the specified color :param color: RGB tuple to color the residues or empty tuple to clear :type color: tuple """ sel_res = self.res_selection_model.getSelection() redo, undo = self._getHighlightCommands(sel_res, color) if color: color_name = QtGui.QColor(*color).name() desc = f"Apply Color {color_name} to" else: desc = "Remove Highlight from" n_res = len(sel_res) residue_text = inflect.engine().plural("Residue", n_res) desc += f" {n_res} Selected {residue_text}" return redo, undo, desc @command.do_command def setResidueHighlight(self, residues, color): """ Set the specified residues to the specified color :param color: RGB tuple to color the residues or empty tuple to clear :type color: tuple """ redo, undo = self._getHighlightCommands(residues, color) desc = "Pick" if color else "Unpick" n_res = len(residues) residue_text = inflect.engine().plural("Residue", n_res) desc += f" {n_res} {residue_text}" return redo, undo, desc @command.do_command def clearAllHighlights(self): """ Clear all residue highlights """ orig_color_map = self._getHighlightColorMap() orig_outline_map = self.getOutlineMap().copy() def redo(): self._setResHighlights({}) self._residue_outlines.setResOutlines({}) def undo(): self._setResHighlights(orig_color_map) self._residue_outlines.setResOutlines(orig_outline_map) desc = "Clear All Residue Highlights" return redo, undo, desc def getHighlightColorMap(self): """ :return: Read-only mapping between residue object and RGB tuple :rtype: types.MappingProxy """ color_map = self._residue_highlights return types.MappingProxyType(color_map) def _getHighlightColorMap(self, copy=True): """ :param copy: Whether to copy the map :type copy: bool :return: Mapping mapping between residue object and RGB tuple :rtype: dict """ color_map = self._residue_highlights if copy: color_map = color_map.copy() return color_map def duplicateSeqs(self, seqs_map, index=None, replace_selection=False, source_aln=None): """ Copies the existing sequences in this alignment to the bottom of this alignment. :param seqs_map: Dictionary of the new sequence copies mapped ot their source sequence :type seqs_map: dict :param index: The index at which to insert; if None, seqs are appended. Must be None if adding single-chain sequences. :type index: int :param replace_selection: If the selection should be replaced with the new sequences :type replace_selection: bool :param source_aln: Alignment to get the color map data, None if this alignment is the source :type source_aln: _ProteinAlignment or None """ seqs = list(seqs_map.keys()) if index == 0: raise RuntimeError("Cannot import sequence as the reference.") self.addSeqs(seqs, index=index, replace_selection=replace_selection) self.duplicateSeqsHighlightColorMap(seqs_map, source_aln=source_aln) @command.do_command def duplicateSeqsHighlightColorMap(self, seqs_map, source_aln=None): """ Copies the color map highlighting of the original sequence onto the new sequence. :param seqs_map: Dictionary of new sequence copies mapped to their source sequence :type seqs_map: dict :param source_aln: Alignment to get the color map data, None if this alignment is the source :type source_aln: _ProteinAlignment or None """ orig_color_map = self._getHighlightColorMap() new_color_map = self._getHighlightColorMap(copy=False) if source_aln: source_color_map = source_aln._getHighlightColorMap() else: source_color_map = orig_color_map for seq, orig_seq in seqs_map.items(): for res, orig_res in zip(seq, orig_seq): color = source_color_map.get(orig_res) if color is not None: new_color_map[res] = color redo = partial(self._setResHighlights, new_color_map) undo = partial(self._setResHighlights, orig_color_map) plural_sequences = inflect.engine().plural("Sequence", len(seqs_map)) desc = f"Duplicate {len(seqs_map)} {plural_sequences}" return redo, undo, desc def _getHighlightCommands(self, residues, color): orig_color_map = self._getHighlightColorMap() new_color_map = self._getHighlightColorMap(copy=False) for res in residues: if not res.is_res: # Can't highlight gaps continue if color: new_color_map[res] = color else: new_color_map.pop(res, None) redo = partial(self._setResHighlights, new_color_map) undo = partial(self._setResHighlights, orig_color_map) return redo, undo def _setResHighlights(self, color_map): self._residue_highlights = color_map self.signals.resHighlightStatusChanged.emit(bool(color_map)) def getResOutlinesForSeq(self, seq): """ Get the residue outline blocks for the given sequence """ return self._residue_outlines.getResOutlinesForSeq(seq) def getOutlineMap(self): """ Get the read-only map of outlines """ return self._residue_outlines.getOutlineMap() @command.do_command def setSelectedResOutlineColor(self, color): sel_res = self.res_selection_model.getSelection() return self._residue_outlines._getResOutlineColorCmd(sel_res, color) def isWorkspace(self): """ :return: Whether this alignment is controlled by the structure model and only includes sequences that are currently included in the workspace. :rtype: bool """ return self._is_workspace def insertGapsToLeftOfSelection(self): """ Insert one gap to the left of every selected block of residues/gaps. (I.e., if three contiguous residues are selected, only one gap will be inserted, and it will be placed to the left of the first selected residue.) :raises AnchoredResidueError: if inserting gaps would break anchors """ aln_sel_indices = [[] for _ in self] for seq_i, res_i in self.res_selection_model.getSelectionIndices(): aln_sel_indices[seq_i].append(res_i) aln_gaps_to_add = [] for seq_sel_indices in aln_sel_indices: seq_sel_indices = sorted(seq_sel_indices) seq_gaps_to_add = [] prev_sel_index = -2 for cur_sel_index in seq_sel_indices: if cur_sel_index - 1 != prev_sel_index: seq_gaps_to_add.append(cur_sel_index) prev_sel_index = cur_sel_index aln_gaps_to_add.append(seq_gaps_to_add) self.addGapsBeforeIndices(aln_gaps_to_add) def deselectGaps(self): """ Deselect currently selected gaps """ res_selection_model = self.res_selection_model sel_residues = res_selection_model.getSelection() to_deselect = {elem for elem in sel_residues if elem.is_gap} with command.compress_command(self.undo_stack, "Deselect All Gaps"): res_selection_model.setSelectionState(to_deselect, False) def expandSelectionAlongSequences(self, between_gaps): """ Expand selected gaps along sequences, either expanding selected residues to fill between gaps, or expanding selected gaps to fill along gaps. :param between_gaps: Whether to expand the selection between or along gaps. True for between gaps, False for along gaps. :type between_gaps: bool """ res_selection_model = self.res_selection_model sel_residues = res_selection_model.getSelection() new_selection = set() for seq in {res.sequence for res in sel_residues}: last_div = -1 contains_selected = False for idx, res in enumerate(seq): if res.is_gap == between_gaps: if contains_selected: new_selection.update(seq[last_div + 1:idx]) last_div = idx contains_selected = False elif res in sel_residues: contains_selected = True if contains_selected: new_selection.update(seq[last_div + 1:]) with command.compress_command(self.undo_stack, "Expand Selection Along Sequences"): res_selection_model.setSelectionState(new_selection, True) def expandSelectionAlongColumns(self): """ Expand selection along the columns of selected elements. """ res_selection_model = self.res_selection_model sel_residues = res_selection_model.getSelection() new_selection = set() for col in self.columns(): if sel_residues.intersection(col): new_selection.update(col) with command.compress_command(self.undo_stack, "Expand Selection Along Columns"): res_selection_model.setSelectionState(new_selection, True) def expandSelectionFromReference(self): """ Expand selection along columns of selected reference elements """ res_selection_model = self.res_selection_model sel_residues = res_selection_model.getSelection() new_selection = set() for col in self.columns(): if col[0] in sel_residues: new_selection.update(col) with command.compress_command(self.undo_stack, "Expand Selection From Reference"): res_selection_model.setSelectionState(new_selection, True) def expandSelectionToFullChain(self): """ Select all the residues in any sequence in which there is an already selected residue """ sel_residues = self.res_selection_model.getSelection() sequences = (set(res.sequence) for res in sel_residues) to_select = set.union(*sequences) with command.compress_command(self.undo_stack, self._EXPAND_SELECTION_TO_FULL_CHAIN): self.res_selection_model.setSelectionState(to_select, True) def expandSelectionToAnnotationValues(self, anno, ann_index=0, cdr_scheme=None): """ Expand the selection to other residues with the same annotation values. See `ProteinSequence.getAnnotationValueForComparison` for details of what "the same value" means. :param anno: Protein sequence annotation enum member. If anno.can_expand is False, this method will be a no-op. :type anno: annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES :param ann_index: Annotation index for multi-value annotations :type ann_index: int :param cdr_scheme: CDR scheme for antibody annotation :type cdr_scheme: annotation.AntibodyCDRScheme """ if not anno.can_expand: return def get_anno_val(seq, col): return seq.getAnnotationValueForComparison(col, anno, ann_index, cdr_scheme) res_selection_model = self.res_selection_model sel_residues = res_selection_model.getSelection() anno_values = set() for elem in sel_residues: if elem.is_gap: continue seq = elem.sequence anno_val = get_anno_val(seq, elem.idx_in_seq) if anno_val is not None: anno_values.add(anno_val) new_selection = set() for seq in self: prev_idx = None prev_anno = None for col, elem in enumerate(seq): if elem.is_gap: continue anno_val = get_anno_val(seq, col) if anno_val is not None and anno_val in anno_values: new_selection.add(elem) if anno_val == prev_anno and col - prev_idx > 1: # Also select gaps inside the annotation new_selection.update(seq[prev_idx + 1:col]) prev_idx = col prev_anno = anno_val new_selection -= sel_residues if not new_selection: return with command.compress_command( self.undo_stack, f"Expand selection to same {anno.title} values"): res_selection_model.setSelectionState(new_selection, True) def setResSelectionStateForSelectedSeqs(self, selected: bool): """ Set the selection state for all residues in selected sequences """ sel_seqs = self.getSelectedSequences() if not sel_seqs: return residues = itertools.chain(*sel_seqs) select = "Select" if selected else "Deselect" with command.compress_command( self.undo_stack, f"{select} Residues for Selected Sequences"): self.res_selection_model.setSelectionState(residues, selected) def selectResiduesWithStructure(self): """ Selects all residues with structure """ sel_res_list = self.getResiduesWithStructure() res_selection_model = self.res_selection_model with command.compress_command(self.undo_stack, "Select Residues with Structure"): res_selection_model.clearSelection() res_selection_model.setSelectionState(sel_res_list, True) def selectColumns(self, cols, clear=False): """ Select residues in the specified columns of the alignment :param cols: Columns to be selected :type cols: list(int) """ aln = itertools.zip_longest(*self) res_sel = itertools.chain(*[c for i, c in enumerate(aln) if i in cols]) res_sel = [r for r in res_sel if r is not None] with command.compress_command(self.undo_stack, "Select Columns"): if clear: self.res_selection_model.clearSelection() if res_sel: self.res_selection_model.setSelectionState(res_sel, True) def selectAntibodyCDR(self, scheme): """ Select residues with Antibody CDR. :param scheme: Antibody CDR scheme to use :type scheme: `schrodinger.protein.annotation.AntibodyCDRScheme` """ self.res_selection_model.clearSelection() sel_res = [] for seq in self: for idx, res in enumerate(seq): if res and not res.is_gap: if seq.annotations.getAntibodyCDR( idx, scheme).label != annotation.AntibodyCDRLabel.NotCDR: sel_res.append(res) self.res_selection_model.setSelectionState(sel_res, True) def selectBindingSites(self): """ Select residues with binding site contacts. """ self.res_selection_model.clearSelection() sel_res = [] NC = annotation.BINDING_SITE.NoContact for seq in self: if seq.getStructure() is None: continue bsa = seq.annotations.binding_sites for idx, res in enumerate(seq): if any(b != NC for b in bsa[idx]): sel_res.append(res) if sel_res: self.res_selection_model.setSelectionState(sel_res, True) def selectBindingSitesForLigand(self, ligand, entry_id): """ Select the binding site residues of the ligand across all the sequences of the protein. :param ligand: Ligand name. :type ligand: str :param entry_id: Entry id of the protein-ligand complex. :type entry_id: int """ residues = [] for seq in self: if seq.entry_id != entry_id: continue binding_site_res_for_lig = seq.annotations.binding_site_residues.get( ligand) if binding_site_res_for_lig: residues.extend(binding_site_res_for_lig) undo_desc = f"Select binding site residues for {ligand}" with command.compress_command(self.undo_stack, undo_desc): self.res_selection_model.clearSelection() self.res_selection_model.setSelectionState(residues, True) def selectColsWithStructure(self): """ Select all columns that contain only structured residues """ aln = itertools.zip_longest(*self) cols = [ i for i, c in enumerate(aln) if any(r and r.hasStructure() for r in c) ] self.selectColumns(cols, clear=True) def selectIdentityColumns(self): """ Select all identity columns in the alignment """ cols = [] if self.getReferenceSeq(): for column_idx, column in enumerate(self.columns()): # columns returns None for implicit terminal gaps fmt_column_set = { res.short_code if res.is_res else None for res in column } # The column is identical if it contains exactly one non-gap code identical = len( fmt_column_set) == 1 and None not in fmt_column_set if identical: cols.append(column_idx) self.selectColumns(cols, clear=True) def selectAlignedResidues(self): """ Selects residues in columns containing no gaps. """ self.res_selection_model.clearSelection() aln = itertools.zip_longest(*self) sel_res = [] for col in aln: ref_res = col[0] if not ref_res or ref_res.is_gap: continue col_res = (r for r in col if r is not None and not r.is_gap) aln_res = [r for r in col_res if r.short_code == ref_res.short_code] if len(aln_res) > 1: # More than just the reference seq is aligned. sel_res.extend(aln_res) if sel_res: self.res_selection_model.setSelectionState(sel_res, True) def setSecondaryStructureSelectionState(self, res, select=True): """ Set the selection state of residues in the same secondary structure as a specified residue. :param res: Residue to update the related secondary structure of :type res: residue.Residue :param select: If True, select the related secondary structure. Otherwise deselect it. :type select: bool """ if res.secondary_structure in (None, structure.SS_NONE): return else: seq = res.sequence ss = seq.secondary_structures for (start, end), _ in ss: if start <= res.idx_in_seq <= end: self.res_selection_model.setSelectionState( seq[start:end + 1], select) break def setRunSelectionState(self, res, select, expand_cols=False): """ Select or deselect a run of contiguous residues or gaps from a sequence :param res: Residue or gap element to select the contiguous run for. :type res: residue.Residue :param select: Whether to select the run. If False, the run will be deselected. :type select: bool :param expand_cols: Whether to expand the selection along columns of the alignment. :type expand_cols: bool """ if res is None: return seq = res.sequence idx_to_sel = seq.getRun(res) if expand_cols: res_to_sel = itertools.chain( *[self.getColumn(i) for i in idx_to_sel]) else: res_to_sel = [seq[i] for i in idx_to_sel] self.res_selection_model.setSelectionState(res_to_sel, select) def invertResSelection(self): """ Invert the selection """ res_selection_model = self.res_selection_model all_elements = itertools.chain(*self) sel_residues = res_selection_model.getSelection() with command.compress_command(self.undo_stack, "Invert Residue Selection"): res_selection_model.setSelectionState(all_elements, True) res_selection_model.setSelectionState(sel_residues, False) def deleteSelectedGaps(self): """ Delete all selected gaps. Selected residues will remain unchanged. """ selection = self.res_selection_model.getSelection() res_to_del = [res_elem for res_elem in selection if res_elem.is_gap] self.removeElements(res_to_del) def deleteSelection(self): """ Delete all selected residues and gaps. """ selection = self.res_selection_model.getSelection() self.removeElements(selection) def expandSelectionToRectangle(self): """ When a single block of residues is selected, expand that selection to a rectangle. I.e., if any residue in a column is selected, then select that column in any sequence that has any residues selected. """ selection = self.res_selection_model.getSelection() seqs = {res.sequence for res in selection} indices = {res.idx_in_seq for res in selection} to_select = {seq[i] for seq in seqs for i in indices if len(seq) > i} self.res_selection_model.setSelectionState(to_select, True) def moveSelectionToLeft(self, num_cols): """ Move the selection to the left along existing gaps. Note that this method assumes that there is a single rectangular block of selected residues. :param num_cols: How many columns should we try to move the selection. :type num_cols: int :return: How many columns was the selection moved. Will always be less than or equal to `num_cols`. If less than `num_cols`, it means that the selection couldn't be moved `num_cols` columns because there weren't enough existing gaps or because there were anchored residues. :rtype: int """ selection = self.res_selection_model.getSelection() if selection & self.getAnchoredResiduesWithRef(): # We can't move anchored residues return 0 seqs = {res.sequence for res in selection} indices = {res.idx_in_seq for res in selection} prior_col = min(indices) - 1 moves_possible = 0 gap_cols_to_remove = [] for col in range(prior_col, max(prior_col - num_cols, -1), -1): col_res = [cur_seq[col] for cur_seq in seqs] if all(res.is_gap for res in col_res): gap_cols_to_remove.append(col) moves_possible += 1 else: break if not moves_possible: return 0 last_new_gap_col = max(indices) new_gap_cols = list( range(last_new_gap_col - moves_possible + 1, last_new_gap_col + 1)) self._moveSelectionCommand(seqs, gap_cols_to_remove, new_gap_cols, moves_possible, "Left") return moves_possible def moveSelectionToRight(self, num_cols): """ Move the selection to the right along existing gaps or, if necessary, by inserting new gaps. Note that this method assumes that there is a single rectangular block of selected residues. :param num_cols: How many columns should we try to move the selection. :type num_cols: int :return: How many columns was the selection moved. Will always be less than or equal to `num_cols`. If less than `num_cols`, it means that the selection couldn't be moved `num_cols` columns due to anchored residues. :rtype: int """ selection = self.res_selection_model.getSelection() anchored = self.getAnchoredResiduesWithRef() if selection & anchored: # We can't move anchored residues return 0 seqs = {res.sequence for res in selection} indices = {res.idx_in_seq for res in selection} next_col = max(indices) + 1 gap_cols_to_remove = [] num_existing_gap_cols = 0 for col in range(next_col, next_col + num_cols): col_res = [ cur_seq[col] for cur_seq in seqs if col < len(cur_seq) and cur_seq[col] is not None ] if col_res and all(res.is_gap for res in col_res): gap_cols_to_remove.append(col) num_existing_gap_cols += 1 else: break first_new_gap_col = min(indices) if num_existing_gap_cols < num_cols: # We're not moving entirely along existing gaps, so make sure that # the move won't disrupt any anchoring. downstream_anchors = { res for res in anchored if res.sequence in seqs and res.idx_in_seq >= first_new_gap_col } if downstream_anchors: if num_existing_gap_cols == 0: # We can't move the selection at all. We return early so # that we don't add anything to the undo stack. return 0 num_cols = num_existing_gap_cols new_gap_cols = list( range(first_new_gap_col, first_new_gap_col + num_cols)) self._moveSelectionCommand(seqs, gap_cols_to_remove, new_gap_cols, num_cols, "Right") return num_cols @command.do_command def _moveSelectionCommand(self, seqs, gap_cols_to_remove, new_gap_cols, num_cols, direction): """ Create and run an undoable command to move the selection left or right. :param seqs: The sequences to modify. :type seqs: list(schrodinger.protein.sequence.Sequence) :param gap_cols_to_remove: The indices of columns where gaps should be removed. :type gap_cols_to_remove: list(int) :param new_gap_cols: The indices of columns where gaps should be added. Must be sorted. :type new_gap_cols: list(int) :param num_cols: The number of columns that the selection is being moved by. Only used to describe the command. :type num_cols: int :param direction: The direction that the selection is being moved in. Only used to describe the command. :type direction: str """ def redo(): with self.suspendAnchors(): elements_to_remove = [] gaps_to_insert = [[] for _ in self] for cur_seq in seqs: if gap_cols_to_remove: elements_to_remove.extend(cur_seq[i] for i in gap_cols_to_remove if i < len(cur_seq)) gaps_to_insert[self.index(cur_seq)] = [ col for i, col in enumerate(new_gap_cols) if col < len(cur_seq) + i ] self._aln.removeElements(elements_to_remove) self._aln.addGapsByIndices(gaps_to_insert) def undo(): with self.suspendAnchors(): elements_to_remove = [] gaps_to_insert = [[] for _ in self] for cur_seq in seqs: elements_to_remove.extend( cur_seq[i] for i in new_gap_cols if i < len(cur_seq)) if gap_cols_to_remove: gaps_to_insert[self.index(cur_seq)] = gap_cols_to_remove self._aln.removeElements(elements_to_remove) self._aln.addGapsByIndices(gaps_to_insert) description = f"Move Selection {num_cols} Columns to the {direction}" return redo, undo, description def hideSelectedSeqs(self): """ Hide the selected sequences. If the reference seq is selected, it will not be hidden. """ if not self.seq_selection_model.hasSelection(): return sel_seqs = self.seq_selection_model.getSelection() self._showHideSeqCommand(to_hide=sel_seqs, desc="Hide Selected Sequences") def showAllSeqs(self): """ Show all currently hidden sequences. """ self._showHideSeqCommand(to_show=self._hidden_seqs, desc="Show All Sequences") def showSeqs(self, sequences, hide_others=False): """ Show the specified sequences, optionally hiding others. :param sequences: Sequences to show :type sequences: set :param hide_others: Whether to hide the other sequences (will not hide reference seq). This option is ignored if `sequences` is empty. :type hide_others: bool """ if not sequences: return sequences = set(sequences) desc = "Show Sequences" if hide_others: to_hide = set(self).difference(sequences) desc = f"{desc} and Hide Others" else: to_hide = set() self._showHideSeqCommand(sequences, to_hide, desc=desc) @command.do_command def _showHideSeqCommand(self, to_show=(), to_hide=(), desc=None): """ Commands for hiding/showing sequences. The reference seq cannot be hidden. """ to_show = set(to_show) # Don't re-show seqs that are already shown to_show.difference_update(self.getShownSeqs()) to_hide = set(to_hide) # Don't allow hiding the reference seq to_hide.discard(self.getReferenceSeq()) if not to_show and not to_hide: return command.NO_COMMAND if desc is None: desc_parts = [] if to_hide: desc_parts.append("Hide {len(to_hide)}") if to_show: desc_parts.append("Show {len(to_show)}") # e.g. "Hide 5 and Show 6 Sequences" desc = " and ".join(desc_parts) + " Sequences" def redo(): self._setSeqsHiddenState(to_hide) self._setSeqsHiddenState(to_show, hidden=False) # Need to emit signal before selecting because seq selection model # doesn't allow selecting hidden seqs self._emitHiddenSeqsChanged() self._setHiddenSeqsSelectedState(to_hide) self._setHiddenSeqsSelectedState(to_show, hidden=False) def undo(): self._setSeqsHiddenState(to_hide, hidden=False) self._setSeqsHiddenState(to_show) # Need to emit signal before selecting because seq selection model # doesn't allow selecting hidden seqs self._emitHiddenSeqsChanged() self._setHiddenSeqsSelectedState(to_hide, hidden=False) self._setHiddenSeqsSelectedState(to_show) return redo, undo, desc @command.from_command_only def _setSeqsHiddenState(self, sequences, hidden=True): """ Command implementation for hiding/showing sequences. Calling code is responsible for removing any sequences that should never be hidden (e.g. reference seq) """ if not sequences: return if hidden: self._hidden_seqs.update(sequences) else: self._hidden_seqs -= sequences @command.from_command_only def _setHiddenSeqsSelectedState(self, sequences, hidden=True): """ Command implementation for updating selection state for hiding/showing sequences """ if not sequences: return residues_to_toggle = set() if hidden: # Find and cache residues to deselect for res in self.res_selection_model.getSelection(): seq = res.sequence if seq in sequences: residues_to_toggle.add(res) self._hidden_seq_selected_residues.setdefault( seq, set()).add(res) else: # Retrieve previously selected residues from cache for seq in sequences: residues = self._hidden_seq_selected_residues.pop(seq, ()) residues_to_toggle.update(residues) selected = not hidden # TODO MSV-3094 Once sequence selection is undoable, add # _undoable=False self.seq_selection_model.setSelectionState(sequences, selected) self.res_selection_model.setSelectionState(residues_to_toggle, selected, _undoable=False) def setSeqFilterEnabled(self, enabled): if enabled == self._filter_enabled: return self._filter_enabled = enabled if self._name_query: self._emitHiddenSeqsChanged() def setSeqFilterQuery(self, query): if query == self._name_query: return self._name_query = query if self._filter_enabled: self._emitHiddenSeqsChanged() def _emitHiddenSeqsChanged(self, *, _invalidate=True): if _invalidate: self._invalidateHiddenSeqCache() any_hidden = self.anyHidden() self.signals.hiddenSeqsChanged.emit(any_hidden) def _emitHiddenSeqsChangedOnSeqChange(self): """ If a change in seqs changes hidden sequences, emit hiddenSeqsChanged """ previous_any_hidden = self.anyHidden() self._invalidateHiddenSeqCache() new_any_hidden = self.anyHidden() if new_any_hidden != previous_any_hidden: self._emitHiddenSeqsChanged(_invalidate=False) def getShownSeqs(self): """ Return the sequences that are shown (not hidden or filtered out) :rtype: list """ return [s for s, shown in zip(self, self.getSeqShownStates()) if shown] def getSeqShownStates(self): """ Return whether each sequence in the alignment is shown (not hidden or filtered out) :rtype: list[bool] """ if self._seq_shown_state_cache is None: if self._seqShownEarlyReturn(): self._seq_shown_state_cache = [True] * len(self) else: self._seq_shown_state_cache = list(self._getSeqShownStates()) return self._seq_shown_state_cache def anyHidden(self): if self._any_hidden is None: if self._seqShownEarlyReturn(): self._any_hidden = False else: self._any_hidden = not all(self._getSeqShownStates()) return self._any_hidden def _invalidateHiddenSeqCache(self): self._any_hidden = None self._seq_shown_state_cache = None def _seqShownEarlyReturn(self): """ Check whether any sequences are hidden or filtering is enabled. Even if filtering is enabled, all sequences might still be shown. """ return not self._hidden_seqs and (not self._filter_enabled or not self._name_query) def _getSeqShownStates(self): tests = [] if self._hidden_seqs: tests.append(lambda seq: seq not in self._hidden_seqs) if self._filter_enabled: tests.append(lambda seq: self._name_query in seq.name) for idx, seq in enumerate(self): if idx == 0: yield True else: yield all(func(seq) for func in tests) def isSeqHidden(self, seq): return seq in self._hidden_seqs def _initHomologyCache(self): self._homology_status_cache = weakref.WeakKeyDictionary() def resetHomologyCache(self): changed_seqs = list(self._homology_status_cache.keys()) self._initHomologyCache() for seq in changed_seqs: self.signals.homologyStatusChanged.emit(seq) def getHomologyStatus(self, seq): """ Return the homology modeling status for the given sequence. :param seq: Sequence to check status :type seq: sequence.ProteinSequence :return: Homology modeling status :rtype: schrodinger.application.msv.gui.homology_modeling.hm_models.HomologyStatus or NoneType """ return self._homology_status_cache.get(seq) def setHomologyStatus(self, seq, status): """ Set the homology modeling status for the given sequence. :param seq: Sequence to set status :type seq: sequence.ProteinSequence :param status: Homology modeling status :type status: schrodinger.application.msv.gui.homology_modeling.hm_models.HomologyStatus """ self._homology_status_cache[seq] = status self.signals.homologyStatusChanged.emit(seq) def _initHomologyCompositeResidues(self): self._homology_composite_residues = set() def resetHomologyCompositeResidues(self): """ Reset the homology modeling composite residues, if any """ curr_residues = self.homology_composite_residues if not curr_residues: return self.updateHomologyCompositeResidues(to_add=(), to_remove=curr_residues) @property def homology_composite_residues(self): """ Residues to use for composite homology modeling :rtype: frozenset(residue.Residue) """ return frozenset(self._homology_composite_residues) def updateHomologyCompositeResidues(self, to_add, to_remove, *, signal=True): """ Update the residues to use for composite homology modeling. :param to_add: Residues to add :type to_add: collections.abc.Iterable(residue.Residue) :param to_remove: Residues to remove :type to_remove: collections.abc.Iterable(residue.Residue) :param signal: Whether to emit a signal due to the change :type signal: bool """ if to_remove: self._homology_composite_residues.difference_update(to_remove) if to_add: self._homology_composite_residues.update(to_add) if signal and to_remove or to_add: self.signals.homologyCompositeResiduesChanged.emit() def isHomologyCompositeResidue(self, res): return res in self._homology_composite_residues def _initPairwiseConstraints(self): self._pairwise_constraints = _PairwiseConstraints() def resetPairwiseConstraints(self): self._pairwise_constraints.reset() self.signals.pairwiseConstraintsChanged.emit() @property def pairwise_constraints(self): return self._pairwise_constraints def setRefConstraint(self, res): self._pairwise_constraints.setRefConstraint(res) self.signals.pairwiseConstraintsChanged.emit() def setOtherConstraint(self, res): self._pairwise_constraints.setOtherConstraint(res) self.signals.pairwiseConstraintsChanged.emit() def _initHMLigandConstraints(self): self._hm_ligand_constraints = _HMLigandConstraints() self._hm_ligand_constraints.constraintsChanged.connect( self.signals.homologyLigandConstraintsChanged) @property def hm_ligand_constraints(self): yield from self._hm_ligand_constraints.constraints def hasHMLigandConstraints(self): return self._hm_ligand_constraints.hasConstraints() def isHMLigandConstraint(self, res, ligand): return self._hm_ligand_constraints.isConstraint(res, ligand) def setHMLigandConstraint(self, res, ligand): self._hm_ligand_constraints.handlePick(res, ligand) def clearHMLigandConstraints(self): self._hm_ligand_constraints.reset() def _removeConstraintsForRemovedSequences(self, start_idx, end_idx): sequences = self[start_idx:end_idx + 1] self._hm_ligand_constraints._onSequencesAboutToBeRemoved(sequences) self._hm_proximity_constraints._onSequencesAboutToBeRemoved(sequences) def _removeConstraintsForRemovedResidues(self, residues): self._hm_ligand_constraints._onResiduesAboutToBeRemoved(residues) self._hm_proximity_constraints._onResiduesAboutToBeRemoved(residues) def _initHMProximityConstraints(self): self._hm_proximity_constraints = _HMProximityConstraints() self._hm_proximity_constraints.constraintsChanged.connect( self.signals.homologyProximityConstraintsChanged) @property def proximity_constraints(self): return self._hm_proximity_constraints def setHMProximityConstraint(self, res): self._hm_proximity_constraints.setConstraint(res) def clearHMProximityConstraints(self): self._hm_proximity_constraints.reset() def alnSetResSelected(self): """ Whether any selected residues are in sequences in an alignment set :rtype: bool """ if not self.hasAlnSets() or not self.res_selection_model.hasSelection(): return False seqs_in_sets = set().union(*self.alnSets()) return any(res.sequence in seqs_in_sets for res in self.res_selection_model.getSelection()) def _getCurrentAlnSets(self, seqs): """ Get the current alignment sets for all given sequences (and which sequences aren't part of any alignment set). Also return the current set id counter value, as this will be required for undoing an alignment set change. :param seqs: The sequences to get the alignment sets for. :type seqs: List[sequece.Sequence] :return: A tuple of: - A dictionary of {set name: AlnSetInfo for the set} for all given sequences that are currently in a set. - The current set id counter value (i.e. the set id that will be given to the next set that's created). - A list of sequences that aren't currently in any set. :rype: tuple(dict(str, AlnSetInfo), int, set(sequence.Sequence)) """ aln_sets = {} no_set = set() for cur_seq in seqs: cur_aln_set = self.alnSetForSeq(cur_seq) if cur_aln_set is None: no_set.add(cur_seq) else: if cur_aln_set.name not in aln_sets: aln_sets[cur_aln_set.name] = AlnSetInfo(cur_aln_set.set_id) aln_sets[cur_aln_set.name].seqs.add(cur_seq) return aln_sets, self._aln._set_id_counter, no_set def _restoreAlnSets(self, old_aln_sets, old_set_id_counter, old_no_set=None): """ Restore sequences to their previous alignment sets. Also restore the previous set id counter value. :param old_aln_sets: A dictionary of {set names: `AlnSetInfo` containing sequences that should be restored to the set}. :type old_aln_sets: dict(str, AlnSetInfo) :param old_set_id_counter: The set id counter value to restore (i.e. the set id that will be given to the next set that's created). :type old_set_id_counter: int :param old_no_set: Sequences that should be removed from their current alignment set. :type old_no_set: set(sequence.Sequence) or None """ if old_no_set is not None: self._aln._removeSeqsFromAlnSetNoReordering(old_no_set) for cur_set_name, (cur_set_id, cur_seqs) in old_aln_sets.items(): self._aln._addSeqsToAlnSetNoReordering(cur_seqs, cur_set_name) self._aln.getAlnSet(cur_set_name).set_id = cur_set_id self._aln._set_id_counter = old_set_id_counter @command.do_command def addSeqsToAlnSet(self, seqs, set_name): # See alignment.BaseAlignment for method documentation seqs = set(seqs) old_aln_sets, old_set_id, old_no_set = self._getCurrentAlnSets(seqs) if set_name in old_aln_sets: seqs -= old_aln_sets[set_name].seqs if not seqs: return command.NO_COMMAND move_indices = self._getAddSeqsToAlnSetOrdering(seqs, set_name) move_redo, move_undo = self._getReorderSequencesFuncs(move_indices) def redo(): move_redo() self._aln._addSeqsToAlnSetNoReordering(seqs, set_name) def undo(): self._restoreAlnSets(old_aln_sets, old_set_id, old_no_set) move_undo() return redo, undo, f"Add {len(seqs)} Sequences to Set {set_name}" @command.do_command def removeSeqsFromAlnSet(self, seqs): # See alignment.BaseAlignment for method documentation old_aln_sets, old_set_id, old_no_set = self._getCurrentAlnSets(seqs) seqs = set(seqs) - old_no_set if not seqs: return command.NO_COMMAND move_indices = self._getRemoveFromAlnSetOrdering(seqs) move_redo, move_undo = self._getReorderSequencesFuncs(move_indices) def redo(): move_redo() self._aln._removeSeqsFromAlnSetNoReordering(seqs) def undo(): self._restoreAlnSets(old_aln_sets, old_set_id) move_undo() return redo, undo, f"Remove {len(seqs)} Sequences from Alignment Sets" @command.do_command def renameAlnSet(self, old_name, new_name): original_set_names = self._aln.alnSetNames() if old_name not in original_set_names: raise ValueError(f"No Set {old_name}") if new_name in original_set_names: raise ValueError(f"Set {new_name} already a set") redo = partial(self._aln.renameAlnSet, old_name, new_name) undo = partial(self._aln.renameAlnSet, new_name, old_name) return redo, undo, f'Rename Alignment Set "{old_name}"' @command.do_command def gatherAlnSets(self): seq_indexes = self._getGatherAlnSetsReordering() redo, undo = self._getReorderSequencesFuncs(seq_indexes) return redo, undo, "Gather Alignment Sets"
[docs]class AlnSetInfo: """ Information needed to undo the removal of an alignment set. :ivar set id: The set ID of the set :vartype set_id: int :ivar seqs: The sequences that were removed from the set :vartype seqs: set(sequence.Sequence) """
[docs] def __init__(self, set_id): self.set_id = set_id self.seqs = set()
def __iter__(self): return iter((self.set_id, self.seqs))
[docs]class AnnotationRowInfo(typing.NamedTuple): """ Tuple for information about an annotation row. :ivar seq: The sequence associated with the annotation. Is None for alignment/global annotations. :ivar ann: The annotation type :ivar idx: For multi-row annotations, the index of the row. """ seq: typing.Union[sequence.ProteinSequence, None] ann: typing.Union[annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES, annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES] idx: int = 0
[docs]class SequenceAnnotationRowInfo(AnnotationRowInfo): def __new__(cls, seq, ann, idx=0): return super().__new__(cls, seq, ann, idx)
[docs]class GlobalAnnotationRowInfo(AnnotationRowInfo): """ Annotation info for global annotations. `seq` is always None because the annotation does not apply to a specific sequence. `idx` is always None because there are no multi-row global annotations. """ def __new__(cls, ann): return super().__new__(cls, None, ann, None)
# Register alignments as sequences so Hypothesis doesn't complain about # sampling from them collections.abc.Sequence.register(_ProteinAlignment)
[docs]class GuiProteinAlignment(json.JsonableClassMixin, _ProteinAlignment, metaclass=msv_utils.WrapperMetaClass, wraps=alignment.ProteinAlignment, wrapped_name='_aln', instance_attrs=('annotations',)):
[docs] def toJsonImplementation(self): json_dict = self._aln.toJsonImplementation() res_idxs = self.getResidueIndices(self._residue_highlights.keys(), sort=False) highlight_list = [] for res_idx_tuple, color_tuple in zip( res_idxs, self._residue_highlights.values()): seq_idx, res_idx = res_idx_tuple color_list = list(color_tuple) highlight_list.append([seq_idx, res_idx, color_list]) json_dict['res_highlights'] = highlight_list json_dict['seq_expansion'] = [] for seq in self: json_dict['seq_expansion'].append(self.isSeqExpanded(seq)) return json_dict
[docs] @classmethod def fromJsonImplementation(cls, json_obj, is_workspace=False): undo_aln = cls(is_workspace=is_workspace) deserialized_aln = alignment.ProteinAlignment.fromJson(json_obj) undo_aln._setInnerAlignment(deserialized_aln) highlights = json_obj['res_highlights'] highlight_dict = {} for seq_idx, res_idx, color_list in highlights: res = undo_aln[seq_idx][res_idx] highlight_dict[res] = tuple(color_list) undo_aln._residue_highlights = highlight_dict for seq, expand in zip(undo_aln, json_obj['seq_expansion']): undo_aln.setSeqExpanded(seq, expand) return undo_aln
[docs] @json.adapter(version=49004) def adapter49004(cls, json_dict): json_dict['seq_expansion'] = [] for _ in range(len(json_dict['sequences'])): json_dict['seq_expansion'].append(True) return json_dict
[docs] @json.adapter(version=48008) def adapter48008(cls, json_dict): # res_highlights was not serialized until 48008 json_dict['res_highlights'] = [] return json_dict
[docs]class CombinedChainResidueSelectionModel(ResidueSelectionModel): """ A residue selection model for `CombinedChainProteinAlignment`. This selection model stores the selected combined-chain sequences and also updates the split-chain selection model. That way, the selection changes won't be lost when the user toggles back to split-chain view. Note that the `ResidueSelectionModel` and `CombinedChainResidueSelectionModel` for the same alignment will differ in their return values for `isSingleBlockSelected` and `numBlocksSelected` because of differences in the underlying sequences (but not `isSingleBlockSingleSeqSelected`, since that method requires that all residues be in the same chain). """
[docs] def __init__(self, aln, split_res_selection_model): """ :param aln: The combined-chain alignment :type aln: CombinedChainProteinAlignment :param split_res_selection_model: The residue selection model for the split-chain alignment :type split_res_selection_model: ResidueSelectionModel """ super().__init__(aln) self._split_res_selection_model = split_res_selection_model # Use sets instead of WeakSets because combined-chain sequences don't # store their CombinedChainResidueWrappers. (Instead, they're generated # as needed and garbage collected as soon as they fall out of scope.) self._selection = set() self._old_selection = set() combined_residues = map(aln.combinedResForSplitRes, split_res_selection_model.getSelection()) self._selection.update(combined_residues)
def _toCombinedAndSplitResidues(self, residues): """ Given an iterable of combined or split residues, return a tuple with the corresponding combined AND split residues. :return: A tuple with two lists containing the combined chain residues in the first and the split chain residues in the second. :rtype: tuple(list, list) """ residues = list(residues) if not residues: return residues, residues if isinstance(residues[0], residue.CombinedChainResidueWrapper): split_residues = (res.split_res for res in residues) else: split_residues = residues residues = map(self.aln.combinedResForSplitRes, split_residues) return residues, split_residues
[docs] def setCurrentSelectionState(self, residues, selected): residues, split_residues = self._toCombinedAndSplitResidues(residues) if not residues: return super().setCurrentSelectionState(residues, selected) self._split_res_selection_model.setCurrentSelectionState( split_residues, selected)
[docs] def finishCurrentSelection(self): super().finishCurrentSelection(_undoable=False) # Don't end macro until the very end self._split_res_selection_model.finishCurrentSelection(_undoable=True)
[docs] def setSelectionState(self, residues, selected, *, _undoable=True): """ Set the selection state of the given residues. Either combined-chain or split-chain residues may be given. :param residues: The residues to select or deselect. :type items: Iterable(residue.CombinedChainResidueWrapper) or Iterable(residue.AbstractSequenceElement) :param selected: Whether to select or deselect the residues. :type selected: bool :param bool _undoable: Whether to create an undoable command. Should only be passed by a command implementation. """ residues, split_residues = self._toCombinedAndSplitResidues(residues) if not residues: return with contextlib.ExitStack() as stack: if _undoable: residues, desc = self._getSelectionResiduesAndDesc( set(residues), selected) stack.enter_context( command.compress_command(self.undo_stack, desc)) super().setSelectionState(residues, selected, _undoable=_undoable) self._split_res_selection_model.setSelectionState( split_residues, selected, _undoable=_undoable)
[docs] def clearSelection(self, *, _undoable=True): # See parent class for method documentation with contextlib.ExitStack() as stack: if _undoable: desc = self._CLEAR_DESC stack.enter_context( command.compress_command(self.undo_stack, desc)) super().clearSelection(_undoable=_undoable) self._split_res_selection_model.clearSelection(_undoable=_undoable)
[docs] def isSingleBlockSingleSeqSelected(self): # See parent class for method documentation # Make sure all residues are from the same chain, not just the same # sequence sel_seq = {res.split_sequence for res in self._selection} if len(sel_seq) != 1: return False return super().isSingleBlockSingleSeqSelected()
[docs]class CombinedChainSequenceSelectionModel(SequenceSelectionModel): """ A sequence selection model for `CombinedChainProteinAlignment`. Note that this selection model stores the selected combined-chain sequences and also updates the split-chain selection model. """
[docs] def __init__(self, aln, split_seq_selection_model): """ :param aln: The combined-chain alignment :type aln: CombinedChainProteinAlignment :param split_seq_selection_model: The sequence selection model for the split-chain alignment :type split_seq_selection_model: SequenceSelectionModel """ super().__init__(aln) self._split_seq_selection_model = split_seq_selection_model # select the combined version of everything that's selected in the split # chain alignment initial_selection = set() for seq in aln: for chain in seq.chains: if split_seq_selection_model.isSelected(chain): initial_selection.add(seq) continue self._selection.update(initial_selection) self._old_selection.update(initial_selection)
[docs] def setSelectionState(self, sequences, selected, *, update_split_aln=True): """ See parent class for method and positional argument documentation. :param update_split_aln: Whether to update sequence selection in the split-chain alignment. This class is responsible for keeping split- chain and combined-chain sequence selection in sync, so this should only be False if sequence selection in the split-chain alignment will be updated separately. :type update_split_aln: bool """ super().setSelectionState(sequences, selected) if update_split_aln: split_seqs = itertools.chain.from_iterable( seq.chains for seq in sequences) self._split_seq_selection_model.setSelectionState( split_seqs, selected)
[docs] def clearSelection(self): # See parent class for method documentation super().clearSelection() self._split_seq_selection_model.clearSelection()
[docs]class CombinedChainAnnotationSelectionModel(AnnotationSelectionModel): """ Class that tracks the selection state of sequence annotation as (sequence, annotation enum, annotation index) tuples """
[docs] def __init__(self, aln, split_ann_selection_model): """ :param aln: The combined-chain alignment :type aln: CombinedChainProteinAlignment :param split_ann_selection_model: The annotation selection model for the split-chain alignment :type split_ann_selection_model: AnnotationSelectionModel """ super().__init__(aln) self._split_ann_selection_model = split_ann_selection_model
[docs] def setSelectionState(self, items, selected): # See parent class for method documentation super().setSelectionState(items, selected) split_items = set() for ann_info in items: if isinstance(ann_info, GlobalAnnotationRowInfo): split_items.add(ann_info) elif isinstance(ann_info, SequenceAnnotationRowInfo): combined_seq, *rest = ann_info for seq in combined_seq.chains: new_info = SequenceAnnotationRowInfo(seq, *rest) split_items.add(new_info) else: raise TypeError( "Invalid selection item type, must be " "GlobalAnnotationRowInfo or SequenceAnnotationRowInfo.") self._split_ann_selection_model.setSelectionState(split_items, selected)
[docs] def clearSelection(self): # See parent class for method documentation super().clearSelection() self._split_ann_selection_model.clearSelection()
[docs]class GuiCombinedChainProteinAlignment( _ProteinAlignment, metaclass=msv_utils.WrapperMetaClass, wraps=alignment.CombinedChainProteinAlignment, wrapped_name='_aln', instance_attrs=('annotations',)): """ An undoable alignment containing combined-chain sequences (`sequence.CombinedChainProteinSequence` objects). """ _ANCHORED_RES_JSON_KEY = "anchored_residues"
[docs] def __init__(self, split_undoable_aln, *, chains_to_combine=None): """ :param split_undoable_aln: An undoable alignment containing split chain sequences. Note that, unlike `alignment.CombinedChainProteinAlignment`, changes made to the combined-chain alignment will automatically be reflected in the split-chain alignment. Also note that the reverse is not necessarily true; changes made to the split-chain alignment may not update the combined-chain alignment. If you modify the split-chain alignment and want a corresponding combined-chain alignment, you should create a new `GuiCombinedChainProteinAlignment` instance. :type split_undoable_aln: ProteinAlignment :param chains_to_combine: Information about which split-chain sequences in `split_undoable_aln` should be included in which combined-chain sequence. Should be a list of lists of indices. Each index refers to the split-chain sequence at that position of `split_undoable_aln`, and split-chain sequences that are listed together will be combined into the same combined-chain sequence. Each split-chain sequence from `split_undoable_aln` must be referenced exactly once. :type chains_to_combine: list[list[int]] """ combined_seq_aln = alignment.CombinedChainProteinAlignment( split_undoable_aln, chains_to_combine=chains_to_combine) self._split_undoable_aln = split_undoable_aln super().__init__(aln=combined_seq_aln) super().setUndoStack(split_undoable_aln.undo_stack) self._is_workspace = split_undoable_aln._is_workspace split_color_map = split_undoable_aln.getHighlightColorMap() # We intentionally change _residue_highlights from an IdDict to a # regular dictionary since CombinedChainResidueWrappers need to be # compared using equality, not identity. self._residue_highlights = { self.combinedResForSplitRes(res): color for res, color in split_color_map.items() }
def _initSelectionModels(self): # See parent class for method documentation self.res_selection_model = CombinedChainResidueSelectionModel( self, self._split_undoable_aln.res_selection_model) self.seq_selection_model = CombinedChainSequenceSelectionModel( self, self._split_undoable_aln.seq_selection_model) self.ann_selection_model = CombinedChainAnnotationSelectionModel( self, self._split_undoable_aln.ann_selection_model) def __deepcopy__(self, memo): # figure out which split-chain sequences are grouped together indices = [[ self._split_undoable_aln.index(chain) for chain in seq.chains ] for seq in self] split_aln = copy.deepcopy(self._split_undoable_aln, memo) combined_aln = self.__class__(split_aln, chains_to_combine=indices) self._copyAnchoringTo(combined_aln._aln) self._copyAlnSetsTo(combined_aln._aln) return combined_aln
[docs] def setSeqExpanded(self, seq, expanded=True): for chain in seq.chains: self._split_undoable_aln.setSeqExpanded(chain, expanded) self.signals.seqExpansionChanged.emit(seq, expanded)
[docs] def isSeqExpanded(self, seq): return any( self._split_undoable_aln.isSeqExpanded(chain) for chain in seq.chains)
[docs] def jsonDataWithoutSplitAln(self): """ Return a JSON-able dictionary that can be used to recreate this object using `fromJsonAndSplitAln`. :rtype: dict """ return {self._ANCHORED_RES_JSON_KEY: self._getAnchoredResidueIndices()}
[docs] @classmethod def fromJsonAndSplitAln(cls, split_aln, json_dict): """ Restore a serialized object. :param split_aln: The split-chain alignment. :type split_aln: ProteinAlignment :param json_dict: A dictionary returned by `jsonDataWithoutSplitAln` :type json_dict: dict :return: The restored alignment :rtype: GuiCombinedChainProteinAlignment """ combined_aln = cls(split_aln) indices = json_dict[cls._ANCHORED_RES_JSON_KEY] combined_aln._aln._anchorResidueIndices(indices) return combined_aln
[docs] def setUndoStack(self, undo_stack): # See parent class for method documentation super().setUndoStack(undo_stack) self._split_undoable_aln.setUndoStack(undo_stack)
[docs] def setReferenceSeq(self, seq): # See alignment.BaseAlignment for documentation self._assertCanSetReferenceSeq() desc = self._setReferenceSeqDesc(seq) split_aln_ordering = self._getSplitAlnRefSeqReordering(seq) with command.compress_command(self.undo_stack, desc): super().setReferenceSeq(seq) self._split_undoable_aln.reorderSequences(split_aln_ordering)
def _getSplitAlnRefSeqReordering(self, combined_seq): """ Returns an ordering for the split-chain alignment that will make the first chain of the specified combined-chain sequence the reference sequence and move all other chains of the combined-chain sequence to the top of the alignment (immediately after the reference sequence). :param combined_seq: The combined-chain reference sequence :type combined_seq: sequence.CombinedChainProteinSequence :return: The requested ordering :rtype: list[int] """ ref_chains = [ self._split_undoable_aln.index(chain) for chain in combined_seq.chains ] other_chains = [ i for i in range(len(self._split_undoable_aln)) if i not in ref_chains ] return ref_chains + other_chains
[docs] def addSeqs(self, seqs, index=None, replace_selection=False): """ Add multiple sequences to the alignment. Note that either split-chain sequences or combined-chain sequences may be added (but not both at the same time). This method accepts split-chain sequences so that a caller can add sequences generated by seqio.py (or structure_model.py) without needing to first excplicitly combine chains. :param seqs: Sequences to add. :type seqs: list[sequence.ProteinSequence] or list[sequence.CombinedChainProteinSequence] :param index: The index at which to insert; if None, seqs are appended. Must be None if adding single-chain sequences. :type index: int :param replace_selection: Whether to select the newly added sequences and deselect all other sequences. If False, selection will not be changed. :type replace_selection: bool """ if not seqs: return elif self._isCombinedChainSeq(seqs[0]): self._addCombinedSeqs(seqs, index, replace_selection) elif index is not None: raise RuntimeError("Cannot specify start when adding split-chain " "sequences to a combined-chain alignment.") else: self._addSplitSeqs(seqs, replace_selection)
def _addCombinedSeqs(self, seqs, index, replace_selection): """ Add multiple combined-chain sequences to the alignment. :param seqs: Sequences to add. :type seqs: list[sequence.CombinedChainProteinSequence] :param index: The index at which to insert; if None, seqs are appended. :type index: int :param replace_selection: Whether to select the newly added sequences and deselect all other sequences. If False, selection will not be changed. :type replace_selection: bool """ split_seqs = self._allChains(seqs) if index is None: split_index = None elif index == 0: split_index = 0 else: insert_split_after = self._getSplitAfterSeq(self[index - 1]) split_index = self._split_undoable_aln.index(insert_split_after) + 1 desc = self._addSeqsDesc(seqs) with command.compress_command(self.undo_stack, desc): super().addSeqs(seqs, index, replace_selection) self._split_undoable_aln.addSeqs(split_seqs, split_index, replace_selection) self._emitSyncWsResSelectionOnRedo(seqs) def _allChains(self, seqs): """ Return all chains for a given list of combined-chain sequences. :param seqs: The combined-chain sequences :type seqs: list[sequence.CombinedChainProteinSequence] :return: The split-chain sequences :rtype: list[sequence.ProteinSequence] """ return list( itertools.chain.from_iterable(cur_seq.chains for cur_seq in seqs)) def _addSplitSeqs(self, split_seqs, replace_selection): """ Add multiple split-chain sequences to the alignment. :param split_seqs: Sequences to add. :type split_seqs: list[sequence.ProteinSequence] :param replace_selection: Whether to select the newly added sequences and deselect all other sequences. If False, selection will not be changed. :type replace_selection: bool """ desc = self._addSeqsDesc(split_seqs) with command.compress_command(self.undo_stack, desc): super().addSeqs(split_seqs, replace_selection=replace_selection) self._split_undoable_aln.addSeqs( split_seqs, replace_selection=replace_selection) self._emitSyncWsResSelectionOnRedo(split_seqs) def _addSeqs(self, seqs, index, replace_selection): # See _ProteinAlignment._addSeqs for method documentation to_select = self._aln.addSeqs(seqs, index) if replace_selection: self.seq_selection_model.clearSelection() # We don't update sequence selection in the split alignment because # we haven't added the new sequences to it yet. They'll be selected # as part of the self._split_undoable_aln.addSeqs() call. self.seq_selection_model.setSelectionState(to_select, True, update_split_aln=False) @command.do_command def _emitSyncWsResSelectionOnRedo(self, seqs): """ Return a command that emits `syncWsResSelection` on redo, but does nothing on undo. (On undo, the sequences are about to be removed, so there's no need to worry about residue selection.) :param seqs: The sequences to synchronize the residue selection of. May be either split-chain or combined-chain sequences. (Residue selection will be updated in both the split-chain and combined-chain alignment regardless of sequence type.) :type seqs: list[sequence.AbstractSequence] """ redo = partial(self.signals.syncWsResSelection.emit, seqs) return redo, lambda: None, "Synchronize workspace residue selection"
[docs] def removeSeqs(self, seqs): """ Remove multiple combined-chain sequences from the alignment. :param seqs: Sequences to remove. Note that these must be combined- chain sequences (`sequence.CombinedChainProteinSequence`), not split-chain sequences (`sequence.ProteinSequence`) :type seqs: list[sequence.CombinedChainProteinSequence] """ if self.getReferenceSeq() in seqs: self._assertCanRemoveRef() split_seqs = self._allChains(seqs) desc = self._removeSeqsDesc(seqs) with command.compress_command(self.undo_stack, desc): self._removeSeqs(seqs) self._split_undoable_aln._removeSeqs(split_seqs)
[docs] def renameSeq(self, seq, new_name): # See alignment.BaseAlignment for documentation desc = self._renameSeqDesc(seq) with command.compress_command(self.undo_stack, desc): super().renameSeq(seq, new_name) for chain in seq.chains: self._split_undoable_aln.renameSeq(chain, new_name)
[docs] def clear(self): # See alignment.BaseAlignment for documentation with command.compress_command(self.undo_stack, self._CLEAR_DESC): super().clear() self._split_undoable_aln.clear()
def _getReorderSequencesFuncs(self, seq_indices): # See _ProteinAlignment for method documentation split_indices = [] for i in seq_indices: for chain in self[i].chains: split_index = self._split_undoable_aln.index(chain) split_indices.append(split_index) undo_indices = self._getUndoSequenceOrdering(seq_indices) undo_split_indices = self._split_undoable_aln._getUndoSequenceOrdering( split_indices) def redo(): self._aln.reorderSequences(seq_indices) self._split_undoable_aln._aln.reorderSequences(split_indices) def undo(): self._aln.reorderSequences(undo_indices) self._split_undoable_aln._aln.reorderSequences(undo_split_indices) return redo, undo
[docs] def moveSelectedSequences(self, after_seq): # See _ProteinAlignment for method documentation after_split_seq = self._getSplitAfterSeq(after_seq) selected_seqs = self.seq_selection_model.getSelection() selected_seqs.discard(self.getReferenceSeq()) split_seqs_to_move = self._allChains(selected_seqs) split_indices = self._split_undoable_aln._getMoveSequenceAfterIndices( after_split_seq, split_seqs_to_move) with command.compress_command(self.undo_stack, self._MOVE_SELECTED_SEQS_DESC): super().moveSelectedSequences(after_seq) self._split_undoable_aln.reorderSequences(split_indices)
def _getSplitAfterSeq(self, after_seq): """ If combined-chain sequences are to be inserted after the specified combined-chain sequence, return the split-chain sequence that the equivalent split-chain sequences should be inserted after. By inserting after the returned split-chain sequence we ensure that: - The combined-chain alignment order will be preserved if we were to discard the current combined-chain alignment and generate a new one from the split-chain alignment. - We don't insert the split-chain sequences in between two adjacent sequences from the same protein. :param after_seq: The combined-chain sequence that the combined-chain sequences are going to be inserted after. :type after_seq: sequence.CombinedChainProteinSequence :return: The split-chain sequence that the split-chain sequences should be inserted after. :rtype: sequence.ProteinSequence """ after_split_seq = None for split_seq in self._split_undoable_aln: if split_seq in after_seq.chains: after_split_seq = split_seq elif after_split_seq is not None: return after_split_seq if after_split_seq is not None: return after_split_seq raise RuntimeError("No after_seq chains found in the split-sequence " "alignment.") def _setResHighlights(self, color_map): # See _ProteinAlignment for method documentation super()._setResHighlights(color_map) split_color_map = { res.split_res: color for res, color in color_map.items() } self._split_undoable_aln._setResHighlights(split_color_map)
[docs] @msv_utils.const def findPattern(self, pattern): """ Finds a specified PROSITE pattern in all sequences. :param pattern: PROSITE pattern to search in sequences. See `protein.sequence.find_generalized_pattern` for documentation. :type pattern: str :returns: List of matching residues :rtype: list[protein.residue.CombinedChainResidueWrapper] """ residues = self._split_undoable_aln.findPattern(pattern) return list(map(self.combinedResForSplitRes, residues))
[docs] @command.do_command def alignChainStarts(self): # See CombinedChainProteinAlignment for method documentation if self.getInterChainAnchors(): raise alignment.AnchoredResidueError() gaps, chain_starts, end_of_ref = self._gapsToAddToAlignChainStarts() def redo(): self._aln._addGapsToChainStartsAndEnds(gaps) return chain_starts, end_of_ref undo = partial(self._aln._removeGapsFromChainStartsAndEnds, gaps) return redo, undo, "Align Chain Starts"
[docs] @command.do_command def adjustChainStarts(self, num_gaps): # See CombinedChainProteinAlignment for method documentation gaps_to_remove, gaps_to_add = self._adjustChainStartsToGaps(num_gaps) self._validateGapsToRemoveFromChainStartAndEnds(gaps_to_remove) def redo(): with self.suspendAnchors(): self._aln._removeGapsFromChainStartsAndEnds(gaps_to_remove) self._aln._addGapsToChainStartsAndEnds(gaps_to_add) def undo(): with self.suspendAnchors(): self._aln._removeGapsFromChainStartsAndEnds(gaps_to_add) self._aln._addGapsToChainStartsAndEnds(gaps_to_remove) return redo, undo, "Adjust Chain Starts"
def _validateGapsToRemoveFromChainStartAndEnds(self, gaps): """ Make sure that we can remove the specified number of gaps from the starts and ends of each chain in each sequence. :param gaps: The numbers of gaps to remove, formatted as gaps_to_remove[sequence_index][chain_index] = (gaps to remove to the start of the chain, gaps to remove to the end of the chain) :type gaps: list[list[tuple(int, int)]] :raise AssertionError: If some of the sequence elements to be removed aren't actually gaps. """ for seq, cur_gaps in zip(self, gaps): seq.validateGapsToRemoveFromChainStartAndEnds(cur_gaps) def _createRemoveElementsUndo(self, elements): # See parent class for method documentation. # We must reimplement this method here so that gaps adjacent to chain # boundaries are restored to the correct chain. orig_indices = self.getResidueIndices(elements) elems_to_restore = collections.defaultdict(list) for s_idx, r_idx in orig_indices: seq = self._aln[s_idx] elem = seq[r_idx] chain = elem.split_sequence elems_to_restore[seq].append((r_idx, elem, chain)) has_anchors = len(self.getAnchoredResidues()) > 0 if has_anchors: gaps_to_remove = dict() for seq, ele_info in elems_to_restore.items(): eles = [t[1] for t in ele_info] gaps = self._getAnchorConservingGapIdxs(seq, eles) gaps_to_remove[seq] = gaps selected = self.res_selection_model.getSelection() sel_to_remove = selected.intersection(elements) def undo(): with self.suspendAnchors(): for seq, res_list in elems_to_restore.items(): if has_anchors: gap_idxs = gaps_to_remove[seq] gaps = [seq[g_idx] for g_idx in gap_idxs] self._aln.removeElements(gaps) for r_idx, res, chain in res_list: self._aln._addElementByChain(seq, r_idx, chain, res) self.res_selection_model.setSelectionState(sel_to_remove, True, _undoable=False) return undo def _getRestoreGapsMethod(self): # See parent class for method documentation. # We must reimplement this method here so that gaps adjacent to chain # boundaries are restored to the correct chain. original_gaps = self.getGaps() gap_info = [[(gap.idx_in_seq, gap.split_res, gap.split_sequence) for gap in seq_gaps] for seq_gaps in original_gaps] selection = self.res_selection_model.getSelection() sel_gaps = [elem for elem in selection if elem.is_gap] sel_gap_idxs = self.getResidueIndices(sel_gaps) def undo(): with self.suspendAnchors(): self._aln.removeAllGaps() for seq, seq_gaps in zip(self, gap_info): for (index, gap, chain) in seq_gaps: self._aln._addElementByChain(seq, index, chain, gap) gaps_to_select = [ self._aln[res_i][seq_i] for (res_i, seq_i) in sel_gap_idxs ] self.res_selection_model.setSelectionState(gaps_to_select, True, _undoable=False) return undo
[docs] def expandSelectionToFullChain(self): """ Select all the residues in any sequence in which there is an already selected residue """ sel_residues = self.res_selection_model.getSelection() split_seqs = (res.split_sequence for res in sel_residues) split_residues = set.union(*(set(seq) for seq in split_seqs)) with command.compress_command(self.undo_stack, self._EXPAND_SELECTION_TO_FULL_CHAIN): self.res_selection_model.setSelectionState(split_residues, True)