10

I am just wondering if there is a easy way to implement gaussian/lorentzian fits to 10 peaks and extract fwhm and also to determine the position of fwhm on the x-values. The complicated way is to separate the peaks and fit the data and extract fwhm.

Data is [https://drive.google.com/file/d/0B6sUnnbyNGuOT2RZb2UwYXU4dlE/view?usp=sharing].

Any advise greatly appreciated. Thanks.

from scipy.optimize import curve_fit
import numpy as np
import matplotlib.pyplot as plt

data = np.loadtxt('data.txt', delimiter=',')
x, y = data

plt.plot(x,y)
plt.show()

def func(x, *params):
    y = np.zeros_like(x)
    print len(params)
    for i in range(0, len(params), 3):
        ctr = params[i]
        amp = params[i+1]
        wid = params[i+2]
        y = y + amp * np.exp( -((x - ctr)/wid)**2)



guess = [0, 60000, 80, 1000, 60000, 80]
for i in range(12):
    guess += [60+80*i, 46000, 25]


popt, pcov = curve_fit(func, x, y, p0=guess)
print popt
fit = func(x, *popt)

plt.plot(x, y)
plt.plot(x, fit , 'r-')
plt.show()



Traceback (most recent call last):
File "C:\Users\test.py", line 33, in <module>
popt, pcov = curve_fit(func, x, y, p0=guess)
File "C:\Python27\lib\site-packages\scipy\optimize\minpack.py", line 533, in curve_fit
res = leastsq(func, p0, args=args, full_output=1, **kw)
File "C:\Python27\lib\site-packages\scipy\optimize\minpack.py", line 368, in leastsq
shape, dtype = _check_func('leastsq', 'func', func, x0, args, n)
File "C:\Python27\lib\site-packages\scipy\optimize\minpack.py", line 19, in _check_func
res = atleast_1d(thefunc(*((x0[:numinputs],) + args)))
File "C:\Python27\lib\site-packages\scipy\optimize\minpack.py", line 444, in    _ general_function
return function(xdata, *params) - ydata
TypeError: unsupported operand type(s) for -: 'NoneType' and 'float'
John1024
  • 109,961
  • 14
  • 137
  • 171
Rocky
  • 133
  • 1
  • 1
  • 12

3 Answers3

21

This requires a non-linear fit. A good tool for this is scipy's curve_fit function.

To use curve_fit, we need a model function, call it func, that takes x and our (guessed) parameters as arguments and returns the corresponding values for y. As our model, we use a sum of gaussians:

from scipy.optimize import curve_fit
import numpy as np

def func(x, *params):
    y = np.zeros_like(x)
    for i in range(0, len(params), 3):
        ctr = params[i]
        amp = params[i+1]
        wid = params[i+2]
        y = y + amp * np.exp( -((x - ctr)/wid)**2)
    return y

Now, let's create an initial guess for our parameters. This guess starts with peaks at x=0 and x=1,000 with amplitude 60,000 and e-folding widths of 80. Then, we add candidate peaks at x=60, 140, 220, ... with amplitude 46,000 and width of 25:

guess = [0, 60000, 80, 1000, 60000, 80]
for i in range(12):
    guess += [60+80*i, 46000, 25]

Now, we are ready to perform the fit:

popt, pcov = curve_fit(func, x, y, p0=guess)
fit = func(x, *popt)

To see how well we did, let's plot the actual y values (solid black curve) and the fit (dashed red curve) against x:

enter image description here

As you can see, the fit is fairly good.

Complete working code

from scipy.optimize import curve_fit
import numpy as np
import matplotlib.pyplot as plt

data = np.loadtxt('data.txt', delimiter=',')
x, y = data

plt.plot(x,y)
plt.show()

def func(x, *params):
    y = np.zeros_like(x)
    for i in range(0, len(params), 3):
        ctr = params[i]
        amp = params[i+1]
        wid = params[i+2]
        y = y + amp * np.exp( -((x - ctr)/wid)**2)
    return y

guess = [0, 60000, 80, 1000, 60000, 80]
for i in range(12):
    guess += [60+80*i, 46000, 25]   

popt, pcov = curve_fit(func, x, y, p0=guess)
print popt
fit = func(x, *popt)

plt.plot(x, y)
plt.plot(x, fit , 'r-')
plt.show()
John1024
  • 109,961
  • 14
  • 137
  • 171
  • thank you so much for the wonderful code ! The fit looks almost perfect. I was wondering if I can scale it down somehow. Does it make sense if i add a parameter like y = (y + (scale*amp) * np.exp( -((x - ctr)/wid)**2) and say scale = 0.98 ? How to extract wid for each peak ? – Rocky Nov 16 '14 at 22:24
  • Keep getting this error : File "C:\Python27\lib\site-packages\scipy\optimize\minpack.py", line 444, in _general_function return function(xdata, *params) - ydata TypeError: unsupported operand type(s) for -: 'NoneType' and 'float' – Rocky Nov 16 '14 at 22:28
  • That would likely indicate that `ydata` is `None`. Please check that `x` and `y` were successfully read in. – John1024 Nov 17 '14 at 00:23
  • please have look at the above code and it does have a valid x and y values. PLz let me know where the trick is .. – Rocky Nov 17 '14 at 01:03
  • @Rocky My fault: I failed to copy and paste the last line of `func` into the answer. The answer is now updated with the complete working code. – John1024 Nov 17 '14 at 01:34
  • you are a saver !! plz hint me how to extract the wid for each peak. – Rocky Nov 17 '14 at 01:45
  • @Rocky The returned value of `popt` has the center, amplitude, and (e-folding half) width of all peaks. `popt` elements have the same meaning as elements in `guess`. – John1024 Nov 17 '14 at 02:04
  • 1
    Beautiful code - compact and fits any number of peaks. "Brevity is the soul of wit." – Earthling75 Apr 03 '23 at 23:15
1

@john1024's answer is good, but requires a manual process to generate the initial guess. here's an easy way to automate the starting guess. replace the relevant 3 lines of john1024's code by the following:

    import scipy.signal
    i_pk = scipy.signal.find_peaks_cwt(y, widths=range(3,len(x)//Npks))
    DX = (np.max(x)-np.min(x))/float(Npks) # starting guess for component width
    guess = np.ravel([[x[i], y[i], DX] for i in i_pk]) # starting guess for (x, amp, width) for each component
-1

IMHO it is always advisable to plot the residual (data - model) in problems such as this. You will also want to the look at the ChiSq of the fit.

Doug Wood
  • 9
  • 2