I have measured a voltage over a LCR tank circuit (with unknown components) to determine the resonance frequency. Ik have performed a broad frequency sweep and measured the voltages. Now I want to determine the exact location of the resonance peak by adding a fit to the data. The curve looks like a damped, driven, harmonic oscillator so I used the following function to fit the data to: A = F0 / sqrt((k-mw^2)^2 + (bw)^2).
This is the code I have for now, but I get the following error: "Optimal parameters not found: Number of calls to function has reached maxfev = 5000."
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
def fit( f, F0 , k, m, b):
w = 2 * np.pi * f
return F0 / np.sqrt( ( k - m*w**2)**2 + ( b * w )**2 )
fuData = np.loadtxt( "ohlVW.txt", delimiter=',' )
fuData = fuData[ fuData[:,0].argsort()]
f = fuData[:,0]
U = fuData[:,1]
popt, _ = curve_fit(fit, f, U, maxfev=5000)
F0, k, m, b = popt
print(popt)
plt.scatter(f, U)
x_line = np.arange(min(f), max(f), 1)
y_line = fit(x_line, F0, k, m, b)
plt.figure()
plt.plot(f, U)
plt.plot(x_line, y_line, '--', color='red')
plt.show()
Increasing maxfev did not work. How can I adjust the code to get a nice fit over the data?
Related
I had been given the harmonic oscillator equation in the form of : y'' = -w^2 * y with initial conditions y(0) = 1 and y'(0) = 0 and w = 2*pi and I had to compare the analytical solution with the exact solution in the form of y(t) = cos(wt).
So the first part went smoothly and I saw that my result using solve_ivp gives me the exact same curve as the exact solution.
But then I had to compare the evolution of energy for RK45, RK23, and DOP853.
The energy is written : E = 0.5 * k * y^2 + 0.5 * v^2 with k = w^2 and v the velocity v = y'
I was expecting to get a straigh tline, since my harmonic oscillator isn't damped but I got a decreasing curve for each integrations, and I do not know why. If anyone has any idea, I post my code Here. Thanks in advance for the help !
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
#Exercice 1
#We first write the initial conditions
omega = (2*np.pi)
omega_sq = omega**2
y_0 = [1., 0.]
t = np.linspace(0, 4 ,100) #Time interval between 0 and 4 as asked in the problem
def dY_dt (t, y): #Definition of the system
solution = [y[1], - omega_sq *y[0]]
return solution
result = solve_ivp(dY_dt, (0, 100),y0 = y_0, method = 'RK45', t_eval = t)
z = np.cos(omega*t) #Exact solution, for comparison
plt.plot(t, z, 'r', lw = 5) #Plot of the exact solution
plt.plot(t,result.y[0], 'b') #Plot of the analytical solution
#Exercice 2
def Energie(Y): #Energy
k = omega_sq
E = 0.5*(result.y[1])**2 + 0.5*k*(result.y[0])**2
return E
E = Energie(result)
#Legendes
plt.ylabel("Position")
plt.xlabel("Time in seconds")
plt.title('Comparison between analytical and exact')
plt.legend(['Solution exacte', 'Solution solve_ivp'], loc='lower left')
plt.figure()
plt.plot(t, E)
plt.show
I have the following code to fit the curve and show me the time constant tau. However, when I try to fit the experimental data it doesn't work. Anybody can help?
from scipy.signal import lti
import pylab as plt
from scipy.optimize import curve_fit
import math as m
def model1(x, gain1, tau1):
y = lti(gain1, [tau1, 1]).step(T=x)[1]
return y
def get_time_constants(time_vector, power_vector):
time_constant = []
for i in range(len(power_vector)):
# fit to output and estimate parameters - gain and tau
par1 = curve_fit(model, time_vector[i], power_vector[i])
y_fit = model(time_vector[i], par1[0][0], par1[0][1])
time_constant.append(par1[0][1])
time_constant_mean = sum(time_constant) / len(time_constant)
plt.plot(time_vector[i], power_vector[i])
plt.plot(time_vector[i], y_fit, label='Time Constant: %.2f s' %par1[0][1])
plt.show()
power = [[0.0, 68.13, 108.445, 133.43, 158.56, 164.575, 168.26, 172.035, 172.94, 173.795, 173.96, 174.145, 174.165, 174.195, 174.215, 174.29, 174.305, 174.325]]
time = [[0.0, 1.0, 2.0, 3.0999999999999943, 5.099999999999994, 6.099999999999994, 7.099999999999994, 9.099999999999994, 10.199999999999989, 12.199999999999989, 13.199999999999989, 14.199999999999989, 16.19999999999999, 17.299999999999983, 19.299999999999983, 20.30000000000001, 21.30000000000001, 23.30000000000001]]
get_time_constants(time, power)
This is the result I am getting:
As so many times ... just a question of starting values
def get_time_constants(time_vector, power_vector):
time_constant = []
for t, p in zip( time_vector, power_vector ):
guess_amp = max( p )
guess_tau = guess_amp / ( ( p[1] - p[0]) / ( t[1] - t[0] ) )
par1, _ = curve_fit(model, t, p, p0=[ guess_amp, guess_tau ])
y_fit = model(t, *par1)
time_constant.append(par1[1])
time_constant_mean = sum(time_constant) / len(time_constant)
plt.plot( t, p, ls='', marker='+' )
plt.plot( t, y_fit, label='Time Constant: %.2f s' %par1[1])
plt.legend(loc=0)
plt.show()
works just fine.
I am interesting in knowing the phase shift between two sine-waves type. For that I am trying to fit each wave with scipy.cuve_fit. I have been following this post. However I obtain negative amplitudes and the phase shift looks like forwarded pi radians sometimes.
The code that I am using is that one below:
def fit_sin_LD(t_LD, y_LD):
'''Fit sin to the input time sequence, and return fitting parameters "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc"'''
ff = np.fft.fftfreq(len(t_LD), (t_LD[1]-t_LD[0])) # assume uniform spacing
Fyy = abs(np.fft.fft(y_LD))
guess_freq = abs(ff[np.argmax(Fyy[1:])+1]) # excluding the zero frequency "peak", which is related to offset
guess_amp = np.std(y_LD) * 2.**0.5
guess_offset = np.mean(y_LD)
guess = np.array([guess_amp, 2.*np.pi*guess_freq, 0., guess_offset])
def sinfunc_LD(t_LD, A, w, p, c):
return A * np.sin(w*t_LD + p) + c
#boundary=([0,-np.inf,-np.pi, 1.5],[0.8, +np.inf, np.pi, 2.5])
popt, pcov = scipy.optimize.curve_fit(sinfunc_LD, t_LD, y_LD, p0=guess, maxfev=3000) # with maxfev= number I can increase the number of iterations
A, w, p, c = popt
f = w/(2.*np.pi)
fitfunc_LD = lambda t_LD: A*np.sin(w*t_LD + p) + c
fitted_LD = fitfunc_LD(t_LD)
dic_LD = {"amp_LD": A, "omega_LD": w, "phase_LD": p, "offset_LD": c, "freq_LD": f, "period_LD": 1./f, "fitfunc_LD": fitted_LD, "maxcov_LD": np.max(pcov), "rawres_LD": (guess, popt, pcov)}
return dic_LD
def fit_sin_APD(t_APD, y_APD):
''' Fit sin to the input time sequence, and return fitting parameters "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc" '''
ff = np.fft.fftfreq(len(t_APD), (t_APD[1]-t_APD[0])) # assume uniform spacing
Fyy = abs(np.fft.fft(y_APD))
guess_freq = abs(ff[np.argmax(Fyy[1:])+1]) # excluding the zero frequency "peak", which is related to offset
guess_amp = np.std(y_APD) * 2.**0.5
guess_offset = np.mean(y_APD)
guess = np.array([guess_amp, 2.*np.pi*guess_freq, 0., guess_offset])
def sinfunc_APD(t_APD, A, w, p, c):
return A * np.sin(w*t_APD + p) + c
#boundary=([0,0,-np.pi, 0.0],[np.inf, np.inf, np.pi, 0.7])
popt, pcov = scipy.optimize.curve_fit(sinfunc_APD, t_APD, y_APD, p0=guess, maxfev=5000) # with maxfev= number I can increase the number of iterations
A, w, p, c = popt
f = w/(2.*np.pi)
fitfunc_APD = lambda t_APD: A*np.sin(w*t_APD + p) + c
fitted_APD = fitfunc_APD(t_APD)
dic_APD = {"amp_APD": A, "omega_APD": w, "phase_APD": p, "offset_APD": c, "freq_APD": f, "period_APD": 1./f, "fitfunc_APD": fitted_APD, "maxcov_APD": np.max(pcov), "rawres_APD": (guess, popt, pcov)}
return dic_APD
I dont understand why curve_fit is returning a negative amplitude (that in terms of physics has not sense). I have tried as well setting boundary conditions as **kwargs* with:
bounds=([0.0, -np.inf,-np.pi, 0.0],[+np.inf, +np.inf,-np.pi, +np.inf])
but it yields a more weird result.
I added an image showing this difference:
Does anyone how to overcome this issue with phases and amplitudes?
Thanks in advance
There are a few issues here that I do not understand:
There is no need to define the fit function inside the "fit function"
There is no need to define it twice if the only difference is the naming of the dictionary. (While I do not understand why this has to be named differently in the first place)
One could directly fit the frequency instead of omega
When pre-calculating the fitted values, directly use the given fitfunction
Overall I don't see why the second fit should fail and using some generic data here, it doesn't. Considering the fact that in physics an amplitude can be complex I don't have a problem with a negative results. Nevertheless, I understand the point in the OP. Surely, a fit algorithm does not know about physics and, mathematically, there is no problem with the amplitude being negative. This just gives an additional phase shift of pi. Hence, one can easily force positive amplitudes when taking care of the required phase shift. I introduced this here as possible keyword argument. Moreover I reduced this to one fit function with possible "renaming" of the output dictionary keys as keyword argument.
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import curve_fit
def sinfunc( t, A, f, p, c ):
return A * np.sin( 2.0 * np.pi * f * t + p) + c
def fit_sin(t_APD, y_APD, addName="", posamp=False):
''' Fit sin to the input time sequence, and return fitting parameters "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc" '''
ff = np.fft.fftfreq( len( t_APD ), t_APD[1] - t_APD[0] ) # assume uniform spacing
Fyy = abs( np.fft.fft( y_APD ) )
guess_freq = abs( ff[np.argmax( Fyy[1:] ) + 1] ) # excluding the zero frequency "peak", which is related to offset
guess_amp = np.std( y_APD ) * 2.**0.5
guess_offset = np.mean( y_APD )
guess = np.array( [ guess_amp, guess_freq, 0., guess_offset ] )
popt, pcov = curve_fit(sinfunc, t_APD, y_APD, p0=guess, maxfev=500) # with maxfev= number I can increase the number of iterations
if popt[0] < 0 and posamp:
popt[0] = -popt[0]
popt[2] += np.pi
popt[2] = popt[2] % ( 2 * np.pi )
A, f, p, c = popt
fitted_APD = sinfunc( t_APD, *popt )
dic_APD = {
"amp{}".format(addName): A,
"omega{}".format(addName): 2.0 * np.pi * f,
"phase{}".format(addName): p,
"offset{}".format(addName): c,
"freq{}".format(addName): f,
"period{}".format(addName): 1.0 / f,
"fitfunc{}".format(addName): fitted_APD,
"maxcov{}".format(addName): np.max( pcov ),
"rawres{}".format(addName): ( guess, popt, pcov ) }
return dic_APD
tl = np.linspace(0,1e-6, 150 )
sl1 = np.fromiter( (sinfunc(t, .18, 4998735, 3.6, 2.0 ) + .01 *( 1 - 2 * np.random.random() ) for t in tl ), np.float )
sl2 = np.fromiter( (sinfunc(t, .06, 4998735, 2.1, 0.4 ) + .01 *( 1 - 2 * np.random.random() ) for t in tl ), np.float )
ld = fit_sin(tl, sl1, addName="_ld" )
print ld["amp_ld"]
ld = fit_sin(tl, sl1, addName="_ld", posamp=True )
print ld["amp_ld"]
apd = fit_sin(tl, sl2 )
fig = plt.figure("1")
ax = fig.add_subplot( 1, 1, 1 )
ax.plot( tl, sl1, color="r" )
ax.plot( tl, ld["fitfunc_ld"], color="k", ls="--" )
ax.plot( tl, sl2, color="#50FF80" )
ax.plot( tl, apd["fitfunc"], color="k", ls="--" )
ax.grid()
plt.show()
This gives me:
-0.180108427200549
0.180108427200549
i.e. in the first try, despite the good guess for the amplitude, it turns out negative. This is probably due to the large phase. As that guess is zero, it is easier for the algorithm to switch sign of the amplitude first and then adjusting the phase. As mentioned above, this is corrected easily and does not even require error propagation.
I recently started with Python because I have an enormous amount of data where I want to automatically fit a Gaussian to the peaks in spectra. Below is an example of three peaks that I want to fit with three individual peaks.
I have found a question where someone is looking for something very similar, How can I fit multiple Gaussian curved to mass spectrometry data in Python?, and adopted it to my script.
I have added my code at the bottom and when I run the last section I get the error "RuntimeError: Optimal parameters not found: Number of calls to function has reached maxfev = 800." What am I missing?
The data can be downloaded at https://www.dropbox.com/s/zowawljcjco70yh/data_so.h5?dl=0
#%%
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import sparse
from scipy.sparse.linalg import spsolve
from scipy.optimize import curve_fit
#%% Read data
path = 'D:/Python/data_so.h5'
f = pd.read_hdf(path, mode = 'r')
t = f.loc[:, 'Time stamp']
d = f.drop(['Time stamp', 'Name spectrum'], axis = 1)
#%% Extract desired wavenumber range
wn_st=2000
wn_ed=2500
ix_st=np.argmin(abs(d.columns.values-wn_st))
ix_ed=np.argmin(abs(d.columns.values-wn_ed))
d = d.iloc[:, ix_st:ix_ed+1]
#%% AsLS baseline correction
spectrum = 230
y = d.iloc[spectrum]
niter = 10
lam = 200000
p = 0.005
L = len(y)
D = sparse.diags([1,-2,1],[0,-1,-2], shape=(L,L-2))
w = np.ones(L)
for i in range(niter):
W = sparse.spdiags(w, 0, L, L)
Z = W + lam * D.dot(D.transpose())
z = spsolve(Z, w*y)
w = p * (y > z) + (1-p) * (y < z)
corr = d.iloc[spectrum,:] - z
#%% Plot spectrum, baseline and corrected spectrum
plt.clf()
plt.plot(d.columns, d.iloc[spectrum,:])
plt.plot(d.columns, z)
plt.plot(d.columns, corr)
plt.gca().invert_xaxis()
plt.show()
#%%
x = d.columns.values
def gauss(x, a, mu, sig):
return a*np.exp(-(x.astype(float)-mu)**2/(2*sig**2))
fitx = x[(x>2232)*(x<2252)]
fity = y[(x>2232)*(x<2252)]
mu=np.sum(fitx*fity)/np.sum(fity)
sig=np.sqrt(np.sum(fity*(fitx-mu)**2)/np.sum(fity))
popt, pcov = curve_fit(gauss, fitx, fity, p0=[max(fity),mu, sig])
plt.plot(x, gauss(x, popt[0],popt[1],popt[2]), 'r-', label='fit')
I need to count the number of particle under the fitted Gaussian curve. The area of the fitted curve can be found by integrating the function within the limit (mean-3*sigma) to (mean+3*sigma). Would you please help me to solve this. Thanks for your kind consideration.
import pylab as py
import numpy as np
from scipy import optimize
from scipy.stats import stats
import matplotlib.pyplot as plt
import pandas as pd
BackPFT='T067.csv'
df_180 = pd.read_csv(BackPFT, error_bad_lines=False, header=1)
x_180=df_180.iloc[:,3]
y_180=df_180.iloc[:,4]
#want to plot the distribution of s calculated by the following equation
s=np.sqrt((((16*x_180**2*38.22**2)/((4*38.22**2-y_180**2)**2))+1))-1
#Shape of this distribution is Gaussian
#I need to fit this distribution by following parameter
mean=0.433
sigma=0.014
draw=s
#Definition of bin number
bi=np.linspace(0.01,8, 1000)
data = py.hist(draw.dropna(), bins = bi)
#Definition of Gaussian function
def f(x, a, b, c):
return (a * py.exp(-(x - mean)**2.0 / (2 *sigma**2)))
x = [0.5 * (data[1][i] + data[1][i+1]) for i in xrange(len(data[1])-1)]
y = data[0]
#Fitting the peak of the distribution
popt, pcov = optimize.curve_fit(f, x, y)
chi2, p = stats.chisquare(popt)
x_fit = py.linspace(x[0], x[-1], 80000)
y_fit = f(x_fit, *popt)
plot(x_fit, y_fit, lw=3, color="r",ls="--")
plt.xlim(0,2)
plt.tick_params(axis='both', which='major', labelsize=20)
plt.show()
The problem is how to integrate the defined function (f) and count the number under the area. Here I attach the file T067.csv. Thanks in advance for your kind consideration.
BackPFT='T061.csv'
df_180 = pd.read_csv(BackPFT, skip_blank_lines=True ,skiprows=1,header=None,skipfooter=None,engine='python')
x_180=df_180.iloc[:,3]
y_180=df_180.iloc[:,4]
b=42.4
E=109.8
LET=24.19
REL=127.32
mean=0.339; m1=0.259
sigma=0.012; s1=0.015
s=np.sqrt((((16*x_180**2*b**2)/((4*b**2-y_180**2)**2))+1))-1
draw=s
bi=np.linspace(0,8, 2000)
binwidth=0.004
#I want to plot the dsitribution of s. This distribution has three gaussian peaks
data = py.hist(draw.dropna(), bins = bi,color='gray',)
#first Gaussian function for the first peak (peaks counted from the right)
def f(x, a, b, c):
return (a * py.exp(-(x - mean)**2.0 / (2 *sigma**2)))
# fitting the function (Gaussian)
x = [0.5 * (data[1][i] + data[1][i+1]) for i in xrange(len(data[1])-1)]
y = data[0]
popt, pcov = optimize.curve_fit(f, x, y)
chi, p = stats.chisquare(popt)
x_fit = py.linspace(x[0], x[-1], 80000)
y_fit = f(x_fit, *popt)
plot(x_fit, y_fit, lw=5, color="r",ls="--")
#integration of first function f
gaussF = lambda x, a: f(x, a, sigma, mean)
bins=((6*sigma)/(binwidth))
delta = ((mean+3*sigma) - (mean-3*sigma))/bins
f1 = lambda x : f(x, popt[0], sigma, mean)
result = quad(f1,mean-3*sigma,mean+3*sigma)
area = result[0] # this give the area after integration of the gaussian
numPar = area / delta # this gives the number of particle under the integrated area
print"\n\tArea under curve = ", area, "\n\tNumber of particel= ", numPar
The file T061.csv here. Thanks Dr. I Putu Susila for his kind co-operation and interest.