"""
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-09-13 19:21:20 +0100 (Mon, September 13, 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.util import Common as commonUtil
from ccpn.core.lib import Pid
from ccpn.core.lib.Util import AtomIdTuple
from ccpn.core.Residue import Residue
from ccpn.core._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpnmodel.ccpncore.api.ccp.molecule.MolSystem import Atom as ApiAtom
from ccpn.core.lib.ContextManagers import newObject
from ccpn.util.Logging import getLogger
[docs]class Atom(AbstractWrapperObject):
"""A molecular Atom, contained in a Residue."""
#: Class name and Short class name, for PID.
shortClassName = 'MA'
# Attribute it necessary as subclasses must use superclass className
className = 'Atom'
_parentClass = Residue
#: Name of plural link to instances of class
_pluralLinkName = 'atoms'
#: List of child classes.
_childClasses = []
# Qualified name of matching API class
_apiClassQualifiedName = ApiAtom._metaclass.qualifiedName()
# CCPN properties
@property
def _apiAtom(self) -> ApiAtom:
""" CCPN atom matching Atom"""
return self._wrappedData
@property
def _parent(self) -> Residue:
"""Residue containing Atom."""
return self._project._data2Obj[self._wrappedData.residue]
residue = _parent
@property
def _key(self) -> str:
"""Atom name string (e.g. 'HA') regularised as used for ID"""
return self._wrappedData.name.translate(Pid.remapSeparators)
@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
@property
def boundAtoms(self) -> typing.Tuple['Atom']:
"""Atoms that are covalently bound to this Atom"""
getDataObj = self._project._data2Obj.get
apiAtom = self._wrappedData
boundApiAtoms = list(apiAtom.boundAtoms)
for apiBond in apiAtom.genericBonds:
ll = list(apiBond.atoms)
apiAtom2 = ll[0] if apiAtom is ll[1] else ll[1]
boundApiAtoms.append(apiAtom2)
boundAtoms = (getDataObj(x) for x in boundApiAtoms)
return tuple(sorted(x for x in boundAtoms if x is not None))
@property
def componentAtoms(self) -> typing.Tuple['Atom']:
"""Atoms that are combined to make up this atom - reverse of 'compoundAtoms'
For simple atoms this is empty.
For wildcard atoms (e.g. HB%, QB) this gives the individual atoms that combine into atom.
For non-stereo atoms (e.g. HBx, HBy, HGx%) it gives the two alternative stereospecific atoms
Compound atoms may be nested - e.g. Valine HG1% has the components HG11, HG12, HG13
and is itself a component of HGx%, HGy%, HG%, and QG"""
getDataObj = self._project._data2Obj.get
apiAtom = self._wrappedData
return tuple(getDataObj(x) for x in self._wrappedData.components)
@property
def compoundAtoms(self) -> typing.Tuple['Atom']:
"""wildcard-, pseudo-, and nonstereo- atoms that incorporate this atom.
- reverse of 'componentAtoms'
Compound atoms may be nested - e.g. Valine HG1% has the components HG11, HG12, HG13
and is itself a component of HGx%, HGy%, HG%, and QG"""
getDataObj = self._project._data2Obj.get
apiAtom = self._wrappedData
return tuple(getDataObj(x) for x in self._wrappedData.atomGroups)
@property
def exchangesWithWater(self) -> bool:
"""True if atom exchanges with water on a msx time scale, and so is mostly unobservable.
Holds for e.g. OH atoms, NH£ groups and His side chain NH protons, but NOT for amide protons """
apiAtom = self._wrappedData
components = apiAtom.components
while components:
for apiAtom in components:
# Fastest way to get an arbitrary element from a frozen set
break
components = apiAtom.components
#
apiChemAtom = apiAtom.chemAtom
if apiChemAtom:
return apiChemAtom.waterExchangeable
else:
return False
@property
def isEquivalentAtomGroup(self) -> typing.Optional[bool]:
"""Is this atom a group of equivalent atoms?
Values are:
- True (group of equivalent atoms, e.g. H%, ALA HB%, LYS HZ%, VAL HG1% or any M pseudoatom)
- False (all single atoms, all xy nonstereo atoms, LEU HB%, ILE HG1%, VAL HG%,
or any Q non-aromatic pseudoatom)
- None = sometimes equivalent (TYR and PHE HD%, HE%, CD%, CE%, QD, QE)
"""
apiChemAtomSet = self._wrappedData.chemAtomSet
if apiChemAtomSet is None:
return False
else:
# NB isEquivalent is None for symmetric aromatic rings. We return True for that
return apiChemAtomSet.isEquivalent != False
@property
def elementSymbol(self):
return self._wrappedData.elementSymbol
[docs] def addInterAtomBond(self, atom: 'Atom'):
"""ADVANCED Add generic bond between atoms - for creating disulfides or other crosslinks
The bound-to atom will appear in self.boundAtoms.
NB This function does not remove superfluous atoms (like CYS HG),
or check for chemical plausibility. Programmer beware!"""
project = self._project
project._wrappedData.molSystem.newGenericBond(atoms=(self._wrappedData, atom._wrappedData))
#from ccpn.core.NmrAtom import NmrAtom: This will break the import sequence
@property
def nmrAtom(self) -> typing.Optional['NmrAtom']:
"""NmrAtom to which Atom is assigned
NB Atom<->NmrAtom link depends solely on the NmrAtom name.
So no notifiers on the link - notify on the NmrAtom rename instead.
"""
try:
return self._project.getNmrAtom(self._id)
except:
return None
# GWV 20181122: removed setters between Chain/NmrChain, Residue/NmrResidue, Atom/NmrAtom
# @nmrAtom.setter
# def nmrAtom(self, value:'NmrAtom'):
# oldValue = self.nmrAtom
# if oldValue is value:
# return
# elif value is None:
# raise ValueError("Cannot set Atom.nmrAtom to None")
# elif oldValue is not None:
# raise ValueError("New assignment of Atom clashes with existing assignment")
# else:
# value.atom = self
@property
def isAssigned(self) -> bool:
"""
:return: True if Atom has as NmrAtom with an associated ChemicalShift object
"""
if self.nmrAtom is None: return False
if not self.nmrAtom.chemicalShifts: return False # either None or len==0
return True
#=========================================================================================
# Implementation functions
#=========================================================================================
@classmethod
def _getAllWrappedData(cls, parent: Residue) -> list:
"""get wrappedData (MolSystem.Atoms) for all Atom children of parent Residue"""
return parent._wrappedData.sortedAtoms()
#=========================================================================================
# CCPN functions
#=========================================================================================
#===========================================================================================
# new'Object' and other methods
# Call appropriate routines in their respective locations
#===========================================================================================
#=========================================================================================
# Connections to parents:
#=========================================================================================
@newObject(Atom)
def _newAtom(self: Residue, name: str, elementSymbol: str = None) -> 'Atom':
"""Create new Atom within Residue. If elementSymbol is None, it is derived from the name
See the Atom class for details.
:param name:
:param elementSymbol:
:return: a new Atom instance.
"""
from ccpn.util.isotopes import name2ElementSymbol
lastAtom = self.getAtom(name)
if lastAtom is not None:
raise ValueError("Cannot create %s, atom name %s already in use" % (lastAtom.longPid, name))
if elementSymbol is None:
elementSymbol = name2ElementSymbol(name)
apiAtom = self._wrappedData.newAtom(name=name, elementSymbol=elementSymbol)
result = self._project._data2Obj[apiAtom]
if result is None:
raise RuntimeError('Unable to generate new Atom item')
apiAtom.expandNewAtom()
return result
#EJB 20181204: moved to Residue
# Residue.newAtom = _newAtom
# Connections to parents: