Mercurial > repos > rv43 > test_tomo_reconstruct
comparison fit.py @ 0:98e23dff1de2 draft default tip
planemo upload for repository https://github.com/rolfverberg/galaxytools commit f8c4bdb31c20c468045ad5e6eb255a293244bc6c-dirty
author | rv43 |
---|---|
date | Tue, 21 Mar 2023 16:22:42 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:98e23dff1de2 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # -*- coding: utf-8 -*- | |
4 """ | |
5 Created on Mon Dec 6 15:36:22 2021 | |
6 | |
7 @author: rv43 | |
8 """ | |
9 | |
10 import logging | |
11 | |
12 from asteval import Interpreter, get_ast_names | |
13 from copy import deepcopy | |
14 from lmfit import Model, Parameters | |
15 from lmfit.model import ModelResult | |
16 from lmfit.models import ConstantModel, LinearModel, QuadraticModel, PolynomialModel,\ | |
17 ExponentialModel, StepModel, RectangleModel, ExpressionModel, GaussianModel,\ | |
18 LorentzianModel | |
19 import numpy as np | |
20 from os import cpu_count, getpid, listdir, mkdir, path | |
21 from re import compile, sub | |
22 from shutil import rmtree | |
23 try: | |
24 from sympy import diff, simplify | |
25 except: | |
26 pass | |
27 try: | |
28 from joblib import Parallel, delayed | |
29 have_joblib = True | |
30 except: | |
31 have_joblib = False | |
32 try: | |
33 import xarray as xr | |
34 have_xarray = True | |
35 except: | |
36 have_xarray = False | |
37 | |
38 try: | |
39 from .general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \ | |
40 almost_equal, quick_plot #, eval_expr | |
41 except: | |
42 try: | |
43 from sys import path as syspath | |
44 syspath.append(f'/nfs/chess/user/rv43/msnctools/msnctools') | |
45 from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \ | |
46 almost_equal, quick_plot #, eval_expr | |
47 except: | |
48 from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \ | |
49 almost_equal, quick_plot #, eval_expr | |
50 | |
51 from sys import float_info | |
52 float_min = float_info.min | |
53 float_max = float_info.max | |
54 | |
55 # sigma = fwhm_factor*fwhm | |
56 fwhm_factor = { | |
57 'gaussian': f'fwhm/(2*sqrt(2*log(2)))', | |
58 'lorentzian': f'0.5*fwhm', | |
59 'splitlorentzian': f'0.5*fwhm', # sigma = sigma_r | |
60 'voight': f'0.2776*fwhm', # sigma = gamma | |
61 'pseudovoight': f'0.5*fwhm'} # fraction = 0.5 | |
62 | |
63 # amplitude = height_factor*height*fwhm | |
64 height_factor = { | |
65 'gaussian': f'height*fwhm*0.5*sqrt(pi/log(2))', | |
66 'lorentzian': f'height*fwhm*0.5*pi', | |
67 'splitlorentzian': f'height*fwhm*0.5*pi', # sigma = sigma_r | |
68 'voight': f'3.334*height*fwhm', # sigma = gamma | |
69 'pseudovoight': f'1.268*height*fwhm'} # fraction = 0.5 | |
70 | |
71 class Fit: | |
72 """Wrapper class for lmfit | |
73 """ | |
74 def __init__(self, y, x=None, models=None, normalize=True, **kwargs): | |
75 if not isinstance(normalize, bool): | |
76 raise ValueError(f'Invalid parameter normalize ({normalize})') | |
77 self._mask = None | |
78 self._model = None | |
79 self._norm = None | |
80 self._normalized = False | |
81 self._parameters = Parameters() | |
82 self._parameter_bounds = None | |
83 self._parameter_norms = {} | |
84 self._linear_parameters = [] | |
85 self._nonlinear_parameters = [] | |
86 self._result = None | |
87 self._try_linear_fit = True | |
88 self._y = None | |
89 self._y_norm = None | |
90 self._y_range = None | |
91 if 'try_linear_fit' in kwargs: | |
92 try_linear_fit = kwargs.pop('try_linear_fit') | |
93 if not isinstance(try_linear_fit, bool): | |
94 illegal_value(try_linear_fit, 'try_linear_fit', 'Fit.fit', raise_error=True) | |
95 self._try_linear_fit = try_linear_fit | |
96 if y is not None: | |
97 if isinstance(y, (tuple, list, np.ndarray)): | |
98 self._x = np.asarray(x) | |
99 elif have_xarray and isinstance(y, xr.DataArray): | |
100 if x is not None: | |
101 logging.warning('Ignoring superfluous input x ({x}) in Fit.__init__') | |
102 if y.ndim != 1: | |
103 illegal_value(y.ndim, 'DataArray dimensions', 'Fit:__init__', raise_error=True) | |
104 self._x = np.asarray(y[y.dims[0]]) | |
105 else: | |
106 illegal_value(y, 'y', 'Fit:__init__', raise_error=True) | |
107 self._y = y | |
108 if self._x.ndim != 1: | |
109 raise ValueError(f'Invalid dimension for input x ({self._x.ndim})') | |
110 if self._x.size != self._y.size: | |
111 raise ValueError(f'Inconsistent x and y dimensions ({self._x.size} vs '+ | |
112 f'{self._y.size})') | |
113 if 'mask' in kwargs: | |
114 self._mask = kwargs.pop('mask') | |
115 if self._mask is None: | |
116 y_min = float(self._y.min()) | |
117 self._y_range = float(self._y.max())-y_min | |
118 if normalize and self._y_range > 0.0: | |
119 self._norm = (y_min, self._y_range) | |
120 else: | |
121 self._mask = np.asarray(self._mask).astype(bool) | |
122 if self._x.size != self._mask.size: | |
123 raise ValueError(f'Inconsistent x and mask dimensions ({self._x.size} vs '+ | |
124 f'{self._mask.size})') | |
125 y_masked = np.asarray(self._y)[~self._mask] | |
126 y_min = float(y_masked.min()) | |
127 self._y_range = float(y_masked.max())-y_min | |
128 if normalize and self._y_range > 0.0: | |
129 if normalize and self._y_range > 0.0: | |
130 self._norm = (y_min, self._y_range) | |
131 if models is not None: | |
132 if callable(models) or isinstance(models, str): | |
133 kwargs = self.add_model(models, **kwargs) | |
134 elif isinstance(models, (tuple, list)): | |
135 for model in models: | |
136 kwargs = self.add_model(model, **kwargs) | |
137 self.fit(**kwargs) | |
138 | |
139 @classmethod | |
140 def fit_data(cls, y, models, x=None, normalize=True, **kwargs): | |
141 return(cls(y, x=x, models=models, normalize=normalize, **kwargs)) | |
142 | |
143 @property | |
144 def best_errors(self): | |
145 if self._result is None: | |
146 return(None) | |
147 return({name:self._result.params[name].stderr for name in sorted(self._result.params) | |
148 if name != 'tmp_normalization_offset_c'}) | |
149 | |
150 @property | |
151 def best_fit(self): | |
152 if self._result is None: | |
153 return(None) | |
154 return(self._result.best_fit) | |
155 | |
156 @property | |
157 def best_parameters(self): | |
158 if self._result is None: | |
159 return(None) | |
160 parameters = {} | |
161 for name in sorted(self._result.params): | |
162 if name != 'tmp_normalization_offset_c': | |
163 par = self._result.params[name] | |
164 parameters[name] = {'value': par.value, 'error': par.stderr, | |
165 'init_value': par.init_value, 'min': par.min, 'max': par.max, | |
166 'vary': par.vary, 'expr': par.expr} | |
167 return(parameters) | |
168 | |
169 @property | |
170 def best_results(self): | |
171 """Convert the input data array to a data set and add the fit results. | |
172 """ | |
173 if self._result is None: | |
174 return(None) | |
175 if isinstance(self._y, xr.DataArray): | |
176 best_results = self._y.to_dataset() | |
177 dims = self._y.dims | |
178 fit_name = f'{self._y.name}_fit' | |
179 else: | |
180 coords = {'x': (['x'], self._x)} | |
181 dims = ('x') | |
182 best_results = xr.Dataset(coords=coords) | |
183 best_results['y'] = (dims, self._y) | |
184 fit_name = 'y_fit' | |
185 best_results[fit_name] = (dims, self.best_fit) | |
186 if self._mask is not None: | |
187 best_results['mask'] = self._mask | |
188 best_results.coords['par_names'] = ('peak', [name for name in self.best_values.keys()]) | |
189 best_results['best_values'] = (['par_names'], [v for v in self.best_values.values()]) | |
190 best_results['best_errors'] = (['par_names'], [v for v in self.best_errors.values()]) | |
191 best_results.attrs['components'] = self.components | |
192 return(best_results) | |
193 | |
194 @property | |
195 def best_values(self): | |
196 if self._result is None: | |
197 return(None) | |
198 return({name:self._result.params[name].value for name in sorted(self._result.params) | |
199 if name != 'tmp_normalization_offset_c'}) | |
200 | |
201 @property | |
202 def chisqr(self): | |
203 if self._result is None: | |
204 return(None) | |
205 return(self._result.chisqr) | |
206 | |
207 @property | |
208 def components(self): | |
209 components = {} | |
210 if self._result is None: | |
211 logging.warning('Unable to collect components in Fit.components') | |
212 return(components) | |
213 for component in self._result.components: | |
214 if 'tmp_normalization_offset_c' in component.param_names: | |
215 continue | |
216 parameters = {} | |
217 for name in component.param_names: | |
218 par = self._parameters[name] | |
219 parameters[name] = {'free': par.vary, 'value': self._result.params[name].value} | |
220 if par.expr is not None: | |
221 parameters[name]['expr'] = par.expr | |
222 expr = None | |
223 if isinstance(component, ExpressionModel): | |
224 name = component._name | |
225 if name[-1] == '_': | |
226 name = name[:-1] | |
227 expr = component.expr | |
228 else: | |
229 prefix = component.prefix | |
230 if len(prefix): | |
231 if prefix[-1] == '_': | |
232 prefix = prefix[:-1] | |
233 name = f'{prefix} ({component._name})' | |
234 else: | |
235 name = f'{component._name}' | |
236 if expr is None: | |
237 components[name] = {'parameters': parameters} | |
238 else: | |
239 components[name] = {'expr': expr, 'parameters': parameters} | |
240 return(components) | |
241 | |
242 @property | |
243 def covar(self): | |
244 if self._result is None: | |
245 return(None) | |
246 return(self._result.covar) | |
247 | |
248 @property | |
249 def init_parameters(self): | |
250 if self._result is None or self._result.init_params is None: | |
251 return(None) | |
252 parameters = {} | |
253 for name in sorted(self._result.init_params): | |
254 if name != 'tmp_normalization_offset_c': | |
255 par = self._result.init_params[name] | |
256 parameters[name] = {'value': par.value, 'min': par.min, 'max': par.max, | |
257 'vary': par.vary, 'expr': par.expr} | |
258 return(parameters) | |
259 | |
260 @property | |
261 def init_values(self): | |
262 if self._result is None or self._result.init_params is None: | |
263 return(None) | |
264 return({name:self._result.init_params[name].value for name in | |
265 sorted(self._result.init_params) if name != 'tmp_normalization_offset_c'}) | |
266 | |
267 @property | |
268 def normalization_offset(self): | |
269 if self._result is None: | |
270 return(None) | |
271 if self._norm is None: | |
272 return(0.0) | |
273 else: | |
274 if self._result.init_params is not None: | |
275 normalization_offset = self._result.init_params['tmp_normalization_offset_c'] | |
276 else: | |
277 normalization_offset = self._result.params['tmp_normalization_offset_c'] | |
278 return(normalization_offset) | |
279 | |
280 @property | |
281 def num_func_eval(self): | |
282 if self._result is None: | |
283 return(None) | |
284 return(self._result.nfev) | |
285 | |
286 @property | |
287 def parameters(self): | |
288 return({name:{'min': par.min, 'max': par.max, 'vary': par.vary, 'expr': par.expr} | |
289 for name, par in self._parameters.items() if name != 'tmp_normalization_offset_c'}) | |
290 | |
291 @property | |
292 def redchi(self): | |
293 if self._result is None: | |
294 return(None) | |
295 return(self._result.redchi) | |
296 | |
297 @property | |
298 def residual(self): | |
299 if self._result is None: | |
300 return(None) | |
301 return(self._result.residual) | |
302 | |
303 @property | |
304 def success(self): | |
305 if self._result is None: | |
306 return(None) | |
307 if not self._result.success: | |
308 # print(f'ier = {self._result.ier}') | |
309 # print(f'lmdif_message = {self._result.lmdif_message}') | |
310 # print(f'message = {self._result.message}') | |
311 # print(f'nfev = {self._result.nfev}') | |
312 # print(f'redchi = {self._result.redchi}') | |
313 # print(f'success = {self._result.success}') | |
314 if self._result.ier == 0 or self._result.ier == 5: | |
315 logging.warning(f'ier = {self._result.ier}: {self._result.message}') | |
316 else: | |
317 logging.warning(f'ier = {self._result.ier}: {self._result.message}') | |
318 return(True) | |
319 # self.print_fit_report() | |
320 # self.plot() | |
321 return(self._result.success) | |
322 | |
323 @property | |
324 def var_names(self): | |
325 """Intended to be used with covar | |
326 """ | |
327 if self._result is None: | |
328 return(None) | |
329 return(getattr(self._result, 'var_names', None)) | |
330 | |
331 @property | |
332 def x(self): | |
333 return(self._x) | |
334 | |
335 @property | |
336 def y(self): | |
337 return(self._y) | |
338 | |
339 def print_fit_report(self, result=None, show_correl=False): | |
340 if result is None: | |
341 result = self._result | |
342 if result is not None: | |
343 print(result.fit_report(show_correl=show_correl)) | |
344 | |
345 def add_parameter(self, **parameter): | |
346 if not isinstance(parameter, dict): | |
347 raise ValueError(f'Invalid parameter ({parameter})') | |
348 if parameter.get('expr') is not None: | |
349 raise KeyError(f'Illegal "expr" key in parameter {parameter}') | |
350 name = parameter['name'] | |
351 if not isinstance(name, str): | |
352 raise ValueError(f'Illegal "name" value ({name}) in parameter {parameter}') | |
353 if parameter.get('norm') is None: | |
354 self._parameter_norms[name] = False | |
355 else: | |
356 norm = parameter.pop('norm') | |
357 if self._norm is None: | |
358 logging.warning(f'Ignoring norm in parameter {name} in '+ | |
359 f'Fit.add_parameter (normalization is turned off)') | |
360 self._parameter_norms[name] = False | |
361 else: | |
362 if not isinstance(norm, bool): | |
363 raise ValueError(f'Illegal "norm" value ({norm}) in parameter {parameter}') | |
364 self._parameter_norms[name] = norm | |
365 vary = parameter.get('vary') | |
366 if vary is not None: | |
367 if not isinstance(vary, bool): | |
368 raise ValueError(f'Illegal "vary" value ({vary}) in parameter {parameter}') | |
369 if not vary: | |
370 if 'min' in parameter: | |
371 logging.warning(f'Ignoring min in parameter {name} in '+ | |
372 f'Fit.add_parameter (vary = {vary})') | |
373 parameter.pop('min') | |
374 if 'max' in parameter: | |
375 logging.warning(f'Ignoring max in parameter {name} in '+ | |
376 f'Fit.add_parameter (vary = {vary})') | |
377 parameter.pop('max') | |
378 if self._norm is not None and name not in self._parameter_norms: | |
379 raise ValueError(f'Missing parameter normalization type for paremeter {name}') | |
380 self._parameters.add(**parameter) | |
381 | |
382 def add_model(self, model, prefix=None, parameters=None, parameter_norms=None, **kwargs): | |
383 # Create the new model | |
384 # print(f'at start add_model:\nself._parameters:\n{self._parameters}') | |
385 # print(f'at start add_model: kwargs = {kwargs}') | |
386 # print(f'parameters = {parameters}') | |
387 # print(f'parameter_norms = {parameter_norms}') | |
388 # if len(self._parameters.keys()): | |
389 # print('\nAt start adding model:') | |
390 # self._parameters.pretty_print() | |
391 # print(f'parameter_norms:\n{self._parameter_norms}') | |
392 if prefix is not None and not isinstance(prefix, str): | |
393 logging.warning('Ignoring illegal prefix: {model} {type(model)}') | |
394 prefix = None | |
395 if prefix is None: | |
396 pprefix = '' | |
397 else: | |
398 pprefix = prefix | |
399 if parameters is not None: | |
400 if isinstance(parameters, dict): | |
401 parameters = (parameters, ) | |
402 elif not is_dict_series(parameters): | |
403 illegal_value(parameters, 'parameters', 'Fit.add_model', raise_error=True) | |
404 parameters = deepcopy(parameters) | |
405 if parameter_norms is not None: | |
406 if isinstance(parameter_norms, dict): | |
407 parameter_norms = (parameter_norms, ) | |
408 if not is_dict_series(parameter_norms): | |
409 illegal_value(parameter_norms, 'parameter_norms', 'Fit.add_model', raise_error=True) | |
410 new_parameter_norms = {} | |
411 if callable(model): | |
412 # Linear fit not yet implemented for callable models | |
413 self._try_linear_fit = False | |
414 if parameter_norms is None: | |
415 if parameters is None: | |
416 raise ValueError('Either "parameters" or "parameter_norms" is required in '+ | |
417 f'{model}') | |
418 for par in parameters: | |
419 name = par['name'] | |
420 if not isinstance(name, str): | |
421 raise ValueError(f'Illegal "name" value ({name}) in input parameters') | |
422 if par.get('norm') is not None: | |
423 norm = par.pop('norm') | |
424 if not isinstance(norm, bool): | |
425 raise ValueError(f'Illegal "norm" value ({norm}) in input parameters') | |
426 new_parameter_norms[f'{pprefix}{name}'] = norm | |
427 else: | |
428 for par in parameter_norms: | |
429 name = par['name'] | |
430 if not isinstance(name, str): | |
431 raise ValueError(f'Illegal "name" value ({name}) in input parameters') | |
432 norm = par.get('norm') | |
433 if norm is None or not isinstance(norm, bool): | |
434 raise ValueError(f'Illegal "norm" value ({norm}) in input parameters') | |
435 new_parameter_norms[f'{pprefix}{name}'] = norm | |
436 if parameters is not None: | |
437 for par in parameters: | |
438 if par.get('expr') is not None: | |
439 raise KeyError(f'Illegal "expr" key ({par.get("expr")}) in parameter '+ | |
440 f'{name} for a callable model {model}') | |
441 name = par['name'] | |
442 if not isinstance(name, str): | |
443 raise ValueError(f'Illegal "name" value ({name}) in input parameters') | |
444 # RV FIX callable model will need partial deriv functions for any linear pars to get the linearized matrix, so for now skip linear solution option | |
445 newmodel = Model(model, prefix=prefix) | |
446 elif isinstance(model, str): | |
447 if model == 'constant': # Par: c | |
448 newmodel = ConstantModel(prefix=prefix) | |
449 new_parameter_norms[f'{pprefix}c'] = True | |
450 self._linear_parameters.append(f'{pprefix}c') | |
451 elif model == 'linear': # Par: slope, intercept | |
452 newmodel = LinearModel(prefix=prefix) | |
453 new_parameter_norms[f'{pprefix}slope'] = True | |
454 new_parameter_norms[f'{pprefix}intercept'] = True | |
455 self._linear_parameters.append(f'{pprefix}slope') | |
456 self._linear_parameters.append(f'{pprefix}intercept') | |
457 elif model == 'quadratic': # Par: a, b, c | |
458 newmodel = QuadraticModel(prefix=prefix) | |
459 new_parameter_norms[f'{pprefix}a'] = True | |
460 new_parameter_norms[f'{pprefix}b'] = True | |
461 new_parameter_norms[f'{pprefix}c'] = True | |
462 self._linear_parameters.append(f'{pprefix}a') | |
463 self._linear_parameters.append(f'{pprefix}b') | |
464 self._linear_parameters.append(f'{pprefix}c') | |
465 elif model == 'gaussian': # Par: amplitude, center, sigma (fwhm, height) | |
466 newmodel = GaussianModel(prefix=prefix) | |
467 new_parameter_norms[f'{pprefix}amplitude'] = True | |
468 new_parameter_norms[f'{pprefix}center'] = False | |
469 new_parameter_norms[f'{pprefix}sigma'] = False | |
470 self._linear_parameters.append(f'{pprefix}amplitude') | |
471 self._nonlinear_parameters.append(f'{pprefix}center') | |
472 self._nonlinear_parameters.append(f'{pprefix}sigma') | |
473 # parameter norms for height and fwhm are needed to get correct errors | |
474 new_parameter_norms[f'{pprefix}height'] = True | |
475 new_parameter_norms[f'{pprefix}fwhm'] = False | |
476 elif model == 'lorentzian': # Par: amplitude, center, sigma (fwhm, height) | |
477 newmodel = LorentzianModel(prefix=prefix) | |
478 new_parameter_norms[f'{pprefix}amplitude'] = True | |
479 new_parameter_norms[f'{pprefix}center'] = False | |
480 new_parameter_norms[f'{pprefix}sigma'] = False | |
481 self._linear_parameters.append(f'{pprefix}amplitude') | |
482 self._nonlinear_parameters.append(f'{pprefix}center') | |
483 self._nonlinear_parameters.append(f'{pprefix}sigma') | |
484 # parameter norms for height and fwhm are needed to get correct errors | |
485 new_parameter_norms[f'{pprefix}height'] = True | |
486 new_parameter_norms[f'{pprefix}fwhm'] = False | |
487 elif model == 'exponential': # Par: amplitude, decay | |
488 newmodel = ExponentialModel(prefix=prefix) | |
489 new_parameter_norms[f'{pprefix}amplitude'] = True | |
490 new_parameter_norms[f'{pprefix}decay'] = False | |
491 self._linear_parameters.append(f'{pprefix}amplitude') | |
492 self._nonlinear_parameters.append(f'{pprefix}decay') | |
493 elif model == 'step': # Par: amplitude, center, sigma | |
494 form = kwargs.get('form') | |
495 if form is not None: | |
496 kwargs.pop('form') | |
497 if form is None or form not in ('linear', 'atan', 'arctan', 'erf', 'logistic'): | |
498 raise ValueError(f'Invalid parameter form for build-in step model ({form})') | |
499 newmodel = StepModel(prefix=prefix, form=form) | |
500 new_parameter_norms[f'{pprefix}amplitude'] = True | |
501 new_parameter_norms[f'{pprefix}center'] = False | |
502 new_parameter_norms[f'{pprefix}sigma'] = False | |
503 self._linear_parameters.append(f'{pprefix}amplitude') | |
504 self._nonlinear_parameters.append(f'{pprefix}center') | |
505 self._nonlinear_parameters.append(f'{pprefix}sigma') | |
506 elif model == 'rectangle': # Par: amplitude, center1, center2, sigma1, sigma2 | |
507 form = kwargs.get('form') | |
508 if form is not None: | |
509 kwargs.pop('form') | |
510 if form is None or form not in ('linear', 'atan', 'arctan', 'erf', 'logistic'): | |
511 raise ValueError('Invalid parameter form for build-in rectangle model '+ | |
512 f'({form})') | |
513 newmodel = RectangleModel(prefix=prefix, form=form) | |
514 new_parameter_norms[f'{pprefix}amplitude'] = True | |
515 new_parameter_norms[f'{pprefix}center1'] = False | |
516 new_parameter_norms[f'{pprefix}center2'] = False | |
517 new_parameter_norms[f'{pprefix}sigma1'] = False | |
518 new_parameter_norms[f'{pprefix}sigma2'] = False | |
519 self._linear_parameters.append(f'{pprefix}amplitude') | |
520 self._nonlinear_parameters.append(f'{pprefix}center1') | |
521 self._nonlinear_parameters.append(f'{pprefix}center2') | |
522 self._nonlinear_parameters.append(f'{pprefix}sigma1') | |
523 self._nonlinear_parameters.append(f'{pprefix}sigma2') | |
524 elif model == 'expression': # Par: by expression | |
525 expr = kwargs['expr'] | |
526 if not isinstance(expr, str): | |
527 raise ValueError(f'Illegal "expr" value ({expr}) in {model}') | |
528 kwargs.pop('expr') | |
529 if parameter_norms is not None: | |
530 logging.warning('Ignoring parameter_norms (normalization determined from '+ | |
531 'linearity)}') | |
532 if parameters is not None: | |
533 for par in parameters: | |
534 if par.get('expr') is not None: | |
535 raise KeyError(f'Illegal "expr" key ({par.get("expr")}) in parameter '+ | |
536 f'({par}) for an expression model') | |
537 if par.get('norm') is not None: | |
538 logging.warning(f'Ignoring "norm" key in parameter ({par}) '+ | |
539 '(normalization determined from linearity)}') | |
540 par.pop('norm') | |
541 name = par['name'] | |
542 if not isinstance(name, str): | |
543 raise ValueError(f'Illegal "name" value ({name}) in input parameters') | |
544 ast = Interpreter() | |
545 expr_parameters = [name for name in get_ast_names(ast.parse(expr)) | |
546 if name != 'x' and name not in self._parameters | |
547 and name not in ast.symtable] | |
548 # print(f'\nexpr_parameters: {expr_parameters}') | |
549 # print(f'expr = {expr}') | |
550 if prefix is None: | |
551 newmodel = ExpressionModel(expr=expr) | |
552 else: | |
553 for name in expr_parameters: | |
554 expr = sub(rf'\b{name}\b', f'{prefix}{name}', expr) | |
555 expr_parameters = [f'{prefix}{name}' for name in expr_parameters] | |
556 # print(f'\nexpr_parameters: {expr_parameters}') | |
557 # print(f'expr = {expr}') | |
558 newmodel = ExpressionModel(expr=expr, name=name) | |
559 # print(f'\nnewmodel = {newmodel.__dict__}') | |
560 # print(f'params_names = {newmodel._param_names}') | |
561 # print(f'params_names = {newmodel.param_names}') | |
562 # Remove already existing names | |
563 for name in newmodel.param_names.copy(): | |
564 if name not in expr_parameters: | |
565 newmodel._func_allargs.remove(name) | |
566 newmodel._param_names.remove(name) | |
567 # print(f'params_names = {newmodel._param_names}') | |
568 # print(f'params_names = {newmodel.param_names}') | |
569 else: | |
570 raise ValueError(f'Unknown build-in fit model ({model})') | |
571 else: | |
572 illegal_value(model, 'model', 'Fit.add_model', raise_error=True) | |
573 | |
574 # Add the new model to the current one | |
575 # print('\nBefore adding model:') | |
576 # print(f'\nnewmodel = {newmodel.__dict__}') | |
577 # if len(self._parameters): | |
578 # self._parameters.pretty_print() | |
579 if self._model is None: | |
580 self._model = newmodel | |
581 else: | |
582 self._model += newmodel | |
583 new_parameters = newmodel.make_params() | |
584 self._parameters += new_parameters | |
585 # print('\nAfter adding model:') | |
586 # print(f'\nnewmodel = {newmodel.__dict__}') | |
587 # print(f'\nnew_parameters = {new_parameters}') | |
588 # self._parameters.pretty_print() | |
589 | |
590 # Check linearity of expression model paremeters | |
591 if isinstance(newmodel, ExpressionModel): | |
592 for name in newmodel.param_names: | |
593 if not diff(newmodel.expr, name, name): | |
594 if name not in self._linear_parameters: | |
595 self._linear_parameters.append(name) | |
596 new_parameter_norms[name] = True | |
597 # print(f'\nADDING {name} TO LINEAR') | |
598 else: | |
599 if name not in self._nonlinear_parameters: | |
600 self._nonlinear_parameters.append(name) | |
601 new_parameter_norms[name] = False | |
602 # print(f'\nADDING {name} TO NONLINEAR') | |
603 # print(f'new_parameter_norms:\n{new_parameter_norms}') | |
604 | |
605 # Scale the default initial model parameters | |
606 if self._norm is not None: | |
607 for name, norm in new_parameter_norms.copy().items(): | |
608 par = self._parameters.get(name) | |
609 if par is None: | |
610 new_parameter_norms.pop(name) | |
611 continue | |
612 if par.expr is None and norm: | |
613 value = par.value*self._norm[1] | |
614 _min = par.min | |
615 _max = par.max | |
616 if not np.isinf(_min) and abs(_min) != float_min: | |
617 _min *= self._norm[1] | |
618 if not np.isinf(_max) and abs(_max) != float_min: | |
619 _max *= self._norm[1] | |
620 par.set(value=value, min=_min, max=_max) | |
621 # print('\nAfter norm defaults:') | |
622 # self._parameters.pretty_print() | |
623 # print(f'parameters:\n{parameters}') | |
624 # print(f'all_parameters:\n{list(self.parameters)}') | |
625 # print(f'new_parameter_norms:\n{new_parameter_norms}') | |
626 # print(f'parameter_norms:\n{self._parameter_norms}') | |
627 | |
628 # Initialize the model parameters from parameters | |
629 if prefix is None: | |
630 prefix = "" | |
631 if parameters is not None: | |
632 for parameter in parameters: | |
633 name = parameter['name'] | |
634 if not isinstance(name, str): | |
635 raise ValueError(f'Illegal "name" value ({name}) in input parameters') | |
636 if name not in new_parameters: | |
637 name = prefix+name | |
638 parameter['name'] = name | |
639 if name not in new_parameters: | |
640 logging.warning(f'Ignoring superfluous parameter info for {name}') | |
641 continue | |
642 if name in self._parameters: | |
643 parameter.pop('name') | |
644 if 'norm' in parameter: | |
645 if not isinstance(parameter['norm'], bool): | |
646 illegal_value(parameter['norm'], 'norm', 'Fit.add_model', | |
647 raise_error=True) | |
648 new_parameter_norms[name] = parameter['norm'] | |
649 parameter.pop('norm') | |
650 if parameter.get('expr') is not None: | |
651 if 'value' in parameter: | |
652 logging.warning(f'Ignoring value in parameter {name} '+ | |
653 f'(set by expression: {parameter["expr"]})') | |
654 parameter.pop('value') | |
655 if 'vary' in parameter: | |
656 logging.warning(f'Ignoring vary in parameter {name} '+ | |
657 f'(set by expression: {parameter["expr"]})') | |
658 parameter.pop('vary') | |
659 if 'min' in parameter: | |
660 logging.warning(f'Ignoring min in parameter {name} '+ | |
661 f'(set by expression: {parameter["expr"]})') | |
662 parameter.pop('min') | |
663 if 'max' in parameter: | |
664 logging.warning(f'Ignoring max in parameter {name} '+ | |
665 f'(set by expression: {parameter["expr"]})') | |
666 parameter.pop('max') | |
667 if 'vary' in parameter: | |
668 if not isinstance(parameter['vary'], bool): | |
669 illegal_value(parameter['vary'], 'vary', 'Fit.add_model', | |
670 raise_error=True) | |
671 if not parameter['vary']: | |
672 if 'min' in parameter: | |
673 logging.warning(f'Ignoring min in parameter {name} in '+ | |
674 f'Fit.add_model (vary = {parameter["vary"]})') | |
675 parameter.pop('min') | |
676 if 'max' in parameter: | |
677 logging.warning(f'Ignoring max in parameter {name} in '+ | |
678 f'Fit.add_model (vary = {parameter["vary"]})') | |
679 parameter.pop('max') | |
680 self._parameters[name].set(**parameter) | |
681 parameter['name'] = name | |
682 else: | |
683 illegal_value(parameter, 'parameter name', 'Fit.model', raise_error=True) | |
684 self._parameter_norms = {**self._parameter_norms, **new_parameter_norms} | |
685 # print('\nAfter parameter init:') | |
686 # self._parameters.pretty_print() | |
687 # print(f'parameters:\n{parameters}') | |
688 # print(f'new_parameter_norms:\n{new_parameter_norms}') | |
689 # print(f'parameter_norms:\n{self._parameter_norms}') | |
690 # print(f'kwargs:\n{kwargs}') | |
691 | |
692 # Initialize the model parameters from kwargs | |
693 for name, value in {**kwargs}.items(): | |
694 full_name = f'{pprefix}{name}' | |
695 if full_name in new_parameter_norms and isinstance(value, (int, float)): | |
696 kwargs.pop(name) | |
697 if self._parameters[full_name].expr is None: | |
698 self._parameters[full_name].set(value=value) | |
699 else: | |
700 logging.warning(f'Ignoring parameter {name} in Fit.fit (set by expression: '+ | |
701 f'{self._parameters[full_name].expr})') | |
702 # print('\nAfter kwargs init:') | |
703 # self._parameters.pretty_print() | |
704 # print(f'parameter_norms:\n{self._parameter_norms}') | |
705 # print(f'kwargs:\n{kwargs}') | |
706 | |
707 # Check parameter norms (also need it for expressions to renormalize the errors) | |
708 if self._norm is not None and (callable(model) or model == 'expression'): | |
709 missing_norm = False | |
710 for name in new_parameters.valuesdict(): | |
711 if name not in self._parameter_norms: | |
712 print(f'new_parameters:\n{new_parameters.valuesdict()}') | |
713 print(f'self._parameter_norms:\n{self._parameter_norms}') | |
714 logging.error(f'Missing parameter normalization type for {name} in {model}') | |
715 missing_norm = True | |
716 if missing_norm: | |
717 raise ValueError | |
718 | |
719 # print(f'at end add_model:\nself._parameters:\n{list(self.parameters)}') | |
720 # print(f'at end add_model: kwargs = {kwargs}') | |
721 # print(f'\nat end add_model: newmodel:\n{newmodel.__dict__}\n') | |
722 return(kwargs) | |
723 | |
724 def fit(self, interactive=False, guess=False, **kwargs): | |
725 # Check inputs | |
726 if self._model is None: | |
727 logging.error('Undefined fit model') | |
728 return | |
729 if not isinstance(interactive, bool): | |
730 illegal_value(interactive, 'interactive', 'Fit.fit', raise_error=True) | |
731 if not isinstance(guess, bool): | |
732 illegal_value(guess, 'guess', 'Fit.fit', raise_error=True) | |
733 if 'try_linear_fit' in kwargs: | |
734 try_linear_fit = kwargs.pop('try_linear_fit') | |
735 if not isinstance(try_linear_fit, bool): | |
736 illegal_value(try_linear_fit, 'try_linear_fit', 'Fit.fit', raise_error=True) | |
737 if not self._try_linear_fit: | |
738 logging.warning('Ignore superfluous keyword argument "try_linear_fit" (not '+ | |
739 'yet supported for callable models)') | |
740 else: | |
741 self._try_linear_fit = try_linear_fit | |
742 # if self._result is None: | |
743 # if 'parameters' in kwargs: | |
744 # raise ValueError('Invalid parameter parameters ({kwargs["parameters"]})') | |
745 # else: | |
746 if self._result is not None: | |
747 if guess: | |
748 logging.warning('Ignoring input parameter guess in Fit.fit during refitting') | |
749 guess = False | |
750 | |
751 # Check for circular expressions | |
752 # FIX TODO | |
753 # for name1, par1 in self._parameters.items(): | |
754 # if par1.expr is not None: | |
755 | |
756 # Apply mask if supplied: | |
757 if 'mask' in kwargs: | |
758 self._mask = kwargs.pop('mask') | |
759 if self._mask is not None: | |
760 self._mask = np.asarray(self._mask).astype(bool) | |
761 if self._x.size != self._mask.size: | |
762 raise ValueError(f'Inconsistent x and mask dimensions ({self._x.size} vs '+ | |
763 f'{self._mask.size})') | |
764 | |
765 # Estimate initial parameters with build-in lmfit guess method (only for a single model) | |
766 # print(f'\nat start fit: kwargs = {kwargs}') | |
767 #RV print('\nAt start of fit:') | |
768 #RV self._parameters.pretty_print() | |
769 # print(f'parameter_norms:\n{self._parameter_norms}') | |
770 if guess: | |
771 if self._mask is None: | |
772 self._parameters = self._model.guess(self._y, x=self._x) | |
773 else: | |
774 self._parameters = self._model.guess(np.asarray(self._y)[~self._mask], | |
775 x=self._x[~self._mask]) | |
776 # print('\nAfter guess:') | |
777 # self._parameters.pretty_print() | |
778 | |
779 # Add constant offset for a normalized model | |
780 if self._result is None and self._norm is not None and self._norm[0]: | |
781 self.add_model('constant', prefix='tmp_normalization_offset_', parameters={'name': 'c', | |
782 'value': -self._norm[0], 'vary': False, 'norm': True}) | |
783 #'value': -self._norm[0]/self._norm[1], 'vary': False, 'norm': False}) | |
784 | |
785 # Adjust existing parameters for refit: | |
786 if 'parameters' in kwargs: | |
787 parameters = kwargs.pop('parameters') | |
788 if isinstance(parameters, dict): | |
789 parameters = (parameters, ) | |
790 elif not is_dict_series(parameters): | |
791 illegal_value(parameters, 'parameters', 'Fit.fit', raise_error=True) | |
792 for par in parameters: | |
793 name = par['name'] | |
794 if name not in self._parameters: | |
795 raise ValueError(f'Unable to match {name} parameter {par} to an existing one') | |
796 if self._parameters[name].expr is not None: | |
797 raise ValueError(f'Unable to modify {name} parameter {par} (currently an '+ | |
798 'expression)') | |
799 if par.get('expr') is not None: | |
800 raise KeyError(f'Illegal "expr" key in {name} parameter {par}') | |
801 self._parameters[name].set(vary=par.get('vary')) | |
802 self._parameters[name].set(min=par.get('min')) | |
803 self._parameters[name].set(max=par.get('max')) | |
804 self._parameters[name].set(value=par.get('value')) | |
805 #RV print('\nAfter adjust:') | |
806 #RV self._parameters.pretty_print() | |
807 | |
808 # Apply parameter updates through keyword arguments | |
809 # print(f'kwargs = {kwargs}') | |
810 # print(f'parameter_norms = {self._parameter_norms}') | |
811 for name in set(self._parameters) & set(kwargs): | |
812 value = kwargs.pop(name) | |
813 if self._parameters[name].expr is None: | |
814 self._parameters[name].set(value=value) | |
815 else: | |
816 logging.warning(f'Ignoring parameter {name} in Fit.fit (set by expression: '+ | |
817 f'{self._parameters[name].expr})') | |
818 | |
819 # Check for uninitialized parameters | |
820 for name, par in self._parameters.items(): | |
821 if par.expr is None: | |
822 value = par.value | |
823 if value is None or np.isinf(value) or np.isnan(value): | |
824 if interactive: | |
825 value = input_num(f'Enter an initial value for {name}', default=1.0) | |
826 else: | |
827 value = 1.0 | |
828 if self._norm is None or name not in self._parameter_norms: | |
829 self._parameters[name].set(value=value) | |
830 elif self._parameter_norms[name]: | |
831 self._parameters[name].set(value=value*self._norm[1]) | |
832 | |
833 # Check if model is linear | |
834 try: | |
835 linear_model = self._check_linearity_model() | |
836 except: | |
837 linear_model = False | |
838 # print(f'\n\n--------> linear_model = {linear_model}\n') | |
839 if kwargs.get('check_only_linearity') is not None: | |
840 return(linear_model) | |
841 | |
842 # Normalize the data and initial parameters | |
843 #RV print('\nBefore normalization:') | |
844 #RV self._parameters.pretty_print() | |
845 # print(f'parameter_norms:\n{self._parameter_norms}') | |
846 self._normalize() | |
847 # print(f'norm = {self._norm}') | |
848 #RV print('\nAfter normalization:') | |
849 #RV self._parameters.pretty_print() | |
850 # self.print_fit_report() | |
851 # print(f'parameter_norms:\n{self._parameter_norms}') | |
852 | |
853 if linear_model: | |
854 # Perform a linear fit by direct matrix solution with numpy | |
855 try: | |
856 if self._mask is None: | |
857 self._fit_linear_model(self._x, self._y_norm) | |
858 else: | |
859 self._fit_linear_model(self._x[~self._mask], | |
860 np.asarray(self._y_norm)[~self._mask]) | |
861 except: | |
862 linear_model = False | |
863 if not linear_model: | |
864 # Perform a non-linear fit with lmfit | |
865 # Prevent initial values from sitting at boundaries | |
866 self._parameter_bounds = {name:{'min': par.min, 'max': par.max} for name, par in | |
867 self._parameters.items() if par.vary} | |
868 for par in self._parameters.values(): | |
869 if par.vary: | |
870 par.set(value=self._reset_par_at_boundary(par, par.value)) | |
871 # print('\nAfter checking boundaries:') | |
872 # self._parameters.pretty_print() | |
873 | |
874 # Perform the fit | |
875 # fit_kws = None | |
876 # if 'Dfun' in kwargs: | |
877 # fit_kws = {'Dfun': kwargs.pop('Dfun')} | |
878 # self._result = self._model.fit(self._y_norm, self._parameters, x=self._x, | |
879 # fit_kws=fit_kws, **kwargs) | |
880 if self._mask is None: | |
881 self._result = self._model.fit(self._y_norm, self._parameters, x=self._x, **kwargs) | |
882 else: | |
883 self._result = self._model.fit(np.asarray(self._y_norm)[~self._mask], | |
884 self._parameters, x=self._x[~self._mask], **kwargs) | |
885 #RV print('\nAfter fit:') | |
886 # print(f'\nself._result ({self._result}):\n\t{self._result.__dict__}') | |
887 #RV self._parameters.pretty_print() | |
888 # self.print_fit_report() | |
889 | |
890 # Set internal parameter values to fit results upon success | |
891 if self.success: | |
892 for name, par in self._parameters.items(): | |
893 if par.expr is None and par.vary: | |
894 par.set(value=self._result.params[name].value) | |
895 # print('\nAfter update parameter values:') | |
896 # self._parameters.pretty_print() | |
897 | |
898 # Renormalize the data and results | |
899 self._renormalize() | |
900 #RV print('\nAfter renormalization:') | |
901 #RV self._parameters.pretty_print() | |
902 # self.print_fit_report() | |
903 | |
904 def plot(self, y=None, y_title=None, result=None, skip_init=False, plot_comp_legends=False, | |
905 plot_residual=False, plot_masked_data=True, **kwargs): | |
906 if result is None: | |
907 result = self._result | |
908 if result is None: | |
909 return | |
910 plots = [] | |
911 legend = [] | |
912 if self._mask is None: | |
913 mask = np.zeros(self._x.size).astype(bool) | |
914 plot_masked_data = False | |
915 else: | |
916 mask = self._mask | |
917 if y is not None: | |
918 if not isinstance(y, (tuple, list, np.ndarray)): | |
919 illegal_value(y, 'y', 'Fit.plot') | |
920 if len(y) != len(self._x): | |
921 logging.warning('Ignoring parameter y in Fit.plot (wrong dimension)') | |
922 y = None | |
923 if y is not None: | |
924 if y_title is None or not isinstance(y_title, str): | |
925 y_title = 'data' | |
926 plots += [(self._x, y, '.')] | |
927 legend += [y_title] | |
928 if self._y is not None: | |
929 plots += [(self._x, np.asarray(self._y), 'b.')] | |
930 legend += ['data'] | |
931 if plot_masked_data: | |
932 plots += [(self._x[mask], np.asarray(self._y)[mask], 'bx')] | |
933 legend += ['masked data'] | |
934 if isinstance(plot_residual, bool) and plot_residual: | |
935 plots += [(self._x[~mask], result.residual, 'k-')] | |
936 legend += ['residual'] | |
937 plots += [(self._x[~mask], result.best_fit, 'k-')] | |
938 legend += ['best fit'] | |
939 if not skip_init and hasattr(result, 'init_fit'): | |
940 plots += [(self._x[~mask], result.init_fit, 'g-')] | |
941 legend += ['init'] | |
942 components = result.eval_components(x=self._x[~mask]) | |
943 num_components = len(components) | |
944 if 'tmp_normalization_offset_' in components: | |
945 num_components -= 1 | |
946 if num_components > 1: | |
947 eval_index = 0 | |
948 for modelname, y in components.items(): | |
949 if modelname == 'tmp_normalization_offset_': | |
950 continue | |
951 if modelname == '_eval': | |
952 modelname = f'eval{eval_index}' | |
953 if len(modelname) > 20: | |
954 modelname = f'{modelname[0:16]} ...' | |
955 if isinstance(y, (int, float)): | |
956 y *= np.ones(self._x[~mask].size) | |
957 plots += [(self._x[~mask], y, '--')] | |
958 if plot_comp_legends: | |
959 if modelname[-1] == '_': | |
960 legend.append(modelname[:-1]) | |
961 else: | |
962 legend.append(modelname) | |
963 title = kwargs.get('title') | |
964 if title is not None: | |
965 kwargs.pop('title') | |
966 quick_plot(tuple(plots), legend=legend, title=title, block=True, **kwargs) | |
967 | |
968 @staticmethod | |
969 def guess_init_peak(x, y, *args, center_guess=None, use_max_for_center=True): | |
970 """ Return a guess for the initial height, center and fwhm for a peak | |
971 """ | |
972 # print(f'\n\nargs = {args}') | |
973 # print(f'center_guess = {center_guess}') | |
974 # quick_plot(x, y, vlines=center_guess, block=True) | |
975 center_guesses = None | |
976 x = np.asarray(x) | |
977 y = np.asarray(y) | |
978 if len(x) != len(y): | |
979 logging.error(f'Invalid x and y lengths ({len(x)}, {len(y)}), skip initial guess') | |
980 return(None, None, None) | |
981 if isinstance(center_guess, (int, float)): | |
982 if len(args): | |
983 logging.warning('Ignoring additional arguments for single center_guess value') | |
984 center_guesses = [center_guess] | |
985 elif isinstance(center_guess, (tuple, list, np.ndarray)): | |
986 if len(center_guess) == 1: | |
987 logging.warning('Ignoring additional arguments for single center_guess value') | |
988 if not isinstance(center_guess[0], (int, float)): | |
989 raise ValueError(f'Invalid parameter center_guess ({type(center_guess[0])})') | |
990 center_guess = center_guess[0] | |
991 else: | |
992 if len(args) != 1: | |
993 raise ValueError(f'Invalid number of arguments ({len(args)})') | |
994 n = args[0] | |
995 if not is_index(n, 0, len(center_guess)): | |
996 raise ValueError('Invalid argument') | |
997 center_guesses = center_guess | |
998 center_guess = center_guesses[n] | |
999 elif center_guess is not None: | |
1000 raise ValueError(f'Invalid center_guess type ({type(center_guess)})') | |
1001 # print(f'x = {x}') | |
1002 # print(f'y = {y}') | |
1003 # print(f'center_guess = {center_guess}') | |
1004 | |
1005 # Sort the inputs | |
1006 index = np.argsort(x) | |
1007 x = x[index] | |
1008 y = y[index] | |
1009 miny = y.min() | |
1010 # print(f'miny = {miny}') | |
1011 # print(f'x_range = {x[0]} {x[-1]} {len(x)}') | |
1012 # print(f'y_range = {y[0]} {y[-1]} {len(y)}') | |
1013 # quick_plot(x, y, vlines=center_guess, block=True) | |
1014 | |
1015 # xx = x | |
1016 # yy = y | |
1017 # Set range for current peak | |
1018 # print(f'n = {n}') | |
1019 # print(f'center_guesses = {center_guesses}') | |
1020 if center_guesses is not None: | |
1021 if len(center_guesses) > 1: | |
1022 index = np.argsort(center_guesses) | |
1023 n = list(index).index(n) | |
1024 # print(f'n = {n}') | |
1025 # print(f'index = {index}') | |
1026 center_guesses = np.asarray(center_guesses)[index] | |
1027 # print(f'center_guesses = {center_guesses}') | |
1028 if n == 0: | |
1029 low = 0 | |
1030 upp = index_nearest(x, (center_guesses[0]+center_guesses[1])/2) | |
1031 elif n == len(center_guesses)-1: | |
1032 low = index_nearest(x, (center_guesses[n-1]+center_guesses[n])/2) | |
1033 upp = len(x) | |
1034 else: | |
1035 low = index_nearest(x, (center_guesses[n-1]+center_guesses[n])/2) | |
1036 upp = index_nearest(x, (center_guesses[n]+center_guesses[n+1])/2) | |
1037 # print(f'low = {low}') | |
1038 # print(f'upp = {upp}') | |
1039 x = x[low:upp] | |
1040 y = y[low:upp] | |
1041 # quick_plot(x, y, vlines=(x[0], center_guess, x[-1]), block=True) | |
1042 | |
1043 # Estimate FHHM | |
1044 maxy = y.max() | |
1045 # print(f'x_range = {x[0]} {x[-1]} {len(x)}') | |
1046 # print(f'y_range = {y[0]} {y[-1]} {len(y)} {miny} {maxy}') | |
1047 # print(f'center_guess = {center_guess}') | |
1048 if center_guess is None: | |
1049 center_index = np.argmax(y) | |
1050 center = x[center_index] | |
1051 height = maxy-miny | |
1052 else: | |
1053 if use_max_for_center: | |
1054 center_index = np.argmax(y) | |
1055 center = x[center_index] | |
1056 if center_index < 0.1*len(x) or center_index > 0.9*len(x): | |
1057 center_index = index_nearest(x, center_guess) | |
1058 center = center_guess | |
1059 else: | |
1060 center_index = index_nearest(x, center_guess) | |
1061 center = center_guess | |
1062 height = y[center_index]-miny | |
1063 # print(f'center_index = {center_index}') | |
1064 # print(f'center = {center}') | |
1065 # print(f'height = {height}') | |
1066 half_height = miny+0.5*height | |
1067 # print(f'half_height = {half_height}') | |
1068 fwhm_index1 = 0 | |
1069 for i in range(center_index, fwhm_index1, -1): | |
1070 if y[i] < half_height: | |
1071 fwhm_index1 = i | |
1072 break | |
1073 # print(f'fwhm_index1 = {fwhm_index1} {x[fwhm_index1]}') | |
1074 fwhm_index2 = len(x)-1 | |
1075 for i in range(center_index, fwhm_index2): | |
1076 if y[i] < half_height: | |
1077 fwhm_index2 = i | |
1078 break | |
1079 # print(f'fwhm_index2 = {fwhm_index2} {x[fwhm_index2]}') | |
1080 # quick_plot((x,y,'o'), vlines=(x[fwhm_index1], center, x[fwhm_index2]), block=True) | |
1081 if fwhm_index1 == 0 and fwhm_index2 < len(x)-1: | |
1082 fwhm = 2*(x[fwhm_index2]-center) | |
1083 elif fwhm_index1 > 0 and fwhm_index2 == len(x)-1: | |
1084 fwhm = 2*(center-x[fwhm_index1]) | |
1085 else: | |
1086 fwhm = x[fwhm_index2]-x[fwhm_index1] | |
1087 # print(f'fwhm_index1 = {fwhm_index1} {x[fwhm_index1]}') | |
1088 # print(f'fwhm_index2 = {fwhm_index2} {x[fwhm_index2]}') | |
1089 # print(f'fwhm = {fwhm}') | |
1090 | |
1091 # Return height, center and FWHM | |
1092 # quick_plot((x,y,'o'), (xx,yy), vlines=(x[fwhm_index1], center, x[fwhm_index2]), block=True) | |
1093 return(height, center, fwhm) | |
1094 | |
1095 def _check_linearity_model(self): | |
1096 """Identify the linearity of all model parameters and check if the model is linear or not | |
1097 """ | |
1098 if not self._try_linear_fit: | |
1099 logging.info('Skip linearity check (not yet supported for callable models)') | |
1100 return(False) | |
1101 free_parameters = [name for name, par in self._parameters.items() if par.vary] | |
1102 for component in self._model.components: | |
1103 if 'tmp_normalization_offset_c' in component.param_names: | |
1104 continue | |
1105 if isinstance(component, ExpressionModel): | |
1106 for name in free_parameters: | |
1107 if diff(component.expr, name, name): | |
1108 # print(f'\t\t{component.expr} is non-linear in {name}') | |
1109 self._nonlinear_parameters.append(name) | |
1110 if name in self._linear_parameters: | |
1111 self._linear_parameters.remove(name) | |
1112 else: | |
1113 model_parameters = component.param_names.copy() | |
1114 for basename, hint in component.param_hints.items(): | |
1115 name = f'{component.prefix}{basename}' | |
1116 if hint.get('expr') is not None: | |
1117 model_parameters.remove(name) | |
1118 for name in model_parameters: | |
1119 expr = self._parameters[name].expr | |
1120 if expr is not None: | |
1121 for nname in free_parameters: | |
1122 if name in self._nonlinear_parameters: | |
1123 if diff(expr, nname): | |
1124 # print(f'\t\t{component} is non-linear in {nname} (through {name} = "{expr}")') | |
1125 self._nonlinear_parameters.append(nname) | |
1126 if nname in self._linear_parameters: | |
1127 self._linear_parameters.remove(nname) | |
1128 else: | |
1129 assert(name in self._linear_parameters) | |
1130 # print(f'\n\nexpr ({type(expr)}) = {expr}\nnname ({type(nname)}) = {nname}\n\n') | |
1131 if diff(expr, nname, nname): | |
1132 # print(f'\t\t{component} is non-linear in {nname} (through {name} = "{expr}")') | |
1133 self._nonlinear_parameters.append(nname) | |
1134 if nname in self._linear_parameters: | |
1135 self._linear_parameters.remove(nname) | |
1136 # print(f'\nfree parameters:\n\t{free_parameters}') | |
1137 # print(f'linear parameters:\n\t{self._linear_parameters}') | |
1138 # print(f'nonlinear parameters:\n\t{self._nonlinear_parameters}\n') | |
1139 if any(True for name in self._nonlinear_parameters if self._parameters[name].vary): | |
1140 return(False) | |
1141 return(True) | |
1142 | |
1143 def _fit_linear_model(self, x, y): | |
1144 """Perform a linear fit by direct matrix solution with numpy | |
1145 """ | |
1146 # Construct the matrix and the free parameter vector | |
1147 # print(f'\nparameters:') | |
1148 # self._parameters.pretty_print() | |
1149 # print(f'\nparameter_norms:\n\t{self._parameter_norms}') | |
1150 # print(f'\nlinear_parameters:\n\t{self._linear_parameters}') | |
1151 # print(f'nonlinear_parameters:\n\t{self._nonlinear_parameters}') | |
1152 free_parameters = [name for name, par in self._parameters.items() if par.vary] | |
1153 # print(f'free parameters:\n\t{free_parameters}\n') | |
1154 expr_parameters = {name:par.expr for name, par in self._parameters.items() | |
1155 if par.expr is not None} | |
1156 model_parameters = [] | |
1157 for component in self._model.components: | |
1158 if 'tmp_normalization_offset_c' in component.param_names: | |
1159 continue | |
1160 model_parameters += component.param_names | |
1161 for basename, hint in component.param_hints.items(): | |
1162 name = f'{component.prefix}{basename}' | |
1163 if hint.get('expr') is not None: | |
1164 expr_parameters.pop(name) | |
1165 model_parameters.remove(name) | |
1166 # print(f'expr parameters:\n{expr_parameters}') | |
1167 # print(f'model parameters:\n\t{model_parameters}\n') | |
1168 norm = 1.0 | |
1169 if self._normalized: | |
1170 norm = self._norm[1] | |
1171 # print(f'\n\nself._normalized = {self._normalized}\nnorm = {norm}\nself._norm = {self._norm}\n') | |
1172 # Add expression parameters to asteval | |
1173 ast = Interpreter() | |
1174 # print(f'Adding to asteval sym table:') | |
1175 for name, expr in expr_parameters.items(): | |
1176 # print(f'\tadding {name} {expr}') | |
1177 ast.symtable[name] = expr | |
1178 # Add constant parameters to asteval | |
1179 # (renormalize to use correctly in evaluation of expression models) | |
1180 for name, par in self._parameters.items(): | |
1181 if par.expr is None and not par.vary: | |
1182 if self._parameter_norms[name]: | |
1183 # print(f'\tadding {name} {par.value*norm}') | |
1184 ast.symtable[name] = par.value*norm | |
1185 else: | |
1186 # print(f'\tadding {name} {par.value}') | |
1187 ast.symtable[name] = par.value | |
1188 A = np.zeros((len(x), len(free_parameters)), dtype='float64') | |
1189 y_const = np.zeros(len(x), dtype='float64') | |
1190 have_expression_model = False | |
1191 for component in self._model.components: | |
1192 if isinstance(component, ConstantModel): | |
1193 name = component.param_names[0] | |
1194 # print(f'\nConstant model: {name} {self._parameters[name]}\n') | |
1195 if name in free_parameters: | |
1196 # print(f'\t\t{name} is a free constant set matrix column {free_parameters.index(name)} to 1.0') | |
1197 A[:,free_parameters.index(name)] = 1.0 | |
1198 else: | |
1199 if self._parameter_norms[name]: | |
1200 delta_y_const = self._parameters[name]*np.ones(len(x)) | |
1201 else: | |
1202 delta_y_const = (self._parameters[name]*norm)*np.ones(len(x)) | |
1203 y_const += delta_y_const | |
1204 # print(f'\ndelta_y_const ({type(delta_y_const)}):\n{delta_y_const}\n') | |
1205 elif isinstance(component, ExpressionModel): | |
1206 have_expression_model = True | |
1207 const_expr = component.expr | |
1208 # print(f'\nExpression model:\nconst_expr: {const_expr}\n') | |
1209 for name in free_parameters: | |
1210 dexpr_dname = diff(component.expr, name) | |
1211 if dexpr_dname: | |
1212 const_expr = f'{const_expr}-({str(dexpr_dname)})*{name}' | |
1213 # print(f'\tconst_expr: {const_expr}') | |
1214 if not self._parameter_norms[name]: | |
1215 dexpr_dname = f'({dexpr_dname})/{norm}' | |
1216 # print(f'\t{component.expr} is linear in {name}\n\t\tadd "{str(dexpr_dname)}" to matrix as column {free_parameters.index(name)}') | |
1217 fx = [(lambda _: ast.eval(str(dexpr_dname)))(ast(f'x={v}')) for v in x] | |
1218 # print(f'\tfx:\n{fx}') | |
1219 if len(ast.error): | |
1220 raise ValueError(f'Unable to evaluate {dexpr_dname}') | |
1221 A[:,free_parameters.index(name)] += fx | |
1222 # if self._parameter_norms[name]: | |
1223 # print(f'\t\t{component.expr} is linear in {name} add "{str(dexpr_dname)}" to matrix as column {free_parameters.index(name)}') | |
1224 # A[:,free_parameters.index(name)] += fx | |
1225 # else: | |
1226 # print(f'\t\t{component.expr} is linear in {name} add "({str(dexpr_dname)})/{norm}" to matrix as column {free_parameters.index(name)}') | |
1227 # A[:,free_parameters.index(name)] += np.asarray(fx)/norm | |
1228 # FIX: find another solution if expr not supported by simplify | |
1229 const_expr = str(simplify(f'({const_expr})/{norm}')) | |
1230 # print(f'\nconst_expr: {const_expr}') | |
1231 delta_y_const = [(lambda _: ast.eval(const_expr))(ast(f'x = {v}')) for v in x] | |
1232 y_const += delta_y_const | |
1233 # print(f'\ndelta_y_const ({type(delta_y_const)}):\n{delta_y_const}\n') | |
1234 if len(ast.error): | |
1235 raise ValueError(f'Unable to evaluate {const_expr}') | |
1236 else: | |
1237 free_model_parameters = [name for name in component.param_names | |
1238 if name in free_parameters or name in expr_parameters] | |
1239 # print(f'\nBuild-in model ({component}):\nfree_model_parameters: {free_model_parameters}\n') | |
1240 if not len(free_model_parameters): | |
1241 y_const += component.eval(params=self._parameters, x=x) | |
1242 elif isinstance(component, LinearModel): | |
1243 if f'{component.prefix}slope' in free_model_parameters: | |
1244 A[:,free_parameters.index(f'{component.prefix}slope')] = x | |
1245 else: | |
1246 y_const += self._parameters[f'{component.prefix}slope'].value*x | |
1247 if f'{component.prefix}intercept' in free_model_parameters: | |
1248 A[:,free_parameters.index(f'{component.prefix}intercept')] = 1.0 | |
1249 else: | |
1250 y_const += self._parameters[f'{component.prefix}intercept'].value* \ | |
1251 np.ones(len(x)) | |
1252 elif isinstance(component, QuadraticModel): | |
1253 if f'{component.prefix}a' in free_model_parameters: | |
1254 A[:,free_parameters.index(f'{component.prefix}a')] = x**2 | |
1255 else: | |
1256 y_const += self._parameters[f'{component.prefix}a'].value*x**2 | |
1257 if f'{component.prefix}b' in free_model_parameters: | |
1258 A[:,free_parameters.index(f'{component.prefix}b')] = x | |
1259 else: | |
1260 y_const += self._parameters[f'{component.prefix}b'].value*x | |
1261 if f'{component.prefix}c' in free_model_parameters: | |
1262 A[:,free_parameters.index(f'{component.prefix}c')] = 1.0 | |
1263 else: | |
1264 y_const += self._parameters[f'{component.prefix}c'].value*np.ones(len(x)) | |
1265 else: | |
1266 # At this point each build-in model must be strictly proportional to each linear | |
1267 # model parameter. Without this assumption, the model equation is needed | |
1268 # For the current build-in lmfit models, this can only ever be the amplitude | |
1269 assert(len(free_model_parameters) == 1) | |
1270 name = f'{component.prefix}amplitude' | |
1271 assert(free_model_parameters[0] == name) | |
1272 assert(self._parameter_norms[name]) | |
1273 expr = self._parameters[name].expr | |
1274 if expr is None: | |
1275 # print(f'\t{component} is linear in {name} add to matrix as column {free_parameters.index(name)}') | |
1276 parameters = deepcopy(self._parameters) | |
1277 parameters[name].set(value=1.0) | |
1278 index = free_parameters.index(name) | |
1279 A[:,free_parameters.index(name)] += component.eval(params=parameters, x=x) | |
1280 else: | |
1281 const_expr = expr | |
1282 # print(f'\tconst_expr: {const_expr}') | |
1283 parameters = deepcopy(self._parameters) | |
1284 parameters[name].set(value=1.0) | |
1285 dcomp_dname = component.eval(params=parameters, x=x) | |
1286 # print(f'\tdcomp_dname ({type(dcomp_dname)}):\n{dcomp_dname}') | |
1287 for nname in free_parameters: | |
1288 dexpr_dnname = diff(expr, nname) | |
1289 if dexpr_dnname: | |
1290 assert(self._parameter_norms[name]) | |
1291 # print(f'\t\td({expr})/d{nname} = {dexpr_dnname}') | |
1292 # print(f'\t\t{component} is linear in {nname} (through {name} = "{expr}", add to matrix as column {free_parameters.index(nname)})') | |
1293 fx = np.asarray(dexpr_dnname*dcomp_dname, dtype='float64') | |
1294 # print(f'\t\tfx ({type(fx)}): {fx}') | |
1295 # print(f'free_parameters.index({nname}): {free_parameters.index(nname)}') | |
1296 if self._parameter_norms[nname]: | |
1297 A[:,free_parameters.index(nname)] += fx | |
1298 else: | |
1299 A[:,free_parameters.index(nname)] += fx/norm | |
1300 const_expr = f'{const_expr}-({dexpr_dnname})*{nname}' | |
1301 # print(f'\t\tconst_expr: {const_expr}') | |
1302 const_expr = str(simplify(f'({const_expr})/{norm}')) | |
1303 # print(f'\tconst_expr: {const_expr}') | |
1304 fx = [(lambda _: ast.eval(const_expr))(ast(f'x = {v}')) for v in x] | |
1305 # print(f'\tfx: {fx}') | |
1306 delta_y_const = np.multiply(fx, dcomp_dname) | |
1307 y_const += delta_y_const | |
1308 # print(f'\ndelta_y_const ({type(delta_y_const)}):\n{delta_y_const}\n') | |
1309 # print(A) | |
1310 # print(y_const) | |
1311 solution, residual, rank, s = np.linalg.lstsq(A, y-y_const, rcond=None) | |
1312 # print(f'\nsolution ({type(solution)} {solution.shape}):\n\t{solution}') | |
1313 # print(f'\nresidual ({type(residual)} {residual.shape}):\n\t{residual}') | |
1314 # print(f'\nrank ({type(rank)} {rank.shape}):\n\t{rank}') | |
1315 # print(f'\ns ({type(s)} {s.shape}):\n\t{s}\n') | |
1316 | |
1317 # Assemble result (compensate for normalization in expression models) | |
1318 for name, value in zip(free_parameters, solution): | |
1319 self._parameters[name].set(value=value) | |
1320 if self._normalized and (have_expression_model or len(expr_parameters)): | |
1321 for name, norm in self._parameter_norms.items(): | |
1322 par = self._parameters[name] | |
1323 if par.expr is None and norm: | |
1324 self._parameters[name].set(value=par.value*self._norm[1]) | |
1325 # self._parameters.pretty_print() | |
1326 # print(f'\nself._parameter_norms:\n\t{self._parameter_norms}') | |
1327 self._result = ModelResult(self._model, deepcopy(self._parameters)) | |
1328 self._result.best_fit = self._model.eval(params=self._parameters, x=x) | |
1329 if self._normalized and (have_expression_model or len(expr_parameters)): | |
1330 if 'tmp_normalization_offset_c' in self._parameters: | |
1331 offset = self._parameters['tmp_normalization_offset_c'] | |
1332 else: | |
1333 offset = 0.0 | |
1334 self._result.best_fit = (self._result.best_fit-offset-self._norm[0])/self._norm[1] | |
1335 if self._normalized: | |
1336 for name, norm in self._parameter_norms.items(): | |
1337 par = self._parameters[name] | |
1338 if par.expr is None and norm: | |
1339 value = par.value/self._norm[1] | |
1340 self._parameters[name].set(value=value) | |
1341 self._result.params[name].set(value=value) | |
1342 # self._parameters.pretty_print() | |
1343 self._result.residual = self._result.best_fit-y | |
1344 self._result.components = self._model.components | |
1345 self._result.init_params = None | |
1346 # quick_plot((x, y, '.'), (x, y_const, 'g'), (x, self._result.best_fit, 'k'), (x, self._result.residual, 'r'), block=True) | |
1347 | |
1348 def _normalize(self): | |
1349 """Normalize the data and initial parameters | |
1350 """ | |
1351 if self._normalized: | |
1352 return | |
1353 if self._norm is None: | |
1354 if self._y is not None and self._y_norm is None: | |
1355 self._y_norm = np.asarray(self._y) | |
1356 else: | |
1357 if self._y is not None and self._y_norm is None: | |
1358 self._y_norm = (np.asarray(self._y)-self._norm[0])/self._norm[1] | |
1359 self._y_range = 1.0 | |
1360 for name, norm in self._parameter_norms.items(): | |
1361 par = self._parameters[name] | |
1362 if par.expr is None and norm: | |
1363 value = par.value/self._norm[1] | |
1364 _min = par.min | |
1365 _max = par.max | |
1366 if not np.isinf(_min) and abs(_min) != float_min: | |
1367 _min /= self._norm[1] | |
1368 if not np.isinf(_max) and abs(_max) != float_min: | |
1369 _max /= self._norm[1] | |
1370 par.set(value=value, min=_min, max=_max) | |
1371 self._normalized = True | |
1372 | |
1373 def _renormalize(self): | |
1374 """Renormalize the data and results | |
1375 """ | |
1376 if self._norm is None or not self._normalized: | |
1377 return | |
1378 self._normalized = False | |
1379 for name, norm in self._parameter_norms.items(): | |
1380 par = self._parameters[name] | |
1381 if par.expr is None and norm: | |
1382 value = par.value*self._norm[1] | |
1383 _min = par.min | |
1384 _max = par.max | |
1385 if not np.isinf(_min) and abs(_min) != float_min: | |
1386 _min *= self._norm[1] | |
1387 if not np.isinf(_max) and abs(_max) != float_min: | |
1388 _max *= self._norm[1] | |
1389 par.set(value=value, min=_min, max=_max) | |
1390 if self._result is None: | |
1391 return | |
1392 self._result.best_fit = self._result.best_fit*self._norm[1]+self._norm[0] | |
1393 for name, par in self._result.params.items(): | |
1394 if self._parameter_norms.get(name, False): | |
1395 if par.stderr is not None: | |
1396 par.stderr *= self._norm[1] | |
1397 if par.expr is None: | |
1398 _min = par.min | |
1399 _max = par.max | |
1400 value = par.value*self._norm[1] | |
1401 if par.init_value is not None: | |
1402 par.init_value *= self._norm[1] | |
1403 if not np.isinf(_min) and abs(_min) != float_min: | |
1404 _min *= self._norm[1] | |
1405 if not np.isinf(_max) and abs(_max) != float_min: | |
1406 _max *= self._norm[1] | |
1407 par.set(value=value, min=_min, max=_max) | |
1408 if hasattr(self._result, 'init_fit'): | |
1409 self._result.init_fit = self._result.init_fit*self._norm[1]+self._norm[0] | |
1410 if hasattr(self._result, 'init_values'): | |
1411 init_values = {} | |
1412 for name, value in self._result.init_values.items(): | |
1413 if name not in self._parameter_norms or self._parameters[name].expr is not None: | |
1414 init_values[name] = value | |
1415 elif self._parameter_norms[name]: | |
1416 init_values[name] = value*self._norm[1] | |
1417 self._result.init_values = init_values | |
1418 for name, par in self._result.init_params.items(): | |
1419 if par.expr is None and self._parameter_norms.get(name, False): | |
1420 value = par.value | |
1421 _min = par.min | |
1422 _max = par.max | |
1423 value *= self._norm[1] | |
1424 if not np.isinf(_min) and abs(_min) != float_min: | |
1425 _min *= self._norm[1] | |
1426 if not np.isinf(_max) and abs(_max) != float_min: | |
1427 _max *= self._norm[1] | |
1428 par.set(value=value, min=_min, max=_max) | |
1429 par.init_value = par.value | |
1430 # Don't renormalize chisqr, it has no useful meaning in physical units | |
1431 #self._result.chisqr *= self._norm[1]*self._norm[1] | |
1432 if self._result.covar is not None: | |
1433 for i, name in enumerate(self._result.var_names): | |
1434 if self._parameter_norms.get(name, False): | |
1435 for j in range(len(self._result.var_names)): | |
1436 if self._result.covar[i,j] is not None: | |
1437 self._result.covar[i,j] *= self._norm[1] | |
1438 if self._result.covar[j,i] is not None: | |
1439 self._result.covar[j,i] *= self._norm[1] | |
1440 # Don't renormalize redchi, it has no useful meaning in physical units | |
1441 #self._result.redchi *= self._norm[1]*self._norm[1] | |
1442 if self._result.residual is not None: | |
1443 self._result.residual *= self._norm[1] | |
1444 | |
1445 def _reset_par_at_boundary(self, par, value): | |
1446 assert(par.vary) | |
1447 name = par.name | |
1448 _min = self._parameter_bounds[name]['min'] | |
1449 _max = self._parameter_bounds[name]['max'] | |
1450 if np.isinf(_min): | |
1451 if not np.isinf(_max): | |
1452 if self._parameter_norms.get(name, False): | |
1453 upp = _max-0.1*self._y_range | |
1454 elif _max == 0.0: | |
1455 upp = _max-0.1 | |
1456 else: | |
1457 upp = _max-0.1*abs(_max) | |
1458 if value >= upp: | |
1459 return(upp) | |
1460 else: | |
1461 if np.isinf(_max): | |
1462 if self._parameter_norms.get(name, False): | |
1463 low = _min+0.1*self._y_range | |
1464 elif _min == 0.0: | |
1465 low = _min+0.1 | |
1466 else: | |
1467 low = _min+0.1*abs(_min) | |
1468 if value <= low: | |
1469 return(low) | |
1470 else: | |
1471 low = 0.9*_min+0.1*_max | |
1472 upp = 0.1*_min+0.9*_max | |
1473 if value <= low: | |
1474 return(low) | |
1475 elif value >= upp: | |
1476 return(upp) | |
1477 return(value) | |
1478 | |
1479 | |
1480 class FitMultipeak(Fit): | |
1481 """Fit data with multiple peaks | |
1482 """ | |
1483 def __init__(self, y, x=None, normalize=True): | |
1484 super().__init__(y, x=x, normalize=normalize) | |
1485 self._fwhm_max = None | |
1486 self._sigma_max = None | |
1487 | |
1488 @classmethod | |
1489 def fit_multipeak(cls, y, centers, x=None, normalize=True, peak_models='gaussian', | |
1490 center_exprs=None, fit_type=None, background_order=None, background_exp=False, | |
1491 fwhm_max=None, plot_components=False): | |
1492 """Make sure that centers and fwhm_max are in the correct units and consistent with expr | |
1493 for a uniform fit (fit_type == 'uniform') | |
1494 """ | |
1495 fit = cls(y, x=x, normalize=normalize) | |
1496 success = fit.fit(centers, fit_type=fit_type, peak_models=peak_models, fwhm_max=fwhm_max, | |
1497 center_exprs=center_exprs, background_order=background_order, | |
1498 background_exp=background_exp, plot_components=plot_components) | |
1499 if success: | |
1500 return(fit.best_fit, fit.residual, fit.best_values, fit.best_errors, fit.redchi, \ | |
1501 fit.success) | |
1502 else: | |
1503 return(np.array([]), np.array([]), {}, {}, float_max, False) | |
1504 | |
1505 def fit(self, centers, fit_type=None, peak_models=None, center_exprs=None, fwhm_max=None, | |
1506 background_order=None, background_exp=False, plot_components=False, | |
1507 param_constraint=False): | |
1508 self._fwhm_max = fwhm_max | |
1509 # Create the multipeak model | |
1510 self._create_model(centers, fit_type, peak_models, center_exprs, background_order, | |
1511 background_exp, param_constraint) | |
1512 | |
1513 # RV: Obsolete Normalize the data and results | |
1514 # print('\nBefore fit before normalization in FitMultipeak:') | |
1515 # self._parameters.pretty_print() | |
1516 # self._normalize() | |
1517 # print('\nBefore fit after normalization in FitMultipeak:') | |
1518 # self._parameters.pretty_print() | |
1519 | |
1520 # Perform the fit | |
1521 try: | |
1522 if param_constraint: | |
1523 super().fit(fit_kws={'xtol': 1.e-5, 'ftol': 1.e-5, 'gtol': 1.e-5}) | |
1524 else: | |
1525 super().fit() | |
1526 except: | |
1527 return(False) | |
1528 | |
1529 # Check for valid fit parameter results | |
1530 fit_failure = self._check_validity() | |
1531 success = True | |
1532 if fit_failure: | |
1533 if param_constraint: | |
1534 logging.warning(' -> Should not happen with param_constraint set, fail the fit') | |
1535 success = False | |
1536 else: | |
1537 logging.info(' -> Retry fitting with constraints') | |
1538 self.fit(centers, fit_type, peak_models, center_exprs, fwhm_max=fwhm_max, | |
1539 background_order=background_order, background_exp=background_exp, | |
1540 plot_components=plot_components, param_constraint=True) | |
1541 else: | |
1542 # RV: Obsolete Renormalize the data and results | |
1543 # print('\nAfter fit before renormalization in FitMultipeak:') | |
1544 # self._parameters.pretty_print() | |
1545 # self.print_fit_report() | |
1546 # self._renormalize() | |
1547 # print('\nAfter fit after renormalization in FitMultipeak:') | |
1548 # self._parameters.pretty_print() | |
1549 # self.print_fit_report() | |
1550 | |
1551 # Print report and plot components if requested | |
1552 if plot_components: | |
1553 self.print_fit_report() | |
1554 self.plot() | |
1555 | |
1556 return(success) | |
1557 | |
1558 def _create_model(self, centers, fit_type=None, peak_models=None, center_exprs=None, | |
1559 background_order=None, background_exp=False, param_constraint=False): | |
1560 """Create the multipeak model | |
1561 """ | |
1562 if isinstance(centers, (int, float)): | |
1563 centers = [centers] | |
1564 num_peaks = len(centers) | |
1565 if peak_models is None: | |
1566 peak_models = num_peaks*['gaussian'] | |
1567 elif isinstance(peak_models, str): | |
1568 peak_models = num_peaks*[peak_models] | |
1569 if len(peak_models) != num_peaks: | |
1570 raise ValueError(f'Inconsistent number of peaks in peak_models ({len(peak_models)} vs '+ | |
1571 f'{num_peaks})') | |
1572 if num_peaks == 1: | |
1573 if fit_type is not None: | |
1574 logging.debug('Ignoring fit_type input for fitting one peak') | |
1575 fit_type = None | |
1576 if center_exprs is not None: | |
1577 logging.debug('Ignoring center_exprs input for fitting one peak') | |
1578 center_exprs = None | |
1579 else: | |
1580 if fit_type == 'uniform': | |
1581 if center_exprs is None: | |
1582 center_exprs = [f'scale_factor*{cen}' for cen in centers] | |
1583 if len(center_exprs) != num_peaks: | |
1584 raise ValueError(f'Inconsistent number of peaks in center_exprs '+ | |
1585 f'({len(center_exprs)} vs {num_peaks})') | |
1586 elif fit_type == 'unconstrained' or fit_type is None: | |
1587 if center_exprs is not None: | |
1588 logging.warning('Ignoring center_exprs input for unconstrained fit') | |
1589 center_exprs = None | |
1590 else: | |
1591 raise ValueError(f'Invalid fit_type in fit_multigaussian {fit_type}') | |
1592 self._sigma_max = None | |
1593 if param_constraint: | |
1594 min_value = float_min | |
1595 if self._fwhm_max is not None: | |
1596 self._sigma_max = np.zeros(num_peaks) | |
1597 else: | |
1598 min_value = None | |
1599 | |
1600 # Reset the fit | |
1601 self._model = None | |
1602 self._parameters = Parameters() | |
1603 self._result = None | |
1604 | |
1605 # Add background model | |
1606 if background_order is not None: | |
1607 if background_order == 0: | |
1608 self.add_model('constant', prefix='background', parameters= | |
1609 {'name': 'c', 'value': float_min, 'min': min_value}) | |
1610 elif background_order == 1: | |
1611 self.add_model('linear', prefix='background', slope=0.0, intercept=0.0) | |
1612 elif background_order == 2: | |
1613 self.add_model('quadratic', prefix='background', a=0.0, b=0.0, c=0.0) | |
1614 else: | |
1615 raise ValueError(f'Invalid parameter background_order ({background_order})') | |
1616 if background_exp: | |
1617 self.add_model('exponential', prefix='background', parameters=( | |
1618 {'name': 'amplitude', 'value': float_min, 'min': min_value}, | |
1619 {'name': 'decay', 'value': float_min, 'min': min_value})) | |
1620 | |
1621 # Add peaks and guess initial fit parameters | |
1622 ast = Interpreter() | |
1623 if num_peaks == 1: | |
1624 height_init, cen_init, fwhm_init = self.guess_init_peak(self._x, self._y) | |
1625 if self._fwhm_max is not None and fwhm_init > self._fwhm_max: | |
1626 fwhm_init = self._fwhm_max | |
1627 ast(f'fwhm = {fwhm_init}') | |
1628 ast(f'height = {height_init}') | |
1629 sig_init = ast(fwhm_factor[peak_models[0]]) | |
1630 amp_init = ast(height_factor[peak_models[0]]) | |
1631 sig_max = None | |
1632 if self._sigma_max is not None: | |
1633 ast(f'fwhm = {self._fwhm_max}') | |
1634 sig_max = ast(fwhm_factor[peak_models[0]]) | |
1635 self._sigma_max[0] = sig_max | |
1636 self.add_model(peak_models[0], parameters=( | |
1637 {'name': 'amplitude', 'value': amp_init, 'min': min_value}, | |
1638 {'name': 'center', 'value': cen_init, 'min': min_value}, | |
1639 {'name': 'sigma', 'value': sig_init, 'min': min_value, 'max': sig_max})) | |
1640 else: | |
1641 if fit_type == 'uniform': | |
1642 self.add_parameter(name='scale_factor', value=1.0) | |
1643 for i in range(num_peaks): | |
1644 height_init, cen_init, fwhm_init = self.guess_init_peak(self._x, self._y, i, | |
1645 center_guess=centers) | |
1646 if self._fwhm_max is not None and fwhm_init > self._fwhm_max: | |
1647 fwhm_init = self._fwhm_max | |
1648 ast(f'fwhm = {fwhm_init}') | |
1649 ast(f'height = {height_init}') | |
1650 sig_init = ast(fwhm_factor[peak_models[i]]) | |
1651 amp_init = ast(height_factor[peak_models[i]]) | |
1652 sig_max = None | |
1653 if self._sigma_max is not None: | |
1654 ast(f'fwhm = {self._fwhm_max}') | |
1655 sig_max = ast(fwhm_factor[peak_models[i]]) | |
1656 self._sigma_max[i] = sig_max | |
1657 if fit_type == 'uniform': | |
1658 self.add_model(peak_models[i], prefix=f'peak{i+1}_', parameters=( | |
1659 {'name': 'amplitude', 'value': amp_init, 'min': min_value}, | |
1660 {'name': 'center', 'expr': center_exprs[i]}, | |
1661 {'name': 'sigma', 'value': sig_init, 'min': min_value, | |
1662 'max': sig_max})) | |
1663 else: | |
1664 self.add_model('gaussian', prefix=f'peak{i+1}_', parameters=( | |
1665 {'name': 'amplitude', 'value': amp_init, 'min': min_value}, | |
1666 {'name': 'center', 'value': cen_init, 'min': min_value}, | |
1667 {'name': 'sigma', 'value': sig_init, 'min': min_value, | |
1668 'max': sig_max})) | |
1669 | |
1670 def _check_validity(self): | |
1671 """Check for valid fit parameter results | |
1672 """ | |
1673 fit_failure = False | |
1674 index = compile(r'\d+') | |
1675 for name, par in self.best_parameters.items(): | |
1676 if 'background' in name: | |
1677 # if ((name == 'backgroundc' and par['value'] <= 0.0) or | |
1678 # (name.endswith('amplitude') and par['value'] <= 0.0) or | |
1679 if ((name.endswith('amplitude') and par['value'] <= 0.0) or | |
1680 (name.endswith('decay') and par['value'] <= 0.0)): | |
1681 logging.info(f'Invalid fit result for {name} ({par["value"]})') | |
1682 fit_failure = True | |
1683 elif (((name.endswith('amplitude') or name.endswith('height')) and | |
1684 par['value'] <= 0.0) or | |
1685 ((name.endswith('sigma') or name.endswith('fwhm')) and par['value'] <= 0.0) or | |
1686 (name.endswith('center') and par['value'] <= 0.0) or | |
1687 (name == 'scale_factor' and par['value'] <= 0.0)): | |
1688 logging.info(f'Invalid fit result for {name} ({par["value"]})') | |
1689 fit_failure = True | |
1690 if name.endswith('sigma') and self._sigma_max is not None: | |
1691 if name == 'sigma': | |
1692 sigma_max = self._sigma_max[0] | |
1693 else: | |
1694 sigma_max = self._sigma_max[int(index.search(name).group())-1] | |
1695 if par['value'] > sigma_max: | |
1696 logging.info(f'Invalid fit result for {name} ({par["value"]})') | |
1697 fit_failure = True | |
1698 elif par['value'] == sigma_max: | |
1699 logging.warning(f'Edge result on for {name} ({par["value"]})') | |
1700 if name.endswith('fwhm') and self._fwhm_max is not None: | |
1701 if par['value'] > self._fwhm_max: | |
1702 logging.info(f'Invalid fit result for {name} ({par["value"]})') | |
1703 fit_failure = True | |
1704 elif par['value'] == self._fwhm_max: | |
1705 logging.warning(f'Edge result on for {name} ({par["value"]})') | |
1706 return(fit_failure) | |
1707 | |
1708 | |
1709 class FitMap(Fit): | |
1710 """Fit a map of data | |
1711 """ | |
1712 def __init__(self, ymap, x=None, models=None, normalize=True, transpose=None, **kwargs): | |
1713 super().__init__(None) | |
1714 self._best_errors = None | |
1715 self._best_fit = None | |
1716 self._best_parameters = None | |
1717 self._best_values = None | |
1718 self._inv_transpose = None | |
1719 self._max_nfev = None | |
1720 self._memfolder = None | |
1721 self._new_parameters = None | |
1722 self._out_of_bounds = None | |
1723 self._plot = False | |
1724 self._print_report = False | |
1725 self._redchi = None | |
1726 self._redchi_cutoff = 0.1 | |
1727 self._skip_init = True | |
1728 self._success = None | |
1729 self._transpose = None | |
1730 self._try_no_bounds = True | |
1731 | |
1732 # At this point the fastest index should always be the signal dimension so that the slowest | |
1733 # ndim-1 dimensions are the map dimensions | |
1734 if isinstance(ymap, (tuple, list, np.ndarray)): | |
1735 self._x = np.asarray(x) | |
1736 elif have_xarray and isinstance(ymap, xr.DataArray): | |
1737 if x is not None: | |
1738 logging.warning('Ignoring superfluous input x ({x}) in Fit.__init__') | |
1739 self._x = np.asarray(ymap[ymap.dims[-1]]) | |
1740 else: | |
1741 illegal_value(ymap, 'ymap', 'FitMap:__init__', raise_error=True) | |
1742 self._ymap = ymap | |
1743 | |
1744 # Verify the input parameters | |
1745 if self._x.ndim != 1: | |
1746 raise ValueError(f'Invalid dimension for input x {self._x.ndim}') | |
1747 if self._ymap.ndim < 2: | |
1748 raise ValueError('Invalid number of dimension of the input dataset '+ | |
1749 f'{self._ymap.ndim}') | |
1750 if self._x.size != self._ymap.shape[-1]: | |
1751 raise ValueError(f'Inconsistent x and y dimensions ({self._x.size} vs '+ | |
1752 f'{self._ymap.shape[-1]})') | |
1753 if not isinstance(normalize, bool): | |
1754 logging.warning(f'Invalid value for normalize ({normalize}) in Fit.__init__: '+ | |
1755 'setting normalize to True') | |
1756 normalize = True | |
1757 if isinstance(transpose, bool) and not transpose: | |
1758 transpose = None | |
1759 if transpose is not None and self._ymap.ndim < 3: | |
1760 logging.warning(f'Transpose meaningless for {self._ymap.ndim-1}D data maps: ignoring '+ | |
1761 'transpose') | |
1762 if transpose is not None: | |
1763 if self._ymap.ndim == 3 and isinstance(transpose, bool) and transpose: | |
1764 self._transpose = (1, 0) | |
1765 elif not isinstance(transpose, (tuple, list)): | |
1766 logging.warning(f'Invalid data type for transpose ({transpose}, '+ | |
1767 f'{type(transpose)}) in Fit.__init__: setting transpose to False') | |
1768 elif len(transpose) != self._ymap.ndim-1: | |
1769 logging.warning(f'Invalid dimension for transpose ({transpose}, must be equal to '+ | |
1770 f'{self._ymap.ndim-1}) in Fit.__init__: setting transpose to False') | |
1771 elif any(i not in transpose for i in range(len(transpose))): | |
1772 logging.warning(f'Invalid index in transpose ({transpose}) '+ | |
1773 f'in Fit.__init__: setting transpose to False') | |
1774 elif not all(i==transpose[i] for i in range(self._ymap.ndim-1)): | |
1775 self._transpose = transpose | |
1776 if self._transpose is not None: | |
1777 self._inv_transpose = tuple(self._transpose.index(i) | |
1778 for i in range(len(self._transpose))) | |
1779 | |
1780 # Flatten the map (transpose if requested) | |
1781 # Store the flattened map in self._ymap_norm, whether normalized or not | |
1782 if self._transpose is not None: | |
1783 self._ymap_norm = np.transpose(np.asarray(self._ymap), list(self._transpose)+ | |
1784 [len(self._transpose)]) | |
1785 else: | |
1786 self._ymap_norm = np.asarray(self._ymap) | |
1787 self._map_dim = int(self._ymap_norm.size/self._x.size) | |
1788 self._map_shape = self._ymap_norm.shape[:-1] | |
1789 self._ymap_norm = np.reshape(self._ymap_norm, (self._map_dim, self._x.size)) | |
1790 | |
1791 # Check if a mask is provided | |
1792 if 'mask' in kwargs: | |
1793 self._mask = kwargs.pop('mask') | |
1794 if self._mask is None: | |
1795 ymap_min = float(self._ymap_norm.min()) | |
1796 ymap_max = float(self._ymap_norm.max()) | |
1797 else: | |
1798 self._mask = np.asarray(self._mask).astype(bool) | |
1799 if self._x.size != self._mask.size: | |
1800 raise ValueError(f'Inconsistent mask dimension ({self._x.size} vs '+ | |
1801 f'{self._mask.size})') | |
1802 ymap_masked = np.asarray(self._ymap_norm)[:,~self._mask] | |
1803 ymap_min = float(ymap_masked.min()) | |
1804 ymap_max = float(ymap_masked.max()) | |
1805 | |
1806 # Normalize the data | |
1807 self._y_range = ymap_max-ymap_min | |
1808 if normalize and self._y_range > 0.0: | |
1809 self._norm = (ymap_min, self._y_range) | |
1810 self._ymap_norm = (self._ymap_norm-self._norm[0])/self._norm[1] | |
1811 else: | |
1812 self._redchi_cutoff *= self._y_range**2 | |
1813 if models is not None: | |
1814 if callable(models) or isinstance(models, str): | |
1815 kwargs = self.add_model(models, **kwargs) | |
1816 elif isinstance(models, (tuple, list)): | |
1817 for model in models: | |
1818 kwargs = self.add_model(model, **kwargs) | |
1819 self.fit(**kwargs) | |
1820 | |
1821 @classmethod | |
1822 def fit_map(cls, ymap, models, x=None, normalize=True, **kwargs): | |
1823 return(cls(ymap, x=x, models=models, normalize=normalize, **kwargs)) | |
1824 | |
1825 @property | |
1826 def best_errors(self): | |
1827 return(self._best_errors) | |
1828 | |
1829 @property | |
1830 def best_fit(self): | |
1831 return(self._best_fit) | |
1832 | |
1833 @property | |
1834 def best_results(self): | |
1835 """Convert the input data array to a data set and add the fit results. | |
1836 """ | |
1837 if self.best_values is None or self.best_errors is None or self.best_fit is None: | |
1838 return(None) | |
1839 if not have_xarray: | |
1840 logging.warning('Unable to load xarray module') | |
1841 return(None) | |
1842 best_values = self.best_values | |
1843 best_errors = self.best_errors | |
1844 if isinstance(self._ymap, xr.DataArray): | |
1845 best_results = self._ymap.to_dataset() | |
1846 dims = self._ymap.dims | |
1847 fit_name = f'{self._ymap.name}_fit' | |
1848 else: | |
1849 coords = {f'dim{n}_index':([f'dim{n}_index'], range(self._ymap.shape[n])) | |
1850 for n in range(self._ymap.ndim-1)} | |
1851 coords['x'] = (['x'], self._x) | |
1852 dims = list(coords.keys()) | |
1853 best_results = xr.Dataset(coords=coords) | |
1854 best_results['y'] = (dims, self._ymap) | |
1855 fit_name = 'y_fit' | |
1856 best_results[fit_name] = (dims, self.best_fit) | |
1857 if self._mask is not None: | |
1858 best_results['mask'] = self._mask | |
1859 for n in range(best_values.shape[0]): | |
1860 best_results[f'{self._best_parameters[n]}_values'] = (dims[:-1], best_values[n]) | |
1861 best_results[f'{self._best_parameters[n]}_errors'] = (dims[:-1], best_errors[n]) | |
1862 best_results.attrs['components'] = self.components | |
1863 return(best_results) | |
1864 | |
1865 @property | |
1866 def best_values(self): | |
1867 return(self._best_values) | |
1868 | |
1869 @property | |
1870 def chisqr(self): | |
1871 logging.warning('property chisqr not defined for fit.FitMap') | |
1872 return(None) | |
1873 | |
1874 @property | |
1875 def components(self): | |
1876 components = {} | |
1877 if self._result is None: | |
1878 logging.warning('Unable to collect components in FitMap.components') | |
1879 return(components) | |
1880 for component in self._result.components: | |
1881 if 'tmp_normalization_offset_c' in component.param_names: | |
1882 continue | |
1883 parameters = {} | |
1884 for name in component.param_names: | |
1885 if self._parameters[name].vary: | |
1886 parameters[name] = {'free': True} | |
1887 elif self._parameters[name].expr is not None: | |
1888 parameters[name] = {'free': False, 'expr': self._parameters[name].expr} | |
1889 else: | |
1890 parameters[name] = {'free': False, 'value': self.init_parameters[name]['value']} | |
1891 expr = None | |
1892 if isinstance(component, ExpressionModel): | |
1893 name = component._name | |
1894 if name[-1] == '_': | |
1895 name = name[:-1] | |
1896 expr = component.expr | |
1897 else: | |
1898 prefix = component.prefix | |
1899 if len(prefix): | |
1900 if prefix[-1] == '_': | |
1901 prefix = prefix[:-1] | |
1902 name = f'{prefix} ({component._name})' | |
1903 else: | |
1904 name = f'{component._name}' | |
1905 if expr is None: | |
1906 components[name] = {'parameters': parameters} | |
1907 else: | |
1908 components[name] = {'expr': expr, 'parameters': parameters} | |
1909 return(components) | |
1910 | |
1911 @property | |
1912 def covar(self): | |
1913 logging.warning('property covar not defined for fit.FitMap') | |
1914 return(None) | |
1915 | |
1916 @property | |
1917 def max_nfev(self): | |
1918 return(self._max_nfev) | |
1919 | |
1920 @property | |
1921 def num_func_eval(self): | |
1922 logging.warning('property num_func_eval not defined for fit.FitMap') | |
1923 return(None) | |
1924 | |
1925 @property | |
1926 def out_of_bounds(self): | |
1927 return(self._out_of_bounds) | |
1928 | |
1929 @property | |
1930 def redchi(self): | |
1931 return(self._redchi) | |
1932 | |
1933 @property | |
1934 def residual(self): | |
1935 if self.best_fit is None: | |
1936 return(None) | |
1937 if self._mask is None: | |
1938 return(np.asarray(self._ymap)-self.best_fit) | |
1939 else: | |
1940 ymap_flat = np.reshape(np.asarray(self._ymap), (self._map_dim, self._x.size)) | |
1941 ymap_flat_masked = ymap_flat[:,~self._mask] | |
1942 ymap_masked = np.reshape(ymap_flat_masked, | |
1943 list(self._map_shape)+[ymap_flat_masked.shape[-1]]) | |
1944 return(ymap_masked-self.best_fit) | |
1945 | |
1946 @property | |
1947 def success(self): | |
1948 return(self._success) | |
1949 | |
1950 @property | |
1951 def var_names(self): | |
1952 logging.warning('property var_names not defined for fit.FitMap') | |
1953 return(None) | |
1954 | |
1955 @property | |
1956 def y(self): | |
1957 logging.warning('property y not defined for fit.FitMap') | |
1958 return(None) | |
1959 | |
1960 @property | |
1961 def ymap(self): | |
1962 return(self._ymap) | |
1963 | |
1964 def best_parameters(self, dims=None): | |
1965 if dims is None: | |
1966 return(self._best_parameters) | |
1967 if not isinstance(dims, (list, tuple)) or len(dims) != len(self._map_shape): | |
1968 illegal_value(dims, 'dims', 'FitMap.best_parameters', raise_error=True) | |
1969 if self.best_values is None or self.best_errors is None: | |
1970 logging.warning(f'Unable to obtain best parameter values for dims = {dims} in '+ | |
1971 'FitMap.best_parameters') | |
1972 return({}) | |
1973 # Create current parameters | |
1974 parameters = deepcopy(self._parameters) | |
1975 for n, name in enumerate(self._best_parameters): | |
1976 if self._parameters[name].vary: | |
1977 parameters[name].set(value=self.best_values[n][dims]) | |
1978 parameters[name].stderr = self.best_errors[n][dims] | |
1979 parameters_dict = {} | |
1980 for name in sorted(parameters): | |
1981 if name != 'tmp_normalization_offset_c': | |
1982 par = parameters[name] | |
1983 parameters_dict[name] = {'value': par.value, 'error': par.stderr, | |
1984 'init_value': self.init_parameters[name]['value'], 'min': par.min, | |
1985 'max': par.max, 'vary': par.vary, 'expr': par.expr} | |
1986 return(parameters_dict) | |
1987 | |
1988 def freemem(self): | |
1989 if self._memfolder is None: | |
1990 return | |
1991 try: | |
1992 rmtree(self._memfolder) | |
1993 self._memfolder = None | |
1994 except: | |
1995 logging.warning('Could not clean-up automatically.') | |
1996 | |
1997 def plot(self, dims, y_title=None, plot_residual=False, plot_comp_legends=False, | |
1998 plot_masked_data=True): | |
1999 if not isinstance(dims, (list, tuple)) or len(dims) != len(self._map_shape): | |
2000 illegal_value(dims, 'dims', 'FitMap.plot', raise_error=True) | |
2001 if self._result is None or self.best_fit is None or self.best_values is None: | |
2002 logging.warning(f'Unable to plot fit for dims = {dims} in FitMap.plot') | |
2003 return | |
2004 if y_title is None or not isinstance(y_title, str): | |
2005 y_title = 'data' | |
2006 if self._mask is None: | |
2007 mask = np.zeros(self._x.size).astype(bool) | |
2008 plot_masked_data = False | |
2009 else: | |
2010 mask = self._mask | |
2011 plots = [(self._x, np.asarray(self._ymap[dims]), 'b.')] | |
2012 legend = [y_title] | |
2013 if plot_masked_data: | |
2014 plots += [(self._x[mask], np.asarray(self._ymap)[(*dims,mask)], 'bx')] | |
2015 legend += ['masked data'] | |
2016 plots += [(self._x[~mask], self.best_fit[dims], 'k-')] | |
2017 legend += ['best fit'] | |
2018 if plot_residual: | |
2019 plots += [(self._x[~mask], self.residual[dims], 'k--')] | |
2020 legend += ['residual'] | |
2021 # Create current parameters | |
2022 parameters = deepcopy(self._parameters) | |
2023 for name in self._best_parameters: | |
2024 if self._parameters[name].vary: | |
2025 parameters[name].set(value= | |
2026 self.best_values[self._best_parameters.index(name)][dims]) | |
2027 for component in self._result.components: | |
2028 if 'tmp_normalization_offset_c' in component.param_names: | |
2029 continue | |
2030 if isinstance(component, ExpressionModel): | |
2031 prefix = component._name | |
2032 if prefix[-1] == '_': | |
2033 prefix = prefix[:-1] | |
2034 modelname = f'{prefix}: {component.expr}' | |
2035 else: | |
2036 prefix = component.prefix | |
2037 if len(prefix): | |
2038 if prefix[-1] == '_': | |
2039 prefix = prefix[:-1] | |
2040 modelname = f'{prefix} ({component._name})' | |
2041 else: | |
2042 modelname = f'{component._name}' | |
2043 if len(modelname) > 20: | |
2044 modelname = f'{modelname[0:16]} ...' | |
2045 y = component.eval(params=parameters, x=self._x[~mask]) | |
2046 if isinstance(y, (int, float)): | |
2047 y *= np.ones(self._x[~mask].size) | |
2048 plots += [(self._x[~mask], y, '--')] | |
2049 if plot_comp_legends: | |
2050 legend.append(modelname) | |
2051 quick_plot(tuple(plots), legend=legend, title=str(dims), block=True) | |
2052 | |
2053 def fit(self, **kwargs): | |
2054 # t0 = time() | |
2055 # Check input parameters | |
2056 if self._model is None: | |
2057 logging.error('Undefined fit model') | |
2058 if 'num_proc' in kwargs: | |
2059 num_proc = kwargs.pop('num_proc') | |
2060 if not is_int(num_proc, ge=1): | |
2061 illegal_value(num_proc, 'num_proc', 'FitMap.fit', raise_error=True) | |
2062 else: | |
2063 num_proc = cpu_count() | |
2064 if num_proc > 1 and not have_joblib: | |
2065 logging.warning(f'Missing joblib in the conda environment, running FitMap serially') | |
2066 num_proc = 1 | |
2067 if num_proc > cpu_count(): | |
2068 logging.warning(f'The requested number of processors ({num_proc}) exceeds the maximum '+ | |
2069 f'number of processors, num_proc reduced to ({cpu_count()})') | |
2070 num_proc = cpu_count() | |
2071 if 'try_no_bounds' in kwargs: | |
2072 self._try_no_bounds = kwargs.pop('try_no_bounds') | |
2073 if not isinstance(self._try_no_bounds, bool): | |
2074 illegal_value(self._try_no_bounds, 'try_no_bounds', 'FitMap.fit', raise_error=True) | |
2075 if 'redchi_cutoff' in kwargs: | |
2076 self._redchi_cutoff = kwargs.pop('redchi_cutoff') | |
2077 if not is_num(self._redchi_cutoff, gt=0): | |
2078 illegal_value(self._redchi_cutoff, 'redchi_cutoff', 'FitMap.fit', raise_error=True) | |
2079 if 'print_report' in kwargs: | |
2080 self._print_report = kwargs.pop('print_report') | |
2081 if not isinstance(self._print_report, bool): | |
2082 illegal_value(self._print_report, 'print_report', 'FitMap.fit', raise_error=True) | |
2083 if 'plot' in kwargs: | |
2084 self._plot = kwargs.pop('plot') | |
2085 if not isinstance(self._plot, bool): | |
2086 illegal_value(self._plot, 'plot', 'FitMap.fit', raise_error=True) | |
2087 if 'skip_init' in kwargs: | |
2088 self._skip_init = kwargs.pop('skip_init') | |
2089 if not isinstance(self._skip_init, bool): | |
2090 illegal_value(self._skip_init, 'skip_init', 'FitMap.fit', raise_error=True) | |
2091 | |
2092 # Apply mask if supplied: | |
2093 if 'mask' in kwargs: | |
2094 self._mask = kwargs.pop('mask') | |
2095 if self._mask is not None: | |
2096 self._mask = np.asarray(self._mask).astype(bool) | |
2097 if self._x.size != self._mask.size: | |
2098 raise ValueError(f'Inconsistent x and mask dimensions ({self._x.size} vs '+ | |
2099 f'{self._mask.size})') | |
2100 | |
2101 # Add constant offset for a normalized single component model | |
2102 if self._result is None and self._norm is not None and self._norm[0]: | |
2103 self.add_model('constant', prefix='tmp_normalization_offset_', parameters={'name': 'c', | |
2104 'value': -self._norm[0], 'vary': False, 'norm': True}) | |
2105 #'value': -self._norm[0]/self._norm[1], 'vary': False, 'norm': False}) | |
2106 | |
2107 # Adjust existing parameters for refit: | |
2108 if 'parameters' in kwargs: | |
2109 # print('\nIn FitMap before adjusting existing parameters for refit:') | |
2110 # self._parameters.pretty_print() | |
2111 # if self._result is None: | |
2112 # raise ValueError('Invalid parameter parameters ({parameters})') | |
2113 # if self._best_values is None: | |
2114 # raise ValueError('Valid self._best_values required for refitting in FitMap.fit') | |
2115 parameters = kwargs.pop('parameters') | |
2116 # print(f'\nparameters:\n{parameters}') | |
2117 if isinstance(parameters, dict): | |
2118 parameters = (parameters, ) | |
2119 elif not is_dict_series(parameters): | |
2120 illegal_value(parameters, 'parameters', 'Fit.fit', raise_error=True) | |
2121 for par in parameters: | |
2122 name = par['name'] | |
2123 if name not in self._parameters: | |
2124 raise ValueError(f'Unable to match {name} parameter {par} to an existing one') | |
2125 if self._parameters[name].expr is not None: | |
2126 raise ValueError(f'Unable to modify {name} parameter {par} (currently an '+ | |
2127 'expression)') | |
2128 value = par.get('value') | |
2129 vary = par.get('vary') | |
2130 if par.get('expr') is not None: | |
2131 raise KeyError(f'Illegal "expr" key in {name} parameter {par}') | |
2132 self._parameters[name].set(value=value, vary=vary, min=par.get('min'), | |
2133 max=par.get('max')) | |
2134 # Overwrite existing best values for fixed parameters when a value is specified | |
2135 # print(f'best values befored resetting:\n{self._best_values}') | |
2136 if isinstance(value, (int, float)) and vary is False: | |
2137 for i, nname in enumerate(self._best_parameters): | |
2138 if nname == name: | |
2139 self._best_values[i] = value | |
2140 # print(f'best values after resetting (value={value}, vary={vary}):\n{self._best_values}') | |
2141 #RV print('\nIn FitMap after adjusting existing parameters for refit:') | |
2142 #RV self._parameters.pretty_print() | |
2143 | |
2144 # Check for uninitialized parameters | |
2145 for name, par in self._parameters.items(): | |
2146 if par.expr is None: | |
2147 value = par.value | |
2148 if value is None or np.isinf(value) or np.isnan(value): | |
2149 value = 1.0 | |
2150 if self._norm is None or name not in self._parameter_norms: | |
2151 self._parameters[name].set(value=value) | |
2152 elif self._parameter_norms[name]: | |
2153 self._parameters[name].set(value=value*self._norm[1]) | |
2154 | |
2155 # Create the best parameter list, consisting of all varying parameters plus the expression | |
2156 # parameters in order to collect their errors | |
2157 if self._result is None: | |
2158 # Initial fit | |
2159 assert(self._best_parameters is None) | |
2160 self._best_parameters = [name for name, par in self._parameters.items() | |
2161 if par.vary or par.expr is not None] | |
2162 num_new_parameters = 0 | |
2163 else: | |
2164 # Refit | |
2165 assert(len(self._best_parameters)) | |
2166 self._new_parameters = [name for name, par in self._parameters.items() | |
2167 if name != 'tmp_normalization_offset_c' and name not in self._best_parameters and | |
2168 (par.vary or par.expr is not None)] | |
2169 num_new_parameters = len(self._new_parameters) | |
2170 num_best_parameters = len(self._best_parameters) | |
2171 | |
2172 # Flatten and normalize the best values of the previous fit, remove the remaining results | |
2173 # of the previous fit | |
2174 if self._result is not None: | |
2175 # print('\nBefore flatten and normalize:') | |
2176 # print(f'self._best_values:\n{self._best_values}') | |
2177 self._out_of_bounds = None | |
2178 self._max_nfev = None | |
2179 self._redchi = None | |
2180 self._success = None | |
2181 self._best_fit = None | |
2182 self._best_errors = None | |
2183 assert(self._best_values is not None) | |
2184 assert(self._best_values.shape[0] == num_best_parameters) | |
2185 assert(self._best_values.shape[1:] == self._map_shape) | |
2186 if self._transpose is not None: | |
2187 self._best_values = np.transpose(self._best_values, | |
2188 [0]+[i+1 for i in self._transpose]) | |
2189 self._best_values = [np.reshape(self._best_values[i], self._map_dim) | |
2190 for i in range(num_best_parameters)] | |
2191 if self._norm is not None: | |
2192 for i, name in enumerate(self._best_parameters): | |
2193 if self._parameter_norms.get(name, False): | |
2194 self._best_values[i] /= self._norm[1] | |
2195 #RV print('\nAfter flatten and normalize:') | |
2196 #RV print(f'self._best_values:\n{self._best_values}') | |
2197 | |
2198 # Normalize the initial parameters (and best values for a refit) | |
2199 # print('\nIn FitMap before normalize:') | |
2200 # self._parameters.pretty_print() | |
2201 # print(f'\nparameter_norms:\n{self._parameter_norms}\n') | |
2202 self._normalize() | |
2203 # print('\nIn FitMap after normalize:') | |
2204 # self._parameters.pretty_print() | |
2205 # print(f'\nparameter_norms:\n{self._parameter_norms}\n') | |
2206 | |
2207 # Prevent initial values from sitting at boundaries | |
2208 self._parameter_bounds = {name:{'min': par.min, 'max': par.max} | |
2209 for name, par in self._parameters.items() if par.vary} | |
2210 for name, par in self._parameters.items(): | |
2211 if par.vary: | |
2212 par.set(value=self._reset_par_at_boundary(par, par.value)) | |
2213 # print('\nAfter checking boundaries:') | |
2214 # self._parameters.pretty_print() | |
2215 | |
2216 # Set parameter bounds to unbound (only use bounds when fit fails) | |
2217 if self._try_no_bounds: | |
2218 for name in self._parameter_bounds.keys(): | |
2219 self._parameters[name].set(min=-np.inf, max=np.inf) | |
2220 | |
2221 # Allocate memory to store fit results | |
2222 if self._mask is None: | |
2223 x_size = self._x.size | |
2224 else: | |
2225 x_size = self._x[~self._mask].size | |
2226 if num_proc == 1: | |
2227 self._out_of_bounds_flat = np.zeros(self._map_dim, dtype=bool) | |
2228 self._max_nfev_flat = np.zeros(self._map_dim, dtype=bool) | |
2229 self._redchi_flat = np.zeros(self._map_dim, dtype=np.float64) | |
2230 self._success_flat = np.zeros(self._map_dim, dtype=bool) | |
2231 self._best_fit_flat = np.zeros((self._map_dim, x_size), | |
2232 dtype=self._ymap_norm.dtype) | |
2233 self._best_errors_flat = [np.zeros(self._map_dim, dtype=np.float64) | |
2234 for _ in range(num_best_parameters+num_new_parameters)] | |
2235 if self._result is None: | |
2236 self._best_values_flat = [np.zeros(self._map_dim, dtype=np.float64) | |
2237 for _ in range(num_best_parameters)] | |
2238 else: | |
2239 self._best_values_flat = self._best_values | |
2240 self._best_values_flat += [np.zeros(self._map_dim, dtype=np.float64) | |
2241 for _ in range(num_new_parameters)] | |
2242 else: | |
2243 self._memfolder = './joblib_memmap' | |
2244 try: | |
2245 mkdir(self._memfolder) | |
2246 except FileExistsError: | |
2247 pass | |
2248 filename_memmap = path.join(self._memfolder, 'out_of_bounds_memmap') | |
2249 self._out_of_bounds_flat = np.memmap(filename_memmap, dtype=bool, | |
2250 shape=(self._map_dim), mode='w+') | |
2251 filename_memmap = path.join(self._memfolder, 'max_nfev_memmap') | |
2252 self._max_nfev_flat = np.memmap(filename_memmap, dtype=bool, | |
2253 shape=(self._map_dim), mode='w+') | |
2254 filename_memmap = path.join(self._memfolder, 'redchi_memmap') | |
2255 self._redchi_flat = np.memmap(filename_memmap, dtype=np.float64, | |
2256 shape=(self._map_dim), mode='w+') | |
2257 filename_memmap = path.join(self._memfolder, 'success_memmap') | |
2258 self._success_flat = np.memmap(filename_memmap, dtype=bool, | |
2259 shape=(self._map_dim), mode='w+') | |
2260 filename_memmap = path.join(self._memfolder, 'best_fit_memmap') | |
2261 self._best_fit_flat = np.memmap(filename_memmap, dtype=self._ymap_norm.dtype, | |
2262 shape=(self._map_dim, x_size), mode='w+') | |
2263 self._best_errors_flat = [] | |
2264 for i in range(num_best_parameters+num_new_parameters): | |
2265 filename_memmap = path.join(self._memfolder, f'best_errors_memmap_{i}') | |
2266 self._best_errors_flat.append(np.memmap(filename_memmap, dtype=np.float64, | |
2267 shape=self._map_dim, mode='w+')) | |
2268 self._best_values_flat = [] | |
2269 for i in range(num_best_parameters): | |
2270 filename_memmap = path.join(self._memfolder, f'best_values_memmap_{i}') | |
2271 self._best_values_flat.append(np.memmap(filename_memmap, dtype=np.float64, | |
2272 shape=self._map_dim, mode='w+')) | |
2273 if self._result is not None: | |
2274 self._best_values_flat[i][:] = self._best_values[i][:] | |
2275 for i in range(num_new_parameters): | |
2276 filename_memmap = path.join(self._memfolder, | |
2277 f'best_values_memmap_{i+num_best_parameters}') | |
2278 self._best_values_flat.append(np.memmap(filename_memmap, dtype=np.float64, | |
2279 shape=self._map_dim, mode='w+')) | |
2280 | |
2281 # Update the best parameter list | |
2282 if num_new_parameters: | |
2283 self._best_parameters += self._new_parameters | |
2284 | |
2285 # Perform the first fit to get model component info and initial parameters | |
2286 current_best_values = {} | |
2287 # print(f'0 before:\n{current_best_values}') | |
2288 # t1 = time() | |
2289 self._result = self._fit(0, current_best_values, return_result=True, **kwargs) | |
2290 # t2 = time() | |
2291 # print(f'0 after:\n{current_best_values}') | |
2292 # print('\nAfter the first fit:') | |
2293 # self._parameters.pretty_print() | |
2294 # print(self._result.fit_report(show_correl=False)) | |
2295 | |
2296 # Remove all irrelevant content from self._result | |
2297 for attr in ('_abort', 'aborted', 'aic', 'best_fit', 'best_values', 'bic', 'calc_covar', | |
2298 'call_kws', 'chisqr', 'ci_out', 'col_deriv', 'covar', 'data', 'errorbars', | |
2299 'flatchain', 'ier', 'init_vals', 'init_fit', 'iter_cb', 'jacfcn', 'kws', | |
2300 'last_internal_values', 'lmdif_message', 'message', 'method', 'nan_policy', | |
2301 'ndata', 'nfev', 'nfree', 'params', 'redchi', 'reduce_fcn', 'residual', 'result', | |
2302 'scale_covar', 'show_candidates', 'calc_covar', 'success', 'userargs', 'userfcn', | |
2303 'userkws', 'values', 'var_names', 'weights', 'user_options'): | |
2304 try: | |
2305 delattr(self._result, attr) | |
2306 except AttributeError: | |
2307 # logging.warning(f'Unknown attribute {attr} in fit.FtMap._cleanup_result') | |
2308 pass | |
2309 | |
2310 # t3 = time() | |
2311 if num_proc == 1: | |
2312 # Perform the remaining fits serially | |
2313 for n in range(1, self._map_dim): | |
2314 # print(f'{n} before:\n{current_best_values}') | |
2315 self._fit(n, current_best_values, **kwargs) | |
2316 # print(f'{n} after:\n{current_best_values}') | |
2317 else: | |
2318 # Perform the remaining fits in parallel | |
2319 num_fit = self._map_dim-1 | |
2320 # print(f'num_fit = {num_fit}') | |
2321 if num_proc > num_fit: | |
2322 logging.warning(f'The requested number of processors ({num_proc}) exceeds the '+ | |
2323 f'number of fits, num_proc reduced to ({num_fit})') | |
2324 num_proc = num_fit | |
2325 num_fit_per_proc = 1 | |
2326 else: | |
2327 num_fit_per_proc = round((num_fit)/num_proc) | |
2328 if num_proc*num_fit_per_proc < num_fit: | |
2329 num_fit_per_proc +=1 | |
2330 # print(f'num_fit_per_proc = {num_fit_per_proc}') | |
2331 num_fit_batch = min(num_fit_per_proc, 40) | |
2332 # print(f'num_fit_batch = {num_fit_batch}') | |
2333 with Parallel(n_jobs=num_proc) as parallel: | |
2334 parallel(delayed(self._fit_parallel)(current_best_values, num_fit_batch, | |
2335 n_start, **kwargs) for n_start in range(1, self._map_dim, num_fit_batch)) | |
2336 # t4 = time() | |
2337 | |
2338 # Renormalize the initial parameters for external use | |
2339 if self._norm is not None and self._normalized: | |
2340 init_values = {} | |
2341 for name, value in self._result.init_values.items(): | |
2342 if name not in self._parameter_norms or self._parameters[name].expr is not None: | |
2343 init_values[name] = value | |
2344 elif self._parameter_norms[name]: | |
2345 init_values[name] = value*self._norm[1] | |
2346 self._result.init_values = init_values | |
2347 for name, par in self._result.init_params.items(): | |
2348 if par.expr is None and self._parameter_norms.get(name, False): | |
2349 _min = par.min | |
2350 _max = par.max | |
2351 value = par.value*self._norm[1] | |
2352 if not np.isinf(_min) and abs(_min) != float_min: | |
2353 _min *= self._norm[1] | |
2354 if not np.isinf(_max) and abs(_max) != float_min: | |
2355 _max *= self._norm[1] | |
2356 par.set(value=value, min=_min, max=_max) | |
2357 par.init_value = par.value | |
2358 | |
2359 # Remap the best results | |
2360 # t5 = time() | |
2361 self._out_of_bounds = np.copy(np.reshape(self._out_of_bounds_flat, self._map_shape)) | |
2362 self._max_nfev = np.copy(np.reshape(self._max_nfev_flat, self._map_shape)) | |
2363 self._redchi = np.copy(np.reshape(self._redchi_flat, self._map_shape)) | |
2364 self._success = np.copy(np.reshape(self._success_flat, self._map_shape)) | |
2365 self._best_fit = np.copy(np.reshape(self._best_fit_flat, | |
2366 list(self._map_shape)+[x_size])) | |
2367 self._best_values = np.asarray([np.reshape(par, list(self._map_shape)) | |
2368 for par in self._best_values_flat]) | |
2369 self._best_errors = np.asarray([np.reshape(par, list(self._map_shape)) | |
2370 for par in self._best_errors_flat]) | |
2371 if self._inv_transpose is not None: | |
2372 self._out_of_bounds = np.transpose(self._out_of_bounds, self._inv_transpose) | |
2373 self._max_nfev = np.transpose(self._max_nfev, self._inv_transpose) | |
2374 self._redchi = np.transpose(self._redchi, self._inv_transpose) | |
2375 self._success = np.transpose(self._success, self._inv_transpose) | |
2376 self._best_fit = np.transpose(self._best_fit, | |
2377 list(self._inv_transpose)+[len(self._inv_transpose)]) | |
2378 self._best_values = np.transpose(self._best_values, | |
2379 [0]+[i+1 for i in self._inv_transpose]) | |
2380 self._best_errors = np.transpose(self._best_errors, | |
2381 [0]+[i+1 for i in self._inv_transpose]) | |
2382 del self._out_of_bounds_flat | |
2383 del self._max_nfev_flat | |
2384 del self._redchi_flat | |
2385 del self._success_flat | |
2386 del self._best_fit_flat | |
2387 del self._best_values_flat | |
2388 del self._best_errors_flat | |
2389 # t6 = time() | |
2390 | |
2391 # Restore parameter bounds and renormalize the parameters | |
2392 for name, par in self._parameter_bounds.items(): | |
2393 self._parameters[name].set(min=par['min'], max=par['max']) | |
2394 self._normalized = False | |
2395 if self._norm is not None: | |
2396 for name, norm in self._parameter_norms.items(): | |
2397 par = self._parameters[name] | |
2398 if par.expr is None and norm: | |
2399 value = par.value*self._norm[1] | |
2400 _min = par.min | |
2401 _max = par.max | |
2402 if not np.isinf(_min) and abs(_min) != float_min: | |
2403 _min *= self._norm[1] | |
2404 if not np.isinf(_max) and abs(_max) != float_min: | |
2405 _max *= self._norm[1] | |
2406 par.set(value=value, min=_min, max=_max) | |
2407 # t7 = time() | |
2408 # print(f'total run time in fit: {t7-t0:.2f} seconds') | |
2409 # print(f'run time first fit: {t2-t1:.2f} seconds') | |
2410 # print(f'run time remaining fits: {t4-t3:.2f} seconds') | |
2411 # print(f'run time remapping results: {t6-t5:.2f} seconds') | |
2412 | |
2413 # print('\n\nAt end fit:') | |
2414 # self._parameters.pretty_print() | |
2415 # print(f'self._best_values:\n{self._best_values}\n\n') | |
2416 | |
2417 # Free the shared memory | |
2418 self.freemem() | |
2419 | |
2420 def _fit_parallel(self, current_best_values, num, n_start, **kwargs): | |
2421 num = min(num, self._map_dim-n_start) | |
2422 for n in range(num): | |
2423 # print(f'{n_start+n} before:\n{current_best_values}') | |
2424 self._fit(n_start+n, current_best_values, **kwargs) | |
2425 # print(f'{n_start+n} after:\n{current_best_values}') | |
2426 | |
2427 def _fit(self, n, current_best_values, return_result=False, **kwargs): | |
2428 #RV print(f'\n\nstart FitMap._fit {n}\n') | |
2429 #RV print(f'current_best_values = {current_best_values}') | |
2430 #RV print(f'self._best_parameters = {self._best_parameters}') | |
2431 #RV print(f'self._new_parameters = {self._new_parameters}\n\n') | |
2432 # self._parameters.pretty_print() | |
2433 # Set parameters to current best values, but prevent them from sitting at boundaries | |
2434 if self._new_parameters is None: | |
2435 # Initial fit | |
2436 for name, value in current_best_values.items(): | |
2437 par = self._parameters[name] | |
2438 par.set(value=self._reset_par_at_boundary(par, value)) | |
2439 else: | |
2440 # Refit | |
2441 for i, name in enumerate(self._best_parameters): | |
2442 par = self._parameters[name] | |
2443 if name in self._new_parameters: | |
2444 if name in current_best_values: | |
2445 par.set(value=self._reset_par_at_boundary(par, current_best_values[name])) | |
2446 elif par.expr is None: | |
2447 par.set(value=self._best_values[i][n]) | |
2448 #RV print(f'\nbefore fit {n}') | |
2449 #RV self._parameters.pretty_print() | |
2450 if self._mask is None: | |
2451 result = self._model.fit(self._ymap_norm[n], self._parameters, x=self._x, **kwargs) | |
2452 else: | |
2453 result = self._model.fit(self._ymap_norm[n][~self._mask], self._parameters, | |
2454 x=self._x[~self._mask], **kwargs) | |
2455 # print(f'\nafter fit {n}') | |
2456 # self._parameters.pretty_print() | |
2457 # print(result.fit_report(show_correl=False)) | |
2458 out_of_bounds = False | |
2459 for name, par in self._parameter_bounds.items(): | |
2460 value = result.params[name].value | |
2461 if not np.isinf(par['min']) and value < par['min']: | |
2462 out_of_bounds = True | |
2463 break | |
2464 if not np.isinf(par['max']) and value > par['max']: | |
2465 out_of_bounds = True | |
2466 break | |
2467 self._out_of_bounds_flat[n] = out_of_bounds | |
2468 if self._try_no_bounds and out_of_bounds: | |
2469 # Rerun fit with parameter bounds in place | |
2470 for name, par in self._parameter_bounds.items(): | |
2471 self._parameters[name].set(min=par['min'], max=par['max']) | |
2472 # Set parameters to current best values, but prevent them from sitting at boundaries | |
2473 if self._new_parameters is None: | |
2474 # Initial fit | |
2475 for name, value in current_best_values.items(): | |
2476 par = self._parameters[name] | |
2477 par.set(value=self._reset_par_at_boundary(par, value)) | |
2478 else: | |
2479 # Refit | |
2480 for i, name in enumerate(self._best_parameters): | |
2481 par = self._parameters[name] | |
2482 if name in self._new_parameters: | |
2483 if name in current_best_values: | |
2484 par.set(value=self._reset_par_at_boundary(par, | |
2485 current_best_values[name])) | |
2486 elif par.expr is None: | |
2487 par.set(value=self._best_values[i][n]) | |
2488 # print('\nbefore fit') | |
2489 # self._parameters.pretty_print() | |
2490 # print(result.fit_report(show_correl=False)) | |
2491 if self._mask is None: | |
2492 result = self._model.fit(self._ymap_norm[n], self._parameters, x=self._x, **kwargs) | |
2493 else: | |
2494 result = self._model.fit(self._ymap_norm[n][~self._mask], self._parameters, | |
2495 x=self._x[~self._mask], **kwargs) | |
2496 # print(f'\nafter fit {n}') | |
2497 # self._parameters.pretty_print() | |
2498 # print(result.fit_report(show_correl=False)) | |
2499 out_of_bounds = False | |
2500 for name, par in self._parameter_bounds.items(): | |
2501 value = result.params[name].value | |
2502 if not np.isinf(par['min']) and value < par['min']: | |
2503 out_of_bounds = True | |
2504 break | |
2505 if not np.isinf(par['max']) and value > par['max']: | |
2506 out_of_bounds = True | |
2507 break | |
2508 # print(f'{n} redchi < redchi_cutoff = {result.redchi < self._redchi_cutoff} success = {result.success} out_of_bounds = {out_of_bounds}') | |
2509 # Reset parameters back to unbound | |
2510 for name in self._parameter_bounds.keys(): | |
2511 self._parameters[name].set(min=-np.inf, max=np.inf) | |
2512 assert(not out_of_bounds) | |
2513 if result.redchi >= self._redchi_cutoff: | |
2514 result.success = False | |
2515 if result.nfev == result.max_nfev: | |
2516 # print(f'Maximum number of function evaluations reached for n = {n}') | |
2517 # logging.warning(f'Maximum number of function evaluations reached for n = {n}') | |
2518 if result.redchi < self._redchi_cutoff: | |
2519 result.success = True | |
2520 self._max_nfev_flat[n] = True | |
2521 if result.success: | |
2522 assert(all(True for par in current_best_values if par in result.params.values())) | |
2523 for par in result.params.values(): | |
2524 if par.vary: | |
2525 current_best_values[par.name] = par.value | |
2526 else: | |
2527 logging.warning(f'Fit for n = {n} failed: {result.lmdif_message}') | |
2528 # Renormalize the data and results | |
2529 self._renormalize(n, result) | |
2530 if self._print_report: | |
2531 print(result.fit_report(show_correl=False)) | |
2532 if self._plot: | |
2533 dims = np.unravel_index(n, self._map_shape) | |
2534 if self._inv_transpose is not None: | |
2535 dims= tuple(dims[self._inv_transpose[i]] for i in range(len(dims))) | |
2536 super().plot(result=result, y=np.asarray(self._ymap[dims]), plot_comp_legends=True, | |
2537 skip_init=self._skip_init, title=str(dims)) | |
2538 #RV print(f'\n\nend FitMap._fit {n}\n') | |
2539 #RV print(f'current_best_values = {current_best_values}') | |
2540 # self._parameters.pretty_print() | |
2541 # print(result.fit_report(show_correl=False)) | |
2542 #RV print(f'\nself._best_values_flat:\n{self._best_values_flat}\n\n') | |
2543 if return_result: | |
2544 return(result) | |
2545 | |
2546 def _renormalize(self, n, result): | |
2547 self._redchi_flat[n] = np.float64(result.redchi) | |
2548 self._success_flat[n] = result.success | |
2549 if self._norm is None or not self._normalized: | |
2550 self._best_fit_flat[n] = result.best_fit | |
2551 for i, name in enumerate(self._best_parameters): | |
2552 self._best_values_flat[i][n] = np.float64(result.params[name].value) | |
2553 self._best_errors_flat[i][n] = np.float64(result.params[name].stderr) | |
2554 else: | |
2555 pars = set(self._parameter_norms) & set(self._best_parameters) | |
2556 for name, par in result.params.items(): | |
2557 if name in pars and self._parameter_norms[name]: | |
2558 if par.stderr is not None: | |
2559 par.stderr *= self._norm[1] | |
2560 if par.expr is None: | |
2561 par.value *= self._norm[1] | |
2562 if self._print_report: | |
2563 if par.init_value is not None: | |
2564 par.init_value *= self._norm[1] | |
2565 if not np.isinf(par.min) and abs(par.min) != float_min: | |
2566 par.min *= self._norm[1] | |
2567 if not np.isinf(par.max) and abs(par.max) != float_min: | |
2568 par.max *= self._norm[1] | |
2569 self._best_fit_flat[n] = result.best_fit*self._norm[1]+self._norm[0] | |
2570 for i, name in enumerate(self._best_parameters): | |
2571 self._best_values_flat[i][n] = np.float64(result.params[name].value) | |
2572 self._best_errors_flat[i][n] = np.float64(result.params[name].stderr) | |
2573 if self._plot: | |
2574 if not self._skip_init: | |
2575 result.init_fit = result.init_fit*self._norm[1]+self._norm[0] | |
2576 result.best_fit = np.copy(self._best_fit_flat[n]) |