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