Source code for ccpn.core.Residue

"""
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Geerten Vuister $"
__dateModified__ = "$dateModified: 2021-12-23 11:27:16 +0000 (Thu, December 23, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

import typing
from functools import partial
from ccpn.util import Common as commonUtil
from ccpn.core.Project import Project
from ccpn.core.Chain import Chain
from ccpn.core._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpn.core.lib import Pid
from ccpnmodel.ccpncore.api.ccp.molecule.MolSystem import Residue as ApiResidue
from ccpn.util.decorators import logCommand
from ccpn.core.lib.ContextManagers import deleteObject, undoStackBlocking, renameObject, undoBlock, \
    undoBlockWithoutSideBar, notificationEchoBlocking


[docs]class Residue(AbstractWrapperObject): """A molecular Residue, contained in a Chain, and containing Atoms. Crucial attributes: residueType (e.g. 'ALA'), residueVariant (NEF-based), sequenceCode (e.g. '123') """ #: Short class name, for PID. shortClassName = 'MR' # Attribute it necessary as subclasses must use superclass className className = 'Residue' _parentClass = Chain #: Name of plural link to instances of class _pluralLinkName = 'residues' # the attribute name used by current _currentAttributeName = 'residues' #: List of child classes. _childClasses = [] # Qualified name of matching API class _apiClassQualifiedName = ApiResidue._metaclass.qualifiedName() # Number of fields that comprise the object's pid; Used to get parent id's _numberOfIdFields = 2 # CCPN properties @property def _apiResidue(self) -> ApiResidue: """ API residue matching Residue""" return self._wrappedData @property def sequenceCode(self) -> str: """Residue sequence code and id (e.g. '1', '127B') """ obj = self._wrappedData objSeqCode = obj.seqCode result = (obj.seqInsertCode or '').strip() if objSeqCode is not None: result = str(objSeqCode) + result return result @property def _key(self) -> str: """Residue ID. Identical to sequenceCode.residueType. Characters translated for pid""" return Pid.createId(self.sequenceCode, self.residueType) @property def _localCcpnSortKey(self) -> typing.Tuple: """Local sorting key, in context of parent.""" return (self._wrappedData.seqId,) @property def _parent(self) -> Chain: """Chain containing residue.""" return self._project._data2Obj[self._wrappedData.chain] chain = _parent @property def residueType(self) -> str: """Residue type name string (e.g. 'ALA')""" return self._wrappedData.code3Letter or '' @property def shortName(self) -> str: return self._wrappedData.chemCompVar.chemComp.code1Letter or '?' @property def linking(self) -> str: """linking (substitution pattern) code for residue Allowed values are: For linear polymers: 'start', 'end', 'middle', 'single', 'break', 'cyclic' For other molecules: 'nonlinear' 'cyclic' and 'break' are used at the end of linear polymer stretches to signify, respectively, that the polymer is cyclic, or that the residue is bound to an unknown residue or to a cap, so that the linear polymer chain does not continue.""" molType = self._wrappedData.molType if molType in ('protein', 'DNA', 'RNA'): linkString = self._wrappedData.linking if linkString == 'none': return 'single' elif linkString in ('start', 'end'): return linkString else: assert linkString == 'middle', ("Illegal API linking value for linear polymer: %s" % linkString) nextResidue = self.nextResidue previousResidue = self.previousResidue if previousResidue is None: if nextResidue is None: return 'single' elif nextResidue is None: chainResidues = self.chain.residues if self is chainResidues[-1]: # Last residue in chain return 'middle' else: nextInLine = chainResidues[chainResidues.index(self) + 1] if nextInLine._wrappedData.linking in ('start', 'none'): # Next in chain is start or non-linear return 'middle' altSelf = nextInLine.previousResidue if (altSelf and altSelf._wrappedData.seqId > nextInLine.seqId and altSelf._wrappedData.linking == 'middle'): # Next residue is cyclic (start of) return 'middle' else: return 'break' else: # NBNB The detection of 'cyclic' only works if residues are given in # sequential order. This is not given - but is unlikely ever to break. seqId = self._wrappedData.seqId if (previousResidue._wrappedData.seqId > seqId and previousResidue._wrappedData.linking == 'middle'): return 'cyclic' elif (nextResidue._wrappedData.seqId < seqId and nextResidue._wrappedData.linking == 'middle'): return 'cyclic' else: return 'middle' elif molType == 'dummy': return 'dummy' else: # All other types have linking 'non-linear' in the wrapper return 'single' return self._wrappedData.linking @linking.setter def linking(self, value: str): # NBNB TBD FIXME - this will not work as intended when value is 'nonlinear' if value in ('break', 'cyclic'): value = 'middle' elif value == 'single': value = 'none' self._wrappedData.linking = value @property def residueVariant(self) -> typing.Optional[str]: """NEF convention Residue variant descriptor (protonation state etc.) for residue""" atomNamesRemoved, atomNamesAdded = self._wrappedData.getAtomNameDifferences() ll = ['-' + x for x in sorted(atomNamesRemoved)] ll.extend('+' + x for x in sorted(atomNamesAdded)) return ','.join(ll) or None @property def descriptor(self) -> str: """variant descriptor (protonation state etc.) for residue, as defined in the CCPN V2 ChemComp description.""" return self._wrappedData.descriptor @descriptor.setter def descriptor(self, value: str): self._wrappedData.descriptor = value @property def configuration(self) -> typing.Optional[str]: """Residue conformation or other non-covalent distinction. Example: cis/trans/None for the peptide bonds N-terminal to a residue""" return self._wrappedData.configuration @configuration.setter def configuration(self, value): # TODO implement this as a proper enumeration allowedValues = ('cis', 'trans', None) if value in allowedValues: self._wrappedData.configuration = value else: raise ValueError("%s configuration must be one of %s" % (self, allowedValues)) #========================================================================================= # CCPN functions #========================================================================================= @property def nextResidue(self) -> typing.Optional['Residue']: """Next residue in sequence, if any, otherwise None""" apiResidue = self._wrappedData molResidue = apiResidue.molResidue.nextMolResidue if molResidue is None: result = None self._project._logger.debug("No next residue - API ") else: result = self._project._data2Obj.get( apiResidue.chain.findFirstResidue(seqId=molResidue.serial)) return result @property def previousResidue(self) -> typing.Optional['Residue']: """Previous residue in sequence, if any,otherwise None""" apiResidue = self._wrappedData molResidue = apiResidue.molResidue.previousMolResidue if molResidue is None: result = None else: result = self._project._data2Obj.get( apiResidue.chain.findFirstResidue(seqId=molResidue.serial)) return result
[docs] def resetVariantToDefault(self): """Reset Residue.residueVariant to the default variant""" atomNamesMissing, extraAtomNames = self._wrappedData.getAtomNameDifferences() # No need for testing - the names returned are guaranteed to be missing/superfluous for atomName in atomNamesMissing: self.newAtom(name=atomName) for atomName in extraAtomNames: self.getAtom(atomName).delete()
def _setFragmentResidues(self, chainFragment, residues): """set the residues connected to the chainFragment CCPN Internal - ussed to handle removing reside link from the api """ chainFragment.__dict__['residues'] = tuple(residues) # in baseclass # @deleteObject() # def _delete(self): # """Delete the Residue wrapped data. # """ # self._wrappedData.delete()
[docs] @logCommand(get='self') def delete(self): """delete residue. Causes an error when just calling residue._wrappedData.delete() new method to delete from the chainFragment """ chainFragment = self._wrappedData.chainFragment apiResidue = self._wrappedData if self.allNmrResidues: raise TypeError('Cannot delete residue that has assigned nmrResidues') if self._wrappedData in chainFragment.residues: with undoBlock(): oldResidues = list(chainFragment.residues) newResidues = list(chainFragment.residues) # delRes = newResidues.pop(newResidues.index(apiResidue)) # delRes.delete() newResidues.pop(newResidues.index(apiResidue)) self._delete() # delete the residue from the fragment (no undo items entered into stack) chainFragment.__dict__['residues'] = tuple(newResidues) # add new undo item to set the residues in the chainFragment with undoStackBlocking() as addUndoItem: addUndoItem(undo=partial(self._setFragmentResidues, chainFragment, oldResidues), redo=partial(self._setFragmentResidues, chainFragment, newResidues))
#EJB 20181210: defined twice # @property # def nextResidue(self) -> 'Residue': # "Next sequentially connected Residue" # apiResidue = self._wrappedData # nextApiMolResidue = apiResidue.molResidue.nextMolResidue # if nextApiMolResidue is None: # return None # else: # return self._project._data2Obj.get( # apiResidue.chain.findFirstResidue(seqId=nextApiMolResidue.serial)) # # @property # def previousResidue(self) -> 'Residue': # "Previous sequentially connected Residue" # apiResidue = self._wrappedData # previousApiMolResidue = apiResidue.molResidue.previousMolResidue # if previousApiMolResidue is None: # return None # else: # return self._project._data2Obj.get( # apiResidue.chain.findFirstResidue(seqId=previousApiMolResidue.serial)) @property def nmrResidue(self) -> typing.Optional['NmrResidue']: """NmrResidue to which Residue is assigned NB Residue<->NmrResidue link depends solely on the NmrResidue name. So no notifiers on the link - notify on the NmrResidue rename instead. """ try: return self._project.getNmrResidue(self._id) except: return None # GWV 20181122: removed setters between Chain/NmrChain, Residue/NmrResidue, Atom/NmrAtom # @nmrResidue.setter # def nmrResidue(self, value:'NmrResidue'): # oldValue = self.nmrResidue # if oldValue is value: # return # elif oldValue is not None: # oldValue.assignTo() # # # if value is not None: # value.residue = self @property def allNmrResidues(self) -> typing.Tuple['NmrResidue']: """AllNmrResidues corresponding to Residue - E.g. (for MR:A.87) NmrResidues NR:A.87, NR:A.87+0, NR:A.88-1, NR:A.82+5, etc. """ result = [] nmrChain = self.chain.nmrChain if nmrChain is not None: nmrResidue = self.nmrResidue if nmrResidue is not None: result = [nmrResidue] for offset in set(x.relativeOffset for x in nmrChain.nmrResidues): if offset is not None: residue = self if offset > 0: for ii in range(offset): residue = residue.previousResidue if residue is None: break elif offset < 0: for ii in range(-offset): residue = residue.nextResidue if residue is None: break # if residue is not None: sequenceCode = '%s%+d' % (residue.sequenceCode, offset) ll = [x for x in nmrChain.nmrResidues if x.sequenceCode == sequenceCode] if ll: result.extend(ll) return tuple(sorted(result)) @property def hasAssignedAtoms(self) -> bool: """ :return: True if any of its atoms have an assignment """ return any([a.isAssigned for a in self.atoms]) #========================================================================================= # Implementation functions #========================================================================================= @classmethod def _getAllWrappedData(cls, parent: Chain) -> list: """get wrappedData (MolSystem.Residues) for all Residue children of parent Chain""" # NB this sorts in seqId order - which is the order we want. # If the seqId order does not match the sequence we have a problem anyway. # NBNB the doe relies on this sorting order to handle position-specific labeling # for substances return parent._apiChain.sortedResidues()
[docs] @renameObject() @logCommand(get='self') def rename(self, sequenceCode: str = None): """Reset Residue.sequenceCode (residueType is immutable). Renaming to None sets the sequence code to the seqId (serial number equivalent) """ # rename functions from here apiResidue = self._wrappedData if sequenceCode is None: seqCode = apiResidue.seqId seqInsertCode = ' ' else: # Parse values from sequenceCode code, ss, offset = commonUtil.parseSequenceCode(sequenceCode) if code is None or offset is not None: raise ValueError("Illegal value for Residue.sequenceCode: %s" % sequenceCode) seqCode = code seqInsertCode = ss or ' ' previous = apiResidue.chain.findFirstResidue(seqCode=seqCode, seqInsertCode=seqInsertCode) if (previous not in (None, apiResidue)): raise ValueError("New sequenceCode %s clashes with existing Residue %s" % (sequenceCode, self._project._data2Obj.get(previous))) if apiResidue.seqInsertCode and apiResidue.seqInsertCode != ' ': oldSequenceCode = '.'.join((str(apiResidue.seqCode), apiResidue.seqInsertCode)) else: oldSequenceCode = str(apiResidue.seqCode) self._oldPid = self.pid apiResidue.seqCode = seqCode apiResidue.seqInsertCode = seqInsertCode return (oldSequenceCode,)
#=========================================================================================== # new'Object' and other methods # Call appropriate routines in their respective locations #===========================================================================================
[docs] @logCommand(get='self') def newAtom(self, name: str, elementSymbol: str = None, **kwds) -> 'Atom': """Create new Atom within Residue. If elementSymbol is None, it is derived from the name See the Atom class for details. Optional keyword arguments can be passed in; see Atom._newAtom for details. :param name: :param elementSymbol: :return: a new Atom instance. """ from ccpn.core.Atom import _newAtom return _newAtom(self, name=name, elementSymbol=elementSymbol, **kwds)
def _removeNonChemAtoms(self): """ Delete from project all the pseudo atoms which are not present in the original chemComp and were added artificially. """ chemAtomNames = self._getChemAtomNames() pseudoAtoms = [atom for atom in self.atoms if atom.name not in chemAtomNames] with undoBlockWithoutSideBar(): with notificationEchoBlocking(): self.project.deleteObjects(*pseudoAtoms) @property def _chemCompVar(self): """ :return: """ return self._wrappedData.chemCompVar def _getChemCompAtomGroups(self): """ """ atomGroups = {} if not self._chemCompVar: return atomSets = self._chemCompVar.chemAtomSets for atomSet in atomSets: if not atomSet.chemAtomSets: atomGroups[atomSet.name] = [a.name for a in atomSet.chemAtoms] else: atomGroups[atomSet.name] = [a.name for a in atomSet.chemAtomSets] return atomGroups def _addAtomsFromChemSets(self): if not self._chemCompVar: return atomSets = self._chemCompVar.chemAtomSets def _getChemAtomNames(self): """ :return: gets the atom names from the chemCompVar obj. It uses chemCompVar instead of chemCompVar.chemComp.chemAtoms because the latter includes LinkAtom like 'next_1' """ chemCompVar = self._wrappedData.chemCompVar chemAtomNames = [] if chemCompVar: chemAtoms = self._wrappedData.chemCompVar.chemAtoms chemAtomNames = [atom.name for atom in chemAtoms] return chemAtomNames
#========================================================================================= # Connections to parents: #========================================================================================= # GWV 20181122: Moved into class # def getter(self:Residue) -> Residue: # apiResidue = self._wrappedData # nextApiMolResidue = apiResidue.molResidue.nextMolResidue # if nextApiMolResidue is None: # return None # else: # return self._project._data2Obj.get( # apiResidue.chain.findFirstResidue(seqId=nextApiMolResidue.serial)) # Residue.nextResidue = property(getter, None, None, "Next sequentially connected Residue") # GWV 20181122: Moved into class # def getter(self:Residue) -> Residue: # apiResidue = self._wrappedData # previousApiMolResidue = apiResidue.molResidue.previousMolResidue # if previousApiMolResidue is None: # return None # else: # return self._project._data2Obj.get( # apiResidue.chain.findFirstResidue(seqId=previousApiMolResidue.serial)) # Residue.previousResidue = property(getter, None, None, "Previous sequentially connected Residue") # # del getter # No 'new' function - chains are made elsewhere # # Notifiers: # Project._apiNotifiers.extend( # ( # ('_finaliseApiRename', {}, ApiResidue._metaclass.qualifiedName(), 'setSeqCode'), # ('_finaliseApiRename', {}, ApiResidue._metaclass.qualifiedName(), 'setSeqInsertCode'), # ) # )