"""
Module documentation here
"""
#=========================================================================================
# 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: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-12-07 13:30:00 +0000 (Tue, December 07, 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 ccpn.core.NmrChain import NmrChain
from ccpn.core.Project import Project
from ccpn.core.Residue import Residue
from ccpn.core._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpn.core._implementation.AbsorbResonance import absorbResonance
from ccpn.core.lib import Pid
from ccpnmodel.ccpncore.api.ccp.nmr.Nmr import ResonanceGroup as ApiResonanceGroup
from ccpnmodel.ccpncore.lib.Constants import defaultNmrChainCode
from ccpn.core import _importOrder
from ccpn.util.decorators import logCommand
from ccpn.core.lib.ContextManagers import newObject, ccpNmrV3CoreSetter, \
renameObject, undoBlock, deleteObject
from ccpn.util.Common import makeIterableList
from ccpn.util.Logging import getLogger
# Value used for sorting with no offset - puts no_offset just before offset +0
SORT_NO_OFFSET = -0.1
# ASSIGNEDPEAKSCHANGED = '_assignedPeaksChanged'
[docs]class NmrResidue(AbstractWrapperObject):
"""Nmr Residues are used for assignment. An NmrResidue within an assigned NmrChain is
by definition assigned to the Residue with the same sequenceCode
(if any). An NmrResidue is defined by its containing chain and sequenceCode, so you cannot have
two NmrResidues with the same NmrChain and sequenceCode but different residueType.
An NmrResidue created without a name will be given the name
'@ij', where ij is the serial number of the NmrResidue. Names of this form are reserved.
Setting the NmrResidue sequenceCode to None will revert to this default name.
An NmrResidue can be defined by a sequential offset relative to another NmrResidue. E.g. the
NmrResidue i-1 relative to NmrResidue @5.@185.ALA would be named @5.@185-1.VAL. Reassigning
NR:@5.@185.ALA to NR:B.do1.ALA or NR:B.125.THR, would cause the offset NmrResidue
to be reassigned to NR:B.do1-1.VAL or NR:B.125-1.VAL, respectively. Offsets can be any integer
(including '+0').
NmrResidues that are not offset can be linked into consecutive stretches by putting them
into connected NmrChains (see NmrChain).
NmrResidue objects behave in there different ways when sorted:
- If they are assigned to a Residue they sort like the Residue, in sequential order
- If they belong to a connected NmrChain, they sort by the order they appear in the NmrChain.
- In other 4cases they sort by creation order.
- Offset NmrResidues in all cases sort alongside their main NmrResidue, by offset.
"""
#: Short class name, for PID.
shortClassName = 'NR'
# Attribute it necessary as subclasses must use superclass className
className = 'NmrResidue'
_parentClass = NmrChain
#: Name of plural link to instances of class
_pluralLinkName = 'nmrResidues'
# the attribute name used by current
_currentAttributeName = 'nmrResidues'
#: List of child classes.
_childClasses = []
# Qualified name of matching API class
_apiClassQualifiedName = ApiResonanceGroup._metaclass.qualifiedName()
# used in chemical shift mapping
_delta = None
_includeInDeltaShift = True # default included in the calculation
_estimatedKd = None
_colour = None
# Number of fields that comprise the object's pid; Used to get parent id's
_numberOfIdFields = 2
# CCPN properties
@property
def _apiResonanceGroup(self) -> ApiResonanceGroup:
""" CCPN resonanceGroup matching Residue"""
return self._wrappedData
@property
def sequenceCode(self) -> str:
"""Residue sequence code and id (e.g. '1', '127B', '\@157+1)
Names of the form '\@ijk', '\@ijk+n', and '\@ijk-n' (where ijk and n are integers)
are reserved and cannot be set. They are obtained by the deassign command."""
return self._wrappedData.sequenceCode
@property
def serial(self) -> int:
"""NmrResidue serial number - set at creation and unchangeable"""
return self._wrappedData.serial
@property
def _key(self) -> str:
"""Residue local ID"""
return Pid.createId(self.sequenceCode, self.residueType)
@classmethod
def _nextKey(cls):
"""Get the next available key from _serialDict
Limited functionality but helps to get potential Pid of the next _wrapped object
In this case Pid element is of the form '@<num>.<residueType>' but subject to change
residueType will default to ''
"""
from ccpn.framework.Application import getApplication
_project = getApplication().project
_metaName = cls._apiClassQualifiedName.split('.')[-1]
_metaName = _metaName[0].lower() + _metaName[1:] + 's'
_name = f'@{_project._wrappedData.topObject._serialDict[_metaName] + 1}'
return Pid.createId(_name, '')
@property
def _ccpnSortKey(self) -> tuple:
"""Attibute used to sort objects.
Normally this is set on __init__ (for speed) and reset by self._resetIds
(which is called by the rename finaliser and by resetSerial).
But NmrResidue sorting order changes cynamically depending on
what other NmrResidues are iN the same NmrChain. So for this class
we need to set it dynamically, as a property"""
sortKey = self.nmrChain._ccpnSortKey[2:] + self._localCcpnSortKey
result = (id(self._project), _importOrder.index(self.className)) + sortKey
#
return result
@property
def _localCcpnSortKey(self) -> typing.Tuple:
"""Local sorting key, in context of parent."""
unassignedOffset = 1000000000
obj = self._wrappedData
offset = obj.relativeOffset
if offset is None:
# this is a main NmrResidue
offset = SORT_NO_OFFSET
else:
# Offset NmrResidue - get sort key from main Nmr Residue
# NBNB We can NOT rely on the main NmrResidue to be already initialised
obj = obj.mainResonanceGroup
apiNmrChain = obj.nmrChain
if apiNmrChain.isConnected:
result = (apiNmrChain.mainResonanceGroups.index(obj), '', offset)
else:
seqCode = obj.seqCode
if seqCode is None:
result = (unassignedOffset + obj.serial, obj.seqInsertCode or '', offset)
else:
result = (seqCode, obj.seqInsertCode or '', offset)
# if offset is None:
# apiNmrChain = obj.nmrChain
# if apiNmrChain.isConnected:
# result = (apiNmrChain.mainResonanceGroups.index(obj), '', SORT_NO_OFFSET)
# else:
# # this is a main NmrResidue
# seqCode = obj.seqCode
# if seqCode is None:
# result = (Constants.POSINFINITY, '@%s' % obj.serial, SORT_NO_OFFSET)
# else:
# result = (seqCode, obj.seqInsertCode or '', SORT_NO_OFFSET)
# else:
# result = self.mainNmrResidue._localCcpnSortKey[:-1] + (offset,)
#
return result
@property
def _parent(self) -> NmrChain:
"""NmrChain containing NmrResidue. Use self.assignTo to reset the NmrChain"""
return self._project._data2Obj[self._wrappedData.nmrChain]
nmrChain = _parent
@property
def residueType(self) -> str:
"""Residue type string (e.g. 'ALA'). Part of id. Use self.assignTo or
self.rename to reset the residueType"""
return self._wrappedData.residueType or ''
@residueType.setter
def residueType(self, value: typing.Optional[str]):
if not isinstance(value, (str, type(None))):
raise TypeError(f'residueType {repr(value)} must be a string or None')
if isinstance(value, str) and not value:
raise TypeError(f'residueType {repr(value)} must be a non-empty string')
if self.residueType != value:
self._wrappedData.resetResidueType(value)
@property
def relativeOffset(self) -> typing.Optional[int]:
"""Sequential offset of NmrResidue relative to mainNmrResidue
May be 0. Is None for residues that are not offset."""
return self._wrappedData.relativeOffset
@property
def residue(self) -> Residue:
"""Residue to which NmrResidue is assigned"""
return self._project.getResidue(self._id)
# GWV 20181122: removed setters between Chain/NmrChain, Residue/NmrResidue, Atom/NmrAtom
# @residue.setter
# def residue(self, value:Residue):
# if value:
# tt = tuple((x or None) for x in value._id.split('.'))
# self.assignTo(chainCode=tt[0], sequenceCode=tt[1], residueType=tt[2])
# else:
# residueType = self.residueType
# if residueType:
# self.rename('.' + residueType)
# else:
# self.rename(None)
@property
def offsetNmrResidues(self) -> typing.Tuple['NmrResidue', ...]:
""""All other NmrResidues with the same sequenceCode sorted by offSet suffix '-1', '+1', etc."""
getDataObj = self._project._data2Obj.get
return tuple(getDataObj(x) for x in self._wrappedData.offsetResonanceGroups)
[docs] def getOffsetNmrResidue(self, offset: int) -> typing.Optional['NmrResidue']:
"""Get offset NmrResidue with indicated offset
(or None, if no such offset NmrResidue exists"""
for result in self.offsetNmrResidues:
if result.relativeOffset == offset:
return result
#
return None
@property
def mainNmrResidue(self) -> typing.Optional['NmrResidue']:
"""Main NmrResidue (self, or the residue that self is offset relative to"""
return self._project._data2Obj.get(self._wrappedData.mainResonanceGroup)
@property
def nextNmrResidue(self) -> typing.Optional['NmrResidue']:
"""Next sequentially connected NmrResidue (or None, as appropriate).
Either from a connected NmrChain,
or the NmrResidue assigned to the next Residue in the same Chain"""
apiResonanceGroup = self._wrappedData
apiNmrChain = apiResonanceGroup.directNmrChain
residue = self.residue
result = None
if apiNmrChain and apiNmrChain.isConnected:
# Connected stretch
stretch = apiNmrChain.mainResonanceGroups
if apiResonanceGroup is stretch[-1]:
result = None
else:
result = self._project._data2Obj.get(stretch[stretch.index(apiResonanceGroup) + 1])
elif residue:
# Assigned to residue
nextResidue = residue.nextResidue
if nextResidue:
result = nextResidue.nmrResidue
#
return result
[docs] @logCommand(get='self')
def connectNext(self, nmrResidue: typing.Union['NmrResidue', str]) -> NmrChain:
"""Connect free end of self to free end of next residue in sequence,
and return resulting connected NmrChain
Raises error if self is assigned, or if either self or value is offset.
NB Undoing a connection between two connected stretches
will get back a 'value' stretch with a new shortName"""
apiResonanceGroup = self._wrappedData
# apiResidue = apiResonanceGroup.assignedResidue
apiNmrChain = apiResonanceGroup.directNmrChain
project = self._project
if nmrResidue is None:
raise ValueError("Cannot connect to value: None")
elif isinstance(nmrResidue, str):
xx = project.getByPid(nmrResidue)
if xx is None:
raise ValueError("No object found matching Pid %s" % nmrResidue)
else:
nmrResidue = xx
apiValueNmrChain = nmrResidue._wrappedData.nmrChain
if self.relativeOffset is not None:
raise ValueError("Cannot connect from offset residue")
elif nmrResidue.relativeOffset is not None:
raise ValueError("Cannot connect to offset NmrResidue")
elif self.residue is not None:
raise ValueError("Cannot connect assigned NmrResidue - assign the value instead")
elif nmrResidue.residue is not None:
raise ValueError("Cannot connect to assigned NmrResidue - assign the NmrResidue instead")
elif self.nextNmrResidue is not None:
raise ValueError("Cannot connect next NmrResidue - it is already connected")
elif nmrResidue.previousNmrResidue is not None:
raise ValueError("Cannot connect to next NmrResidue - it is already connected")
elif apiNmrChain.isConnected and apiValueNmrChain is apiNmrChain:
raise ValueError("Cannot make cyclical connected NmrChain")
with undoBlock():
if apiNmrChain.isConnected:
# At this point, self must be the last NmrResidue in a connected chain
if apiValueNmrChain.isConnected:
for rg in apiValueNmrChain.mainResonanceGroups:
rg.moveDirectNmrChain(apiNmrChain, 'tail')
# apiValueNmrChain.delete()
# need the V3 operator here for the undo/redo to fire correctly
V3nmrChain = self.project._data2Obj[apiValueNmrChain]
V3nmrChain.delete()
else:
# [connected:NmrChain] -> [Value]
nmrResidue._wrappedData.moveDirectNmrChain(apiNmrChain, 'tail')
result = self.nmrChain
else:
# self is unassigned, unconnected NmrResidue
if apiValueNmrChain.isConnected:
# At this point value must be the first NmrResidue in a connected NmrChain
apiResonanceGroup.moveDirectNmrChain(apiValueNmrChain, 'head')
else:
# [NmrChain] -> [Value]
# newApiNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
# need the V3 operator here for the undo/redo to fire correctly
newV3nmrChain = self.project.newNmrChain(isConnected=True)
newApiNmrChain = newV3nmrChain._apiNmrChain
apiResonanceGroup.directNmrChain = newApiNmrChain
nmrResidue._wrappedData.directNmrChain = newApiNmrChain
result = nmrResidue.nmrChain
return result
[docs] @logCommand(get='self')
def deassignNmrChain(self):
with undoBlock():
if self.residue is not None: # assigned to chain
self._deassignNmrChain()
else:
getLogger().warning('Cannot deassign an unassigned chain')
def _deassignNmrChain(self):
# nmrList = self._getAllConnectedList()
# if nmrList:
# if len(nmrList) > 1:
#
# apiNmrChain = self._wrappedData.directNmrChain
# newNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
#
# for nmr in nmrList:
# nmr._wrappedData.directNmrChain = newNmrChain
# nmr.deassign()
# else:
# nmrList[0]._deassignSingle()
nmrList = self._getAllConnectedList()
if nmrList:
if len(nmrList) > 1:
for nmr in nmrList:
nmr.deassign()
for i in range(len(nmrList) - 1):
nmrList[i].connectNext(nmrList[i + 1])
else:
nmrList[0]._deassignSingle()
if not self.mainNmrResidue.previousNmrResidue:
# a single residue so return to the default
self._deassignSingle()
return None
[docs] @logCommand(get='self')
def disconnectAll(self):
with undoBlock():
if self.residue is not None: # assigned to chain
self._disconnectAssignedAll(assigned=True)
else:
self._disconnectAssignedAll(assigned=False)
def _disconnectAssignedAll(self, assigned=False):
# disconnect all and return to the @- chain
for nmr in self._getAllConnectedList():
nmr._deassignSingle()
[docs] @logCommand(get='self')
def disconnectNext(self) -> typing.Optional['NmrChain']:
with undoBlock():
if self.residue is not None: # assigned to chain
newNmrChain = self._disconnectAssignedNext()
else:
newNmrChain = self._disconnectNext()
return newNmrChain
def _disconnectAssignedNext(self) -> typing.Optional['NmrChain']:
"""Cut connected NmrChain after NmrResidue, creating new connected NmrChain if necessary"""
nmrList = self._getNextConnectedList()
if nmrList:
if len(nmrList) > 1:
for nmr in nmrList:
nmr.deassign()
for i in range(len(nmrList) - 1):
nmrList[i].connectNext(nmrList[i + 1])
else:
nmrList[0]._deassignSingle()
if not self.mainNmrResidue.previousNmrResidue:
# a single residue so return to the default
self._deassignSingle()
return None
def _disconnectNext(self) -> typing.Optional['NmrChain']:
"""Cut connected NmrChain after NmrResidue, creating new connected NmrChain if necessary
Does nothing if nextNmrResidue is empty;
Raises ValueError for assigned NmrResidues"""
apiResonanceGroup = self._wrappedData
apiNmrChain = apiResonanceGroup.directNmrChain
defaultChain = apiNmrChain.nmrProject.findFirstNmrChain(code=defaultNmrChainCode)
if apiNmrChain is None:
# offset residue: no-op
return
elif self.residue is not None:
# Assigned residue with successor residue - error
raise ValueError("Assigned NmrResidue %s cannot be disconnected" % self)
data2Obj = self._project._data2Obj
if apiNmrChain.isConnected:
# Connected stretch - break stretch, keeping first half in the NmrChain
stretch = apiNmrChain.mainResonanceGroups
if apiResonanceGroup is stretch[-1]: # nothing to disconnect on the right
return
if apiResonanceGroup is stretch[0]: # first in the chain
# chop off end ResonanceGroup
if len(stretch) <= 2:
# Chain gets removed
for resonanceGroup in reversed(stretch):
resonanceGroup.directNmrChain = defaultChain
# delete empty chain
# apiNmrChain.delete()
# need the V3 operator here for the undo/redo to fire correctly
V3nmrChain = self.project._data2Obj[apiNmrChain]
V3nmrChain.delete()
else:
apiResonanceGroup.moveDirectNmrChain(defaultChain, 'head')
# newNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
# for rg in reversed(stretch):
# if rg is apiResonanceGroup:
# break
# else:
# rg.moveDirectNmrChain(newNmrChain, 'head')
# apiResonanceGroup.directNmrChain = defaultChain
# apiNmrChain.delete()
elif apiResonanceGroup is stretch[-2]:
# chop off end ResonanceGroup
stretch[-1].directNmrChain = defaultChain
else:
# make new connected NmrChain with rightmost ResonanceGroups
# newNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
# need the V3 operator here for the undo/redo to fire correctly
newV3nmrChain = self.project.newNmrChain(isConnected=True)
newNmrChain = newV3nmrChain._apiNmrChain
for rg in reversed(stretch):
if rg is apiResonanceGroup:
break
else:
rg.moveDirectNmrChain(newNmrChain, 'head')
return newNmrChain # need this when using disconnectPrevious
@property
def previousNmrResidue(self) -> typing.Optional['NmrResidue']:
"""Previous sequentially connected NmrResidue (or None, as appropriate).
Either from a connected NmrChain,
or the NmrResidue assigned to the previous Residue in the same Chain"""
apiResonanceGroup = self._wrappedData
apiNmrChain = apiResonanceGroup.directNmrChain
residue = self.residue
result = None
if apiNmrChain and apiNmrChain.isConnected:
# Connected stretch
stretch = apiNmrChain.mainResonanceGroups
if apiResonanceGroup is stretch[0]:
result = None
else:
result = self._project._data2Obj.get(stretch[stretch.index(apiResonanceGroup) - 1])
elif residue:
# Assigned to residue
previousResidue = residue.previousResidue
if previousResidue:
result = previousResidue.nmrResidue
return result
[docs] @logCommand(get='self')
def connectPrevious(self, nmrResidue=None) -> NmrChain:
"""Connect free end of self to free end of previous residue in sequence,
and return resulting connected NmrChain
Raises error if self is assigned, or if either self or value is offset.
NB Undoing a connection between two connected stretches
will get back a 'value' stretch with a new shortName"""
apiResonanceGroup = self._wrappedData
# apiResidue = apiResonanceGroup.assignedResidue
apiNmrChain = apiResonanceGroup.directNmrChain
project = self._project
if nmrResidue is None:
raise ValueError("Cannot connect to value: None")
elif isinstance(nmrResidue, str):
xx = project.getByPid(nmrResidue)
if xx is None:
raise ValueError("No object found matching Pid %s" % nmrResidue)
else:
nmrResidue = xx
apiValueNmrChain = nmrResidue._wrappedData.nmrChain
if self.relativeOffset is not None:
raise ValueError("Cannot connect from offset residue")
elif nmrResidue.relativeOffset is not None:
raise ValueError("Cannot connect to offset NmrResidue")
elif self.residue is not None:
raise ValueError("Cannot connect assigned NmrResidue - assign the value instead")
elif nmrResidue.residue is not None:
raise ValueError("Cannot connect to assigned NmrResidue - assign the NmrResidue instead")
elif self.previousNmrResidue is not None:
raise ValueError("Cannot connect previous NmrResidue - it is already connected")
elif nmrResidue.nextNmrResidue is not None:
raise ValueError("Cannot connect to previous NmrResidue - it is already connected")
elif apiNmrChain.isConnected and apiValueNmrChain is apiNmrChain:
raise ValueError("Cannot make cyclical connected NmrChain")
with undoBlock():
if apiNmrChain.isConnected:
# At this point, self must be the first NmrResidue in a connected chain
undo = apiValueNmrChain.root._undo
try:
ll = apiNmrChain.__dict__['mainResonanceGroups']
if apiValueNmrChain.isConnected:
for rg in reversed(apiValueNmrChain.mainResonanceGroups):
rg.moveDirectNmrChain(apiNmrChain, 'head')
# apiValueNmrChain.delete()
# need the V3 operator here for the undo/redo to fire correctly
V3nmrChain = self.project._data2Obj[apiValueNmrChain]
V3nmrChain.delete()
else:
nmrResidue._wrappedData.moveDirectNmrChain(apiNmrChain, 'head')
finally:
result = self.nmrChain
else:
# self is unassigned, unconnected NmrResidue
if apiValueNmrChain.isConnected:
# At this point value must be the last NmrResidue in a connected NmrChain
# [connected:Value] <- [NmrChain]
apiResonanceGroup.moveDirectNmrChain(apiValueNmrChain, 'tail')
else:
# [Value] <- [NmrChain]
# newApiNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
# need the V3 operator here for the undo/redo to fire correctly
newV3nmrChain = self.project.newNmrChain(isConnected=True)
newApiNmrChain = newV3nmrChain._apiNmrChain
nmrResidue._wrappedData.directNmrChain = newApiNmrChain
# newApiNmrChain.__dict__['mainResonanceGroups'].reverse()
apiResonanceGroup.directNmrChain = newApiNmrChain
# apiResonanceGroup.moveToNmrChain(newApiNmrChain)
result = nmrResidue.nmrChain
return result
# def _bubbleHead(self, ll):
# ll.insert(0, ll.pop())
# def _bubbleTail(self, ll):
# ll.append(ll.pop(0))
[docs] @logCommand(get='self')
def unlinkPreviousNmrResidue(self):
with undoBlock():
if self.residue is not None: # assigned to chain
self._disconnectAssignedPrevious()
[docs] @logCommand(get='self')
def unlinkNextNmrResidue(self):
with undoBlock():
if self.residue is not None: # assigned to chain
self._disconnectAssignedNext()
[docs] @logCommand(get='self')
def disconnectPrevious(self):
with undoBlock():
if self.residue is not None: # assigned to chain
self._disconnectAssignedPrevious()
else:
self._disconnectPrevious()
def _disconnectAssignedPrevious(self) -> typing.Optional['NmrChain']:
"""Cut connected NmrChain after NmrResidue, creating new connected NmrChain if necessary"""
nmrList = self._getPreviousConnectedList()
if nmrList:
if len(nmrList) > 1:
for nmr in nmrList:
nmr.deassign()
for i in range(len(nmrList) - 1):
nmrList[i].connectNext(nmrList[i + 1])
else:
nmrList[0]._deassignSingle()
if not self.mainNmrResidue.nextNmrResidue:
# a single residue so return to the default
self._deassignSingle()
return None
def _disconnectPrevious(self):
"""Cut connected NmrChain before NmrResidue, creating new connected NmrChain if necessary
Does nothing if previousNmrResidue is empty;
Raises ValueError for assigned NmrResidues
"""
apiResonanceGroup = self._wrappedData
apiNmrChain = apiResonanceGroup.directNmrChain
defaultChain = apiNmrChain.nmrProject.findFirstNmrChain(code=defaultNmrChainCode)
if apiNmrChain is None:
# offset residue: no-op
return
elif self.residue is not None:
# Assigned residue with successor residue - error
raise ValueError("Assigned NmrResidue %s cannot be disconnected" % self)
elif apiNmrChain.isConnected:
# Connected stretch - break stretch, keeping first half in the NmrChain
stretch = apiNmrChain.mainResonanceGroups
if apiResonanceGroup is stretch[0]: # first in the chain
return
if apiResonanceGroup is stretch[-1]: # last in the chain
# chop off end ResonanceGroup
if len(stretch) <= 2:
# Chain gets removed
for resonanceGroup in reversed(stretch):
resonanceGroup.directNmrChain = defaultChain
# delete empty chain
# apiNmrChain.delete()
# need the V3 operator here for the undo/redo to fire correctly
V3nmrChain = self.project._data2Obj[apiNmrChain]
V3nmrChain.delete()
else:
apiResonanceGroup.moveDirectNmrChain(defaultChain, 'tail')
# newNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
# for rg in stretch:
# if rg is apiResonanceGroup:
# break
# else:
# rg.moveDirectNmrChain(newNmrChain, 'tail')
# apiResonanceGroup.directNmrChain = defaultChain
# apiNmrChain.delete()
elif apiResonanceGroup is stretch[1]:
# chop off end ResonanceGroup
stretch[0].moveDirectNmrChain(defaultChain, 'head')
else:
# make new connected NmrChain with rightmost ResonanceGroups
# newNmrChain = apiNmrChain.nmrProject.newNmrChain(isConnected=True)
# need the V3 operator here for the undo/redo to fire correctly
newV3nmrChain = self.project.newNmrChain(isConnected=True)
newNmrChain = newV3nmrChain._apiNmrChain
for rg in stretch:
if rg is apiResonanceGroup:
break
else:
rg.moveDirectNmrChain(newNmrChain, 'tail')
return newNmrChain
[docs] @logCommand(get='self')
def disconnect(self):
with undoBlock():
if self.residue is not None: # assigned to chain
self._disconnectAssigned()
else:
self._disconnect()
def _deassignSingle(self):
# disconnect a single residue - return to @- chain
# apiResonanceGroup = nmrResidue._wrappedData
# apiNmrChain = apiResonanceGroup.directNmrChain
# defaultChain = apiNmrChain.nmrProject.findFirstNmrChain(code=defaultNmrChainCode)
#
# if apiNmrChain:
# apiResonanceGroup.directNmrChain = defaultChain
# nmrResidue.deassign()
self.moveToNmrChain()
self.deassign()
def _getPreviousConnectedList(self):
# generate a list of the previous connected nmrResidues
nmrListPrevious = []
nmr = self.mainNmrResidue
while nmr.previousNmrResidue:
nmr = nmr.previousNmrResidue
nmrListPrevious.insert(0, nmr)
return nmrListPrevious
def _getNextConnectedList(self):
# generate a list of the next connected nmrResidues
nmrListNext = []
nmr = self.mainNmrResidue
while nmr.nextNmrResidue:
nmr = nmr.nextNmrResidue
nmrListNext.append(nmr)
return nmrListNext
def _getAllConnectedList(self):
# generate a list of all the connected nmrResidues
nmrList = []
nmr = self.mainNmrResidue
while nmr.previousNmrResidue:
nmr = nmr.previousNmrResidue
while nmr:
nmrList.append(nmr)
nmr = nmr.nextNmrResidue
return nmrList
def _disconnectAssigned(self):
nmrListPrev = self._getPreviousConnectedList()
nmrListNext = self._getNextConnectedList()
self._deassignSingle()
if len(nmrListPrev) == 1:
nmrListPrev[0]._deassignSingle()
if len(nmrListNext) == 1:
nmrListNext[0]._deassignSingle()
def _disconnect(self):
"""Move NmrResidue from connected NmrChain to default chain,
creating new connected NmrChains as necessary"""
apiResonanceGroup = self._wrappedData
apiNmrChain = apiResonanceGroup.directNmrChain
if not apiNmrChain:
raise ValueError("Offset NmrResidue %s cannot be disconnected" % self)
defaultChain = apiNmrChain.nmrProject.findFirstNmrChain(code=defaultNmrChainCode)
if apiNmrChain is None:
# offset residue: no-op
return
elif self.residue is not None:
# Assigned residue with successor residue - error
raise ValueError("Assigned NmrResidue %s cannot be disconnected" % self)
elif apiNmrChain.isConnected:
# Connected stretch - break stretch, keeping first half in the NmrChain
stretch = apiNmrChain.mainResonanceGroups
if len(stretch) < 3 or (len(stretch) == 3 and apiResonanceGroup is stretch[1]):
for rg in reversed(stretch):
# reversed to add residues back in proper order (they are added to end)
rg.directNmrChain = defaultChain
# apiNmrChain.delete()
# need the V3 operator here for the undo/redo to fire correctly
V3nmrChain = self.project._data2Obj[apiNmrChain]
V3nmrChain.delete()
else:
index = stretch.index(apiResonanceGroup)
data2Obj = self._project._data2Obj
# NB operations are carefully selected to make sure they undo correctly
if apiResonanceGroup is stretch[-1]:
apiResonanceGroup.directNmrChain = defaultChain
elif apiResonanceGroup is stretch[-2]:
stretch[-1].directNmrChain = defaultChain
apiResonanceGroup.directNmrChain = defaultChain
elif index == 0:
data2Obj[stretch[1]]._disconnectPrevious()
elif index == 1:
nmrChain = self.nmrChain
nr1 = data2Obj[stretch[1]]
nr2 = data2Obj[stretch[2]]
nr1._disconnectPrevious()
nr2._disconnectPrevious()
else:
self._disconnectNext()
apiResonanceGroup.directNmrChain = defaultChain
@property
def probableResidues(self) -> typing.Tuple[typing.Tuple[Residue, float], ...]:
"""tuple of (residue, probability) tuples for probable residue assignments
sorted by decreasing probability. Probabilities are normalised to 1"""
getDataObj = self._project._data2Obj.get
ll = sorted((x.weight, x.possibility) for x in self._wrappedData.residueProbs)
totalWeight = sum(tt[0] for tt in ll) or 1.0 # If sum is zero give raw weights
return tuple((getDataObj(tt[1]), tt[0] / totalWeight) for tt in reversed(ll))
@probableResidues.setter
@logCommand(get='self', isProperty=True)
@ccpNmrV3CoreSetter()
def probableResidues(self, value):
apiResonanceGroup = self._wrappedData
for residueProb in apiResonanceGroup.residueProbs:
residueProb.delete()
for residue, weight in value:
apiResonanceGroup.newResidueProb(possibility=residue._wrappedData, weight=weight)
@property
def probableResidueTypes(self) -> typing.Tuple[typing.Tuple[str, float]]:
"""tuple of (residueType, probability) tuples for probable residue types
sorted by decreasing probability"""
ll = sorted((x.weight, x.possibility) for x in self._wrappedData.residueTypeProbs)
totalWeight = sum(tt[0] for tt in ll) or 1.0 # If sum is zero give raw weights
return tuple((tt[1].code3Letter, tt[0] / totalWeight) for tt in reversed(ll))
@probableResidueTypes.setter
@logCommand(get='self', isProperty=True)
@ccpNmrV3CoreSetter()
def probableResidueTypes(self, value):
apiResonanceGroup = self._wrappedData
root = apiResonanceGroup.root
for residueTypeProb in apiResonanceGroup.residueTypeProbs:
residueTypeProb.delete()
for weight, residueType in value:
chemComp = root.findFirstChemComp(code3Letter=residueType)
if chemComp is None:
# print("Residue type %s not recognised - skipping" % residueType)
getLogger().warning("Residue type %s not recognised - skipping" % residueType)
else:
apiResonanceGroup.newResidueTypeProb(chemComp=chemComp, weight=weight)
[docs] @logCommand(get='self')
def deassign(self):
"""Reset sequenceCode and residueType assignment to default values"""
with undoBlock():
apiResonanceGroup = self._apiResonanceGroup
apiResonanceGroup.sequenceCode = None
apiResonanceGroup.resetResidueType(None)
[docs] @logCommand(get='self')
def moveToNmrChain(self, newNmrChain: typing.Union['NmrChain', str] = 'NC:@-', sequenceCode: str = None, residueType: str = None):
"""Move residue to newNmrChain, breaking connected NmrChain if necessary.
Optionally rename residue using sequenceCode and residueType
newNmrChain default resets to NmrChain '@-'
Routine is illegal for offset NmrResidues, use the main nmrResidue instead
Routine will fail if current sequenceCode,residueType already exists in newNmrChain, as the nmrResidue is first moved
then renamed; consider moving to temporary chain first.
"""
apiResonanceGroup = self._apiResonanceGroup
if apiResonanceGroup.relativeOffset is not None:
raise ValueError("Cannot reset NmrChain for offset NmrResidue %s" % self.id)
# optionally get newNmrChain from str object
if isinstance(newNmrChain, str):
nChain = self._project.getByPid(newNmrChain)
if nChain is None:
raise ValueError('Invalid newNmrChain "%s"' % newNmrChain)
newNmrChain = nChain
nmrChain = self.nmrChain
with undoBlock():
try:
# if needed: move self to newNmrChain
movedChain = False
if newNmrChain != nmrChain:
apiResonanceGroup.moveToNmrChain(newNmrChain._wrappedData)
movedChain = True
# optionally rename
if self.sequenceCode != sequenceCode or self.residueType != residueType:
if sequenceCode is None:
sequenceCode = self.sequenceCode
if residueType is None:
residueType = self.residueType
self.rename(sequenceCode, residueType)
except Exception as es:
getLogger().warning(str(es))
if movedChain:
# Need to undo this
apiResonanceGroup.moveToNmrChain(nmrChain._wrappedData)
raise es
[docs] @logCommand(get='self')
def assignTo(self, chainCode: str = None, sequenceCode: typing.Union[int, str] = None,
residueType: str = None, mergeToExisting: bool = False) -> 'NmrResidue':
"""Assign NmrResidue to new assignment, as defined by the naming parameters
and return the result.
Empty parameters (e.g. chainCode=None) retain the previous value. E.g.:
for NmrResidue NR:A.121.ALA
calling with sequenceCode=123 will reassign to 'A.123.ALA'.
If no assignment with the same chainCode and sequenceCode exists, the current NmrResidue
will be reassigned.
If an NmrResidue with the same chainCode and sequenceCode already exists, the function
will either raise ValueError. If mergeToExisting is set to False, it will instead merge the
two NmrResidues, delete the current one, and return the new one .
NB Merging is NOT undoable.
WARNING: When calling with mergeToExisting=True, always use in the form "x = x.assignTo(...)",
as the call 'x.assignTo(...) may cause the source x object to become deleted.
NB resetting the NmrChain for an NmrResidue in the middle of a connected NmrChain
will cause an error. Use moveToNmrChain(newNmrChainOrPid) instead
"""
oldPid = self.longPid
apiResonanceGroup = self._wrappedData
clearUndo = False
undo = apiResonanceGroup.root._undo
with undoBlock():
sequenceCode = str(sequenceCode) if sequenceCode else None
# apiResonanceGroup = self._apiResonanceGroup
# oldNmrChain = apiResonanceGroup.nmrChain
# oldSequenceCode = apiResonanceGroup.sequenceCode
# oldResidueType = apiResonanceGroup.residueType
# Check for illegal separators in input values
for ss in (chainCode, sequenceCode, residueType):
if ss and Pid.altCharacter in ss:
raise ValueError("Character %s not allowed in ccpn.NmrResidue id: %s.%s.%s" %
(Pid.altCharacter, chainCode, sequenceCode, residueType))
# Keep old values to go back to previous state
oldChainCode, oldSequenceCode, oldResidueType = self._id.split('.')
oldResidueType = oldResidueType or None
# set missing parameters to existing or default values
chainCode = chainCode or oldChainCode
sequenceCode = sequenceCode or oldSequenceCode
residueType = residueType or None
partialId = '%s.%s.' % (chainCode, sequenceCode)
ll = self._project.getObjectsByPartialId(className='NmrResidue', idStartsWith=partialId)
if ll:
# There can only ever be one match
result = ll[0]
else:
result = None
if result is self:
# We are reassigning to self - either a no-op or resetting the residueType
result = self
if residueType and self.residueType != residueType:
apiResonanceGroup.resetResidueType(residueType)
elif result is None:
# we are moving to new, free assignment
result = self
newNmrChain = self._project.fetchNmrChain(chainCode)
try:
# NB Complex resetting sequence necessary
# in case we are setting an offset and illegal sequenceCode
apiResonanceGroup.sequenceCode = None # To guarantee against clashes
apiResonanceGroup.directNmrChain = newNmrChain._apiNmrChain # Only directNmrChain is settable
# Now we can (re)set - will throw error for e.g. illegal offset values
apiResonanceGroup.sequenceCode = sequenceCode
if residueType:
apiResonanceGroup.resetResidueType(residueType)
except:
apiResonanceGroup.resetResidueType(oldResidueType)
apiResonanceGroup.sequenceCode = None
apiResonanceGroup.directNmrChain = apiResonanceGroup.nmrProject.findFirstNmrChain(
code=oldChainCode
)
apiResonanceGroup.sequenceCode = oldSequenceCode
self._project._logger.error("Attempt to set illegal or inconsistent assignment: %s.%s.%s"
% (chainCode, sequenceCode, residueType) + "\n Reset to original state"
)
raise
else:
#We are assigning to an existing NmrResidue
if not mergeToExisting:
raise ValueError("New assignment clash with existing assignment,"
" and merging is disallowed")
newApiResonanceGroup = result._wrappedData
if not residueType or result.residueType == residueType:
# Move or merge the NmrAtoms across and delete the current NmrResidue
for resonance in apiResonanceGroup.resonances:
newResonance = newApiResonanceGroup.findFirstResonance(implName=resonance.name)
if newResonance is None:
resonance.resonanceGroup = newApiResonanceGroup
else:
_res = self._project._data2Obj.get(resonance)
_newRes = self._project._data2Obj.get(newResonance)
if not (_res and _newRes):
raise RuntimeError('Cannot find associated v3 resonances')
absorbResonance(_newRes, _res)
apiResonanceGroup.delete()
else:
# We cannot reassign if it involves changing residueType on an existing NmrResidue
raise ValueError("Cannot assign to %s.%s.%s: NR:%s.%s.%s already exists"
% (chainCode, sequenceCode, residueType,
chainCode, sequenceCode, result.residueType))
return result
[docs] @logCommand(get='self')
def mergeNmrResidues(self, nmrResidues: typing.Sequence['NmrResidue']):
nmrResidues = makeIterableList(nmrResidues)
nmrResidues = [self.project.getByPid(nmrResidue) if isinstance(nmrResidue, str) else nmrResidue for nmrResidue in nmrResidues]
if not all(isinstance(nmrResidue, NmrResidue) for nmrResidue in nmrResidues):
raise TypeError('nmrResidues can only contain items of type NmrResidue')
if self in nmrResidues:
raise TypeError('nmrResidue cannot be merged with itself')
with undoBlock():
apiResonanceGroup = self._wrappedData
for nmrResidue in nmrResidues:
for nmrAtom in nmrResidue.nmrAtoms:
existingNmrAtom = self.getNmrAtom(nmrAtom.name)
if existingNmrAtom is None:
# move resonance
resonance = nmrAtom._wrappedData
resonance.resonanceGroup = apiResonanceGroup
else:
absorbResonance(existingNmrAtom, nmrAtom)
nmrResidue.delete()
# def _rebuildAssignedChains(self):
# self._startCommandEchoBlock('_rebuildAssignedChains')
# try:
# assignedChain = self._project.fetchNmrChain('A')
# while assignedChain.nmrResidues:
# startNmrResidue = assignedChain.nmrResidues[0].mainNmrResidue
# startNmrResidue._deassignNmrChain()
# startNmrResidue.nmrChain.reverse()
#
# except Exception as es:
# getLogger().warning(str(es))
# finally:
# self._endCommandEchoBlock()
#=========================================================================================
# Implementation functions
#=========================================================================================
@classmethod
def _getAllWrappedData(cls, parent: NmrChain) -> list:
"""get wrappedData (MolSystem.Residues) for all Residue children of parent Chain"""
return parent._wrappedData.sortedResonanceGroups() if parent._wrappedData else ()
def _reverseChainForDelete(self, apiNmrChain):
"""Reverse the chain.
"""
apiNmrChain.__dict__['mainResonanceGroups'].reverse()
def _finaliseAction(self, action: str):
"""Subclassed to handle associated offsetNMrResidues
"""
if not super()._finaliseAction(action):
return
if action in ['rename']:
for offNmrRes in self.offsetNmrResidues:
offNmrRes._finaliseAction('rename')
if action in ['delete', 'create']:
# notify that the peak labels need to be updated
_peaks = set(pk for nmrAtom in self.nmrAtoms for pk in nmrAtom.assignedPeaks)
for pk in _peaks:
pk._finaliseAction('change')
def _delete(self):
"""Delete object, with all contained objects and underlying data.
"""
atHeadOfChain = False
apiNmrChain = self._wrappedData.directNmrChain
if apiNmrChain and apiNmrChain.isConnected:
stretch = tuple(apiNmrChain.mainResonanceGroups)
atHeadOfChain = True if len(stretch) > 1 and stretch[0] is self._wrappedData else False
if not atHeadOfChain:
with undoBlock():
# remove all the mmrAtoms from their associated chemicalShifts
# - clearing before the delete handles the notifiers nicely
_shs = [sh for nmrAt in self.nmrAtoms for sh in nmrAt.chemicalShifts]
for sh in _shs:
sh.nmrAtom = None
super().delete()
else:
raise ValueError('Cannot delete nmrResidues from the head of a connected chain.')
# # disconnect and then delete
# nextNmrResidue = self.project._data2Obj[stretch[1]]
# removeNmrChain = nextNmrResidue.disconnectPrevious()
# super().delete()
[docs] def delete(self):
"""Delete routine to check whether the item can be deleted otherwise raise api error.
"""
try:
# fetching the api tree will raise api errors for those objects that cannot be deleted/modified
# and skip the actual delete
self._getApiObjectTree()
# need to do a special delete here as the api always reinserts the nmrResidue at the end of the chain
self._delete()
except Exception as es:
raise es
[docs] @renameObject()
@logCommand(get='self')
def rename(self, sequenceCode: str = None, residueType: str = None):
"""Rename NmrResidue. changing its sequenceCode and residueType.
Specifying None for the sequenceCode will reset the nmrResidue to its canonical form, '@<serial>."""
apiResonanceGroup = self._apiResonanceGroup
self._validateStringValue('sequenceCode', sequenceCode, allowNone=True)
self._validateStringValue('residueType', residueType, allowNone=True, allowEmpty=True)
sequenceCode = sequenceCode or None
residueType = residueType or None
if sequenceCode:
# Check if name is not already used
partialId = '%s.%s.' % (self._parent._id, sequenceCode.translate(Pid.remapSeparators))
ll = self._project.getObjectsByPartialId(className=self.className, idStartsWith=partialId)
if ll and ll != [self]:
raise ValueError(f'Cannot rename {self} to {self.nmrChain.id}.{sequenceCode}.{residueType or ""} - assignment already exists')
oldSequenceCode = apiResonanceGroup.sequenceCode
oldResidueType = apiResonanceGroup.residueType
self._oldPid = self.pid
# rename functions from here - both values are always changed
apiResonanceGroup.sequenceCode = sequenceCode
apiResonanceGroup.resetResidueType(residueType)
# now handled by _finaliseAction
# self._renameChildren()
return (oldSequenceCode, oldResidueType)
def _renameChildren(self):
"""Update the chemicalShifts to the rename
"""
for nmrAt in self.nmrAtoms:
# actions to be called outside of rename - must be last thing to set?
nmrAt._childActions.append(nmrAt._renameChemicalShifts)
nmrAt._finaliseChildren.extend((sh, 'change') for sh in nmrAt.chemicalShifts)
#=========================================================================================
# CCPN functions
#=========================================================================================
#===========================================================================================
# new'Object' and other methods
# Call appropriate routines in their respective locations
#===========================================================================================
[docs] @logCommand(get='self')
def newNmrAtom(self, name: str = None, isotopeCode: str = None, comment: str = None, **kwds):
"""Create new NmrAtom within NmrResidue. If name is None, use default name
(of form e.g. '@_123, @H_211', '@N_45', ...)
See the NmrAtom class for details
:param name: string name of the new nmrAtom
:param isotopeCode: isotope code
:param comment: optional string comment
:return: a new NmrAtom instance.
"""
from ccpn.core.NmrAtom import _newNmrAtom # imported here to avoid circular imports
result = _newNmrAtom(self, name=name, isotopeCode=isotopeCode, comment=comment, **kwds)
return result
[docs] def fetchNmrAtom(self, name: str, isotopeCode: str = None):
"""Fetch NmrAtom with name=name, creating it if necessary
:param name: string name for new nmrAtom if created
:param isotopeCode: optional isotope code only used for a new nmrAtom.
:return: new or existing nmrAtom
"""
from ccpn.core.NmrAtom import _fetchNmrAtom # imported here to avoid circular imports
return _fetchNmrAtom(self, name=name, isotopeCode=isotopeCode)
#=========================================================================================
# Connections to parents:
#=========================================================================================
# GWV 20181122: Moved to Residue class
# def getter(self:Residue) -> typing.Optional[NmrResidue]:
# try:
# return self._project.getNmrResidue(self._id)
# except:
# return None
#
# def setter(self:Residue, 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
#
# Residue.nmrResidue = property(getter, setter, None, "NmrResidue to which Residue is assigned")
# GWV 20181122: Mover to Residue class
# def getter(self:Residue) -> typing.Tuple[NmrResidue]:
# 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))
# Residue.allNmrResidues = property(getter, None, None,
# "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.")
# def getter(self: NmrChain) -> typing.Tuple[NmrResidue]:
# if not self._wrappedData:
# return ()
#
# result = list(self._project._data2Obj.get(x) for x in self._wrappedData.mainResonanceGroups)
# if not self.isConnected:
# result.sort()
# return tuple(result)
#
#
# def setter(self: NmrChain, value):
# self._wrappedData.mainResonanceGroups = [x._wrappedData for x in value]
#
#
# NmrChain.mainNmrResidues = property(getter, setter, None, """NmrResidues belonging to NmrChain that are NOT defined relative to another NmrResidue
# (sequenceCode ending in '-1', '+1', etc.) For connected NmrChains in sequential order, otherwise sorted by assignment""")
#
# del getter
# del setter
#=========================================================================================
@newObject(NmrResidue)
def _newNmrResidue(self: NmrChain, sequenceCode: typing.Union[int, str] = None, residueType: str = None,
comment: str = None) -> NmrResidue:
"""Create new NmrResidue within NmrChain.
If NmrChain is connected, append the new NmrResidue to the end of the stretch.
See the NmrResidue class for details.
:param sequenceCode:
:param residueType:
:param comment:
:return: a new NmrResidue instance.
"""
originalSequenceCode = sequenceCode
apiNmrChain = self._wrappedData
nmrProject = apiNmrChain.nmrProject
# TODO:ED residueType cannot be an empty string
if residueType == '':
residueType = None
dd = {'name' : residueType, 'details': comment,
'residueType': residueType, 'directNmrChain': apiNmrChain}
# Convert value to string, and check
if isinstance(sequenceCode, int):
sequenceCode = str(sequenceCode)
elif sequenceCode is not None and not isinstance(sequenceCode, str):
raise ValueError("Invalid sequenceCode %s must be int, str, or None" % repr(sequenceCode))
if sequenceCode:
# Check the sequenceCode is not taken already
partialId = '%s.%s.' % (self._id, sequenceCode.translate(Pid.remapSeparators))
ll = self._project.getObjectsByPartialId(className='NmrResidue', idStartsWith=partialId)
if ll:
raise ValueError("Existing %s clashes with id %s.%s.%s" %
(ll[0].longPid, self.shortName, sequenceCode, residueType or ''))
# Handle reserved names
if sequenceCode[0] == '@' and sequenceCode[1:].isdigit():
# this is a reserved name
serial = int(sequenceCode[1:])
obj = nmrProject.findFirstResonanceGroup(serial=serial)
if obj is None:
# The implied serial is free - we can set it
sequenceCode = None
else:
# Name clashes with existing NmrResidue
raise ValueError("Cannot create NmrResidue with reserved name %s" % sequenceCode)
# # NOTE:ED - renumber the current nmrResidue, instead of error
# serial = obj.parent._serialDict['resonanceGroups'] + 1
# sequenceCode = None
else:
# Just create new ResonanceGroup with default-type name
sequenceCode = None
# Create ResonanceGroup
dd['sequenceCode'] = sequenceCode
apiResonanceGroup = nmrProject.newResonanceGroup(**dd)
result = self._project._data2Obj.get(apiResonanceGroup)
if result is None:
raise RuntimeError('Unable to generate new NmrResidue item')
if residueType is not None:
# get chem comp ID strings from residue type
tt = self._project._residueName2chemCompId.get(residueType)
#tt = MoleculeQuery.fetchStdResNameMap(self._wrappedData.root).get(residueType)
if tt is not None:
apiResonanceGroup.molType, apiResonanceGroup.ccpCode = tt
return result
def _getNmrResidue(self: NmrChain, sequenceCode: typing.Union[int, str] = None,
residueType: str = None) -> typing.Optional[NmrResidue]:
"""Get NmrResidue with sequenceCode=sequenceCode and residueType=residueType,
"""
self = self._project.getByPid(self) if isinstance(self, str) else self
if not self:
getLogger().debug('nmrChain is not defined')
return
partialId = Pid.IDSEP.join([self.id, str(sequenceCode).translate(Pid.remapSeparators), ''])
ll = self._project.getObjectsByPartialId(className='NmrResidue', idStartsWith=partialId)
if ll:
return ll[0]
else:
return self.getNmrResidue(sequenceCode)
def _fetchNmrResidue(self: NmrChain, sequenceCode: typing.Union[int, str] = None,
residueType: str = None) -> NmrResidue:
"""Fetch NmrResidue with sequenceCode=sequenceCode and residueType=residueType,
creating it if necessary.
if sequenceCode is None will create a new NmrResidue
if bool(residueType) is False will return any existing NmrResidue that matches the sequenceCode
:param sequenceCode:
:param residueType:
:return: a new NmrResidue instance.
"""
# defaults = collections.OrderedDict((('sequenceCode', None), ('residueType', None)))
#
# self._startCommandEchoBlock('fetchNmrResidue', values=locals(), defaults=defaults,
# parName='newNmrResidue')
# try:
with undoBlock():
if sequenceCode is None:
# Make new NmrResidue always
result = self.newNmrResidue(sequenceCode=None, residueType=residueType)
else:
# First see if we have the sequenceCode already
partialId = '%s.%s.' % (self._id, str(sequenceCode).translate(Pid.remapSeparators))
partialObj = self._project.getObjectsByPartialId(className='NmrResidue', idStartsWith=partialId)
if partialObj:
# there can never be more than one
result = partialObj[0]
else:
result = None
# Code below superseded as it was extremely slow
# # Should not be necessary, but it is an easy mistake to pass it as integer instead of string
# sequenceCode = str(sequenceCode)
#
# apiResult = self._wrappedData.findFirstResonanceGroup(sequenceCode=sequenceCode)
# result = apiResult and self._project._data2Obj[apiResult]
if result is None:
# NB - if this cannot be created we get the error from newNmrResidue
result = self.newNmrResidue(sequenceCode=sequenceCode, residueType=residueType)
else:
if residueType and result.residueType != residueType:
# Residue types clash - error:
raise ValueError(
"Existing %s does not match residue type %s" % (result.longPid, repr(residueType))
)
# test - missing residueType when loading Nef file
# result.residueType = residueType # can't set attribute,so error when creating
if result is None:
raise RuntimeError('Unable to generate new NmrResidue item')
return result
# Connections to parents:
#EJB 20181130: moved to nmrChain
# NmrChain.newNmrResidue = _newNmrResidue
# del _newNmrResidue
# NmrChain.fetchNmrResidue = _fetchNmrResidue
def _renameNmrResidue(self: Project, apiResonanceGroup: ApiResonanceGroup):
"""Reset pid for NmrResidue and all offset NmrResidues"""
nmrResidue = self._data2Obj.get(apiResonanceGroup)
nmrResidue._finaliseAction('rename')
# for xx in nmrResidue.offsetNmrResidues:
# xx._finaliseAction('rename')
# 20190501:ED haven't investigated this properly
# but not tested fully - but moved the offsetNmrResidue._finaliseAction into nmrResidue._finaliseAction
# Notifiers:
#NBNB TBD We must make Resonance.ResonanceGroup 1..1 when we move beyond transition model
Project._setupApiNotifier(_renameNmrResidue, ApiResonanceGroup, 'setSequenceCode')
Project._setupApiNotifier(_renameNmrResidue, ApiResonanceGroup, 'setDirectNmrChain')
Project._setupApiNotifier(_renameNmrResidue, ApiResonanceGroup, 'setResidueType')
Project._setupApiNotifier(_renameNmrResidue, ApiResonanceGroup, 'setAssignedResidue')
del _renameNmrResidue
# # Rename notifiers put in to ensure renaming of NmrAtoms:
# className = ApiResonanceGroup._metaclass.qualifiedName()
# Project._apiNotifiers.extend(
# (('_finaliseApiRename', {}, className, 'setResonances'),
# ('_finaliseApiRename', {}, className, 'addResonance'),
# )
# )