"""
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (https://www.ccpn.ac.uk) 2014 - 2022"
__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 https://ccpn.ac.uk/software/licensing/")
__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: 2022-03-18 14:09:54 +0000 (Fri, March 18, 2022) $"
__version__ = "$Revision: 3.1.0 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================
import math
from typing import Union, Tuple, Sequence
from ccpn.core.NmrResidue import NmrResidue
from ccpn.core.Project import Project
from ccpn.core._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpn.core._implementation.AbsorbResonance import absorbResonance
from ccpn.core.lib import Pid
from ccpn.core.lib.Util import AtomIdTuple
from ccpnmodel.ccpncore.api.ccp.nmr import Nmr
from ccpnmodel.ccpncore.lib import Constants
from ccpn.util.Common import makeIterableList
from ccpn.util.decorators import logCommand
from ccpn.util.isotopes import isotopeCode2Nucleus, getIsotopeRecords
from ccpn.core.lib.ContextManagers import newObject, renameObject, undoBlock, ccpNmrV3CoreSetter
from ccpn.util.Logging import getLogger
from collections import defaultdict
UnknownIsotopeCode = '?'
[docs]class NmrAtom(AbstractWrapperObject):
"""NmrAtom objects are used for assignment. An NmrAtom within an assigned NmrResidue is
by definition assigned to the Atom with the same name (if any).
NmrAtoms serve as a way of connecting a named nucleus to an observed chemical shift,
and peaks are assigned to NmrAtoms. Renaming an NmrAtom (or its containing NmrResidue or
NmrChain) automatically updates peak assignments and ChemicalShifts that use the NmrAtom,
preserving the link.
"""
#: Short class name, for PID.
shortClassName = 'NA'
# Attribute it necessary as subclasses must use superclass className
className = 'NmrAtom'
_parentClass = NmrResidue
#: Name of plural link to instances of class
_pluralLinkName = 'nmrAtoms'
# the attribute name used by current
_currentAttributeName = 'nmrAtoms'
#: List of child classes.
_childClasses = []
# Qualified name of matching API class
_apiClassQualifiedName = Nmr.Resonance._metaclass.qualifiedName()
# Internal NameSpace
_AMBIGUITYCODE = '_ambiguityCode'
_ORIGINALNAME = '_originalName'
def __init__(self, project: Project, wrappedData):
# internal lists to hold the current chemicalShifts
self._chemicalShifts = []
super().__init__(project, wrappedData)
#=========================================================================================
# CCPN properties
#=========================================================================================
@property
def _apiResonance(self) -> Nmr.Resonance:
""" CCPN atom matching Atom"""
return self._wrappedData
@property
def _parent(self) -> NmrResidue:
"""Parent (containing) object."""
return self._project._data2Obj.get(self._wrappedData.resonanceGroup)
nmrResidue = _parent
@property
def _key(self) -> str:
"""Atom name string (e.g. 'HA') regularised as used for ID"""
return self._wrappedData.name.translate(Pid.remapSeparators)
@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>' but subject to change"""
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]}'
return _name
@property
def _localCcpnSortKey(self) -> Tuple:
"""Local sorting key, in context of parent."""
# We want sorting by name, even though Resonances have serials
return (self._key,)
@property
def _idTuple(self) -> AtomIdTuple:
"""ID as chainCode, sequenceCode, residueType, atomName namedtuple
NB Unlike the _id and key, these do NOT have reserved characters mapped to '^'
NB _idTuple replaces empty strings with None"""
parent = self._parent
ll = [parent._parent.shortName, parent.sequenceCode, parent.residueType, self.name]
return AtomIdTuple(*(x or None for x in ll))
@property
def name(self) -> str:
"""Atom name string (e.g. 'HA')"""
return self._wrappedData.name
@name.setter
def name(self, value: str):
"""set Atom name"""
self.rename(value)
@property
def serial(self) -> int:
"""NmrAtom serial number - set at creation and unchangeable"""
return self._wrappedData.serial
#from ccpn.core.Atom import Atom: This will break the import sequence
@property
def atom(self) -> 'Atom':
"""Atom to which NmrAtom is assigned. NB resetting the atom will rename the NmrAtom"""
return self._project.getAtom(self._id)
@property
def isotopeCode(self) -> str:
"""isotopeCode of NmrAtom. Used to facilitate the nmrAtom assignment."""
value = self._wrappedData.isotopeCode
if value in [UnknownIsotopeCode, self._UNKNOWN_VALUE_STRING]:
value = None
return value
def _setIsotopeCode(self, value):
"""
:param value: value must be defined, if not set then can set to arbitrary value see UnknownIsotopeCode definition
this means it can still be set at any isotopeCode later, otherwise
need to undo or create new nmrAtom
CCPNINTERNAL: used in _newNmrAtom, Peak.assignDimension
"""
if not isinstance(value, (str, type(None))):
raise ValueError('isotopeCode must be of type string (or None); got {}'.format(value))
if value is not None and value not in list(getIsotopeRecords().keys())+[UnknownIsotopeCode]:
raise ValueError('Invalid isotopeCode {}'.format(value))
self._wrappedData.isotopeCode = value if value else UnknownIsotopeCode
@property
def boundNmrAtoms(self) -> 'NmrAtom':
"""NmrAtoms directly bound to this one, as calculated from assignment and
NmrAtom name matches (NOT from peak assignment)"""
getDataObj = self._project._data2Obj.get
ll = self._wrappedData.getBoundResonances()
result = [getDataObj(x) for x in ll]
nmrResidue = self.nmrResidue
if nmrResidue.residue is None:
# NmrResidue is unassigned. Add ad-hoc protein interresidue bonds
if self.name == 'N':
for rx in (nmrResidue.previousNmrResidue, nmrResidue.getOffsetNmrResidue(-1)):
if rx is not None:
na = rx.getNmrAtom('C')
if na is not None:
result.append(na)
elif self.name == 'C':
for rx in (nmrResidue.nextNmrResidue, nmrResidue.getOffsetNmrResidue(1)):
if rx is not None:
na = rx.getNmrAtom('N')
if na is not None:
result.append(na)
#
return result
@property
def assignedPeaks(self) -> Tuple['Peak']:
"""All Peaks assigned to the NmrAtom"""
apiResonance = self._wrappedData
apiPeaks = [x.peakDim.peak for x in apiResonance.peakDimContribs]
apiPeaks += [x.peakDim.peak for x in apiResonance.peakDimContribNs]
data2Obj = self._project._data2Obj
return tuple(data2Obj[x] for x in set(apiPeaks))
@property
def _ambiguityCode(self):
"""Return the ambiguityCode
"""
result = self._getInternalParameter(self._AMBIGUITYCODE)
return result
@_ambiguityCode.setter
@ccpNmrV3CoreSetter()
def _ambiguityCode(self, value):
"""Set the ambiguityCode
"""
self._setInternalParameter(self._AMBIGUITYCODE, value)
@property
def _originalName(self):
"""Return the originalName
"""
result = self._getInternalParameter(self._ORIGINALNAME)
return result
@_originalName.setter
@ccpNmrV3CoreSetter()
def _originalName(self, value):
"""Set the originalName
"""
self._setInternalParameter(self._ORIGINALNAME, value)
[docs] @logCommand(get='self')
def deassign(self):
"""Reset NmrAtom back to its originalName, cutting all assignment links"""
self._wrappedData.name = None
[docs] @logCommand(get='self')
def assignTo(self, chainCode: str = None, sequenceCode: Union[int, str] = None,
residueType: str = None, name: str = None, mergeToExisting=False) -> 'NmrAtom':
"""Assign NmrAtom to naming parameters) and return the reassigned result
If the assignedTo NmrAtom already exists the function raises ValueError.
If mergeToExisting is True it instead merges the current NmrAtom into the target
and returns the merged target. NB Merging is NOT undoable
WARNING: is mergeToExisting is True, always use in the form "x = x.assignTo(...)",
as the call 'x.assignTo(...) may cause the source x object to be deleted.
Passing in empty parameters (e.g. chainCode=None) leaves the current value unchanged. E.g.:
for NmrAtom NR:A.121.ALA.HA calling with sequenceCode=124 will assign to
(chainCode='A', sequenceCode=124, residueType='ALA', atomName='HA')
The function works as:
nmrChain = project.fetchNmrChain(shortName=chainCode)
nmrResidue = nmrChain.fetchNmrResidue(sequenceCode=sequenceCode, residueType=residueType)
(or nmrChain.fetchNmrResidue(sequenceCode=sequenceCode) if residueType is None)
"""
oldPid = self.longPid
apiResonance = self._apiResonance
apiResonanceGroup = apiResonance.resonanceGroup
with undoBlock():
if sequenceCode is not None:
sequenceCode = str(sequenceCode) or None
# set missing parameters to existing values
chainCode = chainCode or apiResonanceGroup.nmrChain.code
sequenceCode = sequenceCode or apiResonanceGroup.sequenceCode
residueType = residueType or apiResonanceGroup.residueType
name = name or apiResonance.name
for ss in chainCode, sequenceCode, residueType, name:
if ss and Pid.altCharacter in ss:
raise ValueError("Character %s not allowed in ccpn.NmrAtom id : %s.%s.%s.%s"
% (Pid.altCharacter, chainCode, sequenceCode, residueType, name))
oldNmrResidue = self.nmrResidue
nmrChain = self._project.fetchNmrChain(chainCode)
if residueType:
nmrResidue = nmrChain.fetchNmrResidue(sequenceCode, residueType)
else:
nmrResidue = nmrChain.fetchNmrResidue(sequenceCode)
if name:
# result is matching NmrAtom, or (if None) self
result = nmrResidue.getNmrAtom(name) or self
else:
# No NmrAtom can match, result is self
result = self
if nmrResidue is oldNmrResidue:
if name != self.name:
# NB self.name can never be returned as None
if result is self:
# self._wrappedData.name = name or None
self.rename(name or None)
elif mergeToExisting:
result.mergeNmrAtoms(self)
else:
raise ValueError("New assignment clash with existing assignment,"
" and merging is disallowed")
else:
if result is self:
# if nmrResidue.getNmrAtom(self.name) is None:
# if name != self.name:
# # self._wrappedData.name = name or None
# self.rename(name or None)
# # self._apiResonance.resonanceGroup = nmrResidue._apiResonanceGroup
# self._setApiResonanceGroup(self._apiResonance, nmrResidue)
#
# elif name is None or oldNmrResidue.getNmrAtom(name) is None:
# if name != self.name:
# # self._wrappedData.name = name or None
# self.rename(name or None)
# # self._apiResonance.resonanceGroup = nmrResidue._apiResonanceGroup
# self._setApiResonanceGroup(self._apiResonance, nmrResidue)
#
# else:
# # self._wrappedData.name = None # Necessary to avoid name clashes
# self.rename(None) # Necessary to avoid name clashes
# self._apiResonance.resonanceGroup = nmrResidue._apiResonanceGroup
# # self._setApiResonanceGroup(self._apiResonance, nmrResidue)
# # self._wrappedData.name = name
# self.rename(name or None)
# Necessary to avoid name clashes - also handles all notifiers
# is it firing too many now though?
self.rename(None)
self._apiResonance.resonanceGroup = nmrResidue._apiResonanceGroup
self.rename(name or None)
elif mergeToExisting:
result.mergeNmrAtoms(self)
else:
raise ValueError("New assignment clash with existing assignment,"
" and merging is disallowed")
return result
[docs] @logCommand(get='self')
def mergeNmrAtoms(self, nmrAtoms: Union['NmrAtom', Sequence['NmrAtom']]):
nmrAtoms = makeIterableList(nmrAtoms)
nmrAtoms = [self.project.getByPid(nmrAtom) if isinstance(nmrAtom, str) else nmrAtom for nmrAtom in nmrAtoms]
if not all(isinstance(nmrAtom, NmrAtom) for nmrAtom in nmrAtoms):
raise TypeError('nmrAtoms can only contain items of type NmrAtom')
if self in nmrAtoms:
raise TypeError('nmrAtom cannot be merged with itself')
with undoBlock():
for nmrAtom in nmrAtoms:
absorbResonance(self, nmrAtom)
@property
def _oldChemicalShifts(self) -> Tuple:
"""Returns ChemicalShift objects connected to NmrAtom"""
getDataObj = self._project._data2Obj.get
return tuple(sorted(getDataObj(x) for x in self._wrappedData.shifts))
def _getAttribute(self, attrName) -> Tuple:
"""Returns contents of api attribute
"""
if hasattr(self._wrappedData, attrName):
return getattr(self._wrappedData, attrName)
raise TypeError('nmrAtom does not have attribute {}'.format(attrName))
@property
def chemicalShifts(self) -> tuple:
"""Return the chemicalShifts containing the nmrAtom
"""
return tuple(self._chemicalShifts)
#=========================================================================================
# Implementation functions
#=========================================================================================
@classmethod
def _restoreObject(cls, project, apiObj):
"""Subclassed to replace unknown isotopCodes"""
result = super(NmrAtom, cls)._restoreObject(project=project, apiObj=apiObj)
# Update Unknown to None
if result.isotopeCode == UnknownIsotopeCode:
result._setIsotopeCode(None)
return result
@classmethod
def _getAllWrappedData(cls, parent: NmrResidue) -> list:
"""get wrappedData (ApiResonance) for all NmrAtom children of parent NmrResidue
"""
return parent._wrappedData.sortedResonances()
@renameObject()
def _setApiName(self, value):
"""Set a serial format name of the form ?@<n> from the current serial number
functionality provided by the api
CCPN Internal - should only be used during nef import
"""
oldName = self._wrappedData.name
self._oldPid = self.pid
self._wrappedData.name = value
self._resetIds()
return (oldName,)
def _makeUniqueName(self) -> str:
"""Generate a unique name in the form @n (e.g. @_123) or @symbol_n (e.g. @H_34)
:return the generated name
"""
if self.isotopeCode is not None and (symbol := isotopeCode2Nucleus(self.isotopeCode)) is not None and len(symbol) > 0:
_name = '@%s_%d' % (symbol[0:1], self._uniqueId)
else:
_name = '@_%d' % self._uniqueId
return _name
# Sub-class two methods to get '@' names
@classmethod
def _defaultName(cls) -> str:
return '@'
@classmethod
def _uniqueName(cls, project, name=None) -> str:
"""Subclassed to get the '@' default name behavior"""
if name is None:
_id = project._queryNextUniqueIdValue(cls.className)
name = '%s_%d' % (cls._defaultName(), _id)
return super(NmrAtom, cls)._uniqueName(project=project, name=name)
def _finaliseAction(self, action: str):
"""Subclassed to handle associated offsetNMrResidues
"""
if action == 'rename':
# rename the nmrAtom in the chemicalShifts
self._childActions.append(self._renameChemicalShifts)
self._finaliseChildren.extend((sh, 'change') for sh in self.chemicalShifts)
if not super()._finaliseAction(action):
return
[docs] @renameObject()
@logCommand(get='self')
def rename(self, value: str = None):
"""Rename the NmrAtom, changing its name, Pid, and internal representation.
"""
if value == self.name: return
if value is None:
value = self._makeUniqueName()
getLogger().debug('Renaming an %s without a specified value. Name set to the auto-generated option: %s.' % (self, value))
NmrAtom._validateStringValue('name', value)
previous = self._parent.getNmrAtom(value.translate(Pid.remapSeparators))
if previous is not None:
raise ValueError('NmrAtom.rename: "%s" conflicts with %s' % (value, previous))
# with renameObjectContextManager(self) as addUndoItem:
isotopeCode = self.isotopeCode
oldName = self.name
self._oldPid = self.pid
# clear the isotopeCode so that the name may be changed (model restriction)
self._wrappedData.isotopeCode = UnknownIsotopeCode
self._wrappedData.name = value
# set isotopeCode to the correct value
self._wrappedData.isotopeCode = isotopeCode if isotopeCode else UnknownIsotopeCode # self._UNKNOWN_VALUE_STRING
# now handled by _finaliseAction
# self._childActions.append(self._renameChemicalShifts)
# self._finaliseChildren.extend((sh, 'change') for sh in self.chemicalShifts)
return (oldName,)
def _renameChemicalShifts(self):
# update chemicalShifts
for cs in self.chemicalShifts:
cs._renameNmrAtom(self)
[docs] def delete(self):
"""Delete self and update the chemicalShift values
"""
_shifts = self.chemicalShifts # tuple from property
with undoBlock():
for sh in _shifts:
sh.nmrAtom = None
# delete the nmrAtom - notifiers handled by decorator
self._delete()
#=========================================================================================
# CCPN functions
#=========================================================================================
def _getAssignedPeakValues(self, spectra, peakLists=None, theProperty='ppmPosition'):
"""
CCPN internal. Used in ChemicalShift mapping and Relaxation Analysis tools.
Convenient routine to avoid nested "for-loops".
Given a set of spectra, get the value of a particular property for the assigned-peak-dimension.
Return a dictionary where the spectrum is the key and the value is the list of a given peak property.
:param spectra: list of CCPN spectra.
:param peakLists: list of CCPN peakLists to use as a sub-filter, otherwise use all available in spectra.
:param theProperty: one of (ppmPosition, pointPosition, lineWidth, height, volume). Notes:
height and volume are not a dim property but a peak property. Taken here to avoid code duplication.
:return: dictionary {obj:list}
E.g.: for <NA:A.53.ASN.N> theProperty='ppmPosition' it returns
{<SP:Tstar-free>: [119.80854378483475], <SP:Tstar-2:0eq>: [119.93958073136751], ...}
"""
from ccpn.core.lib.peakUtils import _POINTPOSITION, _PPMPOSITION, _LINEWIDTH, HEIGHT, VOLUME
if peakLists is None:
peakLists = [pl for sp in spectra for pl in sp.peakLists]
valuesDict = defaultdict(list)
for contrib in self._wrappedData.peakDimContribs:
if contrib.isDeleted:
continue
peakDim = contrib.peakDim
apiPeak = peakDim.peak
if apiPeak.isDeleted or peakDim.isDeleted and apiPeak.figOfMerit == 0.0: #figure of merit shouldn't be filtered here!?
continue
apiPeakList = apiPeak.peakList
peakList = self.project._data2Obj[apiPeakList]
spectrum = peakList.spectrum
if peakList not in peakLists:
continue
propertyDict = {
_POINTPOSITION : peakDim.position,
_PPMPOSITION : peakDim.realValue,
_LINEWIDTH : peakDim.lineWidth,
HEIGHT : apiPeak.height,
VOLUME : apiPeak.volume,
}
valuesDict[spectrum].append(propertyDict.get(theProperty, None))
return valuesDict
def _recalculateShiftValue(self, spectra, simulatedPeakScale: float = 0.0001):
"""Get a new shift value from the assignedPeaks
"""
apiResonance = self._wrappedData
sum1 = sum2 = N = 0.0
peakDims = []
peaks = set()
for contrib in apiResonance.peakDimContribs:
if contrib.isDeleted:
# Function may be called during PeakDimContrib deletion
continue
peakDim = contrib.peakDim
apiPeak = peakDim.peak
if apiPeak.isDeleted or peakDim.isDeleted or apiPeak.figOfMerit == 0.0:
continue
apiPeakList = apiPeak.peakList
spectrum = self.project._data2Obj[apiPeakList].spectrum
if spectrum not in spectra:
continue
# NBNB TBD: Old Rasmus comment - peak splittings are not yet handled in V3. TBD add them
value = peakDim.realValue
weight = apiPeak.figOfMerit
if apiPeakList.isSimulated:
weight *= simulatedPeakScale
peakDims.append(peakDim)
peaks.add(apiPeak)
vw = value * weight
sum1 += vw
sum2 += value * vw
N += weight
if N > 0.0:
mean = sum1 / N
mean2 = sum2 / N
sigma2 = abs(mean2 - (mean * mean))
sigma = math.sqrt(sigma2)
else:
return None, None
return mean, sigma if len(peakDims) > 1 else None
#=========================================================================================
# new'Object' and other methods
# Call appropriate routines in their respective locations
#=========================================================================================
#=========================================================================================
# Connections to parents:
#=========================================================================================
@newObject(NmrAtom)
def _newNmrAtom(self: NmrResidue, name: str = None, isotopeCode: str = None, comment: str = None, **kwds) -> NmrAtom:
"""Create new NmrAtom within NmrResidue.
See the NmrAtom class for details
:param name: string name of the new nmrAtom; If name is None, generate a default name of form
e.g. '@_123, @H_211', '@N_45', ...
:param isotopeCode: optional isotope code
:param comment: optional string comment
:return: a new NmrAtom instance.
"""
apiNmrProject = self._project._wrappedData
resonanceGroup = self._wrappedData
if not isinstance(name, (str, type(None))):
raise TypeError('Name {} must be of type string (or None)'.format(name))
if name is None or len(name) == 0:
# generate (temporary) default name, to be changed later after we created the object
_name = NmrAtom._uniqueName(self.project)
else:
# Check for name clashes
_name = name
previous = self.getNmrAtom(_name.translate(Pid.remapSeparators))
if previous is not None:
raise ValueError('newNmrAtom: name "%s" clashes with %s' % (name, previous))
# Create the api object
# Always create first with unknown isotopeCode
dd = {'resonanceGroup': resonanceGroup, 'isotopeCode': UnknownIsotopeCode, 'name': _name}
obj = apiNmrProject.newResonance(**dd)
if (result := self._project._data2Obj.get(obj)) is None:
raise RuntimeError('Unable to generate new NmrAtom item')
# Check/set isotopeCode; it has to be set after the creation to avoid API errors.
result._setIsotopeCode(isotopeCode)
if comment is not None and len(comment) > 0:
result.comment = comment
# Set additional optional attributes supplied as kwds arguments
for key, value in kwds.items():
setattr(result, key, value)
return result
def _fetchNmrAtom(self: NmrResidue, name: str, isotopeCode=None):
"""Fetch NmrAtom with name=name, creating it if necessary
:param name: string name for new nmrAto if created
:return: new or existing nmrAtom
"""
# resonanceGroup = self._wrappedData
with undoBlock():
result = (self.getNmrAtom(name.translate(Pid.remapSeparators)) or
self.newNmrAtom(name=name, isotopeCode=isotopeCode))
if result is None:
raise RuntimeError('Unable to generate new NmrAtom item')
return result
def _produceNmrAtom(self: Project, atomId: str = None, chainCode: str = None,
sequenceCode: Union[int, str] = None,
residueType: str = None, name: str = None) -> NmrAtom:
"""Get chainCode, sequenceCode, residueType and atomName from dot-separated atomId or Pid
or explicit parameters, and find or create an NmrAtom that matches
Empty chainCode gets NmrChain:@- ; empty sequenceCode get a new NmrResidue"""
with undoBlock():
# Get ID parts to use
if sequenceCode is not None:
sequenceCode = str(sequenceCode) or None
params = [chainCode, sequenceCode, residueType, name]
if atomId:
if any(params):
raise ValueError("_produceNmrAtom: other parameters only allowed if atomId is None")
else:
#TODO: use .fields attribute of Pid instance
# Remove colon prefix, if any
atomId = atomId.split(Pid.PREFIXSEP, 1)[-1]
for ii, val in enumerate(Pid.splitId(atomId)):
if val:
params[ii] = val
chainCode, sequenceCode, residueType, name = params
if name is None:
raise ValueError("NmrAtom name must be set")
elif Pid.altCharacter in name:
raise ValueError("Character %s not allowed in ccpn.NmrAtom.name" % Pid.altCharacter)
# Produce chain
nmrChain = self.fetchNmrChain(shortName=chainCode or Constants.defaultNmrChainCode)
nmrResidue = nmrChain.fetchNmrResidue(sequenceCode=sequenceCode, residueType=residueType)
result = nmrResidue.fetchNmrAtom(name)
if result is None:
raise RuntimeError('Unable to generate new NmrAtom item')
return result
#EJB 20181203: moved to NmrResidue
# NmrResidue.newNmrAtom = _newNmrAtom
# del _newNmrAtom
# NmrResidue.fetchNmrAtom = _fetchNmrAtom
#EJB 20181203: moved to nmrAtom
# Project._produceNmrAtom = _produceNmrAtom
# Notifiers:
# className = Nmr.Resonance._metaclass.qualifiedName()
# Project._apiNotifiers.extend(
# (('_finaliseApiRename', {}, className, 'setImplName'),
# ('_finaliseApiRename', {}, className, 'setResonanceGroup'),
# )
# )
for clazz in Nmr.AbstractPeakDimContrib._metaclass.getNonAbstractSubtypes():
className = clazz.qualifiedName()
Project._apiNotifiers.extend(
(('_modifiedLink', {'classNames': ('NmrAtom', 'Peak')}, className, 'create'),
('_modifiedLink', {'classNames': ('NmrAtom', 'Peak')}, className, 'delete'),
)
)