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.

540 lines
21 KiB

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. import lmfit
  3. from lmfit.models import (ConstantModel, ComplexConstantModel, LinearModel, QuadraticModel,
  4. PolynomialModel, SineModel, GaussianModel, Gaussian2dModel, LorentzianModel,
  5. SplitLorentzianModel, VoigtModel, PseudoVoigtModel, MoffatModel,
  6. Pearson7Model, StudentsTModel, BreitWignerModel, LognormalModel,
  7. DampedOscillatorModel, ExponentialGaussianModel, SkewedGaussianModel,
  8. SkewedVoigtModel, ThermalDistributionModel, DoniachModel, PowerLawModel,
  9. ExponentialModel, StepModel, RectangleModel, ExpressionModel, DampedHarmonicOscillatorModel)
  10. from lmfit.models import (guess_from_peak, guess_from_peak2d, fwhm_expr, height_expr,
  11. update_param_vals)
  12. from lmfit.lineshapes import (not_zero, breit_wigner, damped_oscillator, dho, doniach,
  13. expgaussian, exponential, gaussian, gaussian2d,
  14. linear, lognormal, lorentzian, moffat, parabolic,
  15. pearson7, powerlaw, pvoigt, rectangle, sine,
  16. skewed_gaussian, skewed_voigt, split_lorentzian, step,
  17. students_t, thermal_distribution, tiny, voigt)
  18. from lmfit import Model
  19. import numpy as np
  20. from numpy import (arctan, copysign, cos, exp, isclose, isnan, log, pi, real,
  21. sin, sqrt, where)
  22. from scipy.special import erf, erfc
  23. from scipy.special import gamma as gamfcn
  24. from scipy.special import wofz
  25. from scipy.optimize import curve_fit
  26. import xarray as xr
  27. log2 = log(2)
  28. s2pi = sqrt(2*pi)
  29. s2 = sqrt(2.0)
  30. def gaussianWithOffset(x, amplitude=1.0, center=0.0, sigma=1.0, offset=0.0):
  31. """Return a 1-dimensional Gaussian function with an offset.
  32. gaussian(x, amplitude, center, sigma) =
  33. (amplitude/(s2pi*sigma)) * exp(-(1.0*x-center)**2 / (2*sigma**2))
  34. """
  35. return ((amplitude/(max(tiny, s2pi*sigma)))
  36. * exp(-(1.0*x-center)**2 / max(tiny, (2*sigma**2))) + offset)
  37. def lorentzianWithOffset(x, amplitude=1.0, center=0.0, sigma=1.0, offset=0.0):
  38. return ((amplitude/(1 + ((1.0*x-center)/max(tiny, sigma))**2))
  39. / max(tiny, (pi*sigma)) + offset)
  40. def exponentialWithOffset(x, amplitude=1.0, decay=1.0, offset=0.0):
  41. decay = not_zero(decay)
  42. return amplitude * exp(-x/decay) + offset
  43. def expansion(x, amplitude=1.0, offset=0.0):
  44. return np.sqrt(amplitude*x*x + offset)
  45. def dampingOscillation(x, center=0, amplitude=1.0, frequency=1.0, decay=1.0, offset=0.0):
  46. return amplitude * np.exp(-decay*x)*np.sin(2*np.pi*frequency*(x-center)) + offset
  47. def two_gaussian2d(x, y=0.0, A_amplitude=1.0, A_centerx=0.0, A_centery=0.0, A_sigmax=1.0,
  48. A_sigmay=1.0, B_amplitude=1.0, B_centerx=0.0, B_centery=0.0, B_sigmax=1.0,
  49. B_sigmay=1.0):
  50. """Return a 2-dimensional Gaussian function.
  51. gaussian2d(x, y, amplitude, centerx, centery, sigmax, sigmay) =
  52. amplitude/(2*pi*sigmax*sigmay) * exp(-(x-centerx)**2/(2*sigmax**2)
  53. -(y-centery)**2/(2*sigmay**2))
  54. """
  55. z = A_amplitude*(gaussian(x, amplitude=1, center=A_centerx, sigma=A_sigmax) *
  56. gaussian(y, amplitude=1, center=A_centery, sigma=A_sigmay))
  57. z += B_amplitude*(gaussian(x, amplitude=1, center=B_centerx, sigma=B_sigmax) *
  58. gaussian(y, amplitude=1, center=B_centery, sigma=B_sigmay))
  59. return z
  60. class GaussianWithOffsetModel(Model):
  61. fwhm_factor = 2*np.sqrt(2*np.log(2))
  62. height_factor = 1./np.sqrt(2*np.pi)
  63. def __init__(self, independent_vars=['x'], nan_policy='raise', prefix='', name=None, **kwargs):
  64. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  65. 'independent_vars': independent_vars})
  66. super().__init__(gaussianWithOffset, **kwargs)
  67. self._set_paramhints_prefix()
  68. def _set_paramhints_prefix(self):
  69. self.set_param_hint('sigma', min=0)
  70. self.set_param_hint('fwhm', expr=fwhm_expr(self))
  71. self.set_param_hint('height', expr=height_expr(self))
  72. def guess(self, data, x, negative=False, **kwargs):
  73. offset = np.min(data)
  74. data = data - offset
  75. pars = guess_from_peak(self, data, x, negative)
  76. pars.add('offset', value=offset)
  77. return update_param_vals(pars, self.prefix, **kwargs)
  78. class LorentzianWithOffsetModel(Model):
  79. fwhm_factor = 2.0
  80. height_factor = 1./np.pi
  81. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  82. **kwargs):
  83. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  84. 'independent_vars': independent_vars})
  85. super().__init__(lorentzianWithOffset, **kwargs)
  86. self._set_paramhints_prefix()
  87. def _set_paramhints_prefix(self):
  88. self.set_param_hint('sigma', min=0)
  89. self.set_param_hint('fwhm', expr=fwhm_expr(self))
  90. self.set_param_hint('height', expr=height_expr(self))
  91. def guess(self, data, x, negative=False, **kwargs):
  92. """Estimate initial model parameter values from data."""
  93. offset = np.min(data)
  94. data = data - offset
  95. pars = guess_from_peak(self, data, x, negative, ampscale=1.25)
  96. pars.add('offset', value=offset)
  97. return update_param_vals(pars, self.prefix, **kwargs)
  98. class ExponentialWithOffsetModel(Model):
  99. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  100. **kwargs):
  101. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  102. 'independent_vars': independent_vars})
  103. super().__init__(exponentialWithOffset, **kwargs)
  104. def guess(self, data, x, **kwargs):
  105. """Estimate initial model parameter values from data."""
  106. offset = np.min(data)
  107. data = data - offset
  108. try:
  109. sval, oval = np.polyfit(x, np.log(abs(data)+1.e-15), 1)
  110. except TypeError:
  111. sval, oval = 1., np.log(abs(max(data)+1.e-9))
  112. pars = self.make_params(amplitude=np.exp(oval), decay=-1.0/sval)
  113. pars.add('offset', value=offset)
  114. return update_param_vals(pars, self.prefix, **kwargs)
  115. class ExpansionModel(Model):
  116. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  117. **kwargs):
  118. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  119. 'independent_vars': independent_vars})
  120. super().__init__(expansion, **kwargs)
  121. def guess(self, data, x, **kwargs):
  122. """Estimate initial model parameter values from data."""
  123. popt1, pcov1 = curve_fit(expansion, x, data)
  124. pars = self.make_params(amplitude=popt1[0], offset=popt1[1])
  125. return update_param_vals(pars, self.prefix, **kwargs)
  126. class DampingOscillationModel(Model):
  127. def __init__(self, independent_vars=['x'], prefix='', nan_policy='raise',
  128. **kwargs):
  129. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  130. 'independent_vars': independent_vars})
  131. super().__init__(dampingOscillation, **kwargs)
  132. def guess(self, data, x, **kwargs):
  133. """Estimate initial model parameter values from data."""
  134. try:
  135. popt1, pcov1 = curve_fit(dampingOscillation, x, data, np.array(0, 5, 5e2, 1e3, 16))
  136. pars = self.make_params(center=popt1[0], amplitude=popt1[1], frequency=popt1[2], decay=popt1[3], offset=popt1[4])
  137. except:
  138. pars = self.make_params(center=0, amplitude=5.0, frequency=5e2, decay=1.0e3, offset=16.0)
  139. return update_param_vals(pars, self.prefix, **kwargs)
  140. class TwoGaussian2dModel(Model):
  141. fwhm_factor = 2*np.sqrt(2*np.log(2))
  142. height_factor = 1./2*np.pi
  143. def __init__(self, independent_vars=['x', 'y'], prefix='', nan_policy='raise',
  144. **kwargs):
  145. kwargs.update({'prefix': prefix, 'nan_policy': nan_policy,
  146. 'independent_vars': independent_vars})
  147. self.helperModel = Gaussian2dModel()
  148. super().__init__(two_gaussian2d, **kwargs)
  149. def guess(self, data, x, y, negative=False, **kwargs):
  150. pars_guess = guess_from_peak2d(self.helperModel, data, x, y, negative)
  151. pars = self.make_params(A_amplitude=pars_guess['amplitude'], A_centerx=pars_guess['centerx'], A_centery=pars_guess['centery'],
  152. A_sigmax=pars_guess['sigmax'], A_sigmay=pars_guess['sigmay'],
  153. B_amplitude=pars_guess['amplitude'], B_centerx=pars_guess['centerx'], B_centery=pars_guess['centery'],
  154. B_sigmax=pars_guess['sigmax'], B_sigmay=pars_guess['sigmay'])
  155. pars.add(f'{self.prefix}delta', value=-1, max=0, vary=True)
  156. pars[f'{self.prefix}A_sigmax'].set(expr=f'delta + {self.prefix}B_sigmax')
  157. pars[f'{self.prefix}A_sigmay'].set(min=0.0)
  158. pars[f'{self.prefix}B_sigmax'].set(min=0.0)
  159. pars[f'{self.prefix}B_sigmay'].set(min=0.0)
  160. return pars
  161. lmfit_models = {'Constant': ConstantModel,
  162. 'Complex Constant': ComplexConstantModel,
  163. 'Linear': LinearModel,
  164. 'Quadratic': QuadraticModel,
  165. 'Polynomial': PolynomialModel,
  166. 'Gaussian': GaussianModel,
  167. 'Gaussian-2D': Gaussian2dModel,
  168. 'Lorentzian': LorentzianModel,
  169. 'Split-Lorentzian': SplitLorentzianModel,
  170. 'Voigt': VoigtModel,
  171. 'PseudoVoigt': PseudoVoigtModel,
  172. 'Moffat': MoffatModel,
  173. 'Pearson7': Pearson7Model,
  174. 'StudentsT': StudentsTModel,
  175. 'Breit-Wigner': BreitWignerModel,
  176. 'Log-Normal': LognormalModel,
  177. 'Damped Oscillator': DampedOscillatorModel,
  178. 'Damped Harmonic Oscillator': DampedHarmonicOscillatorModel,
  179. 'Exponential Gaussian': ExponentialGaussianModel,
  180. 'Skewed Gaussian': SkewedGaussianModel,
  181. 'Skewed Voigt': SkewedVoigtModel,
  182. 'Thermal Distribution': ThermalDistributionModel,
  183. 'Doniach': DoniachModel,
  184. 'Power Law': PowerLawModel,
  185. 'Exponential': ExponentialModel,
  186. 'Step': StepModel,
  187. 'Rectangle': RectangleModel,
  188. 'Expression': ExpressionModel,
  189. 'Gaussian With Offset':GaussianWithOffsetModel,
  190. 'Lorentzian With Offset':LorentzianWithOffsetModel,
  191. 'Expansion':ExpansionModel,
  192. 'Damping Oscillation Model':DampingOscillationModel,
  193. 'Two Gaussian-2D':TwoGaussian2dModel,
  194. }
  195. class FitAnalyser():
  196. def __init__(self, fitModel, fitDim=1, **kwargs) -> None:
  197. if isinstance(fitModel, str):
  198. self.fitModel = lmfit_models[fitModel](**kwargs)
  199. else:
  200. self.fitModel = fitModel
  201. self.fitDim = fitDim
  202. def _guess_1D(self, data, x, **kwargs):
  203. return self.fitModel.guess(data=data, x=x, **kwargs)
  204. def _guess_2D(self, data, x, y, **kwargs):
  205. return self.fitModel.guess(data=data, x=x, y=y, **kwargs)
  206. def guess(self, dataArray, x=None, y=None, guess_kwargs={}, input_core_dims=None, dask='parallelized', vectorize=True, keep_attrs=True, **kwargs):
  207. kwargs.update(
  208. {
  209. "dask": dask,
  210. "vectorize": vectorize,
  211. "input_core_dims": input_core_dims,
  212. 'keep_attrs': keep_attrs,
  213. }
  214. )
  215. if input_core_dims is None:
  216. kwargs.update(
  217. {
  218. "input_core_dims": [['x']],
  219. }
  220. )
  221. if x is None:
  222. if 'x' in dataArray.dims:
  223. x = dataArray['x'].to_numpy()
  224. else:
  225. if isinstance(x, str):
  226. if input_core_dims is None:
  227. kwargs.update(
  228. {
  229. "input_core_dims": [[x]],
  230. }
  231. )
  232. x = dataArray[x].to_numpy()
  233. if self.fitDim == 1:
  234. guess_kwargs.update(
  235. {
  236. 'x':x,
  237. }
  238. )
  239. return xr.apply_ufunc(self._guess_1D, dataArray, kwargs=guess_kwargs,
  240. output_dtypes=[type(self.fitModel.make_params())],
  241. **kwargs
  242. )
  243. if self.fitDim == 2:
  244. if y is None:
  245. if 'y' in dataArray.dims:
  246. y = dataArray['y'].to_numpy()
  247. if input_core_dims is None:
  248. kwargs.update(
  249. {
  250. "input_core_dims": [['x', 'y']],
  251. }
  252. )
  253. else:
  254. if isinstance(y, str):
  255. kwargs["input_core_dims"][0] = np.append(kwargs["input_core_dims"][0], y)
  256. y = dataArray[y].to_numpy()
  257. elif input_core_dims is None:
  258. kwargs.update(
  259. {
  260. "input_core_dims": [['x', 'y']],
  261. }
  262. )
  263. _x, _y = np.meshgrid(x, y)
  264. _x = _x.flatten()
  265. _y = _y.flatten()
  266. dataArray = dataArray.stack(_z=(kwargs["input_core_dims"][0][0], kwargs["input_core_dims"][0][1]))
  267. kwargs["input_core_dims"][0] = ['_z']
  268. guess_kwargs.update(
  269. {
  270. 'x':_x,
  271. 'y':_y,
  272. }
  273. )
  274. return xr.apply_ufunc(self._guess_2D, dataArray, kwargs=guess_kwargs,
  275. output_dtypes=[type(self.fitModel.make_params())],
  276. **kwargs
  277. )
  278. def _fit_1D(self, data, params, x):
  279. # try:
  280. return self.fitModel.fit(data=data, x=x, params=params)
  281. def _fit_2D(self, data, params, x, y):
  282. return self.fitModel.fit(data=data, x=x, y=y, params=params)
  283. def fit(self, dataArray, paramsArray, x=None, y=None, input_core_dims=None, dask='parallelized', vectorize=True, keep_attrs=True, **kwargs):
  284. kwargs.update(
  285. {
  286. "dask": dask,
  287. "vectorize": vectorize,
  288. "input_core_dims": input_core_dims,
  289. 'keep_attrs': keep_attrs,
  290. }
  291. )
  292. if input_core_dims is None:
  293. kwargs.update(
  294. {
  295. "input_core_dims": [['x'], []],
  296. }
  297. )
  298. if x is None:
  299. if 'x' in dataArray.dims:
  300. x = dataArray['x'].to_numpy()
  301. else:
  302. if isinstance(x, str):
  303. if input_core_dims is None:
  304. kwargs.update(
  305. {
  306. "input_core_dims": [[x], []],
  307. }
  308. )
  309. x = dataArray[x].to_numpy()
  310. if isinstance(paramsArray, type(self.fitModel.make_params())):
  311. if self.fitDim == 1:
  312. return xr.apply_ufunc(self._fit_1D, dataArray, kwargs={'params':paramsArray,'x':x},
  313. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  314. **kwargs)
  315. if self.fitDim == 2:
  316. if y is None:
  317. if 'y' in dataArray.dims:
  318. y = dataArray['y'].to_numpy()
  319. if input_core_dims is None:
  320. kwargs.update(
  321. {
  322. "input_core_dims": [['x', 'y'], []],
  323. }
  324. )
  325. else:
  326. if isinstance(y, str):
  327. kwargs["input_core_dims"][0] = np.append(kwargs["input_core_dims"][0], y)
  328. y = dataArray[y].to_numpy()
  329. elif input_core_dims is None:
  330. kwargs.update(
  331. {
  332. "input_core_dims": [['x', 'y'], []],
  333. }
  334. )
  335. _x, _y = np.meshgrid(x, y)
  336. _x = _x.flatten()
  337. _y = _y.flatten()
  338. dataArray = dataArray.stack(_z=(kwargs["input_core_dims"][0][0], kwargs["input_core_dims"][0][1]))
  339. kwargs["input_core_dims"][0] = ['_z']
  340. return xr.apply_ufunc(self._fit_2D, dataArray, kwargs={'params':paramsArray,'x':_x, 'y':_y},
  341. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  342. **kwargs)
  343. else:
  344. if self.fitDim == 1:
  345. return xr.apply_ufunc(self._fit_1D, dataArray, paramsArray, kwargs={'x':x},
  346. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  347. **kwargs)
  348. if self.fitDim == 2:
  349. if input_core_dims is None:
  350. kwargs.update(
  351. {
  352. "input_core_dims": [['x', 'y'], []],
  353. }
  354. )
  355. if y is None:
  356. if 'y' in dataArray.dims:
  357. y = dataArray['y'].to_numpy()
  358. else:
  359. if isinstance(y, str):
  360. y = dataArray[y].to_numpy()
  361. kwargs["input_core_dims"][0] = np.append(kwargs["input_core_dims"][0], y)
  362. _x, _y = np.meshgrid(x, y)
  363. _x = _x.flatten()
  364. _y = _y.flatten()
  365. dataArray = dataArray.stack(_z=(kwargs["input_core_dims"][0][0], kwargs["input_core_dims"][0][1]))
  366. kwargs["input_core_dims"][0] = ['_z']
  367. return xr.apply_ufunc(self._fit_2D, dataArray, paramsArray, kwargs={'x':_x, 'y':_y},
  368. output_dtypes=[type(lmfit.model.ModelResult(self.fitModel, self.fitModel.make_params()))],
  369. **kwargs)
  370. def _eval_1D(self, fitResult, x):
  371. return self.fitModel.eval(x=x, **fitResult.best_values)
  372. def _eval_2D(self, fitResult, x, y, shape):
  373. res = self.fitModel.eval(x=x, y=y, **fitResult.best_values)
  374. return res.reshape(shape)
  375. def eval(self, fitResultArray, x=None, y=None, output_core_dims=None, prefix="", dask='parallelized', vectorize=True, **kwargs):
  376. kwargs.update(
  377. {
  378. "dask": dask,
  379. "vectorize": vectorize,
  380. "output_core_dims": output_core_dims,
  381. }
  382. )
  383. if self.fitDim == 1:
  384. if output_core_dims is None:
  385. kwargs.update(
  386. {
  387. "output_core_dims": prefix+'x',
  388. }
  389. )
  390. output_core_dims = [prefix+'x']
  391. kwargs.update(
  392. {
  393. "dask_gufunc_kwargs": {
  394. 'output_sizes': {
  395. output_core_dims[0]: np.size(x),
  396. },
  397. 'meta': np.ndarray((0,0), dtype=float)
  398. },
  399. }
  400. )
  401. return xr.apply_ufunc(self._eval_1D, fitResultArray, kwargs={"x":x}, **kwargs)
  402. if self.fitDim == 2:
  403. if output_core_dims is None:
  404. kwargs.update(
  405. {
  406. "output_core_dims": [[prefix+'x', prefix+'y']],
  407. }
  408. )
  409. output_core_dims = [prefix+'x', prefix+'y']
  410. kwargs.update(
  411. {
  412. "dask_gufunc_kwargs": {
  413. 'output_sizes': {
  414. output_core_dims[0]: np.size(x),
  415. output_core_dims[1]: np.size(y),
  416. },
  417. 'meta': np.ndarray((0,0), dtype=float)
  418. },
  419. }
  420. )
  421. _x, _y = np.meshgrid(x, y)
  422. _x = _x.flatten()
  423. _y = _y.flatten()
  424. return xr.apply_ufunc(self._eval_2D, fitResultArray, kwargs={"x":_x, "y":_y, "shape":(len(x), len(y))}, **kwargs)