"""
"""
#=========================================================================================
# 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-03 20:07:35 +0000 (Fri, December 03, 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
#=========================================================================================
from collections import OrderedDict
import typing
from ccpn.util import Common as commonUtil
from ccpn.core.Project import Project
from ccpn.core.Sample import Sample
from ccpn.core.SampleComponent import SampleComponent
from ccpn.core.Spectrum import Spectrum
from ccpn.core._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpn.core.lib import Pid
from ccpn.util.Constants import DEFAULT_LABELLING
from ccpnmodel.ccpncore.api.ccp.lims.RefSampleComponent import AbstractComponent as ApiRefComponent
from ccpnmodel.ccpncore.api.ccp.nmr import Nmr
from ccpnmodel.ccpncore.lib import Util as coreUtil
from ccpnmodel.ccpncore.lib.molecule import MoleculeModify
from ccpn.util.decorators import logCommand
from ccpn.core.lib.ContextManagers import newObject, renameObject, undoBlock
from ccpn.util.Logging import getLogger
from contextlib import contextmanager
_apiClassNameMap = {
'MolComponent': 'Molecule',
'Substance' : 'Material'
}
[docs]class Substance(AbstractWrapperObject):
"""A Substance is a chemical entity or material that can be added to a Sample.
Substances are defined by their name and labelling attributes (labelling defaults to None).
Renaming a Substance will also rename all SampleComponents and SpectrumHits associated with
it, so as to preserve the link between the objects.
The most common case (by far) is substanceType 'Molecule', which corresponds to a chemical entity,
such as Calmodulin, ATP, or NaCl. This type of Substance will have Smiles strings, sequence,
and other molecular attributes as appropriate. Such a Substance may be associated with one
or more Chains, and can be used as a starting point to generate new Chains, using the
Project.createPolymerSubstance() function.
ADVANCED: It is also possible to create Substances with substanceType 'Material' or 'Cell'.
Materials are used to describe chemical mixtures, such as fetal calf serum, algal lysate, or
'standard experiment buffer number 3'.
"""
#: Short class name, for PID.
shortClassName = 'SU'
# Attribute it necessary as subclasses must use superclass className
className = 'Substance'
_parentClass = Project
#: Name of plural link to instances of class
_pluralLinkName = 'substances'
# the attribute name used by current
_currentAttributeName = 'substances'
#: List of child classes.
_childClasses = []
# Qualified name of matching API class
_apiClassQualifiedName = ApiRefComponent._metaclass.qualifiedName()
# CCPN internal
_linkedSpectraPids = '_linkedSpectraPids'
_SPECIFICATOMLABELLING = 'specificAtomLabelling'
# CCPN properties
@property
def _apiSubstance(self) -> ApiRefComponent:
""" API RefSampleComponent matching Substance"""
return self._wrappedData
@property
def _key(self) -> str:
"""id string - name.labelling"""
obj = self._wrappedData
name = obj.name
labelling = obj.labeling
if labelling == DEFAULT_LABELLING:
labelling = ''
return Pid.createId(name, labelling)
@property
def _localCcpnSortKey(self) -> typing.Tuple:
"""Local sorting key, in context of parent."""
obj = self._wrappedData
labelling = obj.labeling
return (obj.name, '' if labelling == DEFAULT_LABELLING else labelling)
@property
def name(self) -> str:
"""name of Substance"""
return self._wrappedData.name
@property
def labelling(self) -> str:
"""labelling descriptor of Substance (default is 'std')"""
result = self._wrappedData.labeling
if result == DEFAULT_LABELLING:
result = None
#
return result
@property
def _parent(self) -> Sample:
"""Project containing Substance."""
return self._project
@property
def substanceType(self) -> str:
"""Category of substance: Molecule, Cell, Material, or Composite
- Molecule is a single molecule, including plasmids
- Cell is a cell,
- Material is a mixture, like fetal calf serum, growth medium, or standard buffer,
- Composite is multiple components in fixed ratio, like a protein-ligand or multiprotein
complex, or (technically) a Cell containing a particular plasmid.
"""
result = self._wrappedData.className
return _apiClassNameMap.get(result, result)
@property
def synonyms(self) -> typing.Tuple[str, ...]:
"""Synonyms for Substance name"""
return self._wrappedData.synonyms
@synonyms.setter
def synonyms(self, value):
"""Synonyms for Substance name"""
self._wrappedData.synonyms = value
@property
def userCode(self) -> typing.Optional[str]:
"""User-defined compound code"""
return self._wrappedData.userCode
@userCode.setter
def userCode(self, value: str):
self._wrappedData.userCode = value
@property
def smiles(self) -> typing.Optional[str]:
"""Smiles string - for substances that have one"""
apiRefComponent = self._wrappedData
return apiRefComponent.smiles if hasattr(apiRefComponent, 'smiles') else None
@smiles.setter
def smiles(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'smiles'):
apiRefComponent.smiles = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'smiles'" % _apiClassNameMap.get(ss, ss))
@property
def inChi(self) -> typing.Optional[str]:
"""inChi string - for substances that have one"""
apiRefComponent = self._wrappedData
return apiRefComponent.inChi if hasattr(apiRefComponent, 'inChi') else None
@inChi.setter
def inChi(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'inChi'):
apiRefComponent.inChi = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'inChi'" % _apiClassNameMap.get(ss, ss))
@property
def casNumber(self) -> typing.Optional[str]:
"""CAS number string - for substances that have one"""
apiRefComponent = self._wrappedData
return apiRefComponent.casNum if hasattr(apiRefComponent, 'casNum') else None
@casNumber.setter
def casNumber(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'casNum'):
apiRefComponent.casNum = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'casNumber'"
% _apiClassNameMap.get(ss, ss))
@property
def empiricalFormula(self) -> typing.Optional[str]:
"""Empirical molecular formula string - for substances that have one"""
apiRefComponent = self._wrappedData
return (apiRefComponent.empiricalFormula if hasattr(apiRefComponent, 'empiricalFormula')
else None)
@empiricalFormula.setter
def empiricalFormula(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'empiricalFormula'):
apiRefComponent.empiricalFormula = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'empiricalFormula'"
% _apiClassNameMap.get(ss, ss))
@property
def sequenceString(self) -> typing.Optional[str]:
"""Molecular sequence string - set by the createPolymerSubstance function. Substances
created by this function can be used to generate matching chains with the
substance.createChain function
For standard polymers defaults to a string of one-letter codes;
for other molecules to a comma-separated tuple of three-letter codes"""
apiRefComponent = self._wrappedData
return apiRefComponent.seqString if hasattr(apiRefComponent, 'seqString') else None
@sequenceString.setter
def sequenceString(self, value):
self._wrappedData.seqString = value
@property
def molecularMass(self) -> typing.Optional[float]:
"""Molecular mass - for substances that have one"""
apiRefComponent = self._wrappedData
return apiRefComponent.molecularMass if hasattr(apiRefComponent, 'molecularMass') else None
@molecularMass.setter
def molecularMass(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'molecularMass'):
apiRefComponent.molecularMass = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'molecularMass'"
% _apiClassNameMap.get(ss, ss))
@property
def atomCount(self) -> int:
"""Number of atoms in the molecule - for Molecular substances"""
apiRefComponent = self._wrappedData
return apiRefComponent.atomCount if hasattr(apiRefComponent, 'atomCount') else None
@atomCount.setter
def atomCount(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'atomCount'):
apiRefComponent.atomCount = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'atomCount'"
% _apiClassNameMap.get(ss, ss))
@property
def bondCount(self) -> int:
"""Number of bonds in the molecule - for Molecular substances"""
apiRefComponent = self._wrappedData
return apiRefComponent.bondCount if hasattr(apiRefComponent, 'bondCount') else None
@bondCount.setter
def bondCount(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'bondCount'):
apiRefComponent.bondCount = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'bondCount'"
% _apiClassNameMap.get(ss, ss))
@property
def ringCount(self) -> int:
"""Number of rings in the molecule - for Molecular substances"""
apiRefComponent = self._wrappedData
return apiRefComponent.ringCount if hasattr(apiRefComponent, 'ringCount') else None
@ringCount.setter
def ringCount(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'ringCount'):
apiRefComponent.ringCount = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'ringCount'"
% _apiClassNameMap.get(ss, ss))
@property
def hBondDonorCount(self) -> int:
"""Number of hydrogen bond donors in the molecule - for Molecular substances"""
apiRefComponent = self._wrappedData
return apiRefComponent.hBondDonorCount if hasattr(apiRefComponent, 'hBondDonorCount') else None
@hBondDonorCount.setter
def hBondDonorCount(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'hBondDonorCount'):
apiRefComponent.hBondDonorCount = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'hBondDonorCount'"
% _apiClassNameMap.get(ss, ss))
@property
def hBondAcceptorCount(self) -> int:
"""Number of hydrogen bond acceptors in the molecule - for Molecular substances"""
apiRefComponent = self._wrappedData
return (apiRefComponent.hBondAcceptorCount if hasattr(apiRefComponent, 'hBondAcceptorCount')
else None)
@hBondAcceptorCount.setter
def hBondAcceptorCount(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'hBondAcceptorCount'):
apiRefComponent.hBondAcceptorCount = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'hBondAcceptorCount'"
% _apiClassNameMap.get(ss, ss))
@property
def polarSurfaceArea(self) -> typing.Optional[float]:
"""Polar surface area (in square Angstrom) of the molecule - for Molecular substances"""
apiRefComponent = self._wrappedData
return (apiRefComponent.polarSurfaceArea if hasattr(apiRefComponent, 'polarSurfaceArea')
else None)
@polarSurfaceArea.setter
def polarSurfaceArea(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'polarSurfaceArea'):
apiRefComponent.polarSurfaceArea = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'polarSurfaceArea'"
% _apiClassNameMap.get(ss, ss))
@property
def logPartitionCoefficient(self) -> typing.Optional[float]:
"""Logarithm of the octanol-water partition coefficient (logP) - for Molecular substances"""
apiRefComponent = self._wrappedData
return (apiRefComponent.logPartitionCoefficient
if hasattr(apiRefComponent, 'logPartitionCoefficient') else None)
@logPartitionCoefficient.setter
def logPartitionCoefficient(self, value):
apiRefComponent = self._wrappedData
if hasattr(apiRefComponent, 'logPartitionCoefficient'):
apiRefComponent.logPartitionCoefficient = value
else:
ss = apiRefComponent.className
raise TypeError("%s type Substance has no attribute 'logPartitionCoefficient'"
% _apiClassNameMap.get(ss, ss))
@property
def specificAtomLabelling(self) -> typing.Dict[str, typing.Dict[str, float]]:
"""Site-specific labelling for all chains matching Substance
in the form of (atomId:{isotopeCode:fraction}} dictionary.
Note that changing the labelling for a site in one chain
simultaneously affects the matching site in other matching chains.
To modify this attribute use the functions setSpecificAtomLabelling,
removeSpecificAtomLabelling, clearSpecificAtomLabelling,
updateSpecificAtomLabelling
Example value (for two chains where the numbering of B is offset 200 from chain A):
| {'A.11.ALA.CB':{'12C':0.32, '13C':0.68},
| 'B.211.ALA.CB':{'12C':0.32, '13C':0.68},}"""
result = {}
dd = self._getInternalParameter(self._SPECIFICATOMLABELLING)
if dd:
for chain in self.chains:
# NBNB this relies on residues being sorted by seqId, and so being
# in sequence order
residues = chain.residues
for tt, labellingDict in dd.items():
residueIndex, atomName = tt
residue = residues[residueIndex]
atom = residue.getAtom(atomName)
result[atom._id] = labellingDict.copy()
#
return result
[docs] def setSpecificAtomLabelling(self, atom: typing.Union[str, 'Atom'], isotopeLabels: dict):
"""Set isotopeLabels dict as labelling for atom designated by atomId.
NBNB labelling is set for the matching atom in all chains that match the Substance
also if the other chains have a different numbering.
isotopeLabels must be a dictionary of the form (e.g.) {'12C':0.32, '13C':0.68}
where the atom fractions add up to 1.0 and the isotope Codes cover the possibilities
for the atom."""
if isinstance(atom, str):
# Get Atom from id or Pid
ll = atom.split(Pid.PREFIXSEP, 1)
atom = self._project.getAtom(ll[-1])
if atom is None:
raise ValueError("Atom with ID %s does not exist" % atom)
if atom.residue.chain not in self.chains:
raise ValueError("%s and its chain do not match the Substance" % atom.longPid)
dd = self._getInternalParameter(self._SPECIFICATOMLABELLING)
if dd is None:
dd = {}
residue = atom.residue
residueIndex = residue.chain.residues.index(residue)
dd[(residueIndex, atom.name)] = isotopeLabels
self._setInternalParameter(self._SPECIFICATOMLABELLING, dd)
[docs] def removeSpecificAtomLabelling(self, atom: typing.Union[str, 'Atom']):
"""Remove specificAtomLabelling for atom designated by atomId
NBNB labelling is removed for the matching atom in all chains that match the Substance
also if the other chains have a different numbering."""
if isinstance(atom, str):
# Get Atom from id or Pid
ll = atom.split(Pid.PREFIXSEP, 1)
atom = self._project.getAtom(ll[-1])
if atom is None:
raise ValueError("Atom with ID %s does not exist" % atom)
if atom.residue.chain not in self.chains:
raise ValueError("%s and its chain do not match the Substance" % atom.longPid)
dd = self._getInternalParameter(self._SPECIFICATOMLABELLING)
if dd is None:
raise ValueError("Cannot remove - no atom labelling data present.")
# if dd:
residue = atom.residue
residueIndex = residue.chain.residues.index(residue)
tt = (residueIndex, atom.name)
if tt in dd:
del dd[(residueIndex, atom.name)]
else:
raise ValueError("Cannot remove - no atom labelling data for %s." % atom.longPid)
[docs] def getSpecificAtomLabelling(self, atom: typing.Union[str, 'Atom']) -> typing.Dict[str, float]:
"""Get specificAtomLabelling dictionary for atom.
atom may be an Atom object, an atomId or an atom Pid
returns dictionary of the form e.g.
{'12C':0.32, '13C':0.68}"""
if isinstance(atom, str):
# Get Atom from id or Pid
ll = atom.split(Pid.PREFIXSEP, 1)
atom = self._project.getAtom(ll[-1])
if atom is None:
raise ValueError("Atom with ID %s does not exist" % atom)
if atom.residue.chain not in self.chains:
raise ValueError("Atom %s and its chain do not match the Substance" % atom)
dd = self._getInternalParameter(self._SPECIFICATOMLABELLING)
if dd:
residue = atom.residue
residueIndex = residue.chain.residues.index(residue)
return dd.get((residueIndex, atom.name))
[docs] def clearSpecificAtomLabelling(self):
"""Clear specificAtomLabelling"""
self._setInternalParameter(self._SPECIFICATOMLABELLING, {})
[docs] def updateSpecificAtomLabelling(self, dictionary: typing.Dict[str, typing.Dict[str, float]]):
"""Update Site-specific labelling for all chains matching Substance.
The input must be an (atomId:{isotopeCode:fraction}} dictionary.
Note that changing the labelling for a site in one chain
simultaneously affects the matching site in other matching chains,
So you should only update teh labeling for one chain.
Example value (for two chains where the numbering of B is offset 200 from chain A):
{'A.11.ALA.CB':{'12C':0.32, '13C':0.68},}
which will also affect 'B.211.ALA.CB' (if it exists)"""
for atomId, dd in dictionary.items():
self.setSpecificAtomLabelling(atomId, dd)
def _getChemComps(self):
"""
CCPN internal
:param substance:
:return: a ChemComp Obj if available
"""
chemComps = []
molecule = self._wrappedData.getMolecule()
if molecule:
for molResidue in molecule.findAllMolResidues():
if molResidue.chemComp:
chemComps.append(molResidue.chemComp)
return chemComps
@property
def sampleComponents(self) -> typing.Tuple[SampleComponent, ...]:
"""SampleComponents that correspond to Substance"""
relativeId = self._key
return tuple(x for x in self._project.sampleComponents if x._key == relativeId)
# name = self.name
# apiLabeling = self.labelling
# if apiLabeling is None:
# apiLabeling = DEFAULT_LABELLING
# apiSampleStore = self._project._apiNmrProject.sampleStore
# data2Obj = self._project._data2Obj
# return tuple(data2Obj[x]
# for y in apiSampleStore.sortedSamples()
# for x in y.sortedSampleComponents()
# if x.name == name and x.labeling == apiLabeling)
@property
def referenceSpectra(self) -> typing.Tuple[Spectrum, ...]:
"""Reference Spectra acquired for Substance"""
_referenceSpectra = tuple([sp for sp in self.project.spectra if self in sp.referenceSubstances])
# _referenceSpectra = tuple(filter(lambda sp: self in sp.referenceSubstances, self.project.spectra)) # just an alternative way to a loop
return _referenceSpectra
@referenceSpectra.setter
def referenceSpectra(self, spectra):
for spectrum in spectra:
spectrum.referenceSubstances += [self]
@property
def _molecule(self):
"""Get the attached molecule
"""
return self._wrappedData.molecule
#=========================================================================================
# Implementation functions
#=========================================================================================
@classmethod
def _getAllWrappedData(cls, parent: Project) -> list:
"""get wrappedData (SampleComponent) for all SampleComponent children of parent Sample"""
componentStore = parent._wrappedData.sampleStore.refSampleComponentStore
if componentStore is None:
return []
else:
return componentStore.sortedComponents()
def _finaliseAction(self, action: str):
"""Subclassed to notify changes to associated integralListViews
"""
if not super()._finaliseAction(action):
return
try:
if action in ['rename']:
for sampleComponent in self.sampleComponents:
for spectrumHit in sampleComponent.spectrumHits:
spectrumHit._finaliseAction(action)
sampleComponent._finaliseAction(action)
except Exception as es:
raise RuntimeError('Error _finalising Substance.spectrumHits: %s' % str(es))
[docs] @renameObject()
@logCommand(get='self')
def rename(self, name: str = None, labelling: str = None):
"""Rename Substance, changing its name and/or labelling and Pid, and rename
SampleComponents and SpectrumHits with matching names. If name is None, the existing value
will be used. Labelling 'None' means 'Natural abundance'"""
if name is None:
name = self.name
oldName = self.name
self._oldPid = self.pid
self._validateStringValue(attribName='name', value=name)
# name = self._uniqueName(project=self.project, name=name)
oldLabelling = self.labelling
apiLabeling = labelling = labelling or DEFAULT_LABELLING
self._validateStringValue(attribName='labelling', value=labelling, allowNone=True)
apiNmrProject = self.project._wrappedData
_molComponent = apiNmrProject.sampleStore.refSampleComponentStore.findFirstComponent(name=name, labeling=apiLabeling)
if _molComponent is not None and _molComponent != self._wrappedData:
raise ValueError("%s.%s already exists" % (name, labelling if labelling != DEFAULT_LABELLING else ''))
# rename functions from here
for sampleComponent in self.sampleComponents:
for spectrumHit in sampleComponent.spectrumHits:
coreUtil._resetParentLink(spectrumHit._wrappedData, 'spectrumHits',
OrderedDict((('substanceName', name),
('sampledDimension', spectrumHit.pseudoDimensionNumber),
('sampledPoint', spectrumHit.pointNumber)))
)
# renamedObjects.append(spectrumHit)
# NB this must be done AFTER the spectrumHit loop to avoid breaking links
coreUtil._resetParentLink(sampleComponent._wrappedData, 'sampleComponents',
OrderedDict((('name', name), ('labeling', apiLabeling)))
)
# renamedObjects.append(sampleComponent)
# NB this must be done AFTER the sampleComponent loop to avoid breaking links
coreUtil._resetParentLink(self._wrappedData, 'components',
OrderedDict((('name', name), ('labeling', apiLabeling)))
)
return (oldName, oldLabelling,)
#=========================================================================================
# CCPN functions
#=========================================================================================
@classmethod
def _uniqueName(cls, project, name=None) -> str:
"""Return a unique name based on name (set to defaultName if None)
"""
apiComponentStore = project._wrappedData.sampleStore.refSampleComponentStore
apiProject = apiComponentStore.root
if name is None:
name = cls._defaultName()
cls._validateStringValue('name', name)
name = name.strip()
names = [sib.name for sib in getattr(project, cls._pluralLinkName)]
while name in names or (apiProject.findFirstMolecule(name=name) or
apiComponentStore.findFirstComponent(name=name)):
name = commonUtil.incrementName(name)
return name
#===========================================================================================
# new'Object' and other methods
# Call appropriate routines in their respective locations
#===========================================================================================
[docs] @logCommand(get='self')
def createChain(self, shortName: str = None, role: str = None,
comment: str = None, expandFromAtomSets: bool = True,
addPseudoAtoms: bool = True, addNonstereoAtoms: bool = True,
**kwds):
"""Create new Chain that matches Substance
See the Chain class for details.
Optional keyword arguments can be passed in; see Chain._createChainFromSubstance for details.
:param shortName:
:param role:
:param comment: optional comment string
:return: a new Chain instance.
"""
from ccpn.core.Chain import _createChainFromSubstance
return _createChainFromSubstance(self, shortName=shortName, role=role, comment=comment,
expandFromAtomSets=expandFromAtomSets, addPseudoAtoms=addPseudoAtoms,
addNonstereoAtoms=addNonstereoAtoms,
**kwds)
[docs] @logCommand('project.')
def getChain(self, shortName: str = None, role: str = None,
comment: str = None, **kwds):
"""Get existing Chain that matches Substance
See the Chain class for details.
Optional keyword arguments can be passed in; see Chain._createChainFromSubstance for details.
:param shortName:
:param role:
:param comment: optional comment string
:return: a new Chain instance.
"""
from ccpn.core.Chain import _getChainFromSubstance
return _getChainFromSubstance(self, shortName=shortName, role=role, comment=comment, **kwds)
#=========================================================================================
# Connections to parents:
#=========================================================================================
@newObject(Substance)
def _newSubstance(self: Project, name: str = None, labelling: str = None, substanceType: str = 'Molecule',
userCode: str = None, smiles: str = None, inChi: str = None, casNumber: str = None,
empiricalFormula: str = None, molecularMass: float = None, comment: str = None,
synonyms: typing.Sequence[str] = (), atomCount: int = 0, bondCount: int = 0,
ringCount: int = 0, hBondDonorCount: int = 0, hBondAcceptorCount: int = 0,
polarSurfaceArea: float = None, logPartitionCoefficient: float = None,
) -> Substance:
"""Create new substance WITHOUT storing the sequence internally
(and hence not suitable for making chains). SubstanceType defaults to 'Molecule'.
ADVANCED alternatives are 'Cell' and 'Material'
See the Substance class for details.
:param name:
:param labelling:
:param substanceType:
:param userCode:
:param smiles:
:param inChi:
:param casNumber:
:param empiricalFormula:
:param molecularMass:
:param comment:
:param synonyms:
:param atomCount:
:param bondCount:
:param ringCount:
:param hBondDonorCount:
:param hBondAcceptorCount:
:param polarSurfaceArea:
:param logPartitionCoefficient:
:return: a new Substance instance.
"""
apiLabeling = _labelling = labelling or DEFAULT_LABELLING
if isinstance(name, int):
name = str(name)
if not name:
name = Substance._uniqueName(project=self, name=name)
self._validateStringValue(attribName='name', value=name, allowNone=True)
self._validateStringValue(attribName='labelling', value=_labelling, allowNone=True)
apiNmrProject = self._wrappedData
apiComponentStore = apiNmrProject.sampleStore.refSampleComponentStore
if apiComponentStore.findFirstComponent(name=name, labeling=apiLabeling) is not None:
# name = commonUtil._incrementObjectName(self.project, Substance._pluralLinkName, name)
# oldSubstance = apiComponentStore.findFirstComponent(name=name)
raise ValueError('{}.{} already exists'.format(name, _labelling if _labelling != DEFAULT_LABELLING else ''))
else:
oldSubstance = apiComponentStore.findFirstComponent(name=name)
params = {
'name' : name, 'labeling': apiLabeling, 'userCode': userCode, 'synonyms': synonyms,
'details': comment
}
if substanceType == 'Material':
if oldSubstance is not None and oldSubstance.className != 'Substance':
raise ValueError("Substance name %s clashes with substance of different type: %s"
% (name, oldSubstance.className))
else:
apiResult = apiComponentStore.newSubstance(**params)
elif substanceType == 'Cell':
if oldSubstance is not None and oldSubstance.className != 'Cell':
raise ValueError("Substance name %s clashes with substance of different type: %s"
% (name, oldSubstance.className))
else:
apiResult = apiComponentStore.newCell(**params)
elif substanceType == 'Composite':
if oldSubstance is not None and oldSubstance.className != 'Composite':
raise ValueError("Substance name %s clashes with substance of different type: %s"
% (name, oldSubstance.className))
else:
apiResult = apiComponentStore.newComposite(**params)
elif substanceType == 'Molecule':
if oldSubstance is not None and oldSubstance.className != 'MolComponent':
raise ValueError("Substance name %s clashes with substance of different type: %s"
% (name, oldSubstance.className))
else:
apiResult = apiComponentStore.newMolComponent(smiles=smiles, inChi=inChi, casNum=casNumber,
empiricalFormula=empiricalFormula, molecularMass=molecularMass, atomCount=atomCount,
bondCount=bondCount, ringCount=ringCount, hBondDonorCount=hBondDonorCount,
hBondAcceptorCount=hBondAcceptorCount, polarSurfaceArea=polarSurfaceArea,
logPartitionCoefficient=logPartitionCoefficient, **params)
else:
raise ValueError("Substance type %s not recognised" % substanceType)
result = self._data2Obj[apiResult]
if result is None:
raise RuntimeError('Unable to generate new Substance item')
return result
#EJB 20181206: moved to Project
# Project.newSubstance = _newSubstance
# del _newSubstance
@newObject(Substance)
def _fetchNefSubstance(self: Project, sequence: typing.Sequence[dict], name: str = None):
"""Fetch Substance that matches sequence of NEF rows and/or name
:param self:
:param sequence:
:param name:
:return: a new Nef Substance instance.
"""
# TODO add sequence matching and name matching to avoid unnecessary duplicates
apiNmrProject = self._wrappedData
name = name or 'Molecule_1'
while apiNmrProject.root.findFirstMolecule(name=name) is not None:
name = commonUtil.incrementName(name)
apiMolecule = MoleculeModify.createMoleculeFromNef(apiNmrProject.root, name, sequence)
result = self._data2Obj[
apiNmrProject.sampleStore.refSampleComponentStore.fetchMolComponent(apiMolecule)
]
if result is None:
raise RuntimeError('Unable to generate new Nef Substance item')
return result
def _getNefSubstance(self: Project, sequence: typing.Sequence[dict], name: str = None, serial: int = None):
"""Get existing Substance that matches sequence of NEF rows and/or name
:param self:
:param sequence:
:param name:
:return: an existing Nef Substance instance or None.
"""
apiNmrProject = self._wrappedData
name = name or 'Molecule_1'
apiMolecule = apiNmrProject.root.findFirstMolecule(name=name)
if apiMolecule:
apiMolComponent = apiNmrProject.sampleStore.refSampleComponentStore.getMolComponent(apiMolecule)
if apiMolComponent:
if apiMolComponent in self._data2Obj:
return self._data2Obj[apiMolComponent]
else:
raise RuntimeError('Error getting Nef Substance {} - {}'.format(name, apiMolComponent))
#EJB 20181206: moved to Project
# Project.fetchNefSubstance = _fetchNefSubstance
# del _fetchNefSubstance
@newObject(Substance)
def _createPolymerSubstance(self: Project, sequence: typing.Sequence[str], name: str,
labelling: str = None, userCode: str = None, smiles: str = None,
synonyms: typing.Sequence[str] = (), comment: str = None,
startNumber: int = 1, molType: str = None, isCyclic: bool = False,
) -> Substance:
"""Make new Substance from sequence of residue codes, using default linking and variants
NB: For more complex substances, you must use advanced, API-level commands.
See the Substance class for details.
:param Sequence sequence: string of one-letter codes or sequence of residueNames
:param str name: name of new substance
:param str labelling: labelling for new substance. Optional - None means 'natural abundance'
:param str userCode: user code for new substance (optional)
:param str smiles: smiles string for new substance (optional)
:param Sequence[str] synonyms: synonyms for Substance name
:param str comment: comment for new substance (optional)
:param int startNumber: number of first residue in sequence
:param str molType: molType ('protein','DNA', 'RNA'). Required only if sequence is a string.
:param bool isCyclic: Should substance created be cyclic?
:return: a new Substance instance.
"""
apiLabeling = labelling = labelling or DEFAULT_LABELLING
if isinstance(name, int):
name = str(name)
if not name:
name = Substance._uniqueName(project=self, name=name)
self._validateStringValue(attribName='name', value=name, allowNone=True)
self._validateStringValue(attribName='labelling', value=labelling, allowNone=True)
if not sequence:
raise ValueError("createPolymerSubstance requires non-empty sequence")
apiNmrProject = self._wrappedData
if apiNmrProject.sampleStore.refSampleComponentStore.findFirstComponent(name=name, labeling=apiLabeling) is not None:
raise ValueError("%s.%s already exists" % (name, labelling if labelling != DEFAULT_LABELLING else ''))
elif apiNmrProject.root.findFirstMolecule(name=name) is not None:
raise ValueError("Molecule name %s is already in use for API Molecule" % name)
# NOTE: ED I need to open the undoStack here so this adds to the list
apiMolecule = MoleculeModify.createMolecule(apiNmrProject.root, sequence, molType=molType,
name=name, startNumber=startNumber,
isCyclic=isCyclic)
_addUndoApiObject(self.project, apiMolecule)
apiMolecule.commonNames = synonyms
apiMolecule.smiles = smiles
apiMolecule.details = comment
mol = apiNmrProject.sampleStore.refSampleComponentStore.fetchMolComponent(apiMolecule, labeling=apiLabeling)
result = self._data2Obj[mol]
if result is None:
raise RuntimeError('Unable to generate new PolymerSubstance item')
result.userCode = userCode
return result
@contextmanager
def _addUndoApiObject(project, apiObject):
def _getApiObjectTree(apiObject) -> tuple:
"""Retrieve the apiObject tree contained by this object
CCPNINTERNAL used for undo's, redo's
"""
#EJB 20181127: taken from memops.Implementation.DataObject.delete
# should be in the model??
#EJB 20190926: taken from AbstractWrapperObject - needed for apiObjects that do not have a v3 object
from ccpn.util.OrderedSet import OrderedSet
apiObjectlist = OrderedSet()
# objects still to be checked
objsToBeChecked = list()
# counter keyed on (obj, roleName) for how many objects at other end of link
linkCounter = {}
# topObjects to check if modifiable
topObjectsToCheck = set()
objsToBeChecked.append(apiObject)
while len(objsToBeChecked) > 0:
obj = objsToBeChecked.pop()
if obj:
obj._checkDelete(apiObjectlist, objsToBeChecked, linkCounter, topObjectsToCheck) # This builds the list/set
for topObjectToCheck in topObjectsToCheck:
if (not (topObjectToCheck.__dict__.get('isModifiable'))):
raise ValueError("""%s.delete:
Storage not modifiable""" % apiObject.qualifiedName
+ ": %s" % (topObjectToCheck,)
)
return tuple(apiObjectlist)
from ccpn.core.lib.ContextManagers import BlankedPartial
from ccpn.core.lib import Undo
undo = project._undo
undo.decreaseBlocking()
apiObjectsCreated = _getApiObjectTree(apiObject)
undo._newItem(undoPartial=BlankedPartial(Undo._deleteAllApiObjects,
obj=None, trigger='delete', preExecution=True,
objsToBeDeleted=apiObjectsCreated),
redoPartial=BlankedPartial(apiObject.root._unDelete,
topObjectsToCheck=(apiObject.topObject,),
obj=None, trigger='create', preExecution=False,
objsToBeUnDeleted=apiObjectsCreated)
)
undo.increaseBlocking()
#EJB 20181206: moved to Project
# Project.createPolymerSubstance = _createPolymerSubstance
# del _createPolymerSubstance
def _fetchSubstance(self: Project, name: str, labelling: str = None) -> Substance:
"""Get or create Substance with given name and labelling.
:param self:
:param name:
:param labelling:
:return: new or existing Substance instance.
"""
if labelling is None:
apiLabeling = DEFAULT_LABELLING
else:
apiLabeling = labelling
apiRefComponentStore = self._apiNmrProject.sampleStore.refSampleComponentStore
apiResult = apiRefComponentStore.findFirstComponent(name=name, labeling=apiLabeling)
with undoBlock():
if apiResult:
result = self._data2Obj[apiResult]
else:
result = self.newSubstance(name=name, labelling=labelling)
return result
#EJB 20181206: moved to Project
# Project.fetchSubstance = _fetchSubstance
# del _fetchSubstance
[docs]def getter(self: SampleComponent) -> typing.Optional[Substance]:
return self._project.getSubstance(self._key)
# relativeId = '.'.join(Pid.remapSeparators(self.na) for x in (self))
# apiRefComponentStore = self._parent._apiSample.sampleStore.refSampleComponentStore
# apiComponent = apiRefComponentStore.findFirstComponent(name=self.name,
# labeling=self.labelling or DEFAULT_LABELLING)
# if apiComponent is None:
# return None
# else:
# return self._project._data2Obj[apiComponent]
def _getSubstanceByName(self: Project, name:str=None, **kwargs) -> typing.Optional[Substance]:
"""
:param self: project instance
:param kwargs: any substance attribute, e.g.: name, labelling etc
:return: substance
"""
apiNmrProject = self._wrappedData
apiComponentStore = apiNmrProject.sampleStore.refSampleComponentStore
apiComponent = apiComponentStore.findFirstComponent(name=name, **kwargs)
if apiComponent is None:
return None
else:
return self.project._data2Obj[apiComponent]
SampleComponent.substance = property(getter, None, None,
"Substance corresponding to SampleComponent")
####### Moved to spectrum as referenceSubstances.
####### ReferenceSubstance is Deprecated from 3.0.3.
# def getter(self: Spectrum) -> Substance:
# apiRefComponent = self._apiDataSource.experiment.refComponent
# # return apiRefComponent and self._project._data2Obj[apiRefComponent]
#
# return None if apiRefComponent is None else self._project._data2Obj.get(apiRefComponent)
#
#
# def setter(self: Spectrum, value: Substance):
# # apiRefComponent = value and value._apiSubstance
#
# apiRefComponent = None if value is None else value._apiSubstance
#
# self._apiDataSource.experiment.refComponent = apiRefComponent
#
#
# #
# Spectrum.referenceSubstance = property(getter, setter, None,
# "Substance that has this Spectrum as a reference spectrum")
# del getter
# del setter
####### End referenceSubstance link ####
# Notifiers:
# Substance - SampleComponent link is derived through the keys of the linked objects
# There is therefore no need to monitor the link, and notifiers should be put
# on object creation and renaming
className = Nmr.Experiment._metaclass.qualifiedName()
Project._apiNotifiers.append(
('_modifiedLink', {'classNames': ('Spectrum', 'Substance')}, className, 'setRefComponentName'),
)