"""
"""
#=========================================================================================
# 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-06-25 17:35:47 +0100 (Fri, June 25, 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._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpn.core.Spectrum import Spectrum
from ccpn.core.PseudoDimension import PseudoDimension
from ccpnmodel.ccpncore.api.ccp.nmr.Nmr import SpectrumHit as ApiSpectrumHit
from ccpn.core.lib import Pid
from ccpn.util import Constants
from ccpn.core.lib.ContextManagers import newObject
from ccpn.util.Logging import getLogger
import numpy as np
from ccpn.util.Common import makeIterableList
[docs]class SpectrumHit(AbstractWrapperObject):
"""Used in screening and metabolomics implementations to describe
a 'hit', i.e. that a Substance has been found to be present (metabolomics) or active (screening) in a given
spectrum.
The Substance referred to is defined by the SubsanceName attribute, which is part of the ID.
For this reason SpectrumHits cannot be renamed."""
# A spectrumHit will provide extra information for screening 1D. These are stored in _ccpnInternal as dataFrame
#: Short class name, for PID.
shortClassName = 'SH'
# Attribute it necessary as subclasses must use superclass className
className = 'SpectrumHit'
_parentClass = Spectrum
#: Name of plural link to instances of class
_pluralLinkName = 'spectrumHits'
# the attribute name used by current
_currentAttributeName = 'spectrumHit'
#: List of child classes.
_childClasses = []
# Qualified name of matching API class
_apiClassQualifiedName = ApiSpectrumHit._metaclass.qualifiedName()
# CCPN properties
@property
def _apiSpectrumHit(self) -> ApiSpectrumHit:
""" CCPN SpectrumHit matching SpectrumHit"""
return self._wrappedData
@property
def _key(self) -> str:
"""object identifier, used for id"""
obj = self._wrappedData
return Pid.createId(obj.substanceName, obj.sampledDimension, obj.sampledPoint)
@property
def _localCcpnSortKey(self) -> typing.Tuple:
"""Local sorting key, in context of parent."""
obj = self._wrappedData
return (obj.substanceName, obj.sampledDimension, obj.sampledPoint)
@property
def _parent(self) -> Spectrum:
"""Spectrum containing spectrumHit."""
return self._project._data2Obj[self._wrappedData.dataSource]
spectrum = _parent
@property
def substanceName(self) -> str:
"""Name of hit substance"""
return self._wrappedData.substanceName
@property
def pseudoDimensionNumber(self) -> int:
"""Dimension number for pseudoDimension (0 if none),
if the Hit only refers to one point in a pseudoDimension"""
return self._wrappedData.sampledDimension
@property
def pseudoDimension(self) -> PseudoDimension:
"""PseudoDimension,
if the Hit only refers to one point in a pseudoDimension"""
dimensionNumber = self._wrappedData.sampledDimension
if dimensionNumber == 0:
return None
else:
return self.spectrum.getPseudoDimension(dimensionNumber)
@property
def pointNumber(self) -> int:
"""Point number for pseudoDimension (0 if none),
if the Hit only refers to one point in a pseudoDimension"""
return self._wrappedData.sampledPoint
@property
def figureOfMerit(self) -> float:
"""Figure of merit describing quality of hit, between 0.0 and 1.0 inclusive."""
return self._wrappedData.figureOfMerit
@figureOfMerit.setter
def figureOfMerit(self, value: float):
self._wrappedData.figureOfMerit = value
@property
def meritCode(self) -> str:
"""User-defined merit code string describing quality of hit."""
return self._wrappedData.meritCode
@meritCode.setter
def meritCode(self, value: str):
self._wrappedData.meritCode = value
@property
def normalisedChange(self) -> float:
"""Normalized size of effect (normally intensity change). in range -1 <= x <= 1.
Positive values denote expected changes,
while negative values denote changes in the 'wrong' direction,
e.g. intensity increase where a decrease was expected."""
return self._wrappedData.normalisedChange
@normalisedChange.setter
def normalisedChange(self, value: float):
self._wrappedData.normalisedChange = value
@property
def isConfirmed(self) -> typing.Optional[bool]:
"""True if this Hit is confirmed? True: yes; False; no; None: not determined"""
return self._wrappedData.isConfirmed
@isConfirmed.setter
def isConfirmed(self, value: bool):
self._wrappedData.isConfirmed = value
@property
def concentration(self) -> float:
"""Concentration determined for the spectrumHit -
used for e.g. Metabolomics where concentrations are not known a priori."""
return self._wrappedData.concentration
@concentration.setter
def concentration(self, value: float):
self._wrappedData.concentration = value
@property
def concentrationError(self) -> float:
"""Estimated Standard error of SpectrumHit.concentration"""
return self._wrappedData.concentrationError
@concentrationError.setter
def concentrationError(self, value: float):
self._wrappedData.concentrationError = value
@property
def concentrationUnit(self) -> str:
"""Unit of SpectrumHit.concentration, one of: %s% Constants.concentrationUnits """
result = self._wrappedData.concentrationUnit
if result not in Constants.concentrationUnits:
self._project._logger.warning(
"Unsupported stored value %s for SpectrumHit.concentrationUnit."
% result)
return result
@concentrationUnit.setter
def concentrationUnit(self, value: str):
if value not in Constants.concentrationUnits:
if value is not None:
self._project._logger.warning(
"Setting unsupported value %s for SpectrumHit.concentrationUnit."
% value)
self._wrappedData.concentrationUnit = value
#=========================================================================================
# Implementation functions
#=========================================================================================
@classmethod
def _getAllWrappedData(cls, parent: Spectrum) -> list:
"""get wrappedData (Nmr.SpectrumHit) for all SpectrumHit children of parent Spectrum"""
return parent._wrappedData.sortedSpectrumHits()
#===========================================================================================
# new'Object' and other methods
# Call appropriate routines in their respective locations
#===========================================================================================
#=========================================================================================
# Connections to parents:
#=========================================================================================
@newObject(SpectrumHit)
def _newSpectrumHit(self: Spectrum, substanceName: str, pointNumber: int = 0,
pseudoDimensionNumber: int = 0, pseudoDimension: PseudoDimension = None,
figureOfMerit: float = None, meritCode: str = None, normalisedChange: float = None,
isConfirmed: bool = None, concentration: float = None, concentrationError: float = None,
concentrationUnit: str = None, comment: str = None
):
"""Create new SpectrumHit within Spectrum.
See the SpectrumHit class for details.
:param substanceName:
:param pointNumber:
:param pseudoDimensionNumber:
:param pseudoDimension:
:param figureOfMerit:
:param meritCode:
:param normalisedChange:
:param isConfirmed:
:param concentration:
:param concentrationError:
:param concentrationUnit:
:param comment:
:return: a new SpectrumHit instance.
"""
if concentrationUnit not in Constants.concentrationUnits:
if concentrationUnit is not None:
self._project._logger.warning(
"Unsupported value %s for SpectrumHit.concentrationUnit."
% concentrationUnit)
if pseudoDimension is not None:
if not pseudoDimensionNumber:
pseudoDimensionNumber = pseudoDimension.dimension
elif pseudoDimensionNumber != pseudoDimension.dimension:
raise ValueError("pseudoDimension %s incompatible with pseudoDimensionNumber %s"
% (pseudoDimensionNumber, pseudoDimension))
apiSpectrumHit = self._apiDataSource.newSpectrumHit(substanceName=substanceName,
sampledDimension=pseudoDimensionNumber,
sampledPoint=pointNumber, figureOfMerit=figureOfMerit,
meritCode=meritCode, normalisedChange=normalisedChange,
isConfirmed=isConfirmed, concentration=concentration,
concentrationError=concentrationError,
concentrationUnit=concentrationUnit, details=comment)
result = self._project._data2Obj.get(apiSpectrumHit)
if result is None:
raise RuntimeError('Unable to generate new SpectrumHit item')
return result
#EJB 20181203: moved to Spectrum
# Spectrum.newSpectrumHit = _newSpectrumHit
# del _newSpectrumHit
def getter(self: PseudoDimension) -> typing.List[SpectrumHit]:
dimensionNumber = self.dimension
return list(x for x in self.spectrum.spectrumHits if x.dimensionNumber == dimensionNumber)
PseudoDimension.spectrumHits = property(getter, None, None,
"SpectrumHits (for screening/metabolomics) that refer to individual points in the PseudoDimension"
)
del getter
# Additional Notifiers: