You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1638 lines
66 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. import numpy as np
  2. from uncertainties import ufloat
  3. import lmfit
  4. from lmfit.models import (ConstantModel, ComplexConstantModel, LinearModel, QuadraticModel,
  5. PolynomialModel, SineModel, GaussianModel, Gaussian2dModel, LorentzianModel,
  6. SplitLorentzianModel, VoigtModel, PseudoVoigtModel, MoffatModel,
  7. Pearson7Model, StudentsTModel, BreitWignerModel, LognormalModel,
  8. DampedOscillatorModel, ExponentialGaussianModel, SkewedGaussianModel,
  9. SkewedVoigtModel, ThermalDistributionModel, DoniachModel, PowerLawModel,
  10. ExponentialModel, StepModel, RectangleModel, ExpressionModel, DampedHarmonicOscillatorModel)
  11. from lmfit.models import (guess_from_peak, guess_from_peak2d, fwhm_expr, height_expr,
  12. update_param_vals)
  13. from lmfit.lineshapes import (not_zero, breit_wigner, damped_oscillator, dho, doniach,
  14. expgaussian, exponential, gaussian, gaussian2d,
  15. linear, lognormal, lorentzian, moffat, parabolic,
  16. pearson7, powerlaw, pvoigt, rectangle, sine,
  17. skewed_gaussian, skewed_voigt, split_lorentzian, step,
  18. students_t, thermal_distribution, tiny, voigt)
  19. from lmfit import Model
  20. import numpy as np
  21. from numpy import (arctan, copysign, cos, exp, isclose, isnan, log, pi, real,
  22. sin, sqrt, where)
  23. from scipy.special import erf, erfc
  24. from scipy.special import gamma as gamfcn
  25. from scipy.special import wofz
  26. from scipy.optimize import curve_fit
  27. from scipy.interpolate import CubicSpline
  28. from scipy.ndimage import gaussian_filter, rotate
  29. import scipy.constants as const
  30. import xarray as xr
  31. import copy
  32. import matplotlib.pyplot as plt
  33. log2 = log(2)
  34. s2pi = sqrt(2*pi)
  35. s2 = sqrt(2.0)
  36. def gaussianWithOffset(x, amplitude=1.0, center=0.0, sigma=1.0, offset=0.0):
  37. """Return a 1-dimensional Gaussian function with an offset.
  38. gaussian(x, amplitude, center, sigma) =
  39. (amplitude/(s2pi*sigma)) * exp(-(1.0*x-center)**2 / (2*sigma**2))
  40. """
  41. return ((amplitude/(max(tiny, s2pi*sigma)))
  42. * exp(-(1.0*x-center)**2 / max(tiny, (2*sigma**2))) + offset)
  43. def lorentzianWithOffset(x, amplitude=1.0, center=0.0, sigma=1.0, offset=0.0):
  44. return ((amplitude/(1 + ((1.0*x-center)/max(tiny, sigma))**2))
  45. / max(tiny, (pi*sigma)) + offset)
  46. def exponentialWithOffset(x, amplitude=1.0, decay=1.0, offset=0.0):
  47. decay = not_zero(decay)
  48. return amplitude * exp(-x/decay) + offset
  49. def expansion(x, amplitude=1.0, offset=0.0):
  50. return np.sqrt(amplitude*x*x + offset)
  51. def dampingOscillation(x, center=0, amplitude=1.0, frequency=1.0, decay=1.0, offset=0.0):
  52. return amplitude * np.exp(-decay*x)*np.sin(2*np.pi*frequency*(x-center)) + offset
  53. def two_gaussian2d(x, y=0.0, A_amplitude=1.0, A_centerx=0.0, A_centery=0.0, A_sigmax=1.0,
  54. A_sigmay=1.0, B_amplitude=1.0, B_centerx=0.0, B_centery=0.0, B_sigmax=1.0,
  55. B_sigmay=1.0):
  56. """Return a 2-dimensional Gaussian function.
  57. gaussian2d(x, y, amplitude, centerx, centery, sigmax, sigmay) =
  58. amplitude/(2*pi*sigmax*sigmay) * exp(-(x-centerx)**2/(2*sigmax**2)
  59. -(y-centery)**2/(2*sigmay**2))
  60. """
  61. z = A_amplitude*(gaussian(x, amplitude=1, center=A_centerx, sigma=A_sigmax) *
  62. gaussian(y, amplitude=1, center=A_centery, sigma=A_sigmay))
  63. z += B_amplitude*(gaussian(x, amplitude=1, center=B_centerx, sigma=B_sigmax) *
  64. gaussian(y, amplitude=1, center=B_centery, sigma=B_sigmay))
  65. return z
  66. def polylog_tab(pow, x, order=100):
  67. """Calculationg the polylog sum_i(x^i/i^pow), up to the order-th element of the sum
  68. :param pow: power in denominator of sum
  69. :type pow: int (can be float)
  70. :param x: argument of Polylogarithm
  71. :type x: float or 1D numpy array
  72. :return: value of polylog(x)
  73. :rtype: same as x, float or 1D numpy array
  74. """
  75. sum = 0
  76. for k in range(1,order):
  77. sum += x ** k /k **pow
  78. return sum
  79. # Creating array for interpolation
  80. x_int = np.linspace(0, 1.00001, 100000)
  81. poly_tab = polylog_tab(2,x_int)
  82. # Creating function interpolating between the Polylog values calculated before
  83. polylog_int = CubicSpline(x_int, poly_tab)
  84. def thermal(x, x0, amp, sigma):
  85. """Calculating thermal density distribution in 1D (scaled such that if amp=1, return = 1)
  86. :param x: axis
  87. :type x: float or 1d array
  88. :param x0: position of peak along axis
  89. :type x0: float
  90. :param amp: amplitude of function
  91. :type amp: float
  92. :param sigma: width of function
  93. :type sigma: float
  94. :return: calculated function value
  95. :rtype: float or 1D array
  96. """
  97. res = np.exp(-0.5 * (x-x0)**2 / sigma**2)
  98. return amp/1.643 * polylog_int(res)
  99. def Thomas_Fermi_1d(x, x0, amp, sigma):
  100. res = (1- ((x-x0)/sigma)**2)
  101. res = np.where(res > 0, res, 0)
  102. res = res**(3/2)
  103. return amp * res
  104. def density_1d(x, x0_bec, x0_th, amp_bec, amp_th, sigma_bec, sigma_th):
  105. return thermal(x, x0_th, amp_th, sigma_th) + Thomas_Fermi_1d(x, x0_bec, amp_bec, sigma_bec)
  106. def polylog(pow, x):
  107. order = 15
  108. sum = 0
  109. for k in range(1,order):
  110. sum += x ** k /k **pow
  111. return sum
  112. def ThomasFermi_2d(x, y=0.0, centerx=0.0, centery=0.0, amplitude=1.0, sigmax=1.0, sigmay=1.0):
  113. res = (1- ((x-centerx)/(sigmax))**2 - ((y-centery)/(sigmay))**2)
  114. res = np.where(res > 0, res, 0)
  115. res = res**(3/2)
  116. return amplitude * res
  117. def polylog2_2d(x, y=0.0, centerx=0.0, centery=0.0, amplitude=1.0, sigmax=1.0, sigmay=1.0):
  118. ## Approximation of the polylog function with 2D gaussian as argument. -> discribes the thermal part of the cloud
  119. return amplitude/1.643 * polylog_int(np.exp( -((x-centerx)**2/(2 * sigmax**2))-((y-centery)**2/( 2 * sigmay**2)) ))
  120. def rotate_coord(x,y, rot_angle):
  121. rot_angle *= 2*np.pi/360
  122. x_og = x
  123. x = x_og * np.cos(rot_angle) + y * np.sin(rot_angle)
  124. y = -x_og * np.sin(rot_angle) + y * np.cos(rot_angle)
  125. return x, y
  126. def density_profile_BEC_2d(x, y=0.0, amp_bec=1.0, amp_th=1.0, x0_bec=0.0, y0_bec=0.0, x0_th=0.0, y0_th=0.0,
  127. sigmax_bec=1.0, sigmay_bec=1.0, sigma_th=1.0, rot_angle=None):
  128. rot_angle *= -1
  129. if rot_angle is not None:
  130. x, y = rotate_coord(x,y, rot_angle)
  131. x0_bec, y0_bec = rotate_coord(x0_bec, y0_bec, rot_angle)
  132. x0_th, y0_th = rotate_coord(x0_th, y0_th, rot_angle)
  133. return ThomasFermi_2d(x=x, y=y, centerx=x0_bec, centery=y0_bec,
  134. amplitude=amp_bec, sigmax=sigmax_bec, sigmay=sigmay_bec
  135. ) + polylog2_2d(x=x, y=y, centerx=x0_th, centery=y0_th,
  136. amplitude=amp_th, sigmax=sigma_th,sigmay=sigma_th)
  137. class GaussianWithOffsetModel(Model):
  138. fwhm_factor = 2*np.sqrt(2*np.log(2))
  139. height_factor = 1./np.sqrt(2*np.pi)
  140. def __init__(self, independent_vars=['x'], nan_policy='raise', prefix='', name=None, **kwargs):
  141. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  142. 'independent_vars': independent_vars})
  143. super().__init__(gaussianWithOffset, **kwargs)
  144. self._set_paramhints_prefix()
  145. def _set_paramhints_prefix(self):
  146. self.set_param_hint('sigma', min=0)
  147. self.set_param_hint('fwhm', expr=fwhm_expr(self))
  148. self.set_param_hint('height', expr=height_expr(self))
  149. def guess(self, data, x, negative=False, **kwargs):
  150. offset = np.min(data)
  151. data = data - offset
  152. pars = guess_from_peak(self, data, x, negative)
  153. pars.add('offset', value=offset)
  154. return update_param_vals(pars, self.prefix, **kwargs)
  155. class LorentzianWithOffsetModel(Model):
  156. fwhm_factor = 2.0
  157. height_factor = 1./np.pi
  158. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  159. **kwargs):
  160. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  161. 'independent_vars': independent_vars})
  162. super().__init__(lorentzianWithOffset, **kwargs)
  163. self._set_paramhints_prefix()
  164. def _set_paramhints_prefix(self):
  165. self.set_param_hint('sigma', min=0)
  166. self.set_param_hint('fwhm', expr=fwhm_expr(self))
  167. self.set_param_hint('height', expr=height_expr(self))
  168. def guess(self, data, x, negative=False, **kwargs):
  169. """Estimate initial model parameter values from data."""
  170. offset = np.min(data)
  171. data = data - offset
  172. pars = guess_from_peak(self, data, x, negative, ampscale=1.25)
  173. pars.add('offset', value=offset)
  174. return update_param_vals(pars, self.prefix, **kwargs)
  175. class ExponentialWithOffsetModel(Model):
  176. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  177. **kwargs):
  178. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  179. 'independent_vars': independent_vars})
  180. super().__init__(exponentialWithOffset, **kwargs)
  181. def guess(self, data, x, **kwargs):
  182. """Estimate initial model parameter values from data."""
  183. offset = np.min(data)
  184. data = data - offset
  185. try:
  186. sval, oval = np.polyfit(x, np.log(abs(data)+1.e-15), 1)
  187. except TypeError:
  188. sval, oval = 1., np.log(abs(max(data)+1.e-9))
  189. pars = self.make_params(amplitude=np.exp(oval), decay=-1.0/sval)
  190. pars.add('offset', value=offset)
  191. return update_param_vals(pars, self.prefix, **kwargs)
  192. class ExpansionModel(Model):
  193. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  194. **kwargs):
  195. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  196. 'independent_vars': independent_vars})
  197. super().__init__(expansion, **kwargs)
  198. def guess(self, data, x, **kwargs):
  199. """Estimate initial model parameter values from data."""
  200. popt1, pcov1 = curve_fit(expansion, x, data)
  201. pars = self.make_params(amplitude=popt1[0], offset=popt1[1])
  202. return update_param_vals(pars, self.prefix, **kwargs)
  203. class DampingOscillationModel(Model):
  204. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  205. **kwargs):
  206. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  207. 'independent_vars': independent_vars})
  208. super().__init__(dampingOscillation, **kwargs)
  209. def guess(self, data, x, **kwargs):
  210. """Estimate initial model parameter values from data."""
  211. try:
  212. popt1, pcov1 = curve_fit(dampingOscillation, x, data, np.array(0, 5, 5e2, 1e3, 16))
  213. pars = self.make_params(center=popt1[0], amplitude=popt1[1], frequency=popt1[2], decay=popt1[3], offset=popt1[4])
  214. except:
  215. pars = self.make_params(center=0, amplitude=5.0, frequency=5e2, decay=1.0e3, offset=16.0)
  216. return update_param_vals(pars, self.prefix, **kwargs)
  217. class TwoGaussian2dModel(Model):
  218. fwhm_factor = 2*np.sqrt(2*np.log(2))
  219. height_factor = 1./2*np.pi
  220. def __init__(self, independent_vars=['x', 'y'], prefix='', nan_policy='raise',
  221. **kwargs):
  222. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  223. 'independent_vars': independent_vars})
  224. self.helperModel = Gaussian2dModel()
  225. super().__init__(two_gaussian2d, **kwargs)
  226. self._set_paramhints_prefix()
  227. def _set_paramhints_prefix(self):
  228. self.set_param_hint('delta', value=-1, max=0)
  229. self.set_param_hint('A_sigmax', expr=f'{self.prefix}delta + {self.prefix}B_sigmax')
  230. def guess(self, data, x, y, negative=False, **kwargs):
  231. pars_guess = guess_from_peak2d(self.helperModel, data, x, y, negative)
  232. pars = self.make_params(A_amplitude=pars_guess['amplitude'].value, A_centerx=pars_guess['centerx'].value, A_centery=pars_guess['centery'].value,
  233. A_sigmax=pars_guess['sigmax'].value, A_sigmay=pars_guess['sigmay'].value,
  234. B_amplitude=pars_guess['amplitude'].value, B_centerx=pars_guess['centerx'].value, B_centery=pars_guess['centery'].value,
  235. B_sigmax=pars_guess['sigmax'].value, B_sigmay=pars_guess['sigmay'].value)
  236. pars[f'{self.prefix}A_sigmax'].set(expr=f'delta + {self.prefix}B_sigmax')
  237. pars.add(f'{self.prefix}delta', value=-1, max=0, min=-np.inf, vary=True)
  238. pars[f'{self.prefix}A_sigmay'].set(min=0.0)
  239. pars[f'{self.prefix}B_sigmax'].set(min=0.0)
  240. pars[f'{self.prefix}B_sigmay'].set(min=0.0)
  241. return pars
  242. class Polylog22dModel(Model):
  243. fwhm_factor = 2*np.sqrt(2*np.log(2))
  244. height_factor = 1./2*np.pi
  245. def __init__(self, independent_vars=['x', 'y'], prefix='', nan_policy='raise',
  246. **kwargs):
  247. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  248. 'independent_vars': independent_vars})
  249. super().__init__(polylog2_2d, **kwargs)
  250. self._set_paramhints_prefix()
  251. def _set_paramhints_prefix(self):
  252. self.set_param_hint('Rx', min=0)
  253. self.set_param_hint('Ry', min=0)
  254. def guess(self, data, x, y, negative=False, **kwargs):
  255. """Estimate initial model parameter values from data."""
  256. pars = guess_from_peak2d(self, data, x, y, negative)
  257. return update_param_vals(pars, self.prefix, **kwargs)
  258. class ThomasFermi2dModel(Model):
  259. fwhm_factor = 1
  260. height_factor = 0.5
  261. def __init__(self, independent_vars=['x', 'y'], prefix='', nan_policy='raise',
  262. **kwargs):
  263. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  264. 'independent_vars': independent_vars})
  265. super().__init__(ThomasFermi_2d, **kwargs)
  266. self._set_paramhints_prefix()
  267. def _set_paramhints_prefix(self):
  268. self.set_param_hint('Rx', min=0)
  269. self.set_param_hint('Ry', min=0)
  270. def guess(self, data, x, y, negative=False, **kwargs):
  271. """Estimate initial model parameter values from data."""
  272. pars = guess_from_peak2d(self, data, x, y, negative)
  273. # amplitude = pars['amplitude'].value
  274. # simgax = pars['sigmax'].value
  275. # sigmay = pars['sigmay'].value
  276. # pars['amplitude'].set(value=amplitude/s2pi/simgax/sigmay)
  277. simgax = pars['sigmax'].value
  278. sigmay = pars['sigmay'].value
  279. pars['simgax'].set(value=simgax / 2.355)
  280. pars['sigmay'].set(value=sigmay / 2.355)
  281. return update_param_vals(pars, self.prefix, **kwargs)
  282. class DensityProfileBEC2dModel(lmfit.Model):
  283. """ Fitting class to do 2D bimodal fit on OD of absorption images
  284. """
  285. def __init__(self,
  286. independent_vars=['x', 'y'],
  287. prefix='',
  288. nan_policy='raise',
  289. atom_n_conv=144,
  290. pre_check=False,
  291. post_check=False,
  292. is_debug=False,
  293. **kwargs
  294. ):
  295. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  296. 'independent_vars': independent_vars})
  297. self.atom_n_conv = atom_n_conv
  298. self.pre_check = pre_check
  299. self.post_check = post_check
  300. self.is_debug=is_debug
  301. super().__init__(density_profile_BEC_2d, **kwargs)
  302. self._set_paramhints_prefix()
  303. def _set_paramhints_prefix(self):
  304. # self.set_param_hint('BEC_sigmax', min=0)
  305. self.set_param_hint('amp_bec', min=0)
  306. self.set_param_hint('amp_th', min=0)
  307. self.set_param_hint('x0_bec', min=0)
  308. self.set_param_hint('y0_bec', min=0)
  309. self.set_param_hint('x0_th', min=0)
  310. self.set_param_hint('y0_th', min=0)
  311. self.set_param_hint('sigmax_bec', min=0)
  312. self.set_param_hint('sigmay_bec', min=0)
  313. self.set_param_hint('sigma_th', min=0)
  314. self.set_param_hint('rot_angle', min=-90, max=90)
  315. self.set_param_hint('atom_number_bec', expr=f'{self.prefix}amp_bec / 5 * 2 * 3.14159265359 * {self.prefix}sigmax_bec * {self.prefix}sigmay_bec')
  316. self.set_param_hint('atom_number_th', expr=f'{self.prefix}amp_th * 2 * 3.14159265359 * 1.20206 / 1.643 * {self.prefix}sigma_th * {self.prefix}sigma_th')
  317. self.set_param_hint('condensate_fraction', expr=f'{self.prefix}atom_number_bec / ({self.prefix}atom_number_bec + {self.prefix}atom_number_th)')
  318. def guess(self, data, x, y, rot_angle=0, vary_rot=False, is_debug=False, pre_check=False, post_check=False, **kwargs):
  319. """Estimate and create initial model parameters for 2d bimodal fit, by doing a 1d bimodal fit along an integrated slice of the image
  320. :param data: Flattened 2d array, in form [a_00, a_10, a_20, ..., a_01, a_02, .. ,a_XY] with a_xy, x_dim=X, y_dim=Y
  321. :type data: 1d numpy array
  322. :param x: flattened X output of np.meshgrid(x_axis,y_axis) in form: [x1, x2, .., xX, x1, x2, .., xX, .. Y times ..]
  323. :type x: 1d numpy array
  324. :param y: flattened Y output of np.meshgrid(x_axis,y_axis) in form: [y1, y1, .., y1 (X times), y2, y2, .., y2 (X times), .. Y times ..]
  325. :type y: 1d numpy array
  326. :param rot_angle: angle in degrees, The image is rotated counterclockwise by this angle to match the two axes of the cloud with x and y
  327. for the guessing procedure. The 2d-fit is done by rotating the fitting function clockwise with this angle
  328. :type rot_angle: float
  329. :param vary_rot: if True the angle is varied in the 2d-fit
  330. :type vary_rot: bool, optional
  331. :param pre_check: if True the amplitude of the 1d fit is used to guess if the image is purely BEC or thermal and
  332. the corresponding amplitude of the 2d fit is set to zero and not varied to speed up the fitting, defaults to False
  333. :type pre_check: bool, optional
  334. :param post_check: if True, after doing a 2d bimodal fit the number of atoms surrounding the fitted BEC is counted and if the value is
  335. below a certain threshhold the fit is done again with the thermal amplitude set to zero, defaults to False
  336. :type post_check: bool, optional
  337. :return: initial parameters for 2d fit
  338. :rtype: params object (lmfit)
  339. """
  340. self.pre_check = pre_check
  341. self.post_check = post_check
  342. self.is_debug = is_debug
  343. # reshaping the image to 2D in the form [[a_00, a_01, .., a_0Y], [a_10,.., a_1Y], .., [a_X0, .., a_XY]], with a_xy
  344. x_width = len(np.unique(x))
  345. y_width = len(np.unique(y))
  346. x_1d = np.linspace(x[0], x[-1], x_width)
  347. y_1d = np.linspace(y[0], y[-1], y_width)
  348. data = np.reshape(data, (y_width, x_width))
  349. data = data.T
  350. if is_debug:
  351. X, Y = np.meshgrid(x_1d,y_1d)
  352. plt.pcolormesh(X,Y, data.T, cmap='jet')
  353. plt.gca().set_aspect('equal')
  354. plt.title(f'Input data')
  355. plt.xlabel('x_axis')
  356. plt.ylabel('y_axis')
  357. plt.show()
  358. # the image is rotated counterclockwise by rot_angle, CAREFUL: The image has the form a_xy (last coordinate y) and therefore the rotation is done counter-clockwise.
  359. # Doing the same with a standard image with a_yx rotates it clockwise!
  360. if rot_angle != 0:
  361. data = rotate(data, rot_angle, reshape=False)
  362. shape = np.shape(data)
  363. if self.is_debug:
  364. print(f'shape: {shape}')
  365. max_width = np.max(shape)
  366. # binarizing image to guess BEC width and calculate center
  367. thresh = self.calc_thresh(data,thresh_val=0.5)
  368. # calculating center of cloud by statistical distribution of binarized image
  369. center_pix = self.calc_cen_pix(thresh)
  370. center = self.center_pix_conv(center_pix, x_1d, y_1d)
  371. # guessing BEC width, or better of width of center blob if no BEC is present
  372. BEC_width_guess = self.guess_BEC_width(thresh, center_pix)
  373. # plot binarized image and center position for debugging
  374. if self.is_debug:
  375. X, Y = np.meshgrid(x_1d,y_1d)
  376. plt.pcolormesh(X,Y, thresh.T, cmap='jet')
  377. plt.plot(center[0], center[1], marker='x', markersize=25, color='green')
  378. plt.gca().set_aspect('equal')
  379. plt.title(f'Binarized image for guessing BEC width + center position (BEC_width: x={BEC_width_guess[0]:.0f}, y={BEC_width_guess[1]:.0f} pix)')
  380. plt.xlabel('x_axis')
  381. plt.ylabel('y_axis')
  382. plt.show()
  383. # The 1d fit is done along the short axis of the BEC (decided via the BEC_width guess)
  384. if BEC_width_guess[0] < BEC_width_guess[1]:
  385. if self.is_debug:
  386. print(f'x smaller y, 1d fit along x')
  387. s_width_ind = 0
  388. x_fit = x_1d
  389. # slice of the image along the short BEC axis with width of BEC width is taken
  390. X_guess = np.sum(data[:, round(center_pix[1] - BEC_width_guess[1]/2) : round(center_pix[1] + BEC_width_guess[1]/2)], 1) / len(data[0,round(center_pix[1] - BEC_width_guess[1]/2) : round(center_pix[1] + BEC_width_guess[1]/2)])
  391. else:
  392. if self.is_debug:
  393. print(f'y smaller x, 1d fit along y')
  394. s_width_ind = 1
  395. x_fit = y_1d
  396. X_guess = np.sum(data[round(center_pix[0] - BEC_width_guess[0]/2) : round(center_pix[0] + BEC_width_guess[0]/2), :], 0) / len(data[0,round(center_pix[0] - BEC_width_guess[0]/2) : round(center_pix[0] + BEC_width_guess[0]/2)])
  397. # Creating 1d fit init params + Performing fit
  398. max_val = np.max(X_guess)
  399. fitmodel_1d = lmfit.Model(density_1d, independent_vars=['x'])
  400. params_1d = lmfit.Parameters()
  401. params_1d.add_many(
  402. ('x0_bec', center[s_width_ind], True, center[s_width_ind]-10, center[s_width_ind]+10),
  403. ('x0_th',center[s_width_ind], True, center[s_width_ind]-10, center[s_width_ind]+10),
  404. ('amp_bec', 0.5 * max_val, True, 0, 1.3 * max_val),
  405. ('amp_th', 0.5 * max_val, True, 0, 1.3 * max_val),
  406. ('deltax', 3*BEC_width_guess[s_width_ind], True, 0, max_width),
  407. # ('sigma_bec',BEC_width_guess[i,j,0]/1.22, True, 0, 50)
  408. ('sigma_bec',BEC_width_guess[s_width_ind]/1.22, True, 0, BEC_width_guess[s_width_ind]*2)
  409. )
  410. params_1d.add('sigma_th', 3*BEC_width_guess[0], min=0, expr=f'0.632*sigma_bec + 0.518*deltax')
  411. res_1d = fitmodel_1d.fit(X_guess, x=x_fit, params=params_1d)
  412. bval_1d = res_1d.best_values
  413. if self.is_debug:
  414. print('')
  415. print('1d fit initialization')
  416. print(f'center = {center}')
  417. print(f'BEC widths: {BEC_width_guess}')
  418. print('')
  419. print('1d init fit values')
  420. params_1d.pretty_print()
  421. print('1d fitted values')
  422. self.print_bval(res_1d)
  423. plt.plot(x_fit, X_guess, label='1d int. data')
  424. plt.plot(x_fit, density_1d(x_fit,**bval_1d), label='bimodal fit')
  425. plt.plot(x_fit, thermal(x_fit,x0=bval_1d['x0_th'], amp=bval_1d['amp_th'], sigma=bval_1d['sigma_th']), label='thermal part')
  426. plt.legend()
  427. if s_width_ind==0:
  428. plt.title('1d fit of data along x-axis')
  429. plt.xlabel('x_')
  430. else:
  431. plt.title('1d fit of data along y-axis')
  432. plt.xlabel('y_axis')
  433. plt.show()
  434. # scaling amplitudes of 1d fit with the maximum value of blurred 2d data
  435. amp_conv_1d_2d = np.max(gaussian_filter(data, sigma=1)) / (bval_1d['amp_bec'] + bval_1d['amp_th'])
  436. max_val = np.max(data)
  437. params = self.make_params()
  438. # if precheck enabled and amp_th is 7x higher than amp_bec (value might be changed), amplitude of BEC in 2d fit is set to zero
  439. if bval_1d['amp_th']/bval_1d['amp_bec'] > 7 and self.pre_check:
  440. print(f'Image seems to be purely thermal (guessed from 1d fit amplitude)')
  441. params[f'{self.prefix}amp_bec'].set(value=0, vary=False)
  442. params[f'{self.prefix}amp_th'].set(value=amp_conv_1d_2d * bval_1d['amp_th'], max=1.3 * max_val, vary=True)
  443. params[f'{self.prefix}x0_bec'].set(value=1, vary=False)
  444. params[f'{self.prefix}y0_bec'].set(value=1, vary=False)
  445. params[f'{self.prefix}x0_th'].set(value=center[0], min=center[0] -10, max=center[0] + 10, vary=True)
  446. params[f'{self.prefix}y0_th'].set(value=center[1], min=center[1] -10, max=center[1] + 10, vary=True)
  447. params[f'{self.prefix}sigmax_bec'].set(value=1, vary=False)
  448. params[f'{self.prefix}sigmay_bec'].set(value=1, vary=False)
  449. params[f'{self.prefix}sigma_th'].set(value=bval_1d['sigma_th'], max=max_width, vary=True)
  450. # if precheck enabled and amp_bec is 10x higher than amp_th (value might be changed), amplitude of thermal part in 2d fit is set to zero
  451. elif bval_1d['amp_bec']/bval_1d['amp_th'] > 10 and self.pre_check:
  452. print('Image seems to be pure BEC (guessed from 1d fit amplitude)')
  453. params[f'{self.prefix}amp_bec'].set(value=amp_conv_1d_2d * bval_1d['amp_bec'], max=1.3 * max_val, vary=True)
  454. params[f'{self.prefix}amp_th'].set(value=0, vary=False)
  455. params[f'{self.prefix}x0_bec'].set(value=center[0], min=center[0] -10, max=center[0] + 10, vary=True)
  456. params[f'{self.prefix}y0_bec'].set(value=center[1], min=center[1] -10, max=center[1] + 10, vary=True)
  457. params[f'{self.prefix}x0_th'].set(value=1, vary=False)
  458. params[f'{self.prefix}y0_th'].set(value=1, vary=False)
  459. params[f'{self.prefix}sigma_th'].set(value=1, vary=False)
  460. if s_width_ind == 0:
  461. params[f'{self.prefix}sigmax_bec'].set(value=bval_1d['sigma_bec'], max= 2*BEC_width_guess[0], vary=True)
  462. params[f'{self.prefix}sigmay_bec'].set(value=BEC_width_guess[1]/1.22, max= 2*BEC_width_guess[1], vary=True)
  463. elif s_width_ind == 1:
  464. params[f'{self.prefix}sigmax_bec'].set(value=BEC_width_guess[0]/1.22, max= 2*BEC_width_guess[0], vary=True)
  465. params[f'{self.prefix}sigmay_bec'].set(value=bval_1d['sigma_bec'], max= 2*BEC_width_guess[1], vary=True)
  466. else:
  467. print('Error in small width BEC recogintion, s_width_ind should be 0 or 1')
  468. # params for normal 2d bimodal fit are initialized
  469. else:
  470. params[f'{self.prefix}amp_bec'].set(value=amp_conv_1d_2d * bval_1d['amp_bec'], max=1.3 * max_val, vary=True)
  471. params[f'{self.prefix}amp_th'].set(value=amp_conv_1d_2d * bval_1d['amp_th'], max=1.3 * max_val, vary=True)
  472. params[f'{self.prefix}x0_bec'].set(value=center[0], min=center[0] -10, max=center[0] + 10, vary=True)
  473. params[f'{self.prefix}y0_bec'].set(value=center[1], min=center[1] -10, max=center[1] + 10, vary=True)
  474. params[f'{self.prefix}x0_th'].set(value=center[0], min=center[0] -10, max=center[0] + 10, vary=True)
  475. params[f'{self.prefix}y0_th'].set(value=center[1], min=center[1] -10, max=center[1] + 10, vary=True)
  476. params[f'{self.prefix}sigma_th'].set(value=bval_1d['sigma_th'], max=max_width, vary=True)
  477. if s_width_ind == 0:
  478. params[f'{self.prefix}sigmax_bec'].set(value=bval_1d['sigma_bec'], max= 2*BEC_width_guess[0], vary=True)
  479. params[f'{self.prefix}sigmay_bec'].set(value=BEC_width_guess[1]/1.22, max= 2*BEC_width_guess[1], vary=True)
  480. elif s_width_ind == 1:
  481. params[f'{self.prefix}sigmax_bec'].set(value=BEC_width_guess[0]/1.22, max= 2*BEC_width_guess[0], vary=True)
  482. params[f'{self.prefix}sigmay_bec'].set(value=bval_1d['sigma_bec'], max= 2*BEC_width_guess[1], vary=True)
  483. else:
  484. print('Error in small width BEC recogintion, s_width_ind should be 0 or 1')
  485. params[f'{self.prefix}rot_angle'].set(value=rot_angle, min=rot_angle-30, max=rot_angle+30, vary=vary_rot)
  486. if self.is_debug:
  487. print('')
  488. print('Init Params')
  489. params.pretty_print()
  490. print('')
  491. return lmfit.models.update_param_vals(params, self.prefix, **kwargs)
  492. def fit(self, data, **kwargs):
  493. """fitting function overwrites parent class fitting function of lmfit, in order to check (if post_check is enabled)
  494. if thermal fit completely lies in BEC fit by counting sourrounding number of atoms and comparing it to threshold value
  495. :param data: Flattened 2d array, in form [a_00, a_10, a_20, ..., a_01, a_02, .. ,a_XY] with a_xy, x_dim=X, y_dim=Y
  496. :type data: 1d numpy array
  497. :return: result of 2d fit
  498. :rtype: result object (lmfit)
  499. """
  500. res = super().fit(data, **kwargs)
  501. if self.is_debug:
  502. print('bval first fit')
  503. self.print_bval(res)
  504. bval = res.best_values
  505. # Do described post_check if enabled
  506. if res.params['amp_bec'].vary and res.params['amp_th'].vary and bval['amp_bec']>0.5*bval['amp_th'] and self.post_check:
  507. # creating image by cutting out region around BEC and counting number of atoms
  508. sigma_cut = max(bval['sigmay_bec'], bval['sigmax_bec'])
  509. tf_fit = ThomasFermi_2d(kwargs['x'],kwargs['y'],centerx=bval['x0_bec'], centery=bval['y0_bec'], amplitude=bval['amp_bec'], sigmax=bval['sigmax_bec'], sigmay=bval['sigmay_bec'])
  510. tf_fit_2 = ThomasFermi_2d(kwargs['x'],kwargs['y'],centerx=bval['x0_bec'], centery=bval['y0_bec'], amplitude=bval['amp_bec'], sigmax=1.5 * sigma_cut, sigmay=1.5*sigma_cut)
  511. mask = np.where(tf_fit > 0, np.nan, data)
  512. mask = np.where(tf_fit_2 > 0, mask, np.nan)
  513. N_c = np.nansum(mask)
  514. # conversion N_count to Pixels
  515. N_a = self.atom_n_conv * N_c
  516. #TODO change fixed threshhold to variable
  517. # If number of atoms around BEC is small the image is guessed to be purely BEC and another 2d fit is performed with setting the thermal amplitude to zero
  518. if N_a < 6615:
  519. print('No thermal part detected, performing fit without thermal function')
  520. params = res.params
  521. params[f'{self.prefix}amp_th'].set(value=0, vary=False)
  522. params[f'{self.prefix}x0_th'].set(value=1, vary=False)
  523. params[f'{self.prefix}y0_th'].set(value=1, vary=False)
  524. params[f'{self.prefix}sigma_th'].set(value=1, vary=False)
  525. res = super().fit(data, x=kwargs['x'], y=kwargs['y'], params=params)
  526. return res
  527. return res
  528. def calc_thresh(self, data, thresh_val=0.3, sigma=0.4):
  529. """Returns thresholded binary image after blurring to guess BEC size
  530. :param data: 2d image
  531. :type data: 2d numpy array
  532. :param thresh_val: relative threshhold value for binarization with respect to maximum of blurred image
  533. :param sigma: sigma of gaussian blur filter (see scipy.ndimage.gaussian_filter)
  534. :return: binary 2d image
  535. :rtype: 2d numpy array
  536. """
  537. shape = np.shape(data)
  538. thresh = np.zeros(shape)
  539. blurred = gaussian_filter(data, sigma=sigma)
  540. thresh = np.where(blurred < np.max(blurred)*thresh_val, 0, 1)
  541. return thresh
  542. def calc_cen_pix(self, thresh1):
  543. """Calculating the center (in pixel) of a blob (atom cloud) in a binarized image by first calculating the probability distribution along both axes and afterwards the expectation value
  544. :param thresh1: Binary 2D image in the form [[a_00, a_01, .., a_0Y], [a_10,.., a_1Y], .., [a_X0, .., a_XY]], with a_xy, x_dim=X, y_dim=Y
  545. :type thresh1: 2D numpy array
  546. :return: center coordinates of blob in form [x_center, y_center]
  547. :rtype: 1d numpy array (shape=(1,2))
  548. """
  549. cen = np.zeros(2)
  550. (X,Y) = np.shape(thresh1)
  551. thresh1 = thresh1 /np.sum(thresh1)
  552. # marginal distributions
  553. dx = np.sum(thresh1, 1)
  554. dy = np.sum(thresh1, 0)
  555. # expected values
  556. cen[0] = np.sum(dx * np.arange(X))
  557. cen[1] = np.sum(dy * np.arange(Y))
  558. return cen
  559. def center_pix_conv(self, center_pix, x, y):
  560. """Converts center in pixel to center in values of x and y
  561. :param center_pix: pixel values of center
  562. :type center_pix: numpy array (shape=(1,2))
  563. :param x: x-axis
  564. :type x: 1d numpy array
  565. :param y: y-axis
  566. :type y: 1d numpy array
  567. :return: center coordinates in form [x_center, y_center] with respect to the axes
  568. :rtype: numpy array (shap=(1,2))
  569. """
  570. center = np.empty(2)
  571. center[0] = x[round(center_pix[0])]
  572. center[1] = y[round(center_pix[1])]
  573. return center
  574. def guess_BEC_width(self, thresh, center):
  575. """ returns width of blob in binarized image along both axis through the center
  576. :param thresh: Binary 2D image in the form [[a_00, a_01, .., a_0Y], [a_10,.., a_1Y], .., [a_X0, .., a_XY]], with a_xy, x_dim=X, y_dim=Y
  577. :type thresh: 2d numpy array
  578. :param center: center of blob in image in form [x_center, y_center] in pixel
  579. :type center: 1d numpy array (shape=(1,2))
  580. :return: width of blob in image as [x_width, y_width]
  581. :rtype: 1d numpy array (shape=(1,2))
  582. """
  583. shape = np.shape(thresh)
  584. if len(shape) == 2:
  585. BEC_width_guess = np.array([np.sum(thresh[:, round(center[1])]), np.sum(thresh[round(center[0]), :]) ])
  586. for i in range(2):
  587. if BEC_width_guess[i] <= 0:
  588. BEC_width_guess[i] = 1
  589. else:
  590. print("Shape of data is wrong, output is empty")
  591. return BEC_width_guess
  592. def cond_frac(self, results, X, Y):
  593. """Returns condensate fraction of 2d fitting result
  594. :param results: result of 2d bimodal fit
  595. :type results: result object (lmfit)
  596. :param X: X output of np.meshgrid(x_axis,y_axis) in form: [[x1, x2, .., xX], [x1, x2, .., xX] .. Y times ..]
  597. :type X: 2d numpy array
  598. :param Y: Y output of np.meshgrid(x_axis,y_axis) in form: [[y1, y1, .., y1 (X times)], [y2, y2, .., y2 (X times)], .. Y times ..]
  599. :type Y: 2d numpy array
  600. :return: condensate fraction
  601. :rtype: float between 0 and 1
  602. """
  603. bval = results.best_values
  604. tf_fit = ThomasFermi_2d(X,Y,centerx=bval['x0_bec'], centery=bval['y0_bec'], amplitude=bval['amp_bec'], sigmax=bval['sigmax_bec'], sigmay=bval['sigmay_bec'])
  605. N_bec = np.sum(tf_fit)
  606. fit = density_profile_BEC_2d(X,Y, **bval)
  607. N_ges = np.sum(fit)
  608. return N_bec/N_ges
  609. def return_atom_number(self, result, X, Y, is_print=True):
  610. """Calculating (and printing if enabled) fitted atom number in BEC + thermal state, and condensate fraction
  611. :param result: result of 2d bimodal fit
  612. :type result: result object (lmfit)
  613. :param X: X output of np.meshgrid(x_axis,y_axis) in form: [[x1, x2, .., xX], [x1, x2, .., xX] .. Y times ..]
  614. :type X: 2d numpy array
  615. :param Y: Y output of np.meshgrid(x_axis,y_axis) in form: [[y1, y1, .., y1 (X times)], [y2, y2, .., y2 (X times)], .. Y times ..]
  616. :type Y: 2d numpy array
  617. :param is_print: if true results are printed, defaults to True
  618. :type is_print: bool, optional
  619. :return: dictionary with total atom number N, BEC N_bec, thermal N_th and condensate fraction cond_f
  620. :rtype: dictionary
  621. """
  622. bval = result.best_values
  623. tf_fit = ThomasFermi_2d(X,Y,centerx=bval['x0_bec'], centery=bval['y0_bec'], amplitude=bval['amp_bec'], sigmax=bval['sigmax_bec'], sigmay=bval['sigmay_bec'])
  624. N_bec = self.atom_n_conv * np.sum(tf_fit)
  625. th_fit = polylog2_2d(X, Y, centerx=bval['x0_th'], centery=bval['y0_th'], amplitude=bval['amp_th'], sigmax=bval['sigma_th'], sigmay=bval['sigma_th'])
  626. N_th = self.atom_n_conv * np.sum(th_fit)
  627. N = N_bec + N_th
  628. frac = N_bec/N
  629. # fit = density_profile_BEC_2d(X,Y, **bval)
  630. # N_ges = self.atom_n_conv * np.sum(fit)
  631. if is_print:
  632. print()
  633. print('Atom numbers:')
  634. print(f' N_bec: {N_bec :.0f}')
  635. print(f' N_th: {N_th :.0f}')
  636. print(f' N_ges: {N:.0f}')
  637. print(f' Cond. frac: {frac *1e2:.2f} %')
  638. print('')
  639. atom_n = {'N' : N, 'N_bec' : N_bec, 'N_th' : N_th, 'cond_f' : frac}
  640. return atom_n
  641. def return_temperature(self, result, tof, omg=None, is_print=True, eff_pix=2.472e-6):
  642. """Returns temperature of thermal cloud
  643. :param result: result of 2d bimodal fit
  644. :type result: result object (lmfit)
  645. :param tof: time of flight
  646. :type tof: float
  647. :param omg: geometric average of trapping frequencies, defaults to None
  648. :type omg: float, if NONE initial cloud size is neglected optional
  649. :param is_print: if True temperature is printed, defaults to True
  650. :type is_print: bool, optional
  651. :param eff_pix: effective pixel size of imaging setup, defaults to 2.472e-6
  652. :type eff_pix: float, optional
  653. :return: temperature of atom cloud
  654. :rtype: float
  655. """
  656. R_th = result.best_values['sigma_th'] * eff_pix * np.sqrt(2)
  657. # print(R_th)
  658. if omg is None:
  659. T = R_th**2 * 164*const.u/const.k * (tof**2)**(-1)
  660. else:
  661. T = R_th**2 * 164*const.u/const.k * (1/omg**2 + tof**2)**(-1)
  662. if is_print:
  663. print(f'Temperature: {T*1e9:.2f} nK')
  664. return T
  665. def print_bval(self, res_s):
  666. """nicely print best fitted values + init values + bounds
  667. :param res_s: result of 2d bimodal fit
  668. :type res_s: result object (lmfit)
  669. """
  670. keys = res_s.best_values.keys()
  671. bval = res_s.best_values
  672. init = res_s.init_params
  673. for item in keys:
  674. print(f'{item}: {bval[item]:.3f}, (init = {init[item].value:.3f}), bounds = [{init[item].min:.2f} : {init[item].max :.2f}] ')
  675. print('')
  676. class NewFitModel(Model):
  677. def __init__(self, func, independent_vars=['x'], prefix='', nan_policy='raise',
  678. **kwargs):
  679. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  680. 'independent_vars': independent_vars})
  681. super().__init__(func, **kwargs)
  682. def guess(self, *args, **kwargs):
  683. return self.make_params()
  684. lmfit_models = {'Constant': ConstantModel,
  685. 'Complex Constant': ComplexConstantModel,
  686. 'Linear': LinearModel,
  687. 'Quadratic': QuadraticModel,
  688. 'Polynomial': PolynomialModel,
  689. 'Gaussian': GaussianModel,
  690. 'Gaussian-2D': Gaussian2dModel,
  691. 'Lorentzian': LorentzianModel,
  692. 'Split-Lorentzian': SplitLorentzianModel,
  693. 'Voigt': VoigtModel,
  694. 'PseudoVoigt': PseudoVoigtModel,
  695. 'Moffat': MoffatModel,
  696. 'Pearson7': Pearson7Model,
  697. 'StudentsT': StudentsTModel,
  698. 'Breit-Wigner': BreitWignerModel,
  699. 'Log-Normal': LognormalModel,
  700. 'Damped Oscillator': DampedOscillatorModel,
  701. 'Damped Harmonic Oscillator': DampedHarmonicOscillatorModel,
  702. 'Exponential Gaussian': ExponentialGaussianModel,
  703. 'Skewed Gaussian': SkewedGaussianModel,
  704. 'Skewed Voigt': SkewedVoigtModel,
  705. 'Thermal Distribution': ThermalDistributionModel,
  706. 'Doniach': DoniachModel,
  707. 'Power Law': PowerLawModel,
  708. 'Exponential': ExponentialModel,
  709. 'Step': StepModel,
  710. 'Rectangle': RectangleModel,
  711. 'Expression': ExpressionModel,
  712. 'Gaussian With Offset':GaussianWithOffsetModel,
  713. 'Lorentzian With Offset':LorentzianWithOffsetModel,
  714. 'Expansion':ExpansionModel,
  715. 'Damping Oscillation Model':DampingOscillationModel,
  716. 'Two Gaussian-2D':TwoGaussian2dModel,
  717. 'Thomas Fermi-2D': ThomasFermi2dModel,
  718. 'Density Profile of BEC-2D': DensityProfileBEC2dModel,
  719. 'Polylog2-2D': polylog2_2d,
  720. }
  721. class FitAnalyser():
  722. """This is a class integrated all the functions to do a fit.
  723. """
  724. def __init__(self, fitModel, fitDim=1, **kwargs) -> None:
  725. """Initialize function
  726. :param fitModel: The fitting model of fit function
  727. :type fitModel: lmfit Model
  728. :param fitDim: The dimension of the fit, defaults to 1
  729. :type fitDim: int, optional
  730. """
  731. if isinstance(fitModel, str):
  732. self.fitModel = lmfit_models[fitModel](**kwargs)
  733. else:
  734. self.fitModel = fitModel
  735. self.fitDim = fitDim
  736. def print_params_set_template(self, params=None):
  737. """Print a template to manually set the initial parameters of the fit
  738. :param params: An object of Parameters class to print, defaults to None
  739. :type params: lmfit Paraemters, optional
  740. """
  741. if params is None:
  742. params = self.fitModel.make_params()
  743. for key in params:
  744. res = "params.add("
  745. res += "name=\"" + key + "\", "
  746. if not params[key].expr is None:
  747. res += "expr=\"" + params[key].expr +"\")"
  748. else:
  749. res += "value=" + f'{params[key].value:3g}' + ", "
  750. if str(params[key].max)=="inf":
  751. res += "max=np.inf, "
  752. else:
  753. res += "max=" + f'{params[key].max:3g}' + ", "
  754. if str(params[key].min)=="-inf":
  755. res += "min=-np.inf, "
  756. else:
  757. res += "min=" + f'{params[key].min:3g}' + ", "
  758. res += "vary=" + str(params[key].vary) + ")"
  759. print(res)
  760. def _guess_1D(self, data, x, **kwargs):
  761. """Call the guess function of the 1D fit model to guess the initial value.
  762. :param data: The data to fit
  763. :type data: 1D numpy array
  764. :param x: The data of x axis
  765. :type x: 1D numpy array
  766. :return: The guessed initial parameters for the fit
  767. :rtype: lmfit Parameters
  768. """
  769. return self.fitModel.guess(data=data, x=x, **kwargs)
  770. def _guess_2D(self, data, x, y, **kwargs):
  771. """Call the guess function of the 2D fit model to guess the initial value.
  772. :param data: The flattened data to fit
  773. :type data: 1D numpy array
  774. :param x: The flattened data of x axis
  775. :type x: 1D numpy array
  776. :param y: The flattened data of y axis
  777. :type y: 1D numpy array
  778. :return: The guessed initial parameters for the fit
  779. :rtype: lmfit Parameters
  780. """
  781. data = data.flatten(order='F')
  782. return self.fitModel.guess(data=data, x=x, y=y, **kwargs)
  783. def guess(self, dataArray, x=None, y=None, guess_kwargs={}, input_core_dims=None, dask='parallelized', vectorize=True, keep_attrs=True, daskKwargs=None, **kwargs):
  784. """Call the guess function of the fit model to guess the initial value.
  785. :param dataArray: The data for the fit
  786. :type dataArray: xarray DataArray
  787. :param x: The name of x axis in data or the data of x axis, defaults to None
  788. :type x: str or numpy array, optional
  789. :param y: The name of y axis in data or the data of y axis, defaults to None
  790. :type y: str or numpy array, optional
  791. :param guess_kwargs: the keyworded arguments to send to the guess function, defaults to {}
  792. :type guess_kwargs: dict, optional
  793. :param input_core_dims: over write of the same argument in xarray.apply_ufunc, defaults to None
  794. :type input_core_dims: list or array like, optional
  795. :param dask: over write of the same argument in xarray.apply_ufunc,, defaults to 'parallelized'
  796. :type dask: str, optional
  797. :param vectorize: over write of the same argument in xarray.apply_ufunc, defaults to True
  798. :type vectorize: bool, optional
  799. :param keep_attrs: over write of the same argument in xarray.apply_ufunc, defaults to True
  800. :type keep_attrs: bool, optional
  801. :param daskKwargs: over write of the same argument in xarray.apply_ufunc, defaults to None
  802. :type daskKwargs: dict, optional
  803. :return: The guessed initial parameters for the fit
  804. :rtype: xarray DataArray
  805. """
  806. kwargs.update(
  807. {
  808. "dask": dask,
  809. "vectorize": vectorize,
  810. "input_core_dims": input_core_dims,
  811. 'keep_attrs': keep_attrs,
  812. }
  813. )
  814. if not daskKwargs is None:
  815. kwargs.update({"dask_gufunc_kwargs": daskKwargs})
  816. if input_core_dims is None:
  817. kwargs.update(
  818. {
  819. "input_core_dims": [['x']],
  820. }
  821. )
  822. if x is None:
  823. if 'x' in dataArray.dims:
  824. x = dataArray['x'].to_numpy()
  825. else:
  826. if isinstance(x, str):
  827. if input_core_dims is None:
  828. kwargs.update(
  829. {
  830. "input_core_dims": [[x]],
  831. }
  832. )
  833. x = dataArray[x].to_numpy()
  834. if self.fitDim == 1:
  835. guess_kwargs.update(
  836. {
  837. 'x':x,
  838. }
  839. )
  840. return xr.apply_ufunc(self._guess_1D, dataArray, kwargs=guess_kwargs,
  841. output_dtypes=[type(self.fitModel.make_params())],
  842. **kwargs
  843. )
  844. if self.fitDim == 2:
  845. if y is None:
  846. if 'y' in dataArray.dims:
  847. y = dataArray['y'].to_numpy()
  848. if input_core_dims is None:
  849. kwargs.update(
  850. {
  851. "input_core_dims": [['x', 'y']],
  852. }
  853. )
  854. else:
  855. if isinstance(y, str):
  856. kwargs["input_core_dims"][0] = np.append(kwargs["input_core_dims"][0], y)
  857. y = dataArray[y].to_numpy()
  858. elif input_core_dims is None:
  859. kwargs.update(
  860. {
  861. "input_core_dims": [['x', 'y']],
  862. }
  863. )
  864. _x, _y = np.meshgrid(x, y)
  865. _x = _x.flatten()
  866. _y = _y.flatten()
  867. # dataArray = dataArray.stack(_z=(kwargs["input_core_dims"][0][0], kwargs["input_core_dims"][0][1]))
  868. # kwargs["input_core_dims"][0] = ['_z']
  869. guess_kwargs.update(
  870. {
  871. 'x':_x,
  872. 'y':_y,
  873. }
  874. )
  875. return xr.apply_ufunc(self._guess_2D, dataArray, kwargs=guess_kwargs,
  876. output_dtypes=[type(self.fitModel.make_params())],
  877. **kwargs
  878. )
  879. def _fit_1D(self, data, params, x):
  880. """Call the fit function of the 1D fit model to do the fit.
  881. :param data: The data to fit
  882. :type data: 1D numpy array
  883. :param params: The initial paramters of the fit
  884. :type params: lmfit Parameters
  885. :param x: The data of x axis
  886. :type x: 1D numpy array
  887. :return: The result of the fit
  888. :rtype: lmfit FitResult
  889. """
  890. return self.fitModel.fit(data=data, x=x, params=params, nan_policy='omit')
  891. def _fit_2D(self, data, params, x, y):
  892. """Call the fit function of the 2D fit model to do the fit.
  893. :param data: The flattened data to fit
  894. :type data: 1D numpy array
  895. :param params: The flattened initial paramters of the fit
  896. :type params: lmfit Parameters
  897. :param x: The flattened data of x axis
  898. :type x: 1D numpy array
  899. :param y: The flattened data of y axis
  900. :type y: 1D numpy array
  901. :return: The result of the fit
  902. :rtype: lmfit FitResult
  903. """
  904. data = data.flatten(order='F')
  905. return self.fitModel.fit(data=data, x=x, y=y, params=params, nan_policy='omit')
  906. def fit(self, dataArray, paramsArray, x=None, y=None, input_core_dims=None, dask='parallelized', vectorize=True, keep_attrs=True, daskKwargs=None, **kwargs):
  907. """Call the fit function of the fit model to do the fit.
  908. :param dataArray: The data for the fit
  909. :type dataArray: xarray DataArray
  910. :param paramsArray: The flattened initial paramters of the fit
  911. :type paramsArray: xarray DataArray or lmfit Parameters
  912. :param x: The name of x axis in data or the data of x axis, defaults to None
  913. :type x: str or numpy array, optional
  914. :param y: The name of y axis in data or the data of y axis, defaults to None
  915. :type y: str or numpy array, optional
  916. :param input_core_dims: over write of the same argument in xarray.apply_ufunc, defaults to None, defaults to None
  917. :type input_core_dims: list or array like, optional
  918. :param dask: over write of the same argument in xarray.apply_ufunc, defaults to None, defaults to 'parallelized'
  919. :type dask: str, optional
  920. :param vectorize: over write of the same argument in xarray.apply_ufunc, defaults to None, defaults to True
  921. :type vectorize: bool, optional
  922. :param keep_attrs: over write of the same argument in xarray.apply_ufunc, defaults to None, defaults to True
  923. :type keep_attrs: bool, optional
  924. :param daskKwargs: over write of the same argument in xarray.apply_ufunc, defaults to None, defaults to None
  925. :type daskKwargs: dict, optional
  926. :return: The fit result
  927. :rtype: xarray DataArray
  928. """
  929. kwargs.update(
  930. {
  931. "dask": dask,
  932. "vectorize": vectorize,
  933. "input_core_dims": input_core_dims,
  934. 'keep_attrs': keep_attrs,
  935. }
  936. )
  937. if not daskKwargs is None:
  938. kwargs.update({"dask_gufunc_kwargs": daskKwargs})
  939. if isinstance(paramsArray, type(self.fitModel.make_params())):
  940. if input_core_dims is None:
  941. kwargs.update(
  942. {
  943. "input_core_dims": [['x']],
  944. }
  945. )
  946. if x is None:
  947. if 'x' in dataArray.dims:
  948. x = dataArray['x'].to_numpy()
  949. else:
  950. if isinstance(x, str):
  951. if input_core_dims is None:
  952. kwargs.update(
  953. {
  954. "input_core_dims": [[x]],
  955. }
  956. )
  957. x = dataArray[x].to_numpy()
  958. if self.fitDim == 1:
  959. res = xr.apply_ufunc(self._fit_1D, dataArray, kwargs={'params':paramsArray,'x':x},
  960. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  961. **kwargs)
  962. if keep_attrs:
  963. res.attrs = copy.copy(dataArray.attrs)
  964. return res
  965. if self.fitDim == 2:
  966. if y is None:
  967. if 'y' in dataArray.dims:
  968. y = dataArray['y'].to_numpy()
  969. if input_core_dims is None:
  970. kwargs.update(
  971. {
  972. "input_core_dims": [['x', 'y']],
  973. }
  974. )
  975. else:
  976. if isinstance(y, str):
  977. kwargs["input_core_dims"][0] = np.append(kwargs["input_core_dims"][0], y)
  978. y = dataArray[y].to_numpy()
  979. elif input_core_dims is None:
  980. kwargs.update(
  981. {
  982. "input_core_dims": [['x', 'y']],
  983. }
  984. )
  985. _x, _y = np.meshgrid(x, y)
  986. _x = _x.flatten()
  987. _y = _y.flatten()
  988. # dataArray = dataArray.stack(_z=(kwargs["input_core_dims"][0][0], kwargs["input_core_dims"][0][1]))
  989. # kwargs["input_core_dims"][0] = ['_z']
  990. res = xr.apply_ufunc(self._fit_2D, dataArray, kwargs={'params':paramsArray,'x':_x, 'y':_y},
  991. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  992. **kwargs)
  993. if keep_attrs:
  994. res.attrs = copy.copy(dataArray.attrs)
  995. return res
  996. else:
  997. if input_core_dims is None:
  998. kwargs.update(
  999. {
  1000. "input_core_dims": [['x'], []],
  1001. }
  1002. )
  1003. if x is None:
  1004. if 'x' in dataArray.dims:
  1005. x = dataArray['x'].to_numpy()
  1006. else:
  1007. if isinstance(x, str):
  1008. if input_core_dims is None:
  1009. kwargs.update(
  1010. {
  1011. "input_core_dims": [[x], []],
  1012. }
  1013. )
  1014. x = dataArray[x].to_numpy()
  1015. if self.fitDim == 1:
  1016. return xr.apply_ufunc(self._fit_1D, dataArray, paramsArray, kwargs={'x':x},
  1017. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  1018. **kwargs)
  1019. if self.fitDim == 2:
  1020. if input_core_dims is None:
  1021. kwargs.update(
  1022. {
  1023. "input_core_dims": [['x', 'y'], []],
  1024. }
  1025. )
  1026. if y is None:
  1027. if 'y' in dataArray.dims:
  1028. y = dataArray['y'].to_numpy()
  1029. else:
  1030. if isinstance(y, str):
  1031. y = dataArray[y].to_numpy()
  1032. kwargs["input_core_dims"][0] = np.append(kwargs["input_core_dims"][0], y)
  1033. _x, _y = np.meshgrid(x, y)
  1034. _x = _x.flatten()
  1035. _y = _y.flatten()
  1036. # dataArray = dataArray.stack(_z=(kwargs["input_core_dims"][0][0], kwargs["input_core_dims"][0][1]))
  1037. # kwargs["input_core_dims"][0] = ['_z']
  1038. return xr.apply_ufunc(self._fit_2D, dataArray, paramsArray, kwargs={'x':_x, 'y':_y},
  1039. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  1040. **kwargs)
  1041. def _eval_1D(self, fitResult, x):
  1042. """Call the eval function of the 1D fit model to calculate the curve.
  1043. :param fitResult: The result of fit
  1044. :type fitResult: lmfit FitResult
  1045. :param x: The data of x axis
  1046. :type x: 1D numpy array
  1047. :return: The curve based on the fit result
  1048. :rtype: 1D numpy array
  1049. """
  1050. return self.fitModel.eval(x=x, **fitResult.best_values)
  1051. def _eval_2D(self, fitResult, x, y, shape):
  1052. """Call the eval function of the 2D fit model to calculate the curve.
  1053. :param fitResult: The result of fit
  1054. :type fitResult: lmfit FitResult
  1055. :param x: The data of x axis
  1056. :type x: 1D numpy array
  1057. :param y: The flattened data of y axis
  1058. :type y: 1D numpy array
  1059. :param shape: The desired shape for output
  1060. :type shape: list, tuple or array like
  1061. :return: The curve based on the fit result
  1062. :rtype: 2D numpy array
  1063. """
  1064. res = self.fitModel.eval(x=x, y=y, **fitResult.best_values)
  1065. return res.reshape(shape, order='F')
  1066. def eval(self, fitResultArray, x=None, y=None, output_core_dims=None, prefix="", dask='parallelized', vectorize=True, daskKwargs=None, **kwargs):
  1067. """Call the eval function of the fit model to calculate the curve.
  1068. :param fitResultArray: The result of fit
  1069. :type fitResultArray: xarray DataArray
  1070. :param x: The name of x axis in data or the data of x axis, defaults to None
  1071. :type x: str or numpy array, optional
  1072. :param y: The name of y axis in data or the data of y axis, defaults to None
  1073. :type y: str or numpy array, optional
  1074. :param output_core_dims: over write of the same argument in xarray.apply_ufunc, defaults to None
  1075. :type output_core_dims: list, optional
  1076. :param prefix: prefix str of the fit model, defaults to ""
  1077. :type prefix: str, optional
  1078. :param dask: over write of the same argument in xarray.apply_ufunc, defaults to 'parallelized'
  1079. :type dask: str, optional
  1080. :param vectorize: over write of the same argument in xarray.apply_ufunc, defaults to True
  1081. :type vectorize: bool, optional
  1082. :param daskKwargs: over write of the same argument in xarray.apply_ufunc, defaults to None
  1083. :type daskKwargs: dict, optional
  1084. :return: The curve based on the fit result
  1085. :rtype: xarray
  1086. """
  1087. kwargs.update(
  1088. {
  1089. "dask": dask,
  1090. "vectorize": vectorize,
  1091. "output_core_dims": output_core_dims,
  1092. }
  1093. )
  1094. if daskKwargs is None:
  1095. daskKwargs = {}
  1096. if self.fitDim == 1:
  1097. if output_core_dims is None:
  1098. kwargs.update(
  1099. {
  1100. "output_core_dims": prefix+'x',
  1101. }
  1102. )
  1103. output_core_dims = [prefix+'x']
  1104. daskKwargs.update(
  1105. {
  1106. 'output_sizes': {
  1107. output_core_dims[0]: np.size(x),
  1108. },
  1109. 'meta': np.ndarray((0,0), dtype=float)
  1110. }
  1111. )
  1112. kwargs.update(
  1113. {
  1114. "dask_gufunc_kwargs": daskKwargs,
  1115. }
  1116. )
  1117. res = xr.apply_ufunc(self._eval_1D, fitResultArray, kwargs={"x":x}, **kwargs)
  1118. return res.assign_coords({prefix+'x':np.array(x)})
  1119. if self.fitDim == 2:
  1120. if output_core_dims is None:
  1121. kwargs.update(
  1122. {
  1123. "output_core_dims": [[prefix+'x', prefix+'y']],
  1124. }
  1125. )
  1126. output_core_dims = [prefix+'x', prefix+'y']
  1127. daskKwargs.update(
  1128. {
  1129. 'output_sizes': {
  1130. output_core_dims[0]: np.size(x),
  1131. output_core_dims[1]: np.size(y),
  1132. },
  1133. 'meta': np.ndarray((0,0), dtype=float)
  1134. },
  1135. )
  1136. kwargs.update(
  1137. {
  1138. "dask_gufunc_kwargs": daskKwargs,
  1139. }
  1140. )
  1141. _x, _y = np.meshgrid(x, y)
  1142. _x = _x.flatten()
  1143. _y = _y.flatten()
  1144. res = xr.apply_ufunc(self._eval_2D, fitResultArray, kwargs={"x":_x, "y":_y, "shape":(len(x), len(y))}, **kwargs)
  1145. return res.assign_coords({prefix+'x':np.array(x), prefix+'y':np.array(y)})
  1146. def _get_fit_value_single(self, fitResult, key):
  1147. """get value of one single parameter from fit result
  1148. :param fitResult: The result of the fit
  1149. :type fitResult: lmfit FitResult
  1150. :param key: The name of the parameter
  1151. :type key: str
  1152. :return: The vaule of the parameter
  1153. :rtype: float
  1154. """
  1155. return fitResult.params[key].value
  1156. def _get_fit_value(self, fitResult, params):
  1157. """get value of parameters from fit result
  1158. :param fitResult: The result of the fit
  1159. :type fitResult: lmfit FitResult
  1160. :param params: A list of the name of paramerters to read
  1161. :type params: list or array like
  1162. :return: The vaule of the parameter
  1163. :rtype: 1D numpy array
  1164. """
  1165. func = np.vectorize(self._get_fit_value_single)
  1166. res = tuple(
  1167. func(fitResult, key)
  1168. for key in params
  1169. )
  1170. return res
  1171. def get_fit_value(self, fitResult, dask='parallelized', **kwargs):
  1172. """get value of parameters from fit result
  1173. :param fitResult: The result of the fit
  1174. :type fitResult: lmfit FitResult
  1175. :param dask: over write of the same argument in xarray.apply_ufunc, defaults to 'parallelized'
  1176. :type dask: str, optional
  1177. :return: The vaule of the parameter
  1178. :rtype: xarray DataArray
  1179. """
  1180. firstIndex = {
  1181. key: fitResult[key][0]
  1182. for key in fitResult.dims
  1183. }
  1184. firstFitResult = fitResult.sel(firstIndex).item()
  1185. params = list(firstFitResult.params.keys())
  1186. output_core_dims=[ [] for _ in range(len(params))]
  1187. kwargs.update(
  1188. {
  1189. "dask": dask,
  1190. "output_core_dims": output_core_dims,
  1191. }
  1192. )
  1193. value = xr.apply_ufunc(self._get_fit_value, fitResult, kwargs=dict(params=params), **kwargs)
  1194. value = xr.Dataset(
  1195. data_vars={
  1196. params[i]: value[i]
  1197. for i in range(len(params))
  1198. },
  1199. attrs=fitResult.attrs
  1200. )
  1201. return value
  1202. def _get_fit_std_single(self, fitResult, key):
  1203. """get standard deviation of one single parameter from fit result
  1204. :param fitResult: The result of the fit
  1205. :type fitResult: lmfit FitResult
  1206. :param key: The name of the parameter
  1207. :type key: str
  1208. :return: The vaule of the parameter
  1209. :rtype: float
  1210. """
  1211. return fitResult.params[key].stderr
  1212. def _get_fit_std(self, fitResult, params):
  1213. """get standard deviation of parameters from fit result
  1214. :param fitResult: The result of the fit
  1215. :type fitResult: lmfit FitResult
  1216. :param params: A list of the name of paramerters to read
  1217. :type params: list or array like
  1218. :return: The vaule of the parameter
  1219. :rtype: 1D numpy array
  1220. """
  1221. func = np.vectorize(self._get_fit_std_single)
  1222. res = tuple(
  1223. func(fitResult, key)
  1224. for key in params
  1225. )
  1226. return res
  1227. def get_fit_std(self, fitResult, dask='parallelized', **kwargs):
  1228. """get standard deviation of parameters from fit result
  1229. :param fitResult: The result of the fit
  1230. :type fitResult: lmfit FitResult
  1231. :param dask: over write of the same argument in xarray.apply_ufunc, defaults to 'parallelized'
  1232. :type dask: str, optional
  1233. :return: The vaule of the parameter
  1234. :rtype: xarray DataArray
  1235. """
  1236. firstIndex = {
  1237. key: fitResult[key][0]
  1238. for key in fitResult.dims
  1239. }
  1240. firstFitResult = fitResult.sel(firstIndex).item()
  1241. params = list(firstFitResult.params.keys())
  1242. output_core_dims=[ [] for _ in range(len(params))]
  1243. kwargs.update(
  1244. {
  1245. "dask": dask,
  1246. "output_core_dims": output_core_dims,
  1247. }
  1248. )
  1249. value = xr.apply_ufunc(self._get_fit_std, fitResult, kwargs=dict(params=params), **kwargs)
  1250. value = xr.Dataset(
  1251. data_vars={
  1252. params[i]: value[i]
  1253. for i in range(len(params))
  1254. },
  1255. attrs=fitResult.attrs
  1256. )
  1257. return value
  1258. def _get_fit_full_result_single(self, fitResult, key):
  1259. """get the full result of one single parameter from fit result
  1260. :param fitResult: The result of the fit
  1261. :type fitResult: lmfit FitResult
  1262. :param key: The name of the parameter
  1263. :type key: str
  1264. :return: The vaule of the parameter
  1265. :rtype: float
  1266. """
  1267. if not fitResult.params[key].value is None:
  1268. value = fitResult.params[key].value
  1269. else:
  1270. value = np.nan
  1271. if not fitResult.params[key].stderr is None:
  1272. std = fitResult.params[key].stderr
  1273. else:
  1274. std = np.nan
  1275. return ufloat(value, std)
  1276. def _get_fit_full_result(self, fitResult, params):
  1277. """get the full result of parameters from fit result
  1278. :param fitResult: The result of the fit
  1279. :type fitResult: lmfit FitResult
  1280. :param params: A list of the name of paramerters to read
  1281. :type params: list or array like
  1282. :return: The vaule of the parameter
  1283. :rtype: 1D numpy array
  1284. """
  1285. func = np.vectorize(self._get_fit_full_result_single)
  1286. res = tuple(
  1287. func(fitResult, key)
  1288. for key in params
  1289. )
  1290. return res
  1291. def get_fit_full_result(self, fitResult, dask='parallelized', **kwargs):
  1292. """get the full result of parameters from fit result
  1293. :param fitResult: The result of the fit
  1294. :type fitResult: lmfit FitResult
  1295. :param dask: over write of the same argument in xarray.apply_ufunc, defaults to 'parallelized'
  1296. :type dask: str, optional
  1297. :return: The vaule of the parameter
  1298. :rtype: xarray DataArray
  1299. """
  1300. firstIndex = {
  1301. key: fitResult[key][0]
  1302. for key in fitResult.dims
  1303. }
  1304. firstFitResult = fitResult.sel(firstIndex).item()
  1305. params = list(firstFitResult.params.keys())
  1306. output_core_dims=[ [] for _ in range(len(params))]
  1307. kwargs.update(
  1308. {
  1309. "dask": dask,
  1310. "output_core_dims": output_core_dims,
  1311. }
  1312. )
  1313. value = xr.apply_ufunc(self._get_fit_full_result, fitResult, kwargs=dict(params=params), **kwargs)
  1314. value = xr.Dataset(
  1315. data_vars={
  1316. params[i]: value[i]
  1317. for i in range(len(params))
  1318. },
  1319. attrs=fitResult.attrs
  1320. )
  1321. return value