#!/usr/bin/env python
# encoding: utf-8
"""A set of tools for computing Integrals for 1D NMR spectra
If present, it can guess integral zones from an existing peak-list
Adds .integrals into NPKDataset which is an object with its own methods.
First version by DELSUC Marc-André on May-2019.
"""
from __future__ import print_function
import numpy as np
import unittest
from spike.NPKData import NPKData_plugin, parsezoom
from spike.NPKError import NPKError
[docs]class Integralitem(object):
def __init__(self, start, end, curve, value):
"""
the elemental integral item - used by Integrals
start, end : the delimited zone, in pixels
curve : the cumsum over the zone (eventually modified)
value : the calibrated value
"""
self.start = min(start,end)
self.end = max(start,end)
self.curve = curve
self.value = value
[docs] def update(self, data, bias, calibration=1):
"""
given a dataset and a constant bias, recompute the curve and the value
"""
buff = (data.get_buffer().real-bias)/data.cpxsize1
self.curve = buff[int(self.start):int(self.end)].cumsum()
self.value = calibration*self.curve[-1]
def _report(self):
return "%d - %d : %f on %d points\n"%(self.start, self.end, self.value, len(self.curve))
def __str__(self):
return self._report()
def __repr__(self):
return "Integralitem %s"%self._report()
[docs]class Integrals(list):
"""
the class to hold a list of Integral
an item is [start, end, curve as np.array(), value]
start and end are in index !
"""
def __init__(self, data, *args, autothresh=10.0, calibration=None, bias=0.0, separation=3, wings=5, compute=True, **kwds):
"""
computes integral zones and values from peak list,
called without arguments will compute integrals automatically
autothresh : in fully automated mode, noise is multiplied by this number to find peaks from which integrals are defined
separation : if two peaks are less than separation x width n they are aggregated, default = 3
wings : integrals sides are extended by wings x width, default = 5
bias: this value is substracted to data before integration
calibration: a coefficient to multiply all integrals / if None (default) largest is set at 100
"""
# I can't figure out how to explictly specify a keyword arg with *args:
# def __init__(self, *arg, threshold=None, source=None): ...
# so I use **kwds and sqauwk if something unexpected is passed in.
# code taken from lib/python2.7/pstats.py
#
# additional kw are source: the originating dataset, compute: initialize values
self.source = data
self.autothresh = autothresh
self.calibration = calibration # global calibration
self.bias = bias # global bias
self.separation = separation
self.wings = wings
self.compute = compute
super(Integrals, self).__init__(*args, **kwds)
if self.compute:
self.do_compute()
[docs] def do_compute(self):
"realize the computation, using internal parameters"
self.peakstozones()
self.zonestocurves()
def _report(self):
ll = ['calibration: %f\n'%self.calibration]
for i,ii in enumerate(self):
ll.append( "%d: %s"%(i,ii._report ))
return "\n".join(ll)
[docs] def report(self):
for ii in self:
print(ii)
[docs] def to_pandas(self):
"export extract of current integrals list to pandas Dataframe"
import pandas as pd
I1 = pd.DataFrame({
'Start': [self.source.axis1.ixtoc(ii.start) for ii in self],
'End': [self.source.axis1.ixtoc(ii.end) for ii in self],
'Value': [ii.curve[-1] for ii in self],
'Calibration': self.integvalues
})
return I1
[docs] def peakstozones(self):
"""
computes integrals zones from peak list,
separation : if two peaks are less than separation x width n they are aggregated, default = 3
wings : integrals sides are extended by wings x width, default = 5
"""
data = self.source
try:
pk = data.peaks
except AttributeError:
data.pp(autothresh=self.autothresh).centroid() # create one if missing
pk = data.peaks
# then build integral list
if len(pk) == 0:
return [] # return [] if empty peaklist
pk.sort(key = lambda p:p.pos)
prev = data.peaks[0] # initialize
start = prev.pos - self.wings*prev.width
for pk in data.peaks[1:]: # then through remaining
# extending or creating a new zone
if (pk.pos - self.separation*pk.width) > (prev.pos + self.separation*prev.width): # we're done
end = prev.pos + self.wings*prev.width
self.append( Integralitem(start, end, [], 0.0) )
start = pk.pos - self.wings*pk.width
prev = pk
end = data.peaks[-1].pos + self.wings*data.peaks[-1].width
self.append( Integralitem(start, end, [], 0.0) )
[docs] def zonestocurves(self):
"from integral lists, computes curves and values"
curves = []
buff = (self.source.get_buffer().real-self.bias)/self.source.cpxsize1
for iint in self:
curves = buff[int(iint.start):int(iint.end)].cumsum()
iint.curve = curves
iint.value = curves[-1]
# then calibrate
self.calibrate(calibration=self.calibration)
[docs] def calibrate(self, calibration=None):
"""computes integration values from curves
either use calibration value as a scale,
if calibration is None put the largest to 100.0
"""
if len(self) == 0:
return
if not calibration:
intmax = 0.0
for iint in self:
iint.value = iint.curve[-1]
intmax = max(intmax,iint.value)
calibration = 100/intmax
for iint in self:
iint.value = iint.curve[-1]*calibration
self.calibration = calibration
@property
def integzones(self):
"the list of (start, end) of integral zones"
return [(iint.start, iint.end) for iint in self]
@property
def integvalues(self):
"the list of calibrated values"
return [iint.value for iint in self]
[docs] def recalibrate(self, entry, calib_value):
"""
on a dataset already integrated, the integrals are adapted so that
the given entry is set to the given value.
"""
self.calibrate(calibration = calib_value/self[entry].curve[-1])
[docs] def display(self, integoff=0.3, integscale=0.5, color='red',
label=False, labelxposition=1, labelyposition=None,
regions=False, zoom=None, figure=None, curvedict=None, labeldict=None):
"""
displays integrals
figure mpl axes to draw on - will create one if None
zoom zoom
integoff offset of drawing 0..1
integscale scale of the largest integral
color color
regions highlight regions
curvedict additional parameters for the drawing
label draw integral values
labelxposition position of label in x 0..1 with respect to the integral drawing
labelyposition position of label in y 0..1 with respect to the screen - None means top of integral
labeldict additional parameters for the labels
"""
import matplotlib.transforms as transforms
from spike.Display import testplot
if len(self) == 0:
return
plt = testplot.plot()
if figure is None:
ax = plt.subplot(111)
else:
ax = figure
# trans is a coordinate system where x is in current unit and y in 0..1
# used for drawing the integrals
trans = transforms.blended_transform_factory( ax.transData, ax.transAxes )
z1, z2 = parsezoom(self.source, zoom)
sumax = max([c.curve[-1] for c in self])
# copy color to dict if needed
if curvedict is None: curvedict = {}
if labeldict is None: labeldict = {}
for dico in (curvedict, labeldict):
if 'color' not in dico.keys():
dico['color'] = color
for iint in self:
# print(a,b,max(c)/sumax)
if iint.start>z2 or iint.end<z1:
continue # we're outside
xinteg = self.source.axis1.ixtoc( np.linspace(iint.start, iint.end, len(iint.curve)) )
yinteg = integoff + integscale*iint.curve/sumax
ax.plot(xinteg, yinteg, transform=trans, **curvedict)
if label:
xl = xinteg[0] + labelxposition*(xinteg[-1]- xinteg[0])
if labelyposition is not None:
yl = labelyposition
else:
yl = yinteg[-1]
ax.text(xl,yl,"%.2f"%iint.value, transform=trans, **labeldict)
if regions:
ax.plot([xinteg[0],xinteg[0]], [0,1], transform=trans, color=color, alpha=0.1)
ax.plot([xinteg[-1],xinteg[-1]], [0,1], transform=trans, color=color, alpha=0.1 )
[docs]def integrate(npkd, **kw):
"""
computes integral zones and values from peak list,
separation : if two peaks are less than separation x width n they are aggregated, default = 3
wings : integrals sides are extended by wings x width, default = 5
bias: this value is substracted to data before integration
calibration: a coefficient to multiply all integrals / if None (default) largest is set at 100
"""
I1 = Integrals(npkd, **kw)
npkd.integrals = I1
return npkd
[docs]def calibrate(npkd, entry, calib_value):
npkd.integrals.recalibrate(entry, calib_value)
return npkd
calibrate.__doc__ = Integrals.recalibrate.__doc__
[docs]def display(npkd, integoff=0.3, integscale=0.5, color='red',
label=False, labelxposition=1, labelyposition=None, regions=False, zoom=None, figure=None, curvedict=None, labeldict=None):
npkd.integrals.display(integoff=integoff, integscale=integscale, color=color,
label=label, labelxposition=labelxposition, labelyposition=labelyposition,
regions=regions, zoom=zoom, figure=figure, curvedict=curvedict, labeldict=labeldict)
return npkd
display.__doc__ = Integrals.display.__doc__
NPKData_plugin("integrate", integrate)
NPKData_plugin("integral_calibrate", calibrate)
NPKData_plugin("display_integral", display)