2 # -*- coding: utf-8 -*-
7 Force spectroscopy curves basic fitting plugin.
8 Licensed under the GNU GPL version 2
10 Non-standard Dependencies:
11 procplots.py (plot processing plugin)
13 from libhooke import WX_GOOD, ClickedPoint
15 wxversion.select(WX_GOOD)
16 #from wx import PostEvent
17 #from wx.lib.newevent import NewEvent
25 global EVT_MEASURE_WLC
27 #measure_wlc, EVT_MEASURE_WLC = NewEvent()
29 global events_from_fit
30 events_from_fit=Queue.Queue() #GUI ---> CLI COMMUNICATION
37 self.wlccontact_point=None
38 self.wlccontact_index=None
40 def wlc_fit(self,clicked_points,xvector,yvector, pl_value, T=293, return_errors=False):
42 Worm-like chain model fitting.
43 The function is the simple polynomial worm-like chain as proposed by C.Bustamante, J.F.Marko, E.D.Siggia
44 and S.Smith (Science. 1994 Sep 9;265(5178):1599-600.)
48 clicked_points[0] = contact point (calculated or hand-clicked)
49 clicked_points[1] and [2] are edges of chunk
52 #STEP 1: Prepare the vectors to apply the fit.
53 if pl_value is not None:
54 pl_value=pl_value/(10**9)
56 #indexes of the selected chunk
57 first_index=min(clicked_points[1].index, clicked_points[2].index)
58 last_index=max(clicked_points[1].index, clicked_points[2].index)
60 #getting the chunk and reverting it
61 xchunk,ychunk=xvector[first_index:last_index],yvector[first_index:last_index]
64 #put contact point at zero and flip around the contact point (the fit wants a positive growth for extension and force)
65 xchunk_corr_up=[-(x-clicked_points[0].graph_coords[0]) for x in xchunk]
66 ychunk_corr_up=[-(y-clicked_points[0].graph_coords[1]) for y in ychunk]
69 xchunk_corr_up=scipy.array(xchunk_corr_up)
70 ychunk_corr_up=scipy.array(ychunk_corr_up)
73 #STEP 2: actually do the fit
75 #Find furthest point of chunk and add it a bit; the fit must converge
77 xchunk_high=max(xchunk_corr_up)
78 xchunk_high+=(xchunk_high/10)
80 #Here are the linearized start parameters for the WLC.
81 #[lambd=1/Lo , pii=1/P]
83 p0=[(1/xchunk_high),(1/(3.5e-10))]
84 p0_plfix=[(1/xchunk_high)]
87 fixme: remove these comments after testing
91 def f_wlc(params,x,T=T):
93 wlc function for ODR fitting
98 y=(therm*pii/4.0) * (((1-(x*lambd))**-2) - 1 + (4*x*lambd))
101 def f_wlc_plfix(params,x,pl_value=pl_value,T=T):
103 wlc function for ODR fitting
109 y=(therm*pii/4.0) * (((1-(x*lambd))**-2) - 1 + (4*x*lambd))
113 realdata=scipy.odr.RealData(xchunk_corr_up,ychunk_corr_up)
115 model=scipy.odr.Model(f_wlc_plfix)
116 o = scipy.odr.ODR(realdata, model, p0_plfix)
118 model=scipy.odr.Model(f_wlc)
119 o = scipy.odr.ODR(realdata, model, p0)
121 o.set_job(fit_type=2)
123 fit_out=[(1/i) for i in out.beta]
125 #Calculate fit errors from output standard deviations.
126 #We must propagate the error because we fit the *inverse* parameters!
127 #The error = (error of the inverse)*(value**2)
129 for sd,value in zip(out.sd_beta, fit_out):
130 err_real=sd*(value**2)
131 fit_errors.append(err_real)
133 def wlc_eval(x,params,pl_value,T):
135 Evaluates the WLC function
145 Kb=(1.38065e-23) #boltzmann constant
146 therm=Kb*T #so we have thermal energy
148 return ( (therm*pii/4.0) * (((1-(x*lambd))**-2.0) - 1 + (4.0*x*lambd)) )
150 #STEP 3: plotting the fit
152 #obtain domain to plot the fit - from contact point to last_index plus 20 points
153 thule_index=last_index+10
154 if thule_index > len(xvector): #for rare cases in which we fit something at the END of whole curve.
155 thule_index = len(xvector)
156 #reverse etc. the domain
157 xfit_chunk=xvector[clicked_points[0].index:thule_index]
159 xfit_chunk_corr_up=[-(x-clicked_points[0].graph_coords[0]) for x in xfit_chunk]
160 xfit_chunk_corr_up=scipy.array(xfit_chunk_corr_up)
162 #the fitted curve: reflip, re-uncorrect
163 yfit=wlc_eval(xfit_chunk_corr_up, out.beta, pl_value,T)
164 yfit_down=[-y for y in yfit]
165 yfit_corr_down=[y+clicked_points[0].graph_coords[1] for y in yfit_down]
168 return fit_out, yfit_corr_down, xfit_chunk, fit_errors
170 return fit_out, yfit_corr_down, xfit_chunk, None
172 def fjc_fit(self,clicked_points,xvector,yvector, pl_value, T=293, return_errors=False):
174 Freely-jointed chain function
175 ref: C.Ray and B.B. Akhremitchev; http://www.chem.duke.edu/~boris/research/force_spectroscopy/fit_efjc.pdf
178 clicked_points[0] is the contact point (calculated or hand-clicked)
179 clicked_points[1] and [2] are edges of chunk
182 #STEP 1: Prepare the vectors to apply the fit.
183 if pl_value is not None:
184 pl_value=pl_value/(10**9)
186 #indexes of the selected chunk
187 first_index=min(clicked_points[1].index, clicked_points[2].index)
188 last_index=max(clicked_points[1].index, clicked_points[2].index)
190 #getting the chunk and reverting it
191 xchunk,ychunk=xvector[first_index:last_index],yvector[first_index:last_index]
194 #put contact point at zero and flip around the contact point (the fit wants a positive growth for extension and force)
195 xchunk_corr_up=[-(x-clicked_points[0].graph_coords[0]) for x in xchunk]
196 ychunk_corr_up=[-(y-clicked_points[0].graph_coords[1]) for y in ychunk]
200 xchunk_corr_up=scipy.array(xchunk_corr_up)
201 ychunk_corr_up=scipy.array(ychunk_corr_up)
204 #STEP 2: actually do the fit
206 #Find furthest point of chunk and add it a bit; the fit must converge
208 xchunk_high=max(xchunk_corr_up)
209 xchunk_high+=(xchunk_high/10)
211 #Here are the linearized start parameters for the WLC.
212 #[lambd=1/Lo , pii=1/P]
214 p0=[(1/xchunk_high),(1/(3.5e-10))]
215 p0_plfix=[(1/xchunk_high)]
218 fixme: remove these comments after testing
224 return (np.exp(2*z)+1)/(np.exp(2*z)-1)
226 def x_fjc(params,f,T=T):
228 fjc function for ODR fitting
234 #x=(therm*pii/4.0) * (((1-(x*lambd))**-2) - 1 + (4*x*lambd))
235 x=(1/lambd)*(coth(f*(1/pii)/therm) - (therm*pii)/f)
238 def x_fjc_plfix(params,f,pl_value=pl_value,T=T):
240 fjc function for ODR fitting
246 #y=(therm*pii/4.0) * (((1-(x*lambd))**-2) - 1 + (4*x*lambd))
247 x=(1/lambd)*(coth(f*(1/pii)/therm) - (therm*pii)/f)
251 realdata=scipy.odr.RealData(ychunk_corr_up,xchunk_corr_up)
253 model=scipy.odr.Model(x_fjc_plfix)
254 o = scipy.odr.ODR(realdata, model, p0_plfix)
256 model=scipy.odr.Model(x_fjc)
257 o = scipy.odr.ODR(realdata, model, p0)
259 o.set_job(fit_type=2)
261 fit_out=[(1/i) for i in out.beta]
263 #Calculate fit errors from output standard deviations.
264 #We must propagate the error because we fit the *inverse* parameters!
265 #The error = (error of the inverse)*(value**2)
267 for sd,value in zip(out.sd_beta, fit_out):
268 err_real=sd*(value**2)
269 fit_errors.append(err_real)
271 def fjc_eval(y,params,pl_value,T):
273 Evaluates the WLC function
283 Kb=(1.38065e-23) #boltzmann constant
284 therm=Kb*T #so we have thermal energy
285 #return ( (therm*pii/4.0) * (((1-(x*lambd))**-2.0) - 1 + (4.0*x*lambd)) )
286 return (1/lambd)*(coth(y*(1/pii)/therm) - (therm*pii)/y)
289 #STEP 3: plotting the fit
290 #obtain domain to plot the fit - from contact point to last_index plus 20 points
291 thule_index=last_index+10
292 if thule_index > len(xvector): #for rare cases in which we fit something at the END of whole curve.
293 thule_index = len(xvector)
294 #reverse etc. the domain
295 ychunk=yvector[clicked_points[0].index:thule_index]
298 y_evalchunk=np.linspace(min(ychunk),max(ychunk),100)
300 #Empty y-chunk. It happens whenever we set the contact point after a recognized peak,
301 #or other buggy situations. Kludge to live with it now...
302 ychunk=yvector[:thule_index]
303 y_evalchunk=np.linspace(min(ychunk),max(ychunk),100)
305 yfit_down=[-y for y in y_evalchunk]
306 yfit_corr_down=[y+clicked_points[0].graph_coords[1] for y in yfit_down]
307 yfit_corr_down=scipy.array(yfit_corr_down)
309 #the fitted curve: reflip, re-uncorrect
310 xfit=fjc_eval(yfit_corr_down, out.beta, pl_value,T)
313 xfit_chunk_corr_up=[-(x-clicked_points[0].graph_coords[0]) for x in xfit]
315 #xfit_chunk_corr_up=scipy.array(xfit_chunk_corr_up)
316 #deltay=yfit_down[0]-yvector[clicked_points[0].index]
318 #This is a terrible, terrible kludge to find the point where it should normalize (and from where it should plot)
320 for index in scipy.arange(1,len(xfit_chunk_corr_up),1):
321 xxxdists.append((clicked_points[0].graph_coords[0]-xfit_chunk_corr_up[index])**2)
322 normalize_index=xxxdists.index(min(xxxdists))
325 deltay=yfit_down[normalize_index]-clicked_points[0].graph_coords[1]
326 yfit_corr_down=[y-deltay for y in yfit_down]
329 return fit_out, yfit_corr_down[normalize_index+1:], xfit_chunk_corr_up[normalize_index+1:], fit_errors
331 return fit_out, yfit_corr_down[normalize_index+1:], xfit_chunk_corr_up[normalize_index+1:], None
334 def do_wlc(self,args):
343 def do_fjc(self,args):
352 def do_fit(self,args):
356 Fits an entropic elasticity function to a given chunk of the curve.
358 First you have to click a contact point.
359 Then you have to click the two edges of the data you want to fit.
361 The fit function depends on the fit_function variable. You can set it with the command
362 "set fit_function wlc" or "set fit_function fjc" depending on the function you prefer.
364 For WLC, the function is the simple polynomial worm-like chain as proposed by
365 C.Bustamante, J.F.Marko, E.D.Siggia and S.Smith (Science. 1994
366 Sep 9;265(5178):1599-600.)
369 C.Ray and B.B. Akhremitchev; http://www.chem.duke.edu/~boris/research/force_spectroscopy/fit_efjc.pdf
372 pl=[value] : Use a fixed persistent length (WLC) or Kuhn length (FJC) for the fit. If pl is not given,
373 the fit will be a 2-variable
374 fit. DO NOT put spaces between 'pl', '=' and the value.
375 The value must be in nanometers.
377 t=[value] : Use a user-defined temperature. The value must be in
378 kelvins; by default it is 293 K.
379 DO NOT put spaces between 't', '=' and the value.
381 noauto : allows for clicking the contact point by
382 hand (otherwise it is automatically estimated) the first time.
383 If subsequent measurements are made, the same contact point
386 reclick : redefines by hand the contact point, if noauto has been used before
387 but the user is unsatisfied of the previously choosen contact point.
389 Syntax: fit [pl=(value)] [t=value] [noauto]
392 T=self.config['temperature']
393 for arg in args.split():
394 #look for a persistent length argument.
396 pl_expression=arg.split('=')
397 pl_value=float(pl_expression[1]) #actual value
398 #look for a T argument. FIXME: spaces are not allowed between 'pl' and value
399 if ('t=' in arg[0:2]) or ('T=' in arg[0:2]):
400 t_expression=arg.split('=')
401 T=float(t_expression[1])
403 #use the currently displayed plot for the fit
404 displayed_plot=self._get_displayed_plot()
406 #handle contact point arguments correctly
407 if 'reclick' in args.split():
408 print 'Click contact point'
409 contact_point=self._measure_N_points(N=1, whatset=1)[0]
410 contact_point_index=contact_point.index
411 self.wlccontact_point=contact_point
412 self.wlccontact_index=contact_point.index
413 self.wlccurrent=self.current.path
414 elif 'noauto' in args.split():
415 if self.wlccontact_index==None or self.wlccurrent != self.current.path:
416 print 'Click contact point'
417 contact_point=self._measure_N_points(N=1, whatset=1)[0]
418 contact_point_index=contact_point.index
419 self.wlccontact_point=contact_point
420 self.wlccontact_index=contact_point.index
421 self.wlccurrent=self.current.path
423 contact_point=self.wlccontact_point
424 contact_point_index=self.wlccontact_index
426 cindex=self.find_contact_point()
427 contact_point=ClickedPoint()
428 contact_point.absolute_coords=displayed_plot.vectors[1][0][cindex], displayed_plot.vectors[1][1][cindex]
429 contact_point.find_graph_coords(displayed_plot.vectors[1][0], displayed_plot.vectors[1][1])
430 contact_point.is_marker=True
432 print 'Click edges of chunk'
433 points=self._measure_N_points(N=2, whatset=1)
434 points=[contact_point]+points
436 if self.config['fit_function']=='wlc':
437 params, yfit, xfit, fit_errors = self.wlc_fit(points, displayed_plot.vectors[1][0], displayed_plot.vectors[1][1],pl_value,T, return_errors=True )
438 name_of_charlength='Persistent length'
439 elif self.config['fit_function']=='fjc':
440 params, yfit, xfit, fit_errors = self.fjc_fit(points, displayed_plot.vectors[1][0], displayed_plot.vectors[1][1],pl_value,T, return_errors=True )
441 name_of_charlength='Kuhn length'
443 print 'No recognized fit function defined!'
444 print 'Set your fit function to wlc or fjc.'
448 print 'Fit not possible. Probably wrong interval -did you click two *different* points?'
451 #FIXME: print "Kuhn length" for FJC
452 print 'Fit function:',self.config['fit_function']
453 print 'Contour length: ',params[0]*(1.0e+9),' nm'
454 to_dump='contour '+self.current.path+' '+str(params[0]*(1.0e+9))+' nm'
455 self.outlet.push(to_dump)
456 if len(params)==2: #if we did choose 2-value fit
457 print name_of_charlength+': ',params[1]*(1.0e+9),' nm'
458 to_dump='persistent '+self.current.path+' '+str(params[1]*(1.0e+9))+' nm'
459 self.outlet.push(to_dump)
462 fit_nm=[i*(10**9) for i in fit_errors]
463 print 'Standard deviation (contour length)', fit_nm[0]
465 print 'Standard deviation ('+name_of_charlength+')', fit_nm[1]
468 #add the clicked points in the final PlotObject
469 clickvector_x, clickvector_y=[], []
471 clickvector_x.append(item.graph_coords[0])
472 clickvector_y.append(item.graph_coords[1])
474 #create a custom PlotObject to gracefully plot the fit along the curves
476 fitplot=copy.deepcopy(displayed_plot)
477 fitplot.add_set(xfit,yfit)
478 fitplot.add_set(clickvector_x,clickvector_y)
480 #FIXME: this colour/styles stuff must be solved at the root!
481 if fitplot.styles==[]:
482 fitplot.styles=[None,None,None,'scatter']
484 fitplot.styles+=[None,'scatter']
486 if fitplot.colors==[]:
487 fitplot.colors=[None,None,None,None]
489 fitplot.colors+=[None,None]
491 self._send_plot([fitplot])
498 def find_contact_point(self,plot=False):
500 Finds the contact point on the curve.
502 The current algorithm (thanks to Francesco Musiani, francesco.musiani@unibo.it and Massimo Sandal) is:
503 - take care of the PicoForce trigger bug - exclude retraction portions with too high standard deviation
504 - fit the second half of the retraction curve to a line
505 - if the fit is not almost horizontal, take a smaller chunk and repeat
506 - otherwise, we have something horizontal
507 - so take the average of horizontal points and use it as a baseline
509 Then, start from the rise of the retraction curve and look at the first point below the
512 FIXME: should be moved, probably to generalvclamp.py
518 outplot=self.subtract_curves(1)
519 xret=outplot.vectors[1][0]
520 ydiff=outplot.vectors[1][1]
522 xext=plot.vectors[0][0]
523 yext=plot.vectors[0][1]
524 xret2=plot.vectors[1][0]
525 yret=plot.vectors[1][1]
527 #taking care of the picoforce trigger bug: we exclude portions of the curve that have too much
528 #standard deviation. yes, a lot of magic is here.
530 monlength=len(xret)-int(len(xret)/20)
533 monchunk=scipy.array(ydiff[monlength:finalength])
534 if abs(np.std(monchunk)) < 2e-10:
536 else: #move away from the monster
537 monlength-=int(len(xret)/50)
538 finalength-=int(len(xret)/50)
541 #take half of the thing
542 endlength=int(len(xret)/2)
547 xchunk=yext[endlength:monlength]
548 ychunk=yext[endlength:monlength]
549 regr=scipy.stats.linregress(xchunk,ychunk)[0:2]
550 #we stop if we found an almost-horizontal fit or if we're going too short...
551 #FIXME: 0.1 and 6 here are "magic numbers" (although reasonable)
552 if (abs(regr[1]) > 0.1) and ( endlength < len(xret)-int(len(xret)/6) ) :
558 ymean=np.mean(ychunk) #baseline
563 #find the first point below the calculated baseline
569 #The algorithm didn't find anything below the baseline! It should NEVER happen
577 def find_contact_point2(self, debug=False):
579 TO BE DEVELOPED IN THE FUTURE
580 Finds the contact point on the curve.
582 FIXME: should be moved, probably to generalvclamp.py
585 #raw_plot=self.current.curve.default_plots()[0]
586 raw_plot=self.plots[0]
587 '''xext=self.plots[0].vectors[0][0]
588 yext=self.plots[0].vectors[0][1]
589 xret2=self.plots[0].vectors[1][0]
590 yret=self.plots[0].vectors[1][1]
592 xext=raw_plot.vectors[0][0]
593 yext=raw_plot.vectors[0][1]
594 xret2=raw_plot.vectors[1][0]
595 yret=raw_plot.vectors[1][1]
597 first_point=[xext[0], yext[0]]
598 last_point=[xext[-1], yext[-1]]
600 #regr=scipy.polyfit(first_point, last_point,1)[0:2]
601 diffx=abs(first_point[0]-last_point[0])
602 diffy=abs(first_point[1]-last_point[1])
604 #using polyfit results in numerical errors. good old algebra.
606 b=first_point[1]-(a*first_point[0])
607 baseline=scipy.polyval((a,b), xext)
609 ysub=[item-basitem for item,basitem in zip(yext,baseline)]
611 contact=ysub.index(min(ysub))
613 return xext,ysub,contact
615 #now, exploit a ClickedPoint instance to calculate index...
617 dummy.absolute_coords=(x_intercept,y_intercept)
618 dummy.find_graph_coords(xret2,yret)
621 return dummy.index, regr, regr_contact
627 def x_do_contact(self,args):
629 DEBUG COMMAND to be activated in the future
631 xext,ysub,contact=self.find_contact_point2(debug=True)
633 contact_plot=self.plots[0]
634 contact_plot.add_set(xext,ysub)
635 contact_plot.add_set([xext[contact]],[self.plots[0].vectors[0][1][contact]])
636 #contact_plot.add_set([first_point[0]],[first_point[1]])
637 #contact_plot.add_set([last_point[0]],[last_point[1]])
638 contact_plot.styles=[None,None,None,'scatter']
639 self._send_plot([contact_plot])
643 index,regr,regr_contact=self.find_contact_point2(debug=True)
646 raw_plot=self.current.curve.default_plots()[0]
647 xret=raw_plot.vectors[0][0]
648 #nc_line=[(item*regr[0])+regr[1] for item in x_nc]
649 nc_line=scipy.polyval(regr,xret)
650 c_line=scipy.polyval(regr_contact,xret)
653 contact_plot=self.current.curve.default_plots()[0]
654 contact_plot.add_set(xret, nc_line)
655 contact_plot.add_set(xret, c_line)
656 contact_plot.styles=[None,None,None,None]
657 #contact_plot.styles.append(None)
658 contact_plot.destination=1
659 self._send_plot([contact_plot])