bleach_correction.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import numpy as np
  2. import pandas as pd
  3. from matplotlib import pyplot as plt
  4. import logging
  5. # for the linear fit on the logarithm, to estimate initial parameters
  6. def fit_exp_linear_autoOffset(t, y):
  7. # I assume the decay has reached 90% , i.e. I subtract part of the trace
  8. percent_left = 0.01
  9. # to what level do we expect the decay to have gone?
  10. # e.g. percent_left = 0.1 assumes that 10% of bleachable signal is still there at the end of the measurement
  11. # 0.001 assumes that at the end there is almost no bleachabel signal any more, i.e. all resting fluorescence is stable
  12. miny, maxy = np.min(y), np.max(y)
  13. offset = (1 + percent_left) * miny - percent_left * maxy # equals: np.min(y) - 0.1*(np.max(y)-np.min(y))
  14. # the line above fails if the signal is entirely flat - as is the case in some artificial datasets
  15. if offset == miny: # i.e., miny == maxy
  16. return 0, 1, miny
  17. else:
  18. # y is guaranteed to be positive after offset removal (all zero case handled above)
  19. y = y - offset
  20. y_log = np.log(y)
  21. K, A_log = np.polyfit(t, y_log, 1)
  22. A = np.exp(A_log)
  23. return A, K, offset
  24. # the function to be fitted in the end
  25. def model_func(t, A, K, C):
  26. return A * np.exp(K * t) + C
  27. # # debug fitlogdecay
  28. # #create fictive time course
  29. # A = 10
  30. # K = -0.01
  31. # C = 3
  32. # length = 100
  33. # t = np.arange(length)
  34. # lineIn = model_func(t,A,K,C)
  35. # lineIn = np.full((length),1000)
  36. # #lineIn[50] = 100
  37. # plt.plot(lineIn)
  38. # weights = np.full((length),1)
  39. # #test fitlogdecay
  40. # (fittedout, opt_parms) = fitlogdecay(lineIn, weights, True, 'test')
  41. # print(opt_parms)
  42. def fitlogdecay(lineIn, weights, showresults=False, measurement_label=""):
  43. ##python help, look at:
  44. # https://scipython.com/book/chapter-8-scipy/examples/weighted-and-non-weighted-least-squares-fitting/
  45. #copy of IDL FitLogDecay
  46. #but in the essence, not the letter
  47. #lineIn is an array to fit
  48. #return: the fitted modelled
  49. #weights: same length as lineIn. 0: do not consider this value.
  50. # high value: point is important
  51. # low value: point is not important.
  52. #pro FitLogDecay, lineIn, fittedOut, weights, verbose ;, noConverge
  53. #;a fitted output is given
  54. #;Compute the fit to the function we have just defined. First, define the independent and dependent variables:
  55. #
  56. #common data ; necessary in order to write parameters into p structure
  57. ### in the python translation, parameters are not stored (yet)
  58. #;common CFD
  59. #;common CFDconst
  60. #;set verbose to 1 if not explicitly stated
  61. #IF N_PARAMS() GE 4 THEN begin
  62. #endIF else begin
  63. # verbose = 1
  64. #endElse
  65. # replaced by keyword argument verbose=1
  66. #Y = reform(lineIn) M reform in IDL removes empty dimensions
  67. y = lineIn #copy, for now, to keep close to IDL text
  68. #X = FLOAT(INDGEN(N_elements(y)))
  69. t = np.arange(len(y)) #this assumes that time points are equidistant
  70. ##;nov 07, new initial estimates, follow this for python
  71. # A, K = fit_exp_linear(t, y, offset) #
  72. # print('ViewCalcData:fitlogdecay: linear fit parameters A,K are: ',A,' ', K, '. Offest is: ',offset)
  73. # # initial guess done without weights, without excluded values.
  74. ## A and K are the estimates
  75. #;A = [1.0D,-0.1D,offset] ;old presets
  76. #A = [logOffset,logdecay,offset] ;Provide an initial guess of the function's parameters.
  77. #
  78. #;;may 07, new approach for initial estimate - removed but kept for record
  79. #;;a0 is offset
  80. #;A(0) = estimate(0)
  81. #;;a1 = (ln(Fx) - ln(a0)) / x
  82. #;;we need the response at timepoint x.
  83. #;fx = yfit(n_elements(y)-1)
  84. #;vx = x(n_elements(x)-1)
  85. #;;A(1) = (alog(fx) - alog(a(0)) ) / vx
  86. #IF verbose THEN print, 'FitLogDecay.pro: Function estimates: ', A
  87. #now look at the weights
  88. #remove where weights are 0
  89. keep = weights != 0
  90. t_keep = t[keep]
  91. y_keep = y[keep]
  92. weights_keep = weights[keep]
  93. #high weights mean important points, i.e. low variability = sigma
  94. sigma = 1/weights_keep
  95. #;nov 07, new initial estimates, follow this for python
  96. A, K, offset = fit_exp_linear_autoOffset(t_keep, y_keep) #
  97. fittedout_lin = A * np.exp(K*t) + offset
  98. logging.getLogger("VIEW").debug(
  99. f'ViewCalcData:fitlogdecay: linear fit parameters A = {A}, K = {K} . Offest={offset}')
  100. # initial guess done without weights, but excluding excluded values.
  101. # A and K are the estimates
  102. # give feedback on linear fit
  103. # opt_parms = (A, K, offset)
  104. # fittedout = A * np.exp(K*t) + offset
  105. # if showresults: show_fitlogdecay(y, fittedout, t, weights, opt_parms)
  106. #itMax = 1000
  107. #yfit = CURVEFIT(X, Y, weights, A, SIGMA, FUNCTION_NAME='gfunct', ITmax=itMax, ITER=iter, status=status)
  108. import scipy.optimize as spo
  109. # Linear fit is better than exponential fit here, as y_keep is flat
  110. if A == 0:
  111. opt_parms = (A, K, offset)
  112. else:
  113. try:
  114. # default value of maxfev is 800, not always sufficient
  115. opt_parms, parm_cov = spo.curve_fit(
  116. model_func, t_keep, y_keep, p0=(A,K,offset), sigma = sigma, maxfev=3200,
  117. # setting bounds on A, to make sure that the fitted curve does not rise/fall faster than y_keep does.
  118. # it helps to reduce the exponential divergence between fittedout and y for points with 0 weight.
  119. bounds=[
  120. (min(y_keep.min() - offset, A), -np.inf, -np.inf), # lower bounds
  121. (max(y_keep.max() - offset, A), np.inf, np.inf) # upper bounds
  122. ]
  123. )
  124. logging.getLogger("VIEW").debug("Fitting log function converged! Using Log fit")
  125. except RuntimeError as rte:
  126. opt_parms = (A, K, offset)
  127. logging.getLogger("VIEW").debug("Fitting log function did NOT converge. Using linear parameters.")
  128. #;Compute the parameters.
  129. #;IF ((iter gt itmax) OR (iter eq 1)) THEN noConverge=1 ELSE noConverge=0
  130. #IF (status ge 1) THEN noConverge=1 ELSE noConverge=0
  131. #IF verbose THEN PRINT, 'FitLogDecay.pro: Function parameters (check FitLogDecay.pro for function): ', A ;Print the parameters returned in A.
  132. #IF noConverge THEN print, 'FitLogDecay.pro: Failed to converge, setting parameters to 0; check FitLogDecay.pro'
  133. #
  134. #;copy the parameters into the p structure, so it is available for later retrieval
  135. #p1.BleachPar = A
  136. #;IDL prints:
  137. #;Function parameters: 9.91120 -0.100883 2.07773
  138. #;Thus, the function that best fits the data is:
  139. #;F(x) = 9.91120 * exp(-0.100883x) + 2.07773
  140. A, K, C = opt_parms
  141. fittedout = A * np.exp(K*t) + C
  142. ### show result
  143. if showresults: show_fitlogdecay(y, fittedout, fittedout_lin, t, weights, opt_parms, measurement_label)
  144. # end function fitlogdecay
  145. return (fittedout, opt_parms) #, A, K, C
  146. def show_fitlogdecay(lineIn, fittedout_blue, fittedout_green, t, weights, opt_parms, measurement_label):
  147. A, K, C = opt_parms #extract function parameters
  148. fig = plt.figure()
  149. fig.canvas.set_window_title(measurement_label)
  150. ax1 = fig.add_subplot(2,1,1) # (2,1,1)
  151. ax2 = fig.add_subplot(2,1,2)
  152. ax1.set_title('Fitlogdecay: ' + measurement_label)
  153. ax2.set_title('weights')
  154. ax2.plot(t, weights, 'k--',
  155. label='weights')
  156. ax1.plot(t, lineIn, 'ro')
  157. ax1.plot(t, fittedout_blue, 'b-',
  158. label='$y = %0.2f e^{%0.2f t} + %0.2f$' % (A, K, C))
  159. ax1.plot(t, fittedout_green, 'g-',
  160. label='Alternate lin log-fit')
  161. # ax1.legend(bbox_to_anchor=(1, 1), fancybox=True, shadow=True)
  162. ax1.legend(fancybox=True, shadow=True)
  163. plt.show(block=False)
  164. # move window to the front - no idea if this works
  165. def get_bleach_weights(
  166. flags, p1_metadata: pd.Series, movie_size: tuple, exclude_stimulus: bool):
  167. """
  168. Calculates weights to be used for bleach correction. Weights for the frames 0 to `end_background` are set
  169. flags["LELog_InitialFactor"]. If `flags["LE_BleachStartFrame"]` is > 0, frames 0 to this value is set to 0. If
  170. `exclude_stimulus` is True, then the weights for "stimulus frames" are set to 0, where "stimulus frames" are defined
  171. to be from `end_background` to `flags["LELog_ExcludeSeconds"]` seconds after the onset of first stimulus
  172. :param FlagsManager flags:
  173. :param pandas.Series p1_metadata: experimental metadata
  174. :param tuple movie_size: size of raw data, format XYT
  175. :param bool exclude_stimulus: when true, "stimulus frames" as defined above are set to have 0 weight
  176. :return: an array of weights
  177. :rtype: numpy.ndarray
  178. """
  179. n_frames = movie_size[2]
  180. end_background = p1_metadata.background_frames[1]
  181. sampling_frequency = p1_metadata.frequency
  182. # can't exclude stimulus if stimulus info is not provided
  183. exclude_stimulus = exclude_stimulus and p1_metadata.pulsed_stimuli_handler is not None
  184. weights = np.ones(n_frames)
  185. # change weight to initial portion (with safety net)
  186. weights[0: end_background] = flags["LELog_InitialFactor"]
  187. # exclude first frames
  188. bleach_start_frame = flags["LE_BleachStartFrame"]
  189. if bleach_start_frame > 0:
  190. weights[0:bleach_start_frame] = 0
  191. # exclude response interval
  192. if exclude_stimulus:
  193. startweight = end_background
  194. endweight = min([round(end_background + flags["LELog_ExcludeSeconds"] * sampling_frequency), n_frames - 1])
  195. weights[startweight:endweight] = 0
  196. return weights