ChessAnalysisPipeline 0.0.14__py3-none-any.whl → 0.0.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ChessAnalysisPipeline might be problematic. Click here for more details.
- CHAP/__init__.py +1 -1
- CHAP/common/__init__.py +13 -0
- CHAP/common/models/integration.py +29 -26
- CHAP/common/models/map.py +395 -224
- CHAP/common/processor.py +1725 -93
- CHAP/common/reader.py +265 -28
- CHAP/common/writer.py +191 -18
- CHAP/edd/__init__.py +9 -2
- CHAP/edd/models.py +886 -665
- CHAP/edd/processor.py +2592 -936
- CHAP/edd/reader.py +889 -0
- CHAP/edd/utils.py +846 -292
- CHAP/foxden/__init__.py +6 -0
- CHAP/foxden/processor.py +42 -0
- CHAP/foxden/writer.py +65 -0
- CHAP/giwaxs/__init__.py +8 -0
- CHAP/giwaxs/models.py +100 -0
- CHAP/giwaxs/processor.py +520 -0
- CHAP/giwaxs/reader.py +5 -0
- CHAP/giwaxs/writer.py +5 -0
- CHAP/pipeline.py +48 -10
- CHAP/runner.py +161 -72
- CHAP/tomo/models.py +31 -29
- CHAP/tomo/processor.py +169 -118
- CHAP/utils/__init__.py +1 -0
- CHAP/utils/fit.py +1292 -1315
- CHAP/utils/general.py +411 -53
- CHAP/utils/models.py +594 -0
- CHAP/utils/parfile.py +10 -2
- ChessAnalysisPipeline-0.0.16.dist-info/LICENSE +60 -0
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/METADATA +1 -1
- ChessAnalysisPipeline-0.0.16.dist-info/RECORD +62 -0
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/WHEEL +1 -1
- CHAP/utils/scanparsers.py +0 -1431
- ChessAnalysisPipeline-0.0.14.dist-info/LICENSE +0 -21
- ChessAnalysisPipeline-0.0.14.dist-info/RECORD +0 -54
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/entry_points.txt +0 -0
- {ChessAnalysisPipeline-0.0.14.dist-info → ChessAnalysisPipeline-0.0.16.dist-info}/top_level.txt +0 -0
CHAP/utils/fit.py
CHANGED
|
@@ -8,6 +8,7 @@ Description: General curve fitting module
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# System modules
|
|
11
|
+
from collections import Counter
|
|
11
12
|
from copy import deepcopy
|
|
12
13
|
from logging import getLogger
|
|
13
14
|
from os import (
|
|
@@ -15,10 +16,10 @@ from os import (
|
|
|
15
16
|
mkdir,
|
|
16
17
|
path,
|
|
17
18
|
)
|
|
18
|
-
from re import compile as re_compile
|
|
19
19
|
from re import sub
|
|
20
20
|
from shutil import rmtree
|
|
21
21
|
from sys import float_info
|
|
22
|
+
from time import time
|
|
22
23
|
|
|
23
24
|
# Third party modules
|
|
24
25
|
try:
|
|
@@ -29,53 +30,24 @@ try:
|
|
|
29
30
|
HAVE_JOBLIB = True
|
|
30
31
|
except ImportError:
|
|
31
32
|
HAVE_JOBLIB = False
|
|
32
|
-
from
|
|
33
|
-
Parameters,
|
|
34
|
-
Model,
|
|
35
|
-
)
|
|
36
|
-
from lmfit.model import ModelResult
|
|
37
|
-
from lmfit.models import (
|
|
38
|
-
ConstantModel,
|
|
39
|
-
LinearModel,
|
|
40
|
-
QuadraticModel,
|
|
41
|
-
PolynomialModel,
|
|
42
|
-
ExponentialModel,
|
|
43
|
-
StepModel,
|
|
44
|
-
RectangleModel,
|
|
45
|
-
ExpressionModel,
|
|
46
|
-
GaussianModel,
|
|
47
|
-
LorentzianModel,
|
|
48
|
-
)
|
|
33
|
+
from nexusformat.nexus import NXdata
|
|
49
34
|
import numpy as np
|
|
50
|
-
try:
|
|
51
|
-
from sympy import (
|
|
52
|
-
diff,
|
|
53
|
-
simplify,
|
|
54
|
-
)
|
|
55
|
-
except ImportError:
|
|
56
|
-
pass
|
|
57
|
-
try:
|
|
58
|
-
import xarray as xr
|
|
59
|
-
HAVE_XARRAY = True
|
|
60
|
-
except ImportError:
|
|
61
|
-
HAVE_XARRAY = False
|
|
62
35
|
|
|
63
36
|
# Local modules
|
|
37
|
+
from CHAP.processor import Processor
|
|
64
38
|
from CHAP.utils.general import (
|
|
65
39
|
is_int,
|
|
66
40
|
is_num,
|
|
67
|
-
is_str_series,
|
|
68
41
|
is_dict_series,
|
|
69
42
|
is_index,
|
|
70
43
|
index_nearest,
|
|
71
|
-
input_num,
|
|
72
44
|
quick_plot,
|
|
73
45
|
)
|
|
74
|
-
# eval_expr,
|
|
75
46
|
|
|
76
47
|
logger = getLogger(__name__)
|
|
77
48
|
FLOAT_MIN = float_info.min
|
|
78
49
|
FLOAT_MAX = float_info.max
|
|
50
|
+
FLOAT_EPS = float_info.epsilon
|
|
79
51
|
|
|
80
52
|
# sigma = fwhm_factor*fwhm
|
|
81
53
|
fwhm_factor = {
|
|
@@ -96,94 +68,540 @@ height_factor = {
|
|
|
96
68
|
}
|
|
97
69
|
|
|
98
70
|
|
|
71
|
+
class FitProcessor(Processor):
|
|
72
|
+
"""
|
|
73
|
+
A processor to perform a fit on a data set or data map.
|
|
74
|
+
"""
|
|
75
|
+
def process(self, data, config=None):
|
|
76
|
+
"""
|
|
77
|
+
Fit the data and return a CHAP.utils.fit.Fit or
|
|
78
|
+
CHAP.utils.fit.FitMap object depending on the dimensionality
|
|
79
|
+
of the input data. The input data should contain a NeXus NXdata
|
|
80
|
+
object, with properly defined signal and axis.
|
|
81
|
+
|
|
82
|
+
:param data: Input data containing the
|
|
83
|
+
nexusformat.nexus.NXdata object to fit.
|
|
84
|
+
:type data: list[PipelineData]
|
|
85
|
+
:raises ValueError: Invalid input or configuration parameter.
|
|
86
|
+
:return: The fitted data object.
|
|
87
|
+
:rtype: Union[CHAP.utils.fit.Fit, CHAP.utils.fit.FitMap]
|
|
88
|
+
"""
|
|
89
|
+
# Local modules
|
|
90
|
+
from CHAP.utils.models import (
|
|
91
|
+
FitConfig,
|
|
92
|
+
Multipeak,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Unwrap the PipelineData if called as a Pipeline Processor
|
|
96
|
+
if not isinstance(data, (Fit, FitMap)) and not isinstance(data, NXdata):
|
|
97
|
+
data = self.unwrap_pipelinedata(data)[0]
|
|
98
|
+
|
|
99
|
+
if isinstance(data, (Fit, FitMap)):
|
|
100
|
+
|
|
101
|
+
# Refit/continue the fit with possibly updated parameters
|
|
102
|
+
fit = data
|
|
103
|
+
fit_config = None
|
|
104
|
+
if config is not None:
|
|
105
|
+
try:
|
|
106
|
+
fit_config = FitConfig(**config)
|
|
107
|
+
except Exception as dict_exc:
|
|
108
|
+
raise RuntimeError from dict_exc
|
|
109
|
+
|
|
110
|
+
if isinstance(data, FitMap):
|
|
111
|
+
fit.fit(config=fit_config)
|
|
112
|
+
else:
|
|
113
|
+
fit.fit(config=fit_config)
|
|
114
|
+
if fit_config is not None:
|
|
115
|
+
if fit_config.print_report:
|
|
116
|
+
fit.print_fit_report()
|
|
117
|
+
if fit_config.plot:
|
|
118
|
+
fit.plot(skip_init=True)
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
|
|
122
|
+
# Get the default NXdata object
|
|
123
|
+
try:
|
|
124
|
+
nxdata = data.get_default()
|
|
125
|
+
assert nxdata is not None
|
|
126
|
+
except:
|
|
127
|
+
if nxdata is None or nxdata.nxclass != 'NXdata':
|
|
128
|
+
raise ValueError('Invalid default pathway to an NXdata '
|
|
129
|
+
f'object in ({data})')
|
|
130
|
+
|
|
131
|
+
# Get the fit configuration
|
|
132
|
+
try:
|
|
133
|
+
fit_config = self.get_config(data, 'utils.models.FitConfig')
|
|
134
|
+
except Exception as data_exc:
|
|
135
|
+
logger.info('No valid fit config in input pipeline '
|
|
136
|
+
'data, using config parameter instead.')
|
|
137
|
+
try:
|
|
138
|
+
fit_config = FitConfig(**config)
|
|
139
|
+
except Exception as dict_exc:
|
|
140
|
+
raise RuntimeError from dict_exc
|
|
141
|
+
|
|
142
|
+
# Expand multipeak model if present
|
|
143
|
+
found_multipeak = False
|
|
144
|
+
for i, model in enumerate(deepcopy(fit_config.models)):
|
|
145
|
+
if isinstance(model, Multipeak):
|
|
146
|
+
if found_multipeak:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f'Invalid parameter models ({fit_config.models}) '
|
|
149
|
+
'(multiple instances of multipeak not allowed)')
|
|
150
|
+
parameters, models = self.create_multipeak_model(model)
|
|
151
|
+
if parameters:
|
|
152
|
+
fit_config.parameters += parameters
|
|
153
|
+
fit_config.models += models
|
|
154
|
+
fit_config.models.pop(i)
|
|
155
|
+
found_multipeak = True
|
|
156
|
+
|
|
157
|
+
# Instantiate the Fit or FitMap object and fit the data
|
|
158
|
+
if np.squeeze(nxdata.nxsignal).ndim == 1:
|
|
159
|
+
fit = Fit(nxdata, fit_config)
|
|
160
|
+
fit.fit()
|
|
161
|
+
if fit_config.print_report:
|
|
162
|
+
fit.print_fit_report()
|
|
163
|
+
if fit_config.plot:
|
|
164
|
+
fit.plot(skip_init=True)
|
|
165
|
+
else:
|
|
166
|
+
fit = FitMap(nxdata, fit_config)
|
|
167
|
+
fit.fit(
|
|
168
|
+
rel_height_cutoff=fit_config.rel_height_cutoff,
|
|
169
|
+
num_proc=fit_config.num_proc, plot=fit_config.plot,
|
|
170
|
+
print_report=fit_config.print_report)
|
|
171
|
+
|
|
172
|
+
return fit
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def create_multipeak_model(model_config):
|
|
176
|
+
"""Create a multipeak model."""
|
|
177
|
+
# Local modules
|
|
178
|
+
from CHAP.utils.models import (
|
|
179
|
+
FitParameter,
|
|
180
|
+
Gaussian,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
parameters = []
|
|
184
|
+
models = []
|
|
185
|
+
num_peak = len(model_config.centers)
|
|
186
|
+
if num_peak == 1 and model_config.fit_type == 'uniform':
|
|
187
|
+
logger.debug('Ignoring fit_type input for fitting one peak')
|
|
188
|
+
model_config.fit_type = 'unconstrained'
|
|
189
|
+
|
|
190
|
+
sig_min = FLOAT_MIN
|
|
191
|
+
sig_max = np.inf
|
|
192
|
+
if (model_config.fwhm_min is not None
|
|
193
|
+
or model_config.fwhm_max is not None):
|
|
194
|
+
# Third party modules
|
|
195
|
+
from asteval import Interpreter
|
|
196
|
+
ast = Interpreter()
|
|
197
|
+
|
|
198
|
+
if model_config.fwhm_min is not None:
|
|
199
|
+
ast(f'fwhm = {model_config.fwhm_min}')
|
|
200
|
+
sig_min = ast(fwhm_factor[model_config.peak_models])
|
|
201
|
+
if model_config.fwhm_max is not None:
|
|
202
|
+
ast(f'fwhm = {model_config.fwhm_max}')
|
|
203
|
+
sig_max = ast(fwhm_factor[model_config.peak_models])
|
|
204
|
+
|
|
205
|
+
if model_config.fit_type == 'uniform':
|
|
206
|
+
parameters.append(FitParameter(
|
|
207
|
+
name='scale_factor', value=1.0, min=FLOAT_MIN))
|
|
208
|
+
if num_peak == 1:
|
|
209
|
+
prefix = ''
|
|
210
|
+
for i, cen in enumerate(model_config.centers):
|
|
211
|
+
if num_peak > 1:
|
|
212
|
+
prefix = f'peak{i+1}_'
|
|
213
|
+
models.append(Gaussian(
|
|
214
|
+
model='gaussian',
|
|
215
|
+
prefix=prefix,
|
|
216
|
+
parameters=[
|
|
217
|
+
{'name': 'amplitude', 'min': FLOAT_MIN},
|
|
218
|
+
{'name': 'center', 'expr': f'scale_factor*{cen}'},
|
|
219
|
+
{'name': 'sigma', 'min': sig_min, 'max': sig_max}]))
|
|
220
|
+
else:
|
|
221
|
+
if num_peak == 1:
|
|
222
|
+
prefix = ''
|
|
223
|
+
for i, cen in enumerate(model_config.centers):
|
|
224
|
+
if num_peak > 1:
|
|
225
|
+
prefix = f'peak{i+1}_'
|
|
226
|
+
if model_config.centers_range is None:
|
|
227
|
+
cen_min = None
|
|
228
|
+
cen_max = None
|
|
229
|
+
else:
|
|
230
|
+
cen_min = cen - model_config.centers_range
|
|
231
|
+
cen_max = cen + model_config.centers_range
|
|
232
|
+
models.append(Gaussian(
|
|
233
|
+
model='gaussian',
|
|
234
|
+
prefix=prefix,
|
|
235
|
+
parameters=[
|
|
236
|
+
{'name': 'amplitude', 'min': FLOAT_MIN},
|
|
237
|
+
{'name': 'center', 'value': cen, 'min': cen_min,
|
|
238
|
+
'max': cen_max},
|
|
239
|
+
{'name': 'sigma', 'min': sig_min, 'max': sig_max}]))
|
|
240
|
+
|
|
241
|
+
return parameters, models
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Component():
|
|
245
|
+
def __init__(self, model, prefix=''):
|
|
246
|
+
# Local modules
|
|
247
|
+
from CHAP.utils.models import models
|
|
248
|
+
|
|
249
|
+
self.func = models[model.model]
|
|
250
|
+
self.param_names = [f'{prefix}{par.name}' for par in model.parameters]
|
|
251
|
+
self.prefix = prefix
|
|
252
|
+
self._name = model.model
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Components(dict):
|
|
256
|
+
def __init__(self):
|
|
257
|
+
super().__init__(self)
|
|
258
|
+
|
|
259
|
+
def __setitem__(self, key, value):
|
|
260
|
+
if key not in self and not isinstance(key, str):
|
|
261
|
+
raise KeyError(f'Invalid component name ({key})')
|
|
262
|
+
if not isinstance(value, Component):
|
|
263
|
+
raise ValueError(f'Invalid component ({value})')
|
|
264
|
+
dict.__setitem__(self, key, value)
|
|
265
|
+
value.name = key
|
|
266
|
+
|
|
267
|
+
def add(self, model, prefix=''):
|
|
268
|
+
# Local modules
|
|
269
|
+
from CHAP.utils.models import model_classes
|
|
270
|
+
|
|
271
|
+
if not isinstance(model, model_classes):
|
|
272
|
+
raise ValueError(f'Invalid parameter model ({model})')
|
|
273
|
+
if not isinstance(prefix, str):
|
|
274
|
+
raise ValueError(f'Invalid parameter prefix ({prefix})')
|
|
275
|
+
name = f'{prefix}{model.model}'
|
|
276
|
+
self.__setitem__(name, Component(model, prefix))
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def components(self):
|
|
280
|
+
return self.values()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class Parameters(dict):
|
|
284
|
+
"""
|
|
285
|
+
A dictionary of FitParameter objects, mimicking the functionality
|
|
286
|
+
of a similarly named class in the lmfit library.
|
|
287
|
+
"""
|
|
288
|
+
def __init__(self):
|
|
289
|
+
super().__init__(self)
|
|
290
|
+
|
|
291
|
+
def __setitem__(self, key, value):
|
|
292
|
+
# Local modules
|
|
293
|
+
from CHAP.utils.models import FitParameter
|
|
294
|
+
|
|
295
|
+
if key in self:
|
|
296
|
+
raise KeyError(f'Duplicate name for FitParameter ({key})')
|
|
297
|
+
if key not in self and not isinstance(key, str):
|
|
298
|
+
raise KeyError(f'Invalid FitParameter name ({key})')
|
|
299
|
+
if value is not None and not isinstance(value, FitParameter):
|
|
300
|
+
raise ValueError(f'Invalid FitParameter ({value})')
|
|
301
|
+
dict.__setitem__(self, key, value)
|
|
302
|
+
value.name = key
|
|
303
|
+
|
|
304
|
+
def add(self, parameter, prefix=''):
|
|
305
|
+
"""
|
|
306
|
+
Add a fit parameter.
|
|
307
|
+
|
|
308
|
+
:param parameter: The fit parameter to add to the dictionary.
|
|
309
|
+
:type parameter: Union[str, FitParameter]
|
|
310
|
+
:param prefix: The prefix for the model to which this
|
|
311
|
+
parameter belongs, defaults to `''`.
|
|
312
|
+
:type prefix: str, optional
|
|
313
|
+
"""
|
|
314
|
+
# Local modules
|
|
315
|
+
from CHAP.utils.models import FitParameter
|
|
316
|
+
|
|
317
|
+
if isinstance(parameter, FitParameter):
|
|
318
|
+
name = f'{prefix}{parameter.name}'
|
|
319
|
+
self.__setitem__(name, parameter)
|
|
320
|
+
else:
|
|
321
|
+
raise RuntimeError('Must test')
|
|
322
|
+
parameter = f'{prefix}{parameter}'
|
|
323
|
+
self.__setitem__(
|
|
324
|
+
parameter,
|
|
325
|
+
FitParameter(name=parameter))
|
|
326
|
+
setattr(self[parameter.name], '_prefix', prefix)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class ModelResult():
|
|
330
|
+
"""
|
|
331
|
+
The result of a model fit, mimicking the functionality of a
|
|
332
|
+
similarly named class in the lmfit library.
|
|
333
|
+
"""
|
|
334
|
+
def __init__(
|
|
335
|
+
self, model, parameters, x=None, y=None, method=None, ast=None,
|
|
336
|
+
res_par_exprs=None, res_par_indices=None, res_par_names=None,
|
|
337
|
+
result=None):
|
|
338
|
+
self.components = model.components
|
|
339
|
+
self.params = deepcopy(parameters)
|
|
340
|
+
if x is None:
|
|
341
|
+
self.success = False
|
|
342
|
+
return
|
|
343
|
+
if method == 'leastsq':
|
|
344
|
+
best_pars = result[0]
|
|
345
|
+
self.ier = result[4]
|
|
346
|
+
self.message = result[3]
|
|
347
|
+
self.nfev = result[2]['nfev']
|
|
348
|
+
self.residual = result[2]['fvec']
|
|
349
|
+
self.success = 1 <= result[4] <= 4
|
|
350
|
+
else:
|
|
351
|
+
best_pars = result.x
|
|
352
|
+
self.ier = result.status
|
|
353
|
+
self.message = result.message
|
|
354
|
+
self.nfev = result.nfev
|
|
355
|
+
self.residual = result.fun
|
|
356
|
+
self.success = result.success
|
|
357
|
+
self.best_fit = y + self.residual
|
|
358
|
+
self.method = method
|
|
359
|
+
self.ndata = len(self.residual)
|
|
360
|
+
self.nvarys = len(res_par_indices)
|
|
361
|
+
self.x = x
|
|
362
|
+
self._ast = ast
|
|
363
|
+
self._expr_pars = {}
|
|
364
|
+
|
|
365
|
+
# Get the covarience matrix
|
|
366
|
+
self.chisqr = (self.residual**2).sum()
|
|
367
|
+
self.redchi = self.chisqr / (self.ndata-self.nvarys)
|
|
368
|
+
self.covar = None
|
|
369
|
+
if method == 'leastsq':
|
|
370
|
+
if result[1] is not None:
|
|
371
|
+
self.covar = result[1]*self.redchi
|
|
372
|
+
else:
|
|
373
|
+
try:
|
|
374
|
+
self.covar = self.redchi * np.linalg.inv(
|
|
375
|
+
np.dot(result.jac.T, result.jac))
|
|
376
|
+
except:
|
|
377
|
+
self.covar = None
|
|
378
|
+
|
|
379
|
+
# Update the fit parameters with the fit result
|
|
380
|
+
par_names = list(self.params.keys())
|
|
381
|
+
self.var_names = []
|
|
382
|
+
for i, (value, index) in enumerate(zip(best_pars, res_par_indices)):
|
|
383
|
+
par = self.params[par_names[index]]
|
|
384
|
+
par.set(value=value)
|
|
385
|
+
stderr = None
|
|
386
|
+
if self.covar is not None:
|
|
387
|
+
stderr = self.covar[i,i]
|
|
388
|
+
if stderr is not None:
|
|
389
|
+
if stderr < 0.0:
|
|
390
|
+
stderr = None
|
|
391
|
+
else:
|
|
392
|
+
stderr = np.sqrt(stderr)
|
|
393
|
+
self.var_names.append(par.name)
|
|
394
|
+
if res_par_exprs:
|
|
395
|
+
# Third party modules
|
|
396
|
+
from sympy import diff
|
|
397
|
+
for value, name in zip(best_pars, res_par_names):
|
|
398
|
+
self._ast.symtable[name] = value
|
|
399
|
+
for par_expr in res_par_exprs:
|
|
400
|
+
name = par_names[par_expr['index']]
|
|
401
|
+
expr = par_expr['expr']
|
|
402
|
+
par = self.params[name]
|
|
403
|
+
par.set(value=self._ast.eval(expr))
|
|
404
|
+
self._expr_pars[name] = expr
|
|
405
|
+
stderr = None
|
|
406
|
+
if self.covar is not None:
|
|
407
|
+
stderr = 0
|
|
408
|
+
for i, name in enumerate(self.var_names):
|
|
409
|
+
d = diff(expr, name)
|
|
410
|
+
if not d:
|
|
411
|
+
continue
|
|
412
|
+
for ii, nname in enumerate(self.var_names):
|
|
413
|
+
dd = diff(expr, nname)
|
|
414
|
+
if not dd:
|
|
415
|
+
continue
|
|
416
|
+
stderr += (self._ast.eval(str(d))
|
|
417
|
+
* self._ast.eval(str(dd))
|
|
418
|
+
* self.covar[i,ii])
|
|
419
|
+
stderr = np.sqrt(stderr)
|
|
420
|
+
setattr(par, '_stderr', stderr)
|
|
421
|
+
|
|
422
|
+
def eval_components(self, x=None, parameters=None):
|
|
423
|
+
"""
|
|
424
|
+
Evaluate each component of a composite model function.
|
|
425
|
+
|
|
426
|
+
:param x: Independent variable, defaults to `None`, in which
|
|
427
|
+
case the class variable x is used.
|
|
428
|
+
:type x: Union[list, np.ndarray], optional
|
|
429
|
+
:param parameters: Composite model parameters, defaults to
|
|
430
|
+
None, in which case the class variable params is used.
|
|
431
|
+
:type parameters: Parameters, optional
|
|
432
|
+
:return: A dictionary with component name and evealuated
|
|
433
|
+
function values key, value pairs.
|
|
434
|
+
:rtype: dict
|
|
435
|
+
"""
|
|
436
|
+
if x is None:
|
|
437
|
+
x = self.x
|
|
438
|
+
if parameters is None:
|
|
439
|
+
parameters = self.params
|
|
440
|
+
result = {}
|
|
441
|
+
for component in self.components:
|
|
442
|
+
if 'tmp_normalization_offset_c' in component.param_names:
|
|
443
|
+
continue
|
|
444
|
+
par_values = tuple(
|
|
445
|
+
parameters[par].value for par in component.param_names)
|
|
446
|
+
if component.prefix == '':
|
|
447
|
+
name = component._name
|
|
448
|
+
else:
|
|
449
|
+
name = component.prefix
|
|
450
|
+
result[name] = component.func(x, *par_values)
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
def fit_report(self, show_correl=False):
|
|
454
|
+
"""
|
|
455
|
+
Generates a report of the fitting results with their best
|
|
456
|
+
parameter values and uncertainties.
|
|
457
|
+
|
|
458
|
+
:param show_correl: Whether to show list of correlations,
|
|
459
|
+
defaults to `False`.
|
|
460
|
+
:type show_correl: bool, optional
|
|
461
|
+
"""
|
|
462
|
+
# Local modules
|
|
463
|
+
from CHAP.utils.general import (
|
|
464
|
+
getfloat_attr,
|
|
465
|
+
gformat,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
buff = []
|
|
469
|
+
add = buff.append
|
|
470
|
+
parnames = list(self.params.keys())
|
|
471
|
+
namelen = max(len(n) for n in parnames)
|
|
472
|
+
|
|
473
|
+
add("[[Fit Statistics]]")
|
|
474
|
+
add(f" # fitting method = {self.method}")
|
|
475
|
+
add(f" # function evals = {getfloat_attr(self, 'nfev')}")
|
|
476
|
+
add(f" # data points = {getfloat_attr(self, 'ndata')}")
|
|
477
|
+
add(f" # variables = {getfloat_attr(self, 'nvarys')}")
|
|
478
|
+
add(f" chi-square = {getfloat_attr(self, 'chisqr')}")
|
|
479
|
+
add(f" reduced chi-square = {getfloat_attr(self, 'redchi')}")
|
|
480
|
+
# add(f" Akaike info crit = {getfloat_attr(self, 'aic')}")
|
|
481
|
+
# add(f" Bayesian info crit = {getfloat_attr(self, 'bic')}")
|
|
482
|
+
# if hasattr(self, 'rsquared'):
|
|
483
|
+
# add(f" R-squared = {getfloat_attr(self, 'rsquared')}")
|
|
484
|
+
|
|
485
|
+
add("[[Variables]]")
|
|
486
|
+
for name in parnames:
|
|
487
|
+
par = self.params[name]
|
|
488
|
+
space = ' '*(namelen-len(name))
|
|
489
|
+
nout = f'{name}:{space}'
|
|
490
|
+
inval = '(init = ?)'
|
|
491
|
+
if par.init_value is not None:
|
|
492
|
+
inval = f'(init = {par.init_value:.7g})'
|
|
493
|
+
expr = self._expr_pars.get(name, par.expr)
|
|
494
|
+
if expr is not None:
|
|
495
|
+
val = self._ast.eval(expr)
|
|
496
|
+
else:
|
|
497
|
+
val = par.value
|
|
498
|
+
try:
|
|
499
|
+
val = gformat(par.value)
|
|
500
|
+
except (TypeError, ValueError):
|
|
501
|
+
val = ' Non Numeric Value?'
|
|
502
|
+
if par.stderr is not None:
|
|
503
|
+
serr = gformat(par.stderr)
|
|
504
|
+
try:
|
|
505
|
+
spercent = f'({abs(par.stderr/par.value):.2%})'
|
|
506
|
+
except ZeroDivisionError:
|
|
507
|
+
spercent = ''
|
|
508
|
+
val = f'{val} +/-{serr} {spercent}'
|
|
509
|
+
if par.vary:
|
|
510
|
+
add(f' {nout} {val} {inval}')
|
|
511
|
+
elif expr is not None:
|
|
512
|
+
add(f" {nout} {val} == '{expr}'")
|
|
513
|
+
else:
|
|
514
|
+
add(f' {nout} {par.value:.7g} (fixed)')
|
|
515
|
+
|
|
516
|
+
return '\n'.join(buff)
|
|
517
|
+
|
|
518
|
+
|
|
99
519
|
class Fit:
|
|
100
520
|
"""
|
|
101
|
-
Wrapper class for lmfit.
|
|
521
|
+
Wrapper class for scipy/lmfit.
|
|
102
522
|
"""
|
|
103
|
-
def __init__(self,
|
|
523
|
+
def __init__(self, nxdata, config):
|
|
104
524
|
"""Initialize Fit."""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
525
|
+
self._code = config.code
|
|
526
|
+
for model in config.models:
|
|
527
|
+
if model.model == 'expression' and self._code != 'lmfit':
|
|
528
|
+
self._code = 'lmfit'
|
|
529
|
+
logger.warning('Using lmfit instead of scipy with '
|
|
530
|
+
'an expression model')
|
|
531
|
+
if self._code == 'scipy':
|
|
532
|
+
# Local modules
|
|
533
|
+
from CHAP.utils.fit import Parameters
|
|
534
|
+
else:
|
|
535
|
+
# Third party modules
|
|
536
|
+
from lmfit import Parameters
|
|
109
537
|
self._mask = None
|
|
538
|
+
self._method = config.method
|
|
110
539
|
self._model = None
|
|
111
540
|
self._norm = None
|
|
112
541
|
self._normalized = False
|
|
542
|
+
self._free_parameters = []
|
|
113
543
|
self._parameters = Parameters()
|
|
544
|
+
if self._code == 'scipy':
|
|
545
|
+
self._ast = None
|
|
546
|
+
self._res_num_pars = []
|
|
547
|
+
self._res_par_exprs = []
|
|
548
|
+
self._res_par_indices = []
|
|
549
|
+
self._res_par_names = []
|
|
550
|
+
self._res_par_values = []
|
|
114
551
|
self._parameter_bounds = None
|
|
115
|
-
self._parameter_norms = {}
|
|
116
552
|
self._linear_parameters = []
|
|
117
553
|
self._nonlinear_parameters = []
|
|
118
554
|
self._result = None
|
|
119
|
-
self._try_linear_fit = True
|
|
120
|
-
self.
|
|
121
|
-
self.
|
|
122
|
-
self.
|
|
123
|
-
self.
|
|
124
|
-
self.
|
|
555
|
+
# self._try_linear_fit = True
|
|
556
|
+
# self._fwhm_min = None
|
|
557
|
+
# self._fwhm_max = None
|
|
558
|
+
# self._sigma_min = None
|
|
559
|
+
# self._sigma_max = None
|
|
560
|
+
self._x = None
|
|
125
561
|
self._y = None
|
|
126
562
|
self._y_norm = None
|
|
127
563
|
self._y_range = None
|
|
128
|
-
if 'try_linear_fit' in kwargs:
|
|
129
|
-
self._try_linear_fit = kwargs.pop('try_linear_fit')
|
|
130
|
-
if not isinstance(self._try_linear_fit, bool):
|
|
131
|
-
raise ValueError(
|
|
132
|
-
'Invalid value of keyword argument try_linear_fit '
|
|
133
|
-
f'({self._try_linear_fit})')
|
|
134
|
-
if
|
|
135
|
-
if isinstance(
|
|
136
|
-
|
|
137
|
-
self._y = np.asarray(y)
|
|
138
|
-
elif HAVE_XARRAY and isinstance(y, xr.DataArray):
|
|
139
|
-
if x is not None:
|
|
140
|
-
logger.warning('Ignoring superfluous input x ({x})')
|
|
141
|
-
if y.ndim != 1:
|
|
142
|
-
raise ValueError(
|
|
143
|
-
'Invalid DataArray dimensions for parameter y '
|
|
144
|
-
f'({y.ndim})')
|
|
145
|
-
self._x = np.asarray(y[y.dims[0]])
|
|
146
|
-
self._y = y
|
|
564
|
+
# if 'try_linear_fit' in kwargs:
|
|
565
|
+
# self._try_linear_fit = kwargs.pop('try_linear_fit')
|
|
566
|
+
# if not isinstance(self._try_linear_fit, bool):
|
|
567
|
+
# raise ValueError(
|
|
568
|
+
# 'Invalid value of keyword argument try_linear_fit '
|
|
569
|
+
# f'({self._try_linear_fit})')
|
|
570
|
+
if nxdata is not None:
|
|
571
|
+
if isinstance(nxdata.attrs['axes'], str):
|
|
572
|
+
dim_x = nxdata.attrs['axes']
|
|
147
573
|
else:
|
|
148
|
-
|
|
574
|
+
dim_x = nxdata.attrs['axes'][-1]
|
|
575
|
+
self._x = np.asarray(nxdata[dim_x])
|
|
576
|
+
self._y = np.squeeze(nxdata.nxsignal)
|
|
149
577
|
if self._x.ndim != 1:
|
|
150
578
|
raise ValueError(
|
|
151
|
-
f'Invalid dimension
|
|
579
|
+
f'Invalid x dimension ({self._x.ndim})')
|
|
152
580
|
if self._x.size != self._y.size:
|
|
153
581
|
raise ValueError(
|
|
154
582
|
f'Inconsistent x and y dimensions ({self._x.size} vs '
|
|
155
583
|
f'{self._y.size})')
|
|
156
|
-
if 'mask' in kwargs:
|
|
157
|
-
self._mask = kwargs.pop('mask')
|
|
158
|
-
if self._mask is None:
|
|
584
|
+
# if 'mask' in kwargs:
|
|
585
|
+
# self._mask = kwargs.pop('mask')
|
|
586
|
+
if True: #self._mask is None:
|
|
159
587
|
y_min = float(self._y.min())
|
|
160
588
|
self._y_range = float(self._y.max())-y_min
|
|
161
|
-
if
|
|
589
|
+
if self._y_range > 0.0:
|
|
162
590
|
self._norm = (y_min, self._y_range)
|
|
163
|
-
else:
|
|
164
|
-
self._mask = np.asarray(self._mask).astype(bool)
|
|
165
|
-
if self._x.size != self._mask.size:
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f'Inconsistent x and mask dimensions ({self._x.size} '
|
|
168
|
-
f'vs {self._mask.size})')
|
|
169
|
-
y_masked = np.asarray(self._y)[~self._mask]
|
|
170
|
-
y_min = float(y_masked.min())
|
|
171
|
-
self._y_range = float(y_masked.max())-y_min
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
kwargs = self.add_model(models, **kwargs)
|
|
178
|
-
elif isinstance(models, (tuple, list)):
|
|
179
|
-
for model in models:
|
|
180
|
-
kwargs = self.add_model(model, **kwargs)
|
|
181
|
-
self.fit(**kwargs)
|
|
182
|
-
|
|
183
|
-
@classmethod
|
|
184
|
-
def fit_data(cls, y, models, x=None, normalize=True, **kwargs):
|
|
185
|
-
"""Class method for Fit."""
|
|
186
|
-
return cls(y, x=x, models=models, normalize=normalize, **kwargs)
|
|
591
|
+
# else:
|
|
592
|
+
# self._mask = np.asarray(self._mask).astype(bool)
|
|
593
|
+
# if self._x.size != self._mask.size:
|
|
594
|
+
# raise ValueError(
|
|
595
|
+
# f'Inconsistent x and mask dimensions ({self._x.size} '
|
|
596
|
+
# f'vs {self._mask.size})')
|
|
597
|
+
# y_masked = np.asarray(self._y)[~self._mask]
|
|
598
|
+
# y_min = float(y_masked.min())
|
|
599
|
+
# self._y_range = float(y_masked.max())-y_min
|
|
600
|
+
# if self._y_range > 0.0:
|
|
601
|
+
# self._norm = (y_min, self._y_range)
|
|
602
|
+
|
|
603
|
+
# Setup fit model
|
|
604
|
+
self._setup_fit_model(config.parameters, config.models)
|
|
187
605
|
|
|
188
606
|
@property
|
|
189
607
|
def best_errors(self):
|
|
@@ -201,6 +619,7 @@ class Fit:
|
|
|
201
619
|
return None
|
|
202
620
|
return self._result.best_fit
|
|
203
621
|
|
|
622
|
+
@property
|
|
204
623
|
def best_parameters(self):
|
|
205
624
|
"""Return the best fit parameters."""
|
|
206
625
|
if self._result is None:
|
|
@@ -219,39 +638,6 @@ class Fit:
|
|
|
219
638
|
}
|
|
220
639
|
return parameters
|
|
221
640
|
|
|
222
|
-
@property
|
|
223
|
-
def best_results(self):
|
|
224
|
-
"""
|
|
225
|
-
Convert the input DataArray to a data set and add the fit
|
|
226
|
-
results.
|
|
227
|
-
"""
|
|
228
|
-
if self._result is None:
|
|
229
|
-
return None
|
|
230
|
-
if not HAVE_XARRAY:
|
|
231
|
-
logger.warning(
|
|
232
|
-
'fit.best_results requires xarray in the conda environment')
|
|
233
|
-
return None
|
|
234
|
-
if isinstance(self._y, xr.DataArray):
|
|
235
|
-
best_results = self._y.to_dataset()
|
|
236
|
-
dims = self._y.dims
|
|
237
|
-
fit_name = f'{self._y.name}_fit'
|
|
238
|
-
else:
|
|
239
|
-
coords = {'x': (['x'], self._x)}
|
|
240
|
-
dims = ('x',)
|
|
241
|
-
best_results = xr.Dataset(coords=coords)
|
|
242
|
-
best_results['y'] = (dims, self._y)
|
|
243
|
-
fit_name = 'y_fit'
|
|
244
|
-
best_results[fit_name] = (dims, self.best_fit)
|
|
245
|
-
if self._mask is not None:
|
|
246
|
-
best_results['mask'] = self._mask
|
|
247
|
-
best_results.coords['par_names'] = ('peak', self.best_values.keys())
|
|
248
|
-
best_results['best_values'] = \
|
|
249
|
-
(['par_names'], self.best_values.values())
|
|
250
|
-
best_results['best_errors'] = \
|
|
251
|
-
(['par_names'], self.best_errors.values())
|
|
252
|
-
best_results.attrs['components'] = self.components
|
|
253
|
-
return best_results
|
|
254
|
-
|
|
255
641
|
@property
|
|
256
642
|
def best_values(self):
|
|
257
643
|
"""Return values of the best fit parameters."""
|
|
@@ -270,6 +656,9 @@ class Fit:
|
|
|
270
656
|
|
|
271
657
|
@property
|
|
272
658
|
def components(self):
|
|
659
|
+
# Third party modules
|
|
660
|
+
from lmfit.models import ExpressionModel
|
|
661
|
+
|
|
273
662
|
"""Return the fit model components info."""
|
|
274
663
|
components = {}
|
|
275
664
|
if self._result is None:
|
|
@@ -355,10 +744,10 @@ class Fit:
|
|
|
355
744
|
return 0.0
|
|
356
745
|
if self._result.init_params is not None:
|
|
357
746
|
normalization_offset = float(
|
|
358
|
-
self._result.init_params['tmp_normalization_offset_c'])
|
|
747
|
+
self._result.init_params['tmp_normalization_offset_c'].value)
|
|
359
748
|
else:
|
|
360
749
|
normalization_offset = float(
|
|
361
|
-
self._result.params['tmp_normalization_offset_c'])
|
|
750
|
+
self._result.params['tmp_normalization_offset_c'].value)
|
|
362
751
|
return normalization_offset
|
|
363
752
|
|
|
364
753
|
@property
|
|
@@ -389,7 +778,9 @@ class Fit:
|
|
|
389
778
|
"""Return the residual in the best fit."""
|
|
390
779
|
if self._result is None:
|
|
391
780
|
return None
|
|
392
|
-
return
|
|
781
|
+
# lmfit return the negative of the residual in its common
|
|
782
|
+
# definition as (data - fit)
|
|
783
|
+
return -self._result.residual
|
|
393
784
|
|
|
394
785
|
@property
|
|
395
786
|
def success(self):
|
|
@@ -399,7 +790,8 @@ class Fit:
|
|
|
399
790
|
if not self._result.success:
|
|
400
791
|
logger.warning(
|
|
401
792
|
f'ier = {self._result.ier}: {self._result.message}')
|
|
402
|
-
if self.
|
|
793
|
+
if (self._code == 'lmfit' and self._result.ier
|
|
794
|
+
and self._result.ier != 5):
|
|
403
795
|
return True
|
|
404
796
|
return self._result.success
|
|
405
797
|
|
|
@@ -429,728 +821,259 @@ class Fit:
|
|
|
429
821
|
if result is not None:
|
|
430
822
|
print(result.fit_report(show_correl=show_correl))
|
|
431
823
|
|
|
432
|
-
def add_parameter(self,
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
824
|
+
def add_parameter(self, parameter):
|
|
825
|
+
# Local modules
|
|
826
|
+
from CHAP.utils.models import FitParameter
|
|
827
|
+
|
|
828
|
+
"""Add a fit parameter to the fit model."""
|
|
436
829
|
if parameter.get('expr') is not None:
|
|
437
830
|
raise KeyError(f'Invalid "expr" key in parameter {parameter}')
|
|
438
831
|
name = parameter['name']
|
|
439
|
-
if not
|
|
440
|
-
|
|
441
|
-
f'
|
|
442
|
-
|
|
443
|
-
|
|
832
|
+
if not parameter['vary']:
|
|
833
|
+
logger.warning(
|
|
834
|
+
f'Ignoring min in parameter {name} in '
|
|
835
|
+
f'Fit.add_parameter (vary = {parameter["vary"]})')
|
|
836
|
+
parameter['min'] = -np.inf
|
|
837
|
+
logger.warning(
|
|
838
|
+
f'Ignoring max in parameter {name} in '
|
|
839
|
+
f'Fit.add_parameter (vary = {parameter["vary"]})')
|
|
840
|
+
parameter['max'] = np.inf
|
|
841
|
+
if self._code == 'scipy':
|
|
842
|
+
self._parameters.add(FitParameter(**parameter))
|
|
444
843
|
else:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
logger.warning(
|
|
448
|
-
f'Ignoring norm in parameter {name} in Fit.add_parameter '
|
|
449
|
-
'(normalization is turned off)')
|
|
450
|
-
self._parameter_norms[name] = False
|
|
451
|
-
else:
|
|
452
|
-
if not isinstance(norm, bool):
|
|
453
|
-
raise ValueError(
|
|
454
|
-
f'Invalid "norm" value ({norm}) in parameter '
|
|
455
|
-
f'{parameter}')
|
|
456
|
-
self._parameter_norms[name] = norm
|
|
457
|
-
vary = parameter.get('vary')
|
|
458
|
-
if vary is not None:
|
|
459
|
-
if not isinstance(vary, bool):
|
|
460
|
-
raise ValueError(
|
|
461
|
-
f'Invalid "vary" value ({vary}) in parameter {parameter}')
|
|
462
|
-
if not vary:
|
|
463
|
-
if 'min' in parameter:
|
|
464
|
-
logger.warning(
|
|
465
|
-
f'Ignoring min in parameter {name} in '
|
|
466
|
-
f'Fit.add_parameter (vary = {vary})')
|
|
467
|
-
parameter.pop('min')
|
|
468
|
-
if 'max' in parameter:
|
|
469
|
-
logger.warning(
|
|
470
|
-
f'Ignoring max in parameter {name} in '
|
|
471
|
-
f'Fit.add_parameter (vary = {vary})')
|
|
472
|
-
parameter.pop('max')
|
|
473
|
-
if self._norm is not None and name not in self._parameter_norms:
|
|
474
|
-
raise ValueError(
|
|
475
|
-
f'Missing parameter normalization type for parameter {name}')
|
|
476
|
-
self._parameters.add(**parameter)
|
|
844
|
+
self._parameters.add(**parameter)
|
|
845
|
+
self._free_parameters.append(name)
|
|
477
846
|
|
|
478
|
-
def add_model(
|
|
479
|
-
self, model, prefix=None, parameters=None, parameter_norms=None,
|
|
480
|
-
**kwargs):
|
|
847
|
+
def add_model(self, model, prefix):
|
|
481
848
|
"""Add a model component to the fit model."""
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
849
|
+
if self._code == 'lmfit':
|
|
850
|
+
from lmfit.models import (
|
|
851
|
+
ConstantModel,
|
|
852
|
+
LinearModel,
|
|
853
|
+
QuadraticModel,
|
|
854
|
+
# PolynomialModel,
|
|
855
|
+
ExponentialModel,
|
|
856
|
+
GaussianModel,
|
|
857
|
+
LorentzianModel,
|
|
858
|
+
ExpressionModel,
|
|
859
|
+
# StepModel,
|
|
860
|
+
RectangleModel,
|
|
486
861
|
)
|
|
487
862
|
|
|
488
|
-
if
|
|
489
|
-
|
|
490
|
-
|
|
863
|
+
if model.model == 'expression':
|
|
864
|
+
expr = model.expr
|
|
865
|
+
else:
|
|
866
|
+
expr = None
|
|
867
|
+
parameters = model.parameters
|
|
868
|
+
model_name = model.model
|
|
869
|
+
|
|
491
870
|
if prefix is None:
|
|
492
871
|
pprefix = ''
|
|
493
872
|
else:
|
|
494
873
|
pprefix = prefix
|
|
495
|
-
if
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
'Invalid parameter parameters_norms ({parameters_norms})')
|
|
508
|
-
new_parameter_norms = {}
|
|
509
|
-
if callable(model):
|
|
510
|
-
# Linear fit not yet implemented for callable models
|
|
511
|
-
self._try_linear_fit = False
|
|
512
|
-
if parameter_norms is None:
|
|
513
|
-
if parameters is None:
|
|
514
|
-
raise ValueError(
|
|
515
|
-
'Either parameters or parameter_norms is required in '
|
|
516
|
-
f'{model}')
|
|
517
|
-
for par in parameters:
|
|
518
|
-
name = par['name']
|
|
519
|
-
if not isinstance(name, str):
|
|
520
|
-
raise ValueError(
|
|
521
|
-
f'Invalid "name" value ({name}) in input '
|
|
522
|
-
'parameters')
|
|
523
|
-
if par.get('norm') is not None:
|
|
524
|
-
norm = par.pop('norm')
|
|
525
|
-
if not isinstance(norm, bool):
|
|
526
|
-
raise ValueError(
|
|
527
|
-
f'Invalid "norm" value ({norm}) in input '
|
|
528
|
-
'parameters')
|
|
529
|
-
new_parameter_norms[f'{pprefix}{name}'] = norm
|
|
530
|
-
else:
|
|
531
|
-
for par in parameter_norms:
|
|
532
|
-
name = par['name']
|
|
533
|
-
if not isinstance(name, str):
|
|
534
|
-
raise ValueError(
|
|
535
|
-
f'Invalid "name" value ({name}) in input '
|
|
536
|
-
'parameters')
|
|
537
|
-
norm = par.get('norm')
|
|
538
|
-
if norm is None or not isinstance(norm, bool):
|
|
539
|
-
raise ValueError(
|
|
540
|
-
f'Invalid "norm" value ({norm}) in input '
|
|
541
|
-
'parameters')
|
|
542
|
-
new_parameter_norms[f'{pprefix}{name}'] = norm
|
|
543
|
-
if parameters is not None:
|
|
544
|
-
for par in parameters:
|
|
545
|
-
if par.get('expr') is not None:
|
|
546
|
-
raise KeyError(
|
|
547
|
-
f'Invalid "expr" key ({par.get("expr")}) in '
|
|
548
|
-
f'parameter {name} for a callable model {model}')
|
|
549
|
-
name = par['name']
|
|
550
|
-
if not isinstance(name, str):
|
|
551
|
-
raise ValueError(
|
|
552
|
-
f'Invalid "name" value ({name}) in input '
|
|
553
|
-
'parameters')
|
|
554
|
-
# RV callable model will need partial deriv functions for any linear
|
|
555
|
-
# parameter to get the linearized matrix, so for now skip linear
|
|
556
|
-
# solution option
|
|
557
|
-
newmodel = Model(model, prefix=prefix)
|
|
558
|
-
elif isinstance(model, str):
|
|
559
|
-
if model == 'constant':
|
|
560
|
-
# Par: c
|
|
874
|
+
if self._code == 'scipy':
|
|
875
|
+
new_parameters = []
|
|
876
|
+
for par in deepcopy(parameters):
|
|
877
|
+
self._parameters.add(par, pprefix)
|
|
878
|
+
if self._parameters[par.name].expr is None:
|
|
879
|
+
self._parameters[par.name].set(value=par.default)
|
|
880
|
+
new_parameters.append(par.name)
|
|
881
|
+
self._res_num_pars += [len(parameters)]
|
|
882
|
+
|
|
883
|
+
if model_name == 'constant':
|
|
884
|
+
# Par: c
|
|
885
|
+
if self._code == 'lmfit':
|
|
561
886
|
newmodel = ConstantModel(prefix=prefix)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
887
|
+
self._linear_parameters.append(f'{pprefix}c')
|
|
888
|
+
elif model_name == 'linear':
|
|
889
|
+
# Par: slope, intercept
|
|
890
|
+
if self._code == 'lmfit':
|
|
566
891
|
newmodel = LinearModel(prefix=prefix)
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
# Par: a, b, c
|
|
892
|
+
self._linear_parameters.append(f'{pprefix}slope')
|
|
893
|
+
self._linear_parameters.append(f'{pprefix}intercept')
|
|
894
|
+
elif model_name == 'quadratic':
|
|
895
|
+
# Par: a, b, c
|
|
896
|
+
if self._code == 'lmfit':
|
|
573
897
|
newmodel = QuadraticModel(prefix=prefix)
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
898
|
+
self._linear_parameters.append(f'{pprefix}a')
|
|
899
|
+
self._linear_parameters.append(f'{pprefix}b')
|
|
900
|
+
self._linear_parameters.append(f'{pprefix}c')
|
|
901
|
+
# elif model_name == 'polynomial':
|
|
902
|
+
# # Par: c0, c1,..., c7
|
|
903
|
+
# degree = kwargs.get('degree')
|
|
904
|
+
# if degree is not None:
|
|
905
|
+
# kwargs.pop('degree')
|
|
906
|
+
# if degree is None or not is_int(degree, ge=0, le=7):
|
|
907
|
+
# raise ValueError(
|
|
908
|
+
# 'Invalid parameter degree for build-in step model '
|
|
909
|
+
# f'({degree})')
|
|
910
|
+
# if self._code == 'lmfit':
|
|
911
|
+
# newmodel = PolynomialModel(degree=degree, prefix=prefix)
|
|
912
|
+
# for i in range(degree+1):
|
|
913
|
+
# self._linear_parameters.append(f'{pprefix}c{i}')
|
|
914
|
+
elif model_name == 'exponential':
|
|
915
|
+
# Par: amplitude, decay
|
|
916
|
+
if self._code == 'lmfit':
|
|
917
|
+
newmodel = ExponentialModel(prefix=prefix)
|
|
918
|
+
self._linear_parameters.append(f'{pprefix}amplitude')
|
|
919
|
+
self._nonlinear_parameters.append(f'{pprefix}decay')
|
|
920
|
+
elif model_name == 'gaussian':
|
|
921
|
+
# Par: amplitude, center, sigma (fwhm, height)
|
|
922
|
+
if self._code == 'lmfit':
|
|
595
923
|
newmodel = GaussianModel(prefix=prefix)
|
|
596
|
-
new_parameter_norms[f'{pprefix}amplitude'] = True
|
|
597
|
-
new_parameter_norms[f'{pprefix}center'] = False
|
|
598
|
-
new_parameter_norms[f'{pprefix}sigma'] = False
|
|
599
|
-
self._linear_parameters.append(f'{pprefix}amplitude')
|
|
600
|
-
self._nonlinear_parameters.append(f'{pprefix}center')
|
|
601
|
-
self._nonlinear_parameters.append(f'{pprefix}sigma')
|
|
602
924
|
# parameter norms for height and fwhm are needed to
|
|
603
925
|
# get correct errors
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
926
|
+
self._linear_parameters.append(f'{pprefix}amplitude')
|
|
927
|
+
self._nonlinear_parameters.append(f'{pprefix}center')
|
|
928
|
+
self._nonlinear_parameters.append(f'{pprefix}sigma')
|
|
929
|
+
elif model_name == 'lorentzian':
|
|
930
|
+
# Par: amplitude, center, sigma (fwhm, height)
|
|
931
|
+
if self._code == 'lmfit':
|
|
608
932
|
newmodel = LorentzianModel(prefix=prefix)
|
|
609
|
-
new_parameter_norms[f'{pprefix}amplitude'] = True
|
|
610
|
-
new_parameter_norms[f'{pprefix}center'] = False
|
|
611
|
-
new_parameter_norms[f'{pprefix}sigma'] = False
|
|
612
|
-
self._linear_parameters.append(f'{pprefix}amplitude')
|
|
613
|
-
self._nonlinear_parameters.append(f'{pprefix}center')
|
|
614
|
-
self._nonlinear_parameters.append(f'{pprefix}sigma')
|
|
615
933
|
# parameter norms for height and fwhm are needed to
|
|
616
934
|
# get correct errors
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
kwargs.pop('form')
|
|
648
|
-
if (form is None or form not in
|
|
649
|
-
('linear', 'atan', 'arctan', 'erf', 'logistic')):
|
|
650
|
-
raise ValueError(
|
|
651
|
-
'Invalid parameter form for build-in rectangle model '
|
|
652
|
-
f'({form})')
|
|
935
|
+
self._linear_parameters.append(f'{pprefix}amplitude')
|
|
936
|
+
self._nonlinear_parameters.append(f'{pprefix}center')
|
|
937
|
+
self._nonlinear_parameters.append(f'{pprefix}sigma')
|
|
938
|
+
# elif model_name == 'step':
|
|
939
|
+
# # Par: amplitude, center, sigma
|
|
940
|
+
# form = kwargs.get('form')
|
|
941
|
+
# if form is not None:
|
|
942
|
+
# kwargs.pop('form')
|
|
943
|
+
# if (form is None or form not in
|
|
944
|
+
# ('linear', 'atan', 'arctan', 'erf', 'logistic')):
|
|
945
|
+
# raise ValueError(
|
|
946
|
+
# 'Invalid parameter form for build-in step model '
|
|
947
|
+
# f'({form})')
|
|
948
|
+
# if self._code == 'lmfit':
|
|
949
|
+
# newmodel = StepModel(prefix=prefix, form=form)
|
|
950
|
+
# self._linear_parameters.append(f'{pprefix}amplitude')
|
|
951
|
+
# self._nonlinear_parameters.append(f'{pprefix}center')
|
|
952
|
+
# self._nonlinear_parameters.append(f'{pprefix}sigma')
|
|
953
|
+
elif model_name == 'rectangle':
|
|
954
|
+
# Par: amplitude, center1, center2, sigma1, sigma2
|
|
955
|
+
form = 'atan' #kwargs.get('form')
|
|
956
|
+
#if form is not None:
|
|
957
|
+
# kwargs.pop('form')
|
|
958
|
+
# RV: Implement and test other forms when needed
|
|
959
|
+
if (form is None or form not in
|
|
960
|
+
('linear', 'atan', 'arctan', 'erf', 'logistic')):
|
|
961
|
+
raise ValueError(
|
|
962
|
+
'Invalid parameter form for build-in rectangle model '
|
|
963
|
+
f'({form})')
|
|
964
|
+
if self._code == 'lmfit':
|
|
653
965
|
newmodel = RectangleModel(prefix=prefix, form=form)
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
expr
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
f'
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if par.get('expr') is not None:
|
|
678
|
-
raise KeyError(
|
|
679
|
-
f'Invalid "expr" key ({par.get("expr")}) in '
|
|
680
|
-
f'parameter ({par}) for an expression model')
|
|
681
|
-
if par.get('norm') is not None:
|
|
682
|
-
logger.warning(
|
|
683
|
-
f'Ignoring "norm" key in parameter ({par}) '
|
|
684
|
-
'(normalization determined from linearity)')
|
|
685
|
-
par.pop('norm')
|
|
686
|
-
name = par['name']
|
|
687
|
-
if not isinstance(name, str):
|
|
688
|
-
raise ValueError(
|
|
689
|
-
f'Invalid "name" value ({name}) in input '
|
|
690
|
-
'parameters')
|
|
691
|
-
ast = Interpreter()
|
|
692
|
-
expr_parameters = [
|
|
693
|
-
name for name in get_ast_names(ast.parse(expr))
|
|
694
|
-
if (name != 'x' and name not in self._parameters
|
|
695
|
-
and name not in ast.symtable)]
|
|
696
|
-
if prefix is None:
|
|
697
|
-
newmodel = ExpressionModel(expr=expr)
|
|
698
|
-
else:
|
|
699
|
-
for name in expr_parameters:
|
|
700
|
-
expr = sub(rf'\b{name}\b', f'{prefix}{name}', expr)
|
|
701
|
-
expr_parameters = [
|
|
702
|
-
f'{prefix}{name}' for name in expr_parameters]
|
|
703
|
-
newmodel = ExpressionModel(expr=expr, name=name)
|
|
704
|
-
# Remove already existing names
|
|
705
|
-
for name in newmodel.param_names.copy():
|
|
706
|
-
if name not in expr_parameters:
|
|
707
|
-
newmodel._func_allargs.remove(name)
|
|
708
|
-
newmodel._param_names.remove(name)
|
|
966
|
+
self._linear_parameters.append(f'{pprefix}amplitude')
|
|
967
|
+
self._nonlinear_parameters.append(f'{pprefix}center1')
|
|
968
|
+
self._nonlinear_parameters.append(f'{pprefix}center2')
|
|
969
|
+
self._nonlinear_parameters.append(f'{pprefix}sigma1')
|
|
970
|
+
self._nonlinear_parameters.append(f'{pprefix}sigma2')
|
|
971
|
+
elif model_name == 'expression' and self._code == 'lmfit':
|
|
972
|
+
# Third party modules
|
|
973
|
+
from asteval import (
|
|
974
|
+
Interpreter,
|
|
975
|
+
get_ast_names,
|
|
976
|
+
)
|
|
977
|
+
for par in parameters:
|
|
978
|
+
if par.expr is not None:
|
|
979
|
+
raise KeyError(
|
|
980
|
+
f'Invalid "expr" key ({par.expr}) in '
|
|
981
|
+
f'parameter ({par}) for an expression model')
|
|
982
|
+
ast = Interpreter()
|
|
983
|
+
expr_parameters = [
|
|
984
|
+
name for name in get_ast_names(ast.parse(expr))
|
|
985
|
+
if (name != 'x' and name not in self._parameters
|
|
986
|
+
and name not in ast.symtable)]
|
|
987
|
+
if prefix is None:
|
|
988
|
+
newmodel = ExpressionModel(expr=expr)
|
|
709
989
|
else:
|
|
710
|
-
|
|
990
|
+
for name in expr_parameters:
|
|
991
|
+
expr = sub(rf'\b{name}\b', f'{prefix}{name}', expr)
|
|
992
|
+
expr_parameters = [
|
|
993
|
+
f'{prefix}{name}' for name in expr_parameters]
|
|
994
|
+
newmodel = ExpressionModel(expr=expr, name=model_name)
|
|
995
|
+
# Remove already existing names
|
|
996
|
+
for name in newmodel.param_names.copy():
|
|
997
|
+
if name not in expr_parameters:
|
|
998
|
+
newmodel._func_allargs.remove(name)
|
|
999
|
+
newmodel._param_names.remove(name)
|
|
711
1000
|
else:
|
|
712
|
-
raise ValueError('
|
|
1001
|
+
raise ValueError(f'Unknown fit model ({model_name})')
|
|
713
1002
|
|
|
714
1003
|
# Add the new model to the current one
|
|
715
|
-
if self.
|
|
716
|
-
self._model
|
|
1004
|
+
if self._code == 'scipy':
|
|
1005
|
+
if self._model is None:
|
|
1006
|
+
self._model = Components()
|
|
1007
|
+
self._model.add(model, prefix)
|
|
717
1008
|
else:
|
|
718
|
-
self._model
|
|
719
|
-
|
|
720
|
-
|
|
1009
|
+
if self._model is None:
|
|
1010
|
+
self._model = newmodel
|
|
1011
|
+
else:
|
|
1012
|
+
self._model += newmodel
|
|
1013
|
+
new_parameters = newmodel.make_params()
|
|
1014
|
+
self._parameters += new_parameters
|
|
721
1015
|
|
|
722
1016
|
# Check linearity of expression model parameters
|
|
723
|
-
if isinstance(newmodel, ExpressionModel):
|
|
1017
|
+
if self._code == 'lmfit' and isinstance(newmodel, ExpressionModel):
|
|
1018
|
+
# Third party modules
|
|
1019
|
+
from sympy import diff
|
|
724
1020
|
for name in newmodel.param_names:
|
|
725
1021
|
if not diff(newmodel.expr, name, name):
|
|
726
1022
|
if name not in self._linear_parameters:
|
|
727
1023
|
self._linear_parameters.append(name)
|
|
728
|
-
new_parameter_norms[name] = True
|
|
729
1024
|
else:
|
|
730
1025
|
if name not in self._nonlinear_parameters:
|
|
731
1026
|
self._nonlinear_parameters.append(name)
|
|
732
|
-
new_parameter_norms[name] = False
|
|
733
1027
|
|
|
734
1028
|
# Scale the default initial model parameters
|
|
735
1029
|
if self._norm is not None:
|
|
736
|
-
for name
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
name = prefix+name
|
|
762
|
-
parameter['name'] = name
|
|
1030
|
+
for name in new_parameters:
|
|
1031
|
+
if name in self._linear_parameters:
|
|
1032
|
+
par = self._parameters.get(name)
|
|
1033
|
+
if par.expr is None:
|
|
1034
|
+
if self._code == 'scipy':
|
|
1035
|
+
value = par.default
|
|
1036
|
+
else:
|
|
1037
|
+
value = None
|
|
1038
|
+
if value is None:
|
|
1039
|
+
value = par.value
|
|
1040
|
+
if value is not None:
|
|
1041
|
+
value *= self._norm[1]
|
|
1042
|
+
_min = par.min
|
|
1043
|
+
_max = par.max
|
|
1044
|
+
if not np.isinf(_min) and abs(_min) != FLOAT_MIN:
|
|
1045
|
+
_min *= self._norm[1]
|
|
1046
|
+
if not np.isinf(_max) and abs(_max) != FLOAT_MIN:
|
|
1047
|
+
_max *= self._norm[1]
|
|
1048
|
+
par.set(value=value, min=_min, max=_max)
|
|
1049
|
+
|
|
1050
|
+
# Initialize the model parameters
|
|
1051
|
+
for parameter in deepcopy(parameters):
|
|
1052
|
+
name = parameter.name
|
|
1053
|
+
if name not in new_parameters:
|
|
1054
|
+
name = pprefix+name
|
|
763
1055
|
if name not in new_parameters:
|
|
764
|
-
logger.warning(
|
|
765
|
-
f'Ignoring superfluous parameter info for {name}')
|
|
766
|
-
continue
|
|
767
|
-
if name in self._parameters:
|
|
768
|
-
parameter.pop('name')
|
|
769
|
-
if 'norm' in parameter:
|
|
770
|
-
if not isinstance(parameter['norm'], bool):
|
|
771
|
-
raise ValueError(
|
|
772
|
-
f'Invalid "norm" value ({norm}) in the '
|
|
773
|
-
f'input parameter {name}')
|
|
774
|
-
new_parameter_norms[name] = parameter['norm']
|
|
775
|
-
parameter.pop('norm')
|
|
776
|
-
if parameter.get('expr') is not None:
|
|
777
|
-
if 'value' in parameter:
|
|
778
|
-
logger.warning(
|
|
779
|
-
f'Ignoring value in parameter {name} '
|
|
780
|
-
f'(set by expression: {parameter["expr"]})')
|
|
781
|
-
parameter.pop('value')
|
|
782
|
-
if 'vary' in parameter:
|
|
783
|
-
logger.warning(
|
|
784
|
-
f'Ignoring vary in parameter {name} '
|
|
785
|
-
f'(set by expression: {parameter["expr"]})')
|
|
786
|
-
parameter.pop('vary')
|
|
787
|
-
if 'min' in parameter:
|
|
788
|
-
logger.warning(
|
|
789
|
-
f'Ignoring min in parameter {name} '
|
|
790
|
-
f'(set by expression: {parameter["expr"]})')
|
|
791
|
-
parameter.pop('min')
|
|
792
|
-
if 'max' in parameter:
|
|
793
|
-
logger.warning(
|
|
794
|
-
f'Ignoring max in parameter {name} '
|
|
795
|
-
f'(set by expression: {parameter["expr"]})')
|
|
796
|
-
parameter.pop('max')
|
|
797
|
-
if 'vary' in parameter:
|
|
798
|
-
if not isinstance(parameter['vary'], bool):
|
|
799
|
-
raise ValueError(
|
|
800
|
-
f'Invalid "vary" value ({parameter["vary"]}) '
|
|
801
|
-
f'in the input parameter {name}')
|
|
802
|
-
if not parameter['vary']:
|
|
803
|
-
if 'min' in parameter:
|
|
804
|
-
logger.warning(
|
|
805
|
-
f'Ignoring min in parameter {name} '
|
|
806
|
-
f'(vary = {parameter["vary"]})')
|
|
807
|
-
parameter.pop('min')
|
|
808
|
-
if 'max' in parameter:
|
|
809
|
-
logger.warning(
|
|
810
|
-
f'Ignoring max in parameter {name} '
|
|
811
|
-
f'(vary = {parameter["vary"]})')
|
|
812
|
-
parameter.pop('max')
|
|
813
|
-
self._parameters[name].set(**parameter)
|
|
814
|
-
parameter['name'] = name
|
|
815
|
-
else:
|
|
816
1056
|
raise ValueError(
|
|
817
|
-
'
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
# Initialize the model parameters from kwargs
|
|
824
|
-
for name, value in {**kwargs}.items():
|
|
825
|
-
full_name = f'{pprefix}{name}'
|
|
826
|
-
if (full_name in new_parameter_norms
|
|
827
|
-
and isinstance(value, (int, float))):
|
|
828
|
-
kwargs.pop(name)
|
|
829
|
-
if self._parameters[full_name].expr is None:
|
|
830
|
-
self._parameters[full_name].set(value=value)
|
|
831
|
-
else:
|
|
832
|
-
logger.warning(
|
|
833
|
-
f'Ignoring parameter {name} (set by expression: '
|
|
834
|
-
f'{self._parameters[full_name].expr})')
|
|
835
|
-
|
|
836
|
-
# Check parameter norms
|
|
837
|
-
# (also need it for expressions to renormalize the errors)
|
|
838
|
-
if (self._norm is not None
|
|
839
|
-
and (callable(model) or model == 'expression')):
|
|
840
|
-
missing_norm = False
|
|
841
|
-
for name in new_parameters.valuesdict():
|
|
842
|
-
if name not in self._parameter_norms:
|
|
843
|
-
print(f'new_parameters:\n{new_parameters.valuesdict()}')
|
|
844
|
-
print(f'self._parameter_norms:\n{self._parameter_norms}')
|
|
845
|
-
logger.error(
|
|
846
|
-
f'Missing parameter normalization type for {name} in '
|
|
847
|
-
f'{model}')
|
|
848
|
-
missing_norm = True
|
|
849
|
-
if missing_norm:
|
|
850
|
-
raise ValueError
|
|
851
|
-
|
|
852
|
-
return kwargs
|
|
853
|
-
|
|
854
|
-
def create_multipeak_model(
|
|
855
|
-
self, centers=None, fit_type=None, peak_models=None,
|
|
856
|
-
center_exprs=None, background=None, param_constraint=True,
|
|
857
|
-
fwhm_min=None, fwhm_max=None, centers_range=None):
|
|
858
|
-
"""Create a multipeak model."""
|
|
859
|
-
# System modules
|
|
860
|
-
from re import search as re_search
|
|
861
|
-
|
|
862
|
-
# Third party modules
|
|
863
|
-
from asteval import Interpreter
|
|
864
|
-
|
|
865
|
-
# Local modules
|
|
866
|
-
from CHAP.utils.general import is_num_pair
|
|
867
|
-
|
|
868
|
-
if centers_range is None:
|
|
869
|
-
centers_range = (self._x[0], self._x[-1])
|
|
870
|
-
elif (not is_num_pair(centers_range) or len(centers_range) != 2
|
|
871
|
-
or centers_range[0] >= centers_range[1]):
|
|
872
|
-
raise ValueError(
|
|
873
|
-
f'Invalid parameter centers_range ({centers_range})')
|
|
874
|
-
if self._model is not None:
|
|
875
|
-
if self._fit_type == 'uniform' and fit_type != 'uniform':
|
|
876
|
-
logger.info('Use the existing multipeak model to refit a '
|
|
877
|
-
'uniform model with an unconstrained model')
|
|
878
|
-
min_value = FLOAT_MIN if self._param_constraint else None
|
|
879
|
-
if isinstance(self, FitMap):
|
|
880
|
-
scale_factor_index = \
|
|
881
|
-
self._best_parameters.index('scale_factor')
|
|
882
|
-
self._best_parameters.pop(scale_factor_index)
|
|
883
|
-
self._best_values = np.delete(
|
|
884
|
-
self._best_values, scale_factor_index, 0)
|
|
885
|
-
self._best_errors = np.delete(
|
|
886
|
-
self._best_errors, scale_factor_index, 0)
|
|
887
|
-
for name, par in self._parameters.items():
|
|
888
|
-
if re_search('peak\d+_center', name) is not None:
|
|
889
|
-
par.set(
|
|
890
|
-
min=centers_range[0], max=centers_range[1],
|
|
891
|
-
vary=True, expr=None)
|
|
892
|
-
self._parameter_bounds[name] = {
|
|
893
|
-
'min': centers_range[0],
|
|
894
|
-
'max': centers_range[1],
|
|
895
|
-
}
|
|
896
|
-
else:
|
|
897
|
-
for name, par in self._parameters.items():
|
|
898
|
-
if re_search('peak\d+_center', name) is not None:
|
|
899
|
-
par.set(
|
|
900
|
-
value=self._result.params[name].value,
|
|
901
|
-
min=min_value, vary=True, expr=None)
|
|
902
|
-
self._parameter_bounds[name] = {
|
|
903
|
-
'min': min_value,
|
|
904
|
-
'max': np.inf,
|
|
905
|
-
}
|
|
906
|
-
self._parameters.pop('scale_factor')
|
|
907
|
-
self._parameter_bounds.pop('scale_factor')
|
|
908
|
-
self._parameter_norms.pop('scale_factor')
|
|
909
|
-
return
|
|
1057
|
+
f'Unable to match parameter {name}')
|
|
1058
|
+
if parameter.expr is None:
|
|
1059
|
+
self._parameters[name].set(
|
|
1060
|
+
value=parameter.value, min=parameter.min,
|
|
1061
|
+
max=parameter.max, vary=parameter.vary)
|
|
910
1062
|
else:
|
|
911
|
-
|
|
912
|
-
'multipeak model')
|
|
913
|
-
self._model = None
|
|
914
|
-
|
|
915
|
-
if self._model is None and len(self._parameters):
|
|
916
|
-
logger.warning('Existing fit parameters cleared before creating a '
|
|
917
|
-
'new multipeak model')
|
|
918
|
-
self._parameters = Parameters()
|
|
919
|
-
if isinstance(centers, (int, float)):
|
|
920
|
-
centers = [centers]
|
|
921
|
-
elif not isinstance(centers, (tuple, list, np.ndarray)):
|
|
922
|
-
raise ValueError(f'Invalid parameter centers ({centers})')
|
|
923
|
-
num_peaks = len(centers)
|
|
924
|
-
if peak_models is None:
|
|
925
|
-
peak_models = num_peaks*['gaussian']
|
|
926
|
-
elif (isinstance(peak_models, str)
|
|
927
|
-
and peak_models in ('gaussian', 'lorentzian')):
|
|
928
|
-
peak_models = num_peaks*[peak_models]
|
|
929
|
-
else:
|
|
930
|
-
raise ValueError(f'Invalid parameter peak model ({peak_models})')
|
|
931
|
-
if len(peak_models) != num_peaks:
|
|
932
|
-
raise ValueError(
|
|
933
|
-
'Inconsistent number of peaks in peak_models '
|
|
934
|
-
f'({len(peak_models)} vs {num_peaks})')
|
|
935
|
-
if num_peaks == 1:
|
|
936
|
-
if fit_type is not None:
|
|
937
|
-
logger.debug('Ignoring fit_type input for fitting one peak')
|
|
938
|
-
fit_type = None
|
|
939
|
-
if center_exprs is not None:
|
|
940
|
-
logger.debug(
|
|
941
|
-
'Ignoring center_exprs input for fitting one peak')
|
|
942
|
-
center_exprs = None
|
|
943
|
-
else:
|
|
944
|
-
if fit_type == 'uniform':
|
|
945
|
-
if center_exprs is None:
|
|
946
|
-
center_exprs = [f'scale_factor*{cen}' for cen in centers]
|
|
947
|
-
if len(center_exprs) != num_peaks:
|
|
948
|
-
raise ValueError(
|
|
949
|
-
'Inconsistent number of peaks in center_exprs '
|
|
950
|
-
f'({len(center_exprs)} vs {num_peaks})')
|
|
951
|
-
elif fit_type == 'unconstrained' or fit_type is None:
|
|
952
|
-
fit_type = 'unconstrained'
|
|
953
|
-
if center_exprs is not None:
|
|
1063
|
+
if parameter.value is not None:
|
|
954
1064
|
logger.warning(
|
|
955
|
-
'Ignoring
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
min_value = FLOAT_MIN
|
|
968
|
-
if self._fwhm_min is not None:
|
|
969
|
-
self._sigma_min = np.zeros(num_peaks)
|
|
970
|
-
if self._fwhm_max is not None:
|
|
971
|
-
self._sigma_max = np.zeros(num_peaks)
|
|
972
|
-
else:
|
|
973
|
-
min_value = None
|
|
974
|
-
|
|
975
|
-
# Reset the fit
|
|
976
|
-
self._result = None
|
|
977
|
-
self._parameter_norms = {}
|
|
978
|
-
self._linear_parameters = []
|
|
979
|
-
self._nonlinear_parameters = []
|
|
980
|
-
if hasattr(self, "_best_parameters"):
|
|
981
|
-
self._best_parameters = None
|
|
982
|
-
|
|
983
|
-
# Add background model(s)
|
|
984
|
-
if background is not None:
|
|
985
|
-
if isinstance(background, str):
|
|
986
|
-
background = [{'model': background}]
|
|
987
|
-
elif isinstance(background, dict):
|
|
988
|
-
background = [background]
|
|
989
|
-
elif is_str_series(background):
|
|
990
|
-
background = [{'model': model}
|
|
991
|
-
for model in deepcopy(background)]
|
|
992
|
-
if is_dict_series(background):
|
|
993
|
-
num_background = len(background)
|
|
994
|
-
for model in deepcopy(background):
|
|
995
|
-
if 'model' not in model:
|
|
996
|
-
raise KeyError(
|
|
997
|
-
'Missing keyword "model" in model in background '
|
|
998
|
-
f'({model})')
|
|
999
|
-
name = model.pop('model')
|
|
1000
|
-
if num_background == 1:
|
|
1001
|
-
prefix = f'bkgd_'
|
|
1002
|
-
else:
|
|
1003
|
-
prefix = f'bkgd_{name}_'
|
|
1004
|
-
parameters = model.pop('parameters', None)
|
|
1005
|
-
if parameters is not None:
|
|
1006
|
-
if isinstance(parameters, dict):
|
|
1007
|
-
parameters = [parameters, ]
|
|
1008
|
-
elif is_dict_series(parameters):
|
|
1009
|
-
parameters = list(parameters)
|
|
1010
|
-
else:
|
|
1011
|
-
raise ValueError('Invalid parameters value in '
|
|
1012
|
-
f'background model {name} ({parameters})')
|
|
1013
|
-
if min_value is not None and name == 'exponential':
|
|
1014
|
-
if parameters is None:
|
|
1015
|
-
parameters = (
|
|
1016
|
-
{'name': 'amplitude', 'min': min_value},
|
|
1017
|
-
{'name': 'decay', 'min': min_value},
|
|
1018
|
-
)
|
|
1019
|
-
else:
|
|
1020
|
-
for par_name in ('amplitude', 'decay'):
|
|
1021
|
-
index = [i for i, par in enumerate(parameters)
|
|
1022
|
-
if par['name'] == par_name]
|
|
1023
|
-
if not len(index):
|
|
1024
|
-
parameters.append(
|
|
1025
|
-
{'name': par_name, 'min': min_value})
|
|
1026
|
-
elif len(index) == 1:
|
|
1027
|
-
parameter = parameters[index[0]]
|
|
1028
|
-
_min = parameter.get('min', min_value)
|
|
1029
|
-
parameter['min'] = max(_min, min_value)
|
|
1030
|
-
else:
|
|
1031
|
-
raise ValueError(
|
|
1032
|
-
'Invalid parameters value in '
|
|
1033
|
-
f'background model {name} '
|
|
1034
|
-
f'({parameters})')
|
|
1035
|
-
if min_value is not None and name == 'gaussian':
|
|
1036
|
-
if parameters is None:
|
|
1037
|
-
parameters = (
|
|
1038
|
-
{'name': 'amplitude', 'min': min_value},
|
|
1039
|
-
{'name': 'center', 'min': min_value},
|
|
1040
|
-
{'name': 'sigma', 'min': min_value},
|
|
1041
|
-
)
|
|
1042
|
-
else:
|
|
1043
|
-
for par_name in ('amplitude', 'center', 'sigma'):
|
|
1044
|
-
index = [i for i, par in enumerate(parameters)
|
|
1045
|
-
if par['name'] == par_name]
|
|
1046
|
-
if not len(index):
|
|
1047
|
-
parameters.append(
|
|
1048
|
-
{'name': par_name, 'min': min_value})
|
|
1049
|
-
elif len(index) == 1:
|
|
1050
|
-
parameter = parameters[index[0]]
|
|
1051
|
-
_min = parameter.get('min', min_value)
|
|
1052
|
-
parameter['min'] = max(_min, min_value)
|
|
1053
|
-
else:
|
|
1054
|
-
raise ValueError(
|
|
1055
|
-
'Invalid parameters value in '
|
|
1056
|
-
f'background model {name} '
|
|
1057
|
-
f'({parameters})')
|
|
1058
|
-
if name == 'gaussian':
|
|
1059
|
-
if parameters is None:
|
|
1060
|
-
parameters = {
|
|
1061
|
-
'name': 'center',
|
|
1062
|
-
'value': 0.5 * (
|
|
1063
|
-
centers_range[0] + centers_range[1]),
|
|
1064
|
-
'min': centers_range[0],
|
|
1065
|
-
'min': centers_range[1],
|
|
1066
|
-
}
|
|
1067
|
-
else:
|
|
1068
|
-
index = [i for i, par in enumerate(parameters)
|
|
1069
|
-
if par['name'] == 'center']
|
|
1070
|
-
if not len(index):
|
|
1071
|
-
parameters.append({
|
|
1072
|
-
'name': 'center',
|
|
1073
|
-
'value': 0.5 * (
|
|
1074
|
-
centers_range[0] + centers_range[1]),
|
|
1075
|
-
'min': centers_range[0],
|
|
1076
|
-
'max': centers_range[1],
|
|
1077
|
-
})
|
|
1078
|
-
elif len(index) == 1:
|
|
1079
|
-
parameter = parameters[index[0]]
|
|
1080
|
-
if 'value' not in parameter:
|
|
1081
|
-
parameter['value'] = 0.5 * (
|
|
1082
|
-
centers_range[0]+centers_range[1])
|
|
1083
|
-
_min = parameter.get('min', centers_range[0])
|
|
1084
|
-
parameter['min'] = max(_min, centers_range[0])
|
|
1085
|
-
_max = parameter.get('max', centers_range[1])
|
|
1086
|
-
parameter['max'] = min(_max, centers_range[1])
|
|
1087
|
-
else:
|
|
1088
|
-
raise ValueError(
|
|
1089
|
-
'Invalid parameters value in '
|
|
1090
|
-
f'background model {name} '
|
|
1091
|
-
f'({parameters})')
|
|
1092
|
-
self.add_model(
|
|
1093
|
-
name, prefix=prefix, parameters=parameters,
|
|
1094
|
-
**model)
|
|
1095
|
-
else:
|
|
1096
|
-
raise ValueError(
|
|
1097
|
-
f'Invalid parameter background ({background})')
|
|
1098
|
-
|
|
1099
|
-
# Add peaks and set initial fit parameters
|
|
1100
|
-
ast = Interpreter()
|
|
1101
|
-
if num_peaks == 1:
|
|
1102
|
-
sig_min = None
|
|
1103
|
-
if self._sigma_min is not None:
|
|
1104
|
-
ast(f'fwhm = {self._fwhm_min}')
|
|
1105
|
-
sig_min = ast(fwhm_factor[peak_models[0]])
|
|
1106
|
-
self._sigma_min[0] = sig_min
|
|
1107
|
-
sig_max = None
|
|
1108
|
-
if self._sigma_max is not None:
|
|
1109
|
-
ast(f'fwhm = {self._fwhm_max}')
|
|
1110
|
-
sig_max = ast(fwhm_factor[peak_models[0]])
|
|
1111
|
-
self._sigma_max[0] = sig_max
|
|
1112
|
-
self.add_model(
|
|
1113
|
-
peak_models[0],
|
|
1114
|
-
parameters=(
|
|
1115
|
-
{'name': 'amplitude', 'min': min_value},
|
|
1116
|
-
{'name': 'center', 'value': centers[0],
|
|
1117
|
-
'min': centers_range[0], 'max': centers_range[1]},
|
|
1118
|
-
{'name': 'sigma', 'min': sig_min, 'max': sig_max},
|
|
1119
|
-
))
|
|
1120
|
-
else:
|
|
1121
|
-
if fit_type == 'uniform':
|
|
1122
|
-
self.add_parameter(
|
|
1123
|
-
name='scale_factor', value=1.0, min=min_value)
|
|
1124
|
-
for i in range(num_peaks):
|
|
1125
|
-
sig_min = None
|
|
1126
|
-
if self._sigma_min is not None:
|
|
1127
|
-
ast(f'fwhm = {self._fwhm_min}')
|
|
1128
|
-
sig_min = ast(fwhm_factor[peak_models[i]])
|
|
1129
|
-
self._sigma_min[i] = sig_min
|
|
1130
|
-
sig_max = None
|
|
1131
|
-
if self._sigma_max is not None:
|
|
1132
|
-
ast(f'fwhm = {self._fwhm_max}')
|
|
1133
|
-
sig_max = ast(fwhm_factor[peak_models[i]])
|
|
1134
|
-
self._sigma_max[i] = sig_max
|
|
1135
|
-
if fit_type == 'uniform':
|
|
1136
|
-
self.add_model(
|
|
1137
|
-
peak_models[i], prefix=f'peak{i+1}_',
|
|
1138
|
-
parameters=(
|
|
1139
|
-
{'name': 'amplitude', 'min': min_value},
|
|
1140
|
-
{'name': 'center', 'expr': center_exprs[i]},
|
|
1141
|
-
{'name': 'sigma', 'min': sig_min, 'max': sig_max},
|
|
1142
|
-
))
|
|
1143
|
-
else:
|
|
1144
|
-
self.add_model(
|
|
1145
|
-
'gaussian',
|
|
1146
|
-
prefix=f'peak{i+1}_',
|
|
1147
|
-
parameters=(
|
|
1148
|
-
{'name': 'amplitude', 'min': min_value},
|
|
1149
|
-
{'name': 'center', 'value': centers[i],
|
|
1150
|
-
'min': centers_range[0], 'max': centers_range[1]},
|
|
1151
|
-
{'name': 'sigma', 'min': min_value,
|
|
1152
|
-
'max': sig_max},
|
|
1153
|
-
))
|
|
1065
|
+
'Ignoring input "value" for expression parameter'
|
|
1066
|
+
f'{name} = {parameter.expr}')
|
|
1067
|
+
if not np.isinf(parameter.min):
|
|
1068
|
+
logger.warning(
|
|
1069
|
+
'Ignoring input "min" for expression parameter'
|
|
1070
|
+
f'{name} = {parameter.expr}')
|
|
1071
|
+
if not np.isinf(parameter.max):
|
|
1072
|
+
logger.warning(
|
|
1073
|
+
'Ignoring input "max" for expression parameter'
|
|
1074
|
+
f'{name} = {parameter.expr}')
|
|
1075
|
+
self._parameters[name].set(
|
|
1076
|
+
value=None, min=-np.inf, max=np.inf, expr=parameter.expr)
|
|
1154
1077
|
|
|
1155
1078
|
def eval(self, x, result=None):
|
|
1156
1079
|
"""Evaluate the best fit."""
|
|
@@ -1160,170 +1083,39 @@ class Fit:
|
|
|
1160
1083
|
return None
|
|
1161
1084
|
return result.eval(x=np.asarray(x))-self.normalization_offset
|
|
1162
1085
|
|
|
1163
|
-
def fit(self, **kwargs):
|
|
1086
|
+
def fit(self, config=None, **kwargs):
|
|
1164
1087
|
"""Fit the model to the input data."""
|
|
1165
|
-
# Third party modules
|
|
1166
|
-
from asteval import Interpreter
|
|
1167
1088
|
|
|
1168
1089
|
# Check input parameters
|
|
1169
1090
|
if self._model is None:
|
|
1170
1091
|
logger.error('Undefined fit model')
|
|
1171
1092
|
return None
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
f'({interactive})')
|
|
1178
|
-
else:
|
|
1179
|
-
interactive = False
|
|
1180
|
-
if 'guess' in kwargs:
|
|
1181
|
-
guess = kwargs.pop('guess')
|
|
1182
|
-
if not isinstance(guess, bool):
|
|
1183
|
-
raise ValueError(
|
|
1184
|
-
f'Invalid value of keyword argument guess ({guess})')
|
|
1185
|
-
else:
|
|
1186
|
-
guess = False
|
|
1093
|
+
self._mask = kwargs.pop('mask', None)
|
|
1094
|
+
guess = kwargs.pop('guess', False)
|
|
1095
|
+
if not isinstance(guess, bool):
|
|
1096
|
+
raise ValueError(
|
|
1097
|
+
f'Invalid value of keyword argument guess ({guess})')
|
|
1187
1098
|
if self._result is not None:
|
|
1188
1099
|
if guess:
|
|
1189
1100
|
logger.warning(
|
|
1190
1101
|
'Ignoring input parameter guess during refitting')
|
|
1191
1102
|
guess = False
|
|
1192
1103
|
if 'try_linear_fit' in kwargs:
|
|
1104
|
+
raise RuntimeError('try_linear_fit needs testing')
|
|
1193
1105
|
try_linear_fit = kwargs.pop('try_linear_fit')
|
|
1194
1106
|
if not isinstance(try_linear_fit, bool):
|
|
1195
1107
|
raise ValueError(
|
|
1196
1108
|
'Invalid value of keyword argument try_linear_fit '
|
|
1197
1109
|
f'({try_linear_fit})')
|
|
1198
|
-
if not self._try_linear_fit:
|
|
1199
|
-
logger.warning(
|
|
1200
|
-
'Ignore superfluous keyword argument "try_linear_fit" '
|
|
1201
|
-
'(not yet supported for callable models)')
|
|
1202
|
-
else:
|
|
1203
|
-
self._try_linear_fit = try_linear_fit
|
|
1204
|
-
|
|
1205
|
-
# Apply mask if supplied:
|
|
1206
|
-
if 'mask' in kwargs:
|
|
1207
|
-
self._mask = kwargs.pop('mask')
|
|
1208
|
-
if self._mask is not None:
|
|
1209
|
-
self._mask = np.asarray(self._mask).astype(bool)
|
|
1210
|
-
if self._x.size != self._mask.size:
|
|
1211
|
-
raise ValueError(
|
|
1212
|
-
f'Inconsistent x and mask dimensions ({self._x.size} vs '
|
|
1213
|
-
f'{self._mask.size})')
|
|
1214
|
-
|
|
1215
|
-
# Estimate initial parameters
|
|
1216
|
-
if guess:
|
|
1217
|
-
if self._mask is None:
|
|
1218
|
-
xx = self._x
|
|
1219
|
-
yy = self._y
|
|
1220
|
-
else:
|
|
1221
|
-
xx = self._x[~self._mask]
|
|
1222
|
-
yy = np.asarray(self._y)[~self._mask]
|
|
1223
|
-
try:
|
|
1224
|
-
# Try with the build-in lmfit guess method
|
|
1225
|
-
# (only implemented for a single model)
|
|
1226
|
-
self._parameters = self._model.guess(yy, x=xx)
|
|
1227
|
-
except:
|
|
1228
|
-
ast = Interpreter()
|
|
1229
|
-
# Should work for other peak-like models,
|
|
1230
|
-
# but will need tests first
|
|
1231
|
-
for component in self._model.components:
|
|
1232
|
-
if isinstance(component, GaussianModel):
|
|
1233
|
-
center = self._parameters[
|
|
1234
|
-
f"{component.prefix}center"].value
|
|
1235
|
-
height_init, cen_init, fwhm_init = \
|
|
1236
|
-
self.guess_init_peak(
|
|
1237
|
-
xx, yy, center_guess=center,
|
|
1238
|
-
use_max_for_center=False)
|
|
1239
|
-
if (self._fwhm_min is not None
|
|
1240
|
-
and fwhm_init < self._fwhm_min):
|
|
1241
|
-
fwhm_init = self._fwhm_min
|
|
1242
|
-
elif (self._fwhm_max is not None
|
|
1243
|
-
and fwhm_init > self._fwhm_max):
|
|
1244
|
-
fwhm_init = self._fwhm_max
|
|
1245
|
-
ast(f'fwhm = {fwhm_init}')
|
|
1246
|
-
ast(f'height = {height_init}')
|
|
1247
|
-
sig_init = ast(fwhm_factor[component._name])
|
|
1248
|
-
amp_init = ast(height_factor[component._name])
|
|
1249
|
-
par = self._parameters[
|
|
1250
|
-
f"{component.prefix}amplitude"]
|
|
1251
|
-
if par.vary:
|
|
1252
|
-
par.set(value=amp_init)
|
|
1253
|
-
par = self._parameters[
|
|
1254
|
-
f"{component.prefix}center"]
|
|
1255
|
-
if par.vary:
|
|
1256
|
-
par.set(value=cen_init)
|
|
1257
|
-
par = self._parameters[
|
|
1258
|
-
f"{component.prefix}sigma"]
|
|
1259
|
-
if par.vary:
|
|
1260
|
-
par.set(value=sig_init)
|
|
1261
|
-
|
|
1262
|
-
# Add constant offset for a normalized model
|
|
1263
|
-
if self._result is None and self._norm is not None and self._norm[0]:
|
|
1264
|
-
self.add_model(
|
|
1265
|
-
'constant', prefix='tmp_normalization_offset_',
|
|
1266
|
-
parameters={
|
|
1267
|
-
'name': 'c',
|
|
1268
|
-
'value': -self._norm[0],
|
|
1269
|
-
'vary': False,
|
|
1270
|
-
'norm': True,
|
|
1271
|
-
})
|
|
1272
|
-
# 'value': -self._norm[0]/self._norm[1],
|
|
1273
|
-
# 'vary': False,
|
|
1274
|
-
# 'norm': False,
|
|
1275
|
-
|
|
1276
|
-
# Adjust existing parameters for refit:
|
|
1277
|
-
if 'parameters' in kwargs:
|
|
1278
|
-
parameters = kwargs.pop('parameters')
|
|
1279
|
-
if isinstance(parameters, dict):
|
|
1280
|
-
parameters = (parameters, )
|
|
1281
|
-
elif not is_dict_series(parameters):
|
|
1282
|
-
raise ValueError(
|
|
1283
|
-
'Invalid value of keyword argument parameters '
|
|
1284
|
-
f'({parameters})')
|
|
1285
|
-
for par in parameters:
|
|
1286
|
-
name = par['name']
|
|
1287
|
-
if name not in self._parameters:
|
|
1288
|
-
raise ValueError(
|
|
1289
|
-
f'Unable to match {name} parameter {par} to an '
|
|
1290
|
-
'existing one')
|
|
1291
|
-
if self._parameters[name].expr is not None:
|
|
1292
|
-
raise ValueError(
|
|
1293
|
-
f'Unable to modify {name} parameter {par} '
|
|
1294
|
-
'(currently an expression)')
|
|
1295
|
-
if par.get('expr') is not None:
|
|
1296
|
-
raise KeyError(
|
|
1297
|
-
f'Invalid "expr" key in {name} parameter {par}')
|
|
1298
|
-
self._parameters[name].set(vary=par.get('vary'))
|
|
1299
|
-
self._parameters[name].set(min=par.get('min'))
|
|
1300
|
-
self._parameters[name].set(max=par.get('max'))
|
|
1301
|
-
self._parameters[name].set(value=par.get('value'))
|
|
1302
|
-
|
|
1303
|
-
# Apply parameter updates through keyword arguments
|
|
1304
|
-
for name in set(self._parameters) & set(kwargs):
|
|
1305
|
-
value = kwargs.pop(name)
|
|
1306
|
-
if self._parameters[name].expr is None:
|
|
1307
|
-
self._parameters[name].set(value=value)
|
|
1308
|
-
else:
|
|
1110
|
+
if not self._try_linear_fit:
|
|
1309
1111
|
logger.warning(
|
|
1310
|
-
|
|
1311
|
-
|
|
1112
|
+
'Ignore superfluous keyword argument "try_linear_fit" '
|
|
1113
|
+
'(not yet supported for callable models)')
|
|
1114
|
+
else:
|
|
1115
|
+
self._try_linear_fit = try_linear_fit
|
|
1312
1116
|
|
|
1313
|
-
#
|
|
1314
|
-
|
|
1315
|
-
if par.expr is None:
|
|
1316
|
-
value = par.value
|
|
1317
|
-
if value is None or np.isinf(value) or np.isnan(value):
|
|
1318
|
-
if interactive:
|
|
1319
|
-
value = input_num(
|
|
1320
|
-
f'Enter an initial value for {name}', default=1.0)
|
|
1321
|
-
else:
|
|
1322
|
-
value = 1.0
|
|
1323
|
-
if self._norm is None or name not in self._parameter_norms:
|
|
1324
|
-
self._parameters[name].set(value=value)
|
|
1325
|
-
elif self._parameter_norms[name]:
|
|
1326
|
-
self._parameters[name].set(value=value*self._norm[1])
|
|
1117
|
+
# Setup the fit
|
|
1118
|
+
self._setup_fit(config, guess)
|
|
1327
1119
|
|
|
1328
1120
|
# Check if model is linear
|
|
1329
1121
|
try:
|
|
@@ -1337,6 +1129,7 @@ class Fit:
|
|
|
1337
1129
|
self._normalize()
|
|
1338
1130
|
|
|
1339
1131
|
if linear_model:
|
|
1132
|
+
raise RuntimeError('linear solver needs testing')
|
|
1340
1133
|
# Perform a linear fit by direct matrix solution with numpy
|
|
1341
1134
|
try:
|
|
1342
1135
|
if self._mask is None:
|
|
@@ -1348,27 +1141,8 @@ class Fit:
|
|
|
1348
1141
|
except:
|
|
1349
1142
|
linear_model = False
|
|
1350
1143
|
if not linear_model:
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
self._parameter_bounds = {
|
|
1354
|
-
name:{'min': par.min, 'max': par.max}
|
|
1355
|
-
for name, par in self._parameters.items() if par.vary}
|
|
1356
|
-
self._reset_par_at_boundary()
|
|
1357
|
-
|
|
1358
|
-
# Perform the fit
|
|
1359
|
-
fit_kws = {}
|
|
1360
|
-
if self._param_constraint:
|
|
1361
|
-
fit_kws = {'xtol': 1.e-5, 'ftol': 1.e-5, 'gtol': 1.e-5}
|
|
1362
|
-
# if 'Dfun' in kwargs:
|
|
1363
|
-
# fit_kws['Dfun'] = kwargs.pop('Dfun')
|
|
1364
|
-
if self._mask is None:
|
|
1365
|
-
self._result = self._model.fit(
|
|
1366
|
-
self._y_norm, self._parameters, x=self._x, fit_kws=fit_kws,
|
|
1367
|
-
**kwargs)
|
|
1368
|
-
else:
|
|
1369
|
-
self._result = self._model.fit(
|
|
1370
|
-
np.asarray(self._y_norm)[~self._mask], self._parameters,
|
|
1371
|
-
x=self._x[~self._mask], fit_kws=fit_kws, **kwargs)
|
|
1144
|
+
self._result = self._fit_nonlinear_model(
|
|
1145
|
+
self._x, self._y_norm, **kwargs)
|
|
1372
1146
|
|
|
1373
1147
|
# Set internal parameter values to fit results upon success
|
|
1374
1148
|
if self.success:
|
|
@@ -1561,11 +1335,248 @@ class Fit:
|
|
|
1561
1335
|
|
|
1562
1336
|
return height, center, fwhm
|
|
1563
1337
|
|
|
1338
|
+
def _create_prefixes(self, models):
|
|
1339
|
+
# Check for duplicate model names and create prefixes
|
|
1340
|
+
names = []
|
|
1341
|
+
prefixes = []
|
|
1342
|
+
for model in models:
|
|
1343
|
+
names.append(f'{model.prefix}{model.model}')
|
|
1344
|
+
prefixes.append(model.prefix)
|
|
1345
|
+
counts = Counter(names)
|
|
1346
|
+
for model, count in counts.items():
|
|
1347
|
+
if count > 1:
|
|
1348
|
+
n = 0
|
|
1349
|
+
for i, name in enumerate(names):
|
|
1350
|
+
if name == model:
|
|
1351
|
+
n += 1
|
|
1352
|
+
prefixes[i] = f'{name}{n}_'
|
|
1353
|
+
|
|
1354
|
+
return prefixes
|
|
1355
|
+
|
|
1356
|
+
def _setup_fit_model(self, parameters, models):
|
|
1357
|
+
"""Setup the fit model."""
|
|
1358
|
+
# Check for duplicate model names and create prefixes
|
|
1359
|
+
prefixes = self._create_prefixes(models)
|
|
1360
|
+
|
|
1361
|
+
# Add the free fit parameters
|
|
1362
|
+
for par in parameters:
|
|
1363
|
+
self.add_parameter(par.dict())
|
|
1364
|
+
|
|
1365
|
+
# Add the model functions
|
|
1366
|
+
for prefix, model in zip(prefixes, models):
|
|
1367
|
+
self.add_model(model, prefix)
|
|
1368
|
+
|
|
1369
|
+
# Check linearity of free fit parameters:
|
|
1370
|
+
known_parameters = (
|
|
1371
|
+
self._linear_parameters + self._nonlinear_parameters)
|
|
1372
|
+
for name in reversed(self._parameters):
|
|
1373
|
+
if name not in known_parameters:
|
|
1374
|
+
for nname, par in self._parameters.items():
|
|
1375
|
+
if par.expr is not None:
|
|
1376
|
+
# Third party modules
|
|
1377
|
+
from sympy import diff
|
|
1378
|
+
|
|
1379
|
+
if nname in self._nonlinear_parameters:
|
|
1380
|
+
self._nonlinear_parameters.insert(0, name)
|
|
1381
|
+
elif diff(par.expr, name, name):
|
|
1382
|
+
self._nonlinear_parameters.insert(0, name)
|
|
1383
|
+
else:
|
|
1384
|
+
self._linear_parameters.insert(0, name)
|
|
1385
|
+
|
|
1386
|
+
def _setup_fit(self, config, guess=False):
|
|
1387
|
+
"""Setup the fit."""
|
|
1388
|
+
# Apply mask if supplied:
|
|
1389
|
+
if self._mask is not None:
|
|
1390
|
+
raise RuntimeError('mask needs testing')
|
|
1391
|
+
self._mask = np.asarray(self._mask).astype(bool)
|
|
1392
|
+
if self._x.size != self._mask.size:
|
|
1393
|
+
raise ValueError(
|
|
1394
|
+
f'Inconsistent x and mask dimensions ({self._x.size} vs '
|
|
1395
|
+
f'{self._mask.size})')
|
|
1396
|
+
|
|
1397
|
+
# Estimate initial parameters
|
|
1398
|
+
if guess and not isinstance(self, FitMap):
|
|
1399
|
+
raise RuntimeError('Estimate initial parameters needs testing')
|
|
1400
|
+
if self._mask is None:
|
|
1401
|
+
xx = self._x
|
|
1402
|
+
yy = self._y
|
|
1403
|
+
else:
|
|
1404
|
+
xx = self._x[~self._mask]
|
|
1405
|
+
yy = np.asarray(self._y)[~self._mask]
|
|
1406
|
+
try:
|
|
1407
|
+
# Try with the build-in lmfit guess method
|
|
1408
|
+
# (only implemented for a single model)
|
|
1409
|
+
self._parameters = self._model.guess(yy, x=xx)
|
|
1410
|
+
except:
|
|
1411
|
+
# Third party modules
|
|
1412
|
+
from asteval import Interpreter
|
|
1413
|
+
from lmfit.models import GaussianModel
|
|
1414
|
+
|
|
1415
|
+
ast = Interpreter()
|
|
1416
|
+
# Should work for other peak-like models,
|
|
1417
|
+
# but will need tests first
|
|
1418
|
+
for component in self._model.components:
|
|
1419
|
+
if isinstance(component, GaussianModel):
|
|
1420
|
+
center = self._parameters[
|
|
1421
|
+
f"{component.prefix}center"].value
|
|
1422
|
+
height_init, cen_init, fwhm_init = \
|
|
1423
|
+
self.guess_init_peak(
|
|
1424
|
+
xx, yy, center_guess=center,
|
|
1425
|
+
use_max_for_center=False)
|
|
1426
|
+
if (self._fwhm_min is not None
|
|
1427
|
+
and fwhm_init < self._fwhm_min):
|
|
1428
|
+
fwhm_init = self._fwhm_min
|
|
1429
|
+
elif (self._fwhm_max is not None
|
|
1430
|
+
and fwhm_init > self._fwhm_max):
|
|
1431
|
+
fwhm_init = self._fwhm_max
|
|
1432
|
+
ast(f'fwhm = {fwhm_init}')
|
|
1433
|
+
ast(f'height = {height_init}')
|
|
1434
|
+
sig_init = ast(fwhm_factor[component._name])
|
|
1435
|
+
amp_init = ast(height_factor[component._name])
|
|
1436
|
+
par = self._parameters[
|
|
1437
|
+
f"{component.prefix}amplitude"]
|
|
1438
|
+
if par.vary:
|
|
1439
|
+
par.set(value=amp_init)
|
|
1440
|
+
par = self._parameters[
|
|
1441
|
+
f"{component.prefix}center"]
|
|
1442
|
+
if par.vary:
|
|
1443
|
+
par.set(value=cen_init)
|
|
1444
|
+
par = self._parameters[
|
|
1445
|
+
f"{component.prefix}sigma"]
|
|
1446
|
+
if par.vary:
|
|
1447
|
+
par.set(value=sig_init)
|
|
1448
|
+
|
|
1449
|
+
# Add constant offset for a normalized model
|
|
1450
|
+
if self._result is None and self._norm is not None and self._norm[0]:
|
|
1451
|
+
from CHAP.utils.models import Constant
|
|
1452
|
+
model = Constant(
|
|
1453
|
+
model='constant',
|
|
1454
|
+
parameters=[{
|
|
1455
|
+
'name': 'c',
|
|
1456
|
+
'value': -self._norm[0],
|
|
1457
|
+
'vary': False,
|
|
1458
|
+
}])
|
|
1459
|
+
self.add_model(model, 'tmp_normalization_offset_')
|
|
1460
|
+
|
|
1461
|
+
# Adjust existing parameters for refit:
|
|
1462
|
+
if config is not None:
|
|
1463
|
+
# Local modules
|
|
1464
|
+
from CHAP.utils.models import (
|
|
1465
|
+
FitConfig,
|
|
1466
|
+
Multipeak,
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
# Expand multipeak model if present
|
|
1470
|
+
scale_factor = None
|
|
1471
|
+
for i, model in enumerate(deepcopy(config.models)):
|
|
1472
|
+
found_multipeak = False
|
|
1473
|
+
if isinstance(model, Multipeak):
|
|
1474
|
+
if found_multipeak:
|
|
1475
|
+
raise ValueError(
|
|
1476
|
+
f'Invalid parameter models ({config.models}) '
|
|
1477
|
+
'(multiple instances of multipeak not allowed)')
|
|
1478
|
+
if (model.fit_type == 'uniform'
|
|
1479
|
+
and 'scale_factor' not in self._free_parameters):
|
|
1480
|
+
raise ValueError(
|
|
1481
|
+
f'Invalid parameter models ({config.models}) '
|
|
1482
|
+
'(uniform multipeak fit after unconstrained fit)')
|
|
1483
|
+
parameters, models = FitProcessor.create_multipeak_model(
|
|
1484
|
+
model)
|
|
1485
|
+
if (model.fit_type == 'unconstrained'
|
|
1486
|
+
and 'scale_factor' in self._free_parameters):
|
|
1487
|
+
# Third party modules
|
|
1488
|
+
from asteval import Interpreter
|
|
1489
|
+
|
|
1490
|
+
scale_factor = self._parameters['scale_factor'].value
|
|
1491
|
+
self._parameters.pop('scale_factor')
|
|
1492
|
+
self._free_parameters.remove('scale_factor')
|
|
1493
|
+
ast = Interpreter()
|
|
1494
|
+
ast(f'scale_factor = {scale_factor}')
|
|
1495
|
+
if parameters:
|
|
1496
|
+
config.parameters += parameters
|
|
1497
|
+
config.models += models
|
|
1498
|
+
config.models.remove(model)
|
|
1499
|
+
found_multipeak = True
|
|
1500
|
+
|
|
1501
|
+
# Check for duplicate model names and create prefixes
|
|
1502
|
+
prefixes = self._create_prefixes(config.models)
|
|
1503
|
+
if not isinstance(config, FitConfig):
|
|
1504
|
+
raise ValueError(f'Invalid parameter config ({config})')
|
|
1505
|
+
parameters = config.parameters
|
|
1506
|
+
for prefix, model in zip(prefixes, config.models):
|
|
1507
|
+
for par in model.parameters:
|
|
1508
|
+
par.name = f'{prefix}{par.name}'
|
|
1509
|
+
parameters += model.parameters
|
|
1510
|
+
|
|
1511
|
+
# Adjust parameters for refit as needed
|
|
1512
|
+
if isinstance(self, FitMap):
|
|
1513
|
+
scale_factor_index = \
|
|
1514
|
+
self._best_parameters.index('scale_factor')
|
|
1515
|
+
self._best_parameters.pop(scale_factor_index)
|
|
1516
|
+
self._best_values = np.delete(
|
|
1517
|
+
self._best_values, scale_factor_index, 0)
|
|
1518
|
+
self._best_errors = np.delete(
|
|
1519
|
+
self._best_errors, scale_factor_index, 0)
|
|
1520
|
+
for par in parameters:
|
|
1521
|
+
name = par.name
|
|
1522
|
+
if name not in self._parameters:
|
|
1523
|
+
raise ValueError(
|
|
1524
|
+
f'Unable to match {name} parameter {par} to an '
|
|
1525
|
+
'existing one')
|
|
1526
|
+
ppar = self._parameters[name]
|
|
1527
|
+
if ppar.expr is not None:
|
|
1528
|
+
if (scale_factor is not None and 'center' in name
|
|
1529
|
+
and 'scale_factor' in ppar.expr):
|
|
1530
|
+
ppar.set(value=ast(ppar.expr), expr='')
|
|
1531
|
+
value = ppar.value
|
|
1532
|
+
else:
|
|
1533
|
+
raise ValueError(
|
|
1534
|
+
f'Unable to modify {name} parameter {par} '
|
|
1535
|
+
'(currently an expression)')
|
|
1536
|
+
else:
|
|
1537
|
+
value = par.value
|
|
1538
|
+
if par.expr is not None:
|
|
1539
|
+
raise KeyError(
|
|
1540
|
+
f'Invalid "expr" key in {name} parameter {par}')
|
|
1541
|
+
ppar.set(
|
|
1542
|
+
value=value, min=par.min, max=par.max, vary=par.vary)
|
|
1543
|
+
|
|
1544
|
+
# Set parameters configuration
|
|
1545
|
+
if self._code == 'scipy':
|
|
1546
|
+
self._res_par_exprs = []
|
|
1547
|
+
self._res_par_indices = []
|
|
1548
|
+
self._res_par_names = []
|
|
1549
|
+
self._res_par_values = []
|
|
1550
|
+
for i, (name, par) in enumerate(self._parameters.items()):
|
|
1551
|
+
self._res_par_values.append(par.value)
|
|
1552
|
+
if par.expr:
|
|
1553
|
+
self._res_par_exprs.append(
|
|
1554
|
+
{'expr': par.expr, 'index': i})
|
|
1555
|
+
else:
|
|
1556
|
+
if par.vary:
|
|
1557
|
+
self._res_par_indices.append(i)
|
|
1558
|
+
self._res_par_names.append(name)
|
|
1559
|
+
|
|
1560
|
+
# Check for uninitialized parameters
|
|
1561
|
+
for name, par in self._parameters.items():
|
|
1562
|
+
if par.expr is None:
|
|
1563
|
+
value = par.value
|
|
1564
|
+
if value is None or np.isinf(value) or np.isnan(value):
|
|
1565
|
+
if (self._norm is None
|
|
1566
|
+
or name in self._nonlinear_parameters):
|
|
1567
|
+
self._parameters[name].set(value=1.0)
|
|
1568
|
+
else:
|
|
1569
|
+
self._parameters[name].set(value=self._norm[1])
|
|
1570
|
+
|
|
1564
1571
|
def _check_linearity_model(self):
|
|
1565
1572
|
"""
|
|
1566
1573
|
Identify the linearity of all model parameters and check if
|
|
1567
1574
|
the model is linear or not.
|
|
1568
1575
|
"""
|
|
1576
|
+
# Third party modules
|
|
1577
|
+
from lmfit.models import ExpressionModel
|
|
1578
|
+
from sympy import diff
|
|
1579
|
+
|
|
1569
1580
|
if not self._try_linear_fit:
|
|
1570
1581
|
logger.info(
|
|
1571
1582
|
'Skip linearity check (not yet supported for callable models)')
|
|
@@ -1613,6 +1624,18 @@ class Fit:
|
|
|
1613
1624
|
"""
|
|
1614
1625
|
# Third party modules
|
|
1615
1626
|
from asteval import Interpreter
|
|
1627
|
+
from lmfit.model import ModelResult
|
|
1628
|
+
from lmfit.models import (
|
|
1629
|
+
ConstantModel,
|
|
1630
|
+
LinearModel,
|
|
1631
|
+
QuadraticModel,
|
|
1632
|
+
ExpressionModel,
|
|
1633
|
+
)
|
|
1634
|
+
# Third party modules
|
|
1635
|
+
from sympy import (
|
|
1636
|
+
diff,
|
|
1637
|
+
simplify,
|
|
1638
|
+
)
|
|
1616
1639
|
|
|
1617
1640
|
# Construct the matrix and the free parameter vector
|
|
1618
1641
|
free_parameters = \
|
|
@@ -1678,8 +1701,6 @@ class Fit:
|
|
|
1678
1701
|
raise ValueError(
|
|
1679
1702
|
f'Unable to evaluate {dexpr_dname}')
|
|
1680
1703
|
mat_a[:,free_parameters.index(name)] += y_expr
|
|
1681
|
-
# RV find another solution if expr not supported by
|
|
1682
|
-
# simplify
|
|
1683
1704
|
const_expr = str(simplify(f'({const_expr})/{norm}'))
|
|
1684
1705
|
delta_y_const = [(lambda _: ast.eval(const_expr))
|
|
1685
1706
|
(ast(f'x = {v}')) for v in x]
|
|
@@ -1776,7 +1797,9 @@ class Fit:
|
|
|
1776
1797
|
par = self._parameters[name]
|
|
1777
1798
|
if par.expr is None and norm:
|
|
1778
1799
|
self._parameters[name].set(value=par.value*self._norm[1])
|
|
1779
|
-
|
|
1800
|
+
#RV FIX
|
|
1801
|
+
self._result = ModelResult(
|
|
1802
|
+
self._model, deepcopy(self._parameters), 'linear')
|
|
1780
1803
|
self._result.best_fit = self._model.eval(params=self._parameters, x=x)
|
|
1781
1804
|
if (self._normalized
|
|
1782
1805
|
and (have_expression_model or expr_parameters)):
|
|
@@ -1793,10 +1816,103 @@ class Fit:
|
|
|
1793
1816
|
value = par.value/self._norm[1]
|
|
1794
1817
|
self._parameters[name].set(value=value)
|
|
1795
1818
|
self._result.params[name].set(value=value)
|
|
1796
|
-
self._result.residual = self._result.best_fit
|
|
1819
|
+
self._result.residual = y-self._result.best_fit
|
|
1797
1820
|
self._result.components = self._model.components
|
|
1798
1821
|
self._result.init_params = None
|
|
1799
1822
|
|
|
1823
|
+
def _fit_nonlinear_model(self, x, y, **kwargs):
|
|
1824
|
+
"""
|
|
1825
|
+
Perform a nonlinear fit with spipy or lmfit
|
|
1826
|
+
"""
|
|
1827
|
+
# Check bounds and prevent initial values at boundaries
|
|
1828
|
+
have_bounds = False
|
|
1829
|
+
self._parameter_bounds = {}
|
|
1830
|
+
for name, par in self._parameters.items():
|
|
1831
|
+
if par.vary:
|
|
1832
|
+
self._parameter_bounds[name] = {
|
|
1833
|
+
'min': par.min, 'max': par.max}
|
|
1834
|
+
if not have_bounds and (
|
|
1835
|
+
not np.isinf(par.min) or not np.isinf(par.max)):
|
|
1836
|
+
have_bounds = True
|
|
1837
|
+
if have_bounds:
|
|
1838
|
+
self._reset_par_at_boundary()
|
|
1839
|
+
|
|
1840
|
+
# Perform the fit
|
|
1841
|
+
if self._mask is not None:
|
|
1842
|
+
x = x[~self._mask]
|
|
1843
|
+
y = np.asarray(y)[~self._mask]
|
|
1844
|
+
if self._code == 'scipy':
|
|
1845
|
+
# Third party modules
|
|
1846
|
+
from asteval import Interpreter
|
|
1847
|
+
from scipy.optimize import (
|
|
1848
|
+
leastsq,
|
|
1849
|
+
least_squares,
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
assert self._mask is None
|
|
1853
|
+
self._ast = Interpreter()
|
|
1854
|
+
self._ast.basesymtable = {
|
|
1855
|
+
k:v for k, v in self._ast.symtable.items()}
|
|
1856
|
+
pars_init = []
|
|
1857
|
+
for i, (name, par) in enumerate(self._parameters.items()):
|
|
1858
|
+
setattr(par, '_init_value', par.value)
|
|
1859
|
+
self._res_par_values[i] = par.value
|
|
1860
|
+
if par.expr is None:
|
|
1861
|
+
self._ast.symtable[name] = par.value
|
|
1862
|
+
if par.vary:
|
|
1863
|
+
pars_init.append(par.value)
|
|
1864
|
+
if have_bounds:
|
|
1865
|
+
bounds = (
|
|
1866
|
+
[v['min'] for v in self._parameter_bounds.values()],
|
|
1867
|
+
[v['max'] for v in self._parameter_bounds.values()])
|
|
1868
|
+
if self._method in ('lm', 'leastsq'):
|
|
1869
|
+
self._method = 'trf'
|
|
1870
|
+
logger.warning(
|
|
1871
|
+
f'Fit method changed to {self._method} for fit with '
|
|
1872
|
+
'bounds')
|
|
1873
|
+
else:
|
|
1874
|
+
bounds = (-np.inf, np.inf)
|
|
1875
|
+
init_params = deepcopy(self._parameters)
|
|
1876
|
+
# t0 = time()
|
|
1877
|
+
lskws = {
|
|
1878
|
+
'ftol': 1.49012e-08,
|
|
1879
|
+
'xtol': 1.49012e-08,
|
|
1880
|
+
'gtol': 10*FLOAT_EPS,
|
|
1881
|
+
}
|
|
1882
|
+
if self._method == 'leastsq':
|
|
1883
|
+
lskws['maxfev'] = 64000
|
|
1884
|
+
result = leastsq(
|
|
1885
|
+
self._residual, pars_init, args=(x, y), full_output=True,
|
|
1886
|
+
**lskws)
|
|
1887
|
+
else:
|
|
1888
|
+
lskws['max_nfev'] = 64000
|
|
1889
|
+
result = least_squares(
|
|
1890
|
+
self._residual, pars_init, bounds=bounds,
|
|
1891
|
+
method=self._method, args=(x, y), **lskws)
|
|
1892
|
+
# t1 = time()
|
|
1893
|
+
# print(f'\n\nFitting took {1000*(t1-t0):.3f} ms\n\n')
|
|
1894
|
+
model_result = ModelResult(
|
|
1895
|
+
self._model, self._parameters, x, y, self._method, self._ast,
|
|
1896
|
+
self._res_par_exprs, self._res_par_indices,
|
|
1897
|
+
self._res_par_names, result)
|
|
1898
|
+
model_result.init_params = init_params
|
|
1899
|
+
model_result.init_values = {}
|
|
1900
|
+
for name, par in init_params.items():
|
|
1901
|
+
model_result.init_values[name] = par.value
|
|
1902
|
+
model_result.max_nfev = lskws.get('maxfev')
|
|
1903
|
+
else:
|
|
1904
|
+
fit_kws = {}
|
|
1905
|
+
# if 'Dfun' in kwargs:
|
|
1906
|
+
# fit_kws['Dfun'] = kwargs.pop('Dfun')
|
|
1907
|
+
# t0 = time()
|
|
1908
|
+
model_result = self._model.fit(
|
|
1909
|
+
y, self._parameters, x=x, method=self._method, fit_kws=fit_kws,
|
|
1910
|
+
**kwargs)
|
|
1911
|
+
# t1 = time()
|
|
1912
|
+
# print(f'\n\nFitting took {1000*(t1-t0):.3f} ms\n\n')
|
|
1913
|
+
|
|
1914
|
+
return model_result
|
|
1915
|
+
|
|
1800
1916
|
def _normalize(self):
|
|
1801
1917
|
"""Normalize the data and initial parameters."""
|
|
1802
1918
|
if self._normalized:
|
|
@@ -1809,9 +1925,9 @@ class Fit:
|
|
|
1809
1925
|
self._y_norm = \
|
|
1810
1926
|
(np.asarray(self._y)-self._norm[0]) / self._norm[1]
|
|
1811
1927
|
self._y_range = 1.0
|
|
1812
|
-
for name
|
|
1928
|
+
for name in self._linear_parameters:
|
|
1813
1929
|
par = self._parameters[name]
|
|
1814
|
-
if par.expr is None
|
|
1930
|
+
if par.expr is None:
|
|
1815
1931
|
value = par.value/self._norm[1]
|
|
1816
1932
|
_min = par.min
|
|
1817
1933
|
_max = par.max
|
|
@@ -1827,9 +1943,9 @@ class Fit:
|
|
|
1827
1943
|
if self._norm is None or not self._normalized:
|
|
1828
1944
|
return
|
|
1829
1945
|
self._normalized = False
|
|
1830
|
-
for name
|
|
1946
|
+
for name in self._linear_parameters:
|
|
1831
1947
|
par = self._parameters[name]
|
|
1832
|
-
if par.expr is None
|
|
1948
|
+
if par.expr is None:
|
|
1833
1949
|
value = par.value*self._norm[1]
|
|
1834
1950
|
_min = par.min
|
|
1835
1951
|
_max = par.max
|
|
@@ -1843,15 +1959,22 @@ class Fit:
|
|
|
1843
1959
|
self._result.best_fit = (
|
|
1844
1960
|
self._result.best_fit*self._norm[1] + self._norm[0])
|
|
1845
1961
|
for name, par in self._result.params.items():
|
|
1846
|
-
if self.
|
|
1962
|
+
if name in self._linear_parameters:
|
|
1847
1963
|
if par.stderr is not None:
|
|
1848
|
-
|
|
1964
|
+
if self._code == 'scipy':
|
|
1965
|
+
setattr(par, '_stderr', par.stderr*self._norm[1])
|
|
1966
|
+
else:
|
|
1967
|
+
par.stderr *= self._norm[1]
|
|
1849
1968
|
if par.expr is None:
|
|
1850
1969
|
_min = par.min
|
|
1851
1970
|
_max = par.max
|
|
1852
1971
|
value = par.value*self._norm[1]
|
|
1853
1972
|
if par.init_value is not None:
|
|
1854
|
-
|
|
1973
|
+
if self._code == 'scipy':
|
|
1974
|
+
setattr(par, '_init_value',
|
|
1975
|
+
par.init_value*self._norm[1])
|
|
1976
|
+
else:
|
|
1977
|
+
par.init_value *= self._norm[1]
|
|
1855
1978
|
if not np.isinf(_min) and abs(_min) != FLOAT_MIN:
|
|
1856
1979
|
_min *= self._norm[1]
|
|
1857
1980
|
if not np.isinf(_max) and abs(_max) != FLOAT_MIN:
|
|
@@ -1863,14 +1986,15 @@ class Fit:
|
|
|
1863
1986
|
if hasattr(self._result, 'init_values'):
|
|
1864
1987
|
init_values = {}
|
|
1865
1988
|
for name, value in self._result.init_values.items():
|
|
1866
|
-
if
|
|
1867
|
-
or self._parameters[name].expr is not None):
|
|
1868
|
-
init_values[name] = value
|
|
1869
|
-
elif self._parameter_norms[name]:
|
|
1989
|
+
if name in self._linear_parameters:
|
|
1870
1990
|
init_values[name] = value*self._norm[1]
|
|
1991
|
+
else:
|
|
1992
|
+
init_values[name] = value
|
|
1871
1993
|
self._result.init_values = init_values
|
|
1994
|
+
if (hasattr(self._result, 'init_params')
|
|
1995
|
+
and self._result.init_params is not None):
|
|
1872
1996
|
for name, par in self._result.init_params.items():
|
|
1873
|
-
if par.expr is None and self.
|
|
1997
|
+
if par.expr is None and name in self._linear_parameters:
|
|
1874
1998
|
value = par.value
|
|
1875
1999
|
_min = par.min
|
|
1876
2000
|
_max = par.max
|
|
@@ -1880,18 +2004,24 @@ class Fit:
|
|
|
1880
2004
|
if not np.isinf(_max) and abs(_max) != FLOAT_MIN:
|
|
1881
2005
|
_max *= self._norm[1]
|
|
1882
2006
|
par.set(value=value, min=_min, max=_max)
|
|
1883
|
-
|
|
2007
|
+
if self._code == 'scipy':
|
|
2008
|
+
setattr(par, '_init_value', par.value)
|
|
2009
|
+
else:
|
|
2010
|
+
par.init_value = par.value
|
|
1884
2011
|
# Don't renormalize chisqr, it has no useful meaning in
|
|
1885
2012
|
# physical units
|
|
1886
2013
|
# self._result.chisqr *= self._norm[1]*self._norm[1]
|
|
1887
2014
|
if self._result.covar is not None:
|
|
2015
|
+
norm_sq = self._norm[1]*self._norm[1]
|
|
1888
2016
|
for i, name in enumerate(self._result.var_names):
|
|
1889
|
-
if self.
|
|
2017
|
+
if name in self._linear_parameters:
|
|
1890
2018
|
for j in range(len(self._result.var_names)):
|
|
1891
2019
|
if self._result.covar[i,j] is not None:
|
|
1892
|
-
self._result.covar[i,j] *= self._norm[1]
|
|
2020
|
+
#self._result.covar[i,j] *= self._norm[1]
|
|
2021
|
+
self._result.covar[i,j] *= norm_sq
|
|
1893
2022
|
if self._result.covar[j,i] is not None:
|
|
1894
|
-
self._result.covar[j,i] *= self._norm[1]
|
|
2023
|
+
#self._result.covar[j,i] *= self._norm[1]
|
|
2024
|
+
self._result.covar[j,i] *= norm_sq
|
|
1895
2025
|
# Don't renormalize redchi, it has no useful meaning in
|
|
1896
2026
|
# physical units
|
|
1897
2027
|
# self._result.redchi *= self._norm[1]*self._norm[1]
|
|
@@ -1907,7 +2037,7 @@ class Fit:
|
|
|
1907
2037
|
_max = self._parameter_bounds[name]['max']
|
|
1908
2038
|
if np.isinf(_min):
|
|
1909
2039
|
if not np.isinf(_max):
|
|
1910
|
-
if self.
|
|
2040
|
+
if name in self._linear_parameters:
|
|
1911
2041
|
upp = _max - fraction*self._y_range
|
|
1912
2042
|
elif _max == 0.0:
|
|
1913
2043
|
upp = _max - fraction
|
|
@@ -1917,7 +2047,7 @@ class Fit:
|
|
|
1917
2047
|
par.set(value=upp)
|
|
1918
2048
|
else:
|
|
1919
2049
|
if np.isinf(_max):
|
|
1920
|
-
if self.
|
|
2050
|
+
if name in self._linear_parameters:
|
|
1921
2051
|
low = _min + fraction*self._y_range
|
|
1922
2052
|
elif _min == 0.0:
|
|
1923
2053
|
low = _min + fraction
|
|
@@ -1933,16 +2063,32 @@ class Fit:
|
|
|
1933
2063
|
if value >= upp:
|
|
1934
2064
|
par.set(value=upp)
|
|
1935
2065
|
|
|
2066
|
+
def _residual(self, pars, x, y):
|
|
2067
|
+
res = np.zeros((x.size))
|
|
2068
|
+
n_par = len(self._free_parameters)
|
|
2069
|
+
for par, index in zip(pars, self._res_par_indices):
|
|
2070
|
+
self._res_par_values[index] = par
|
|
2071
|
+
if self._res_par_exprs:
|
|
2072
|
+
for par, name in zip(pars, self._res_par_names):
|
|
2073
|
+
self._ast.symtable[name] = par
|
|
2074
|
+
for expr in self._res_par_exprs:
|
|
2075
|
+
self._res_par_values[expr['index']] = \
|
|
2076
|
+
self._ast.eval(expr['expr'])
|
|
2077
|
+
for component, num_par in zip(
|
|
2078
|
+
self._model.components, self._res_num_pars):
|
|
2079
|
+
res += component.func(
|
|
2080
|
+
x, *tuple(self._res_par_values[n_par:n_par+num_par]))
|
|
2081
|
+
n_par += num_par
|
|
2082
|
+
return res - y
|
|
2083
|
+
|
|
1936
2084
|
|
|
1937
2085
|
class FitMap(Fit):
|
|
1938
2086
|
"""
|
|
1939
|
-
Wrapper to the Fit class to fit
|
|
2087
|
+
Wrapper to the Fit class to fit data on a N-dimensional map
|
|
1940
2088
|
"""
|
|
1941
|
-
def __init__(
|
|
1942
|
-
self, ymap, x=None, models=None, normalize=True, transpose=None,
|
|
1943
|
-
**kwargs):
|
|
2089
|
+
def __init__(self, nxdata, config):
|
|
1944
2090
|
"""Initialize FitMap."""
|
|
1945
|
-
super().__init__(None)
|
|
2091
|
+
super().__init__(None, config)
|
|
1946
2092
|
self._best_errors = None
|
|
1947
2093
|
self._best_fit = None
|
|
1948
2094
|
self._best_parameters = None
|
|
@@ -1957,120 +2103,58 @@ class FitMap(Fit):
|
|
|
1957
2103
|
self._print_report = False
|
|
1958
2104
|
self._redchi = None
|
|
1959
2105
|
self._redchi_cutoff = 0.1
|
|
2106
|
+
self._rel_height_cutoff = None
|
|
1960
2107
|
self._skip_init = True
|
|
1961
2108
|
self._success = None
|
|
1962
|
-
self._transpose = None
|
|
1963
2109
|
self._try_no_bounds = True
|
|
1964
2110
|
|
|
1965
2111
|
# At this point the fastest index should always be the signal
|
|
1966
2112
|
# dimension so that the slowest ndim-1 dimensions are the
|
|
1967
2113
|
# map dimensions
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
ymap = np.asarray(ymap)
|
|
1971
|
-
elif HAVE_XARRAY and isinstance(ymap, xr.DataArray):
|
|
1972
|
-
if x is not None:
|
|
1973
|
-
logger.warning('Ignoring superfluous input x ({x})')
|
|
1974
|
-
self._x = np.asarray(ymap[ymap.dims[-1]])
|
|
1975
|
-
else:
|
|
1976
|
-
raise ValueError('Invalid parameter ymap ({ymap})')
|
|
1977
|
-
self._ymap = ymap
|
|
2114
|
+
self._x = np.asarray(nxdata[nxdata.attrs['axes'][-1]])
|
|
2115
|
+
self._ymap = np.asarray(nxdata.nxsignal)
|
|
1978
2116
|
|
|
1979
2117
|
# Check input parameters
|
|
1980
2118
|
if self._x.ndim != 1:
|
|
1981
|
-
raise ValueError(f'Invalid dimension
|
|
1982
|
-
if self._ymap.ndim < 2:
|
|
1983
|
-
raise ValueError(
|
|
1984
|
-
'Invalid number of dimension of the input dataset '
|
|
1985
|
-
f'{self._ymap.ndim}')
|
|
2119
|
+
raise ValueError(f'Invalid x dimension ({self._x.ndim})')
|
|
1986
2120
|
if self._x.size != self._ymap.shape[-1]:
|
|
1987
2121
|
raise ValueError(
|
|
1988
2122
|
f'Inconsistent x and y dimensions ({self._x.size} vs '
|
|
1989
2123
|
f'{self._ymap.shape[-1]})')
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
if isinstance(transpose, bool) and not transpose:
|
|
1996
|
-
transpose = None
|
|
1997
|
-
if transpose is not None and self._ymap.ndim < 3:
|
|
1998
|
-
logger.warning(
|
|
1999
|
-
f'Transpose meaningless for {self._ymap.ndim-1}D data maps: '
|
|
2000
|
-
'ignoring transpose')
|
|
2001
|
-
if transpose is not None:
|
|
2002
|
-
if (self._ymap.ndim == 3 and isinstance(transpose, bool)
|
|
2003
|
-
and transpose):
|
|
2004
|
-
self._transpose = (1, 0)
|
|
2005
|
-
elif not isinstance(transpose, (tuple, list)):
|
|
2006
|
-
logger.warning(
|
|
2007
|
-
f'Invalid data type for transpose ({transpose}, '
|
|
2008
|
-
f'{type(transpose)}): setting transpose to False')
|
|
2009
|
-
elif transpose != self._ymap.ndim-1:
|
|
2010
|
-
logger.warning(
|
|
2011
|
-
f'Invalid dimension for transpose ({transpose}, must be '
|
|
2012
|
-
f'equal to {self._ymap.ndim-1}): '
|
|
2013
|
-
'setting transpose to False')
|
|
2014
|
-
elif any(i not in transpose for i in range(len(transpose))):
|
|
2015
|
-
logger.warning(
|
|
2016
|
-
f'Invalid index in transpose ({transpose}): '
|
|
2017
|
-
'setting transpose to False')
|
|
2018
|
-
elif not all(i == transpose[i] for i in range(self._ymap.ndim-1)):
|
|
2019
|
-
self._transpose = transpose
|
|
2020
|
-
if self._transpose is not None:
|
|
2021
|
-
self._inv_transpose = tuple(
|
|
2022
|
-
self._transpose.index(i)
|
|
2023
|
-
for i in range(len(self._transpose)))
|
|
2024
|
-
|
|
2025
|
-
# Flatten the map (transpose if requested)
|
|
2026
|
-
# Store the flattened map in self._ymap_norm, whether
|
|
2027
|
-
# normalized or not
|
|
2028
|
-
if self._transpose is not None:
|
|
2029
|
-
self._ymap_norm = np.transpose(
|
|
2030
|
-
np.asarray(self._ymap),
|
|
2031
|
-
list(self._transpose) + [len(self._transpose)])
|
|
2032
|
-
else:
|
|
2033
|
-
self._ymap_norm = np.asarray(self._ymap)
|
|
2034
|
-
self._map_dim = int(self._ymap_norm.size/self._x.size)
|
|
2035
|
-
self._map_shape = self._ymap_norm.shape[:-1]
|
|
2124
|
+
|
|
2125
|
+
# Flatten the map
|
|
2126
|
+
# Store the flattened map in self._ymap_norm
|
|
2127
|
+
self._map_dim = int(self._ymap.size/self._x.size)
|
|
2128
|
+
self._map_shape = self._ymap.shape[:-1]
|
|
2036
2129
|
self._ymap_norm = np.reshape(
|
|
2037
|
-
self.
|
|
2130
|
+
self._ymap, (self._map_dim, self._x.size))
|
|
2038
2131
|
|
|
2039
2132
|
# Check if a mask is provided
|
|
2040
|
-
if 'mask' in kwargs:
|
|
2041
|
-
self._mask = kwargs.pop('mask')
|
|
2042
|
-
if self._mask is None:
|
|
2133
|
+
# if 'mask' in kwargs:
|
|
2134
|
+
# self._mask = kwargs.pop('mask')
|
|
2135
|
+
if True: #self._mask is None:
|
|
2043
2136
|
ymap_min = float(self._ymap_norm.min())
|
|
2044
2137
|
ymap_max = float(self._ymap_norm.max())
|
|
2045
|
-
else:
|
|
2046
|
-
self._mask = np.asarray(self._mask).astype(bool)
|
|
2047
|
-
if self._x.size != self._mask.size:
|
|
2048
|
-
raise ValueError(
|
|
2049
|
-
f'Inconsistent mask dimension ({self._x.size} vs '
|
|
2050
|
-
f'{self._mask.size})')
|
|
2051
|
-
ymap_masked = np.asarray(self._ymap_norm)[:,~self._mask]
|
|
2052
|
-
ymap_min = float(ymap_masked.min())
|
|
2053
|
-
ymap_max = float(ymap_masked.max())
|
|
2138
|
+
# else:
|
|
2139
|
+
# self._mask = np.asarray(self._mask).astype(bool)
|
|
2140
|
+
# if self._x.size != self._mask.size:
|
|
2141
|
+
# raise ValueError(
|
|
2142
|
+
# f'Inconsistent mask dimension ({self._x.size} vs '
|
|
2143
|
+
# f'{self._mask.size})')
|
|
2144
|
+
# ymap_masked = np.asarray(self._ymap_norm)[:,~self._mask]
|
|
2145
|
+
# ymap_min = float(ymap_masked.min())
|
|
2146
|
+
# ymap_max = float(ymap_masked.max())
|
|
2054
2147
|
|
|
2055
2148
|
# Normalize the data
|
|
2056
2149
|
self._y_range = ymap_max-ymap_min
|
|
2057
|
-
if
|
|
2150
|
+
if self._y_range > 0.0:
|
|
2058
2151
|
self._norm = (ymap_min, self._y_range)
|
|
2059
2152
|
self._ymap_norm = (self._ymap_norm-self._norm[0]) / self._norm[1]
|
|
2060
2153
|
else:
|
|
2061
2154
|
self._redchi_cutoff *= self._y_range**2
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
elif isinstance(models, (tuple, list)):
|
|
2066
|
-
for model in models:
|
|
2067
|
-
kwargs = self.add_model(model, **kwargs)
|
|
2068
|
-
self.fit(**kwargs)
|
|
2069
|
-
|
|
2070
|
-
@classmethod
|
|
2071
|
-
def fit_map(cls, ymap, models, x=None, normalize=True, **kwargs):
|
|
2072
|
-
"""Class method for FitMap."""
|
|
2073
|
-
return cls(ymap, x=x, models=models, normalize=normalize, **kwargs)
|
|
2155
|
+
|
|
2156
|
+
# Setup fit model
|
|
2157
|
+
self._setup_fit_model(config.parameters, config.models)
|
|
2074
2158
|
|
|
2075
2159
|
@property
|
|
2076
2160
|
def best_errors(self):
|
|
@@ -2082,44 +2166,6 @@ class FitMap(Fit):
|
|
|
2082
2166
|
"""Return the best fits."""
|
|
2083
2167
|
return self._best_fit
|
|
2084
2168
|
|
|
2085
|
-
@property
|
|
2086
|
-
def best_results(self):
|
|
2087
|
-
"""
|
|
2088
|
-
Convert the input DataArray to a data set and add the fit
|
|
2089
|
-
results.
|
|
2090
|
-
"""
|
|
2091
|
-
if (self.best_values is None or self.best_errors is None
|
|
2092
|
-
or self.best_fit is None):
|
|
2093
|
-
return None
|
|
2094
|
-
if not HAVE_XARRAY:
|
|
2095
|
-
logger.warning('Unable to load xarray module')
|
|
2096
|
-
return None
|
|
2097
|
-
best_values = self.best_values
|
|
2098
|
-
best_errors = self.best_errors
|
|
2099
|
-
if isinstance(self._ymap, xr.DataArray):
|
|
2100
|
-
best_results = self._ymap.to_dataset()
|
|
2101
|
-
dims = self._ymap.dims
|
|
2102
|
-
fit_name = f'{self._ymap.name}_fit'
|
|
2103
|
-
else:
|
|
2104
|
-
coords = {
|
|
2105
|
-
f'dim{n}_index':([f'dim{n}_index'], range(self._ymap.shape[n]))
|
|
2106
|
-
for n in range(self._ymap.ndim-1)}
|
|
2107
|
-
coords['x'] = (['x'], self._x)
|
|
2108
|
-
dims = list(coords.keys())
|
|
2109
|
-
best_results = xr.Dataset(coords=coords)
|
|
2110
|
-
best_results['y'] = (dims, self._ymap)
|
|
2111
|
-
fit_name = 'y_fit'
|
|
2112
|
-
best_results[fit_name] = (dims, self.best_fit)
|
|
2113
|
-
if self._mask is not None:
|
|
2114
|
-
best_results['mask'] = self._mask
|
|
2115
|
-
for n in range(best_values.shape[0]):
|
|
2116
|
-
best_results[f'{self._best_parameters[n]}_values'] = \
|
|
2117
|
-
(dims[:-1], best_values[n])
|
|
2118
|
-
best_results[f'{self._best_parameters[n]}_errors'] = \
|
|
2119
|
-
(dims[:-1], best_errors[n])
|
|
2120
|
-
best_results.attrs['components'] = self.components
|
|
2121
|
-
return best_results
|
|
2122
|
-
|
|
2123
2169
|
@property
|
|
2124
2170
|
def best_values(self):
|
|
2125
2171
|
"""Return values of the best fit parameters."""
|
|
@@ -2133,6 +2179,9 @@ class FitMap(Fit):
|
|
|
2133
2179
|
@property
|
|
2134
2180
|
def components(self):
|
|
2135
2181
|
"""Return the fit model components info."""
|
|
2182
|
+
# Third party modules
|
|
2183
|
+
from lmfit.models import ExpressionModel
|
|
2184
|
+
|
|
2136
2185
|
components = {}
|
|
2137
2186
|
if self._result is None:
|
|
2138
2187
|
logger.warning(
|
|
@@ -2292,11 +2341,15 @@ class FitMap(Fit):
|
|
|
2292
2341
|
self, dims=None, y_title=None, plot_residual=False,
|
|
2293
2342
|
plot_comp_legends=False, plot_masked_data=True, **kwargs):
|
|
2294
2343
|
"""Plot the best fits."""
|
|
2344
|
+
# Third party modules
|
|
2345
|
+
from lmfit.models import ExpressionModel
|
|
2346
|
+
|
|
2295
2347
|
if dims is None:
|
|
2296
2348
|
dims = [0]*len(self._map_shape)
|
|
2297
2349
|
if (not isinstance(dims, (list, tuple))
|
|
2298
2350
|
or len(dims) != len(self._map_shape)):
|
|
2299
2351
|
raise ValueError('Invalid parameter dims ({dims})')
|
|
2352
|
+
dims = tuple(dims)
|
|
2300
2353
|
if (self._result is None or self.best_fit is None
|
|
2301
2354
|
or self.best_values is None):
|
|
2302
2355
|
logger.warning(
|
|
@@ -2354,18 +2407,28 @@ class FitMap(Fit):
|
|
|
2354
2407
|
quick_plot(
|
|
2355
2408
|
tuple(plots), legend=legend, title=str(dims), block=True, **kwargs)
|
|
2356
2409
|
|
|
2357
|
-
def fit(self, **kwargs):
|
|
2410
|
+
def fit(self, config=None, **kwargs):
|
|
2358
2411
|
"""Fit the model to the input data."""
|
|
2412
|
+
|
|
2359
2413
|
# Check input parameters
|
|
2360
2414
|
if self._model is None:
|
|
2361
2415
|
logger.error('Undefined fit model')
|
|
2362
|
-
if
|
|
2363
|
-
num_proc = kwargs.pop('num_proc')
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2416
|
+
if config is None:
|
|
2417
|
+
num_proc = kwargs.pop('num_proc', cpu_count())
|
|
2418
|
+
self._rel_height_cutoff = kwargs.pop('rel_height_cutoff')
|
|
2419
|
+
self._try_no_bounds = kwargs.pop('try_no_bounds', False)
|
|
2420
|
+
self._redchi_cutoff = kwargs.pop('redchi_cutoff', 0.1)
|
|
2421
|
+
self._print_report = kwargs.pop('print_report', False)
|
|
2422
|
+
self._plot = kwargs.pop('plot', False)
|
|
2423
|
+
self._skip_init = kwargs.pop('skip_init', True)
|
|
2367
2424
|
else:
|
|
2368
|
-
num_proc =
|
|
2425
|
+
num_proc = config.num_proc
|
|
2426
|
+
self._rel_height_cutoff = config.rel_height_cutoff
|
|
2427
|
+
# self._try_no_bounds = config.try_no_bounds
|
|
2428
|
+
# self._redchi_cutoff = config.redchi_cutoff
|
|
2429
|
+
self._print_report = config.print_report
|
|
2430
|
+
self._plot = config.plot
|
|
2431
|
+
# self._skip_init = config.skip_init
|
|
2369
2432
|
if num_proc > 1 and not HAVE_JOBLIB:
|
|
2370
2433
|
logger.warning(
|
|
2371
2434
|
'Missing joblib in the conda environment, running serially')
|
|
@@ -2376,106 +2439,10 @@ class FitMap(Fit):
|
|
|
2376
2439
|
'maximum number of processors, num_proc reduced to '
|
|
2377
2440
|
f'{cpu_count()}')
|
|
2378
2441
|
num_proc = cpu_count()
|
|
2379
|
-
|
|
2380
|
-
self._try_no_bounds = kwargs.pop('try_no_bounds')
|
|
2381
|
-
if not isinstance(self._try_no_bounds, bool):
|
|
2382
|
-
raise ValueError(
|
|
2383
|
-
'Invalid value for keyword argument try_no_bounds '
|
|
2384
|
-
f'({self._try_no_bounds})')
|
|
2385
|
-
if 'redchi_cutoff' in kwargs:
|
|
2386
|
-
self._redchi_cutoff = kwargs.pop('redchi_cutoff')
|
|
2387
|
-
if not is_num(self._redchi_cutoff, gt=0):
|
|
2388
|
-
raise ValueError(
|
|
2389
|
-
'Invalid value for keyword argument redchi_cutoff'
|
|
2390
|
-
f'({self._redchi_cutoff})')
|
|
2391
|
-
if 'print_report' in kwargs:
|
|
2392
|
-
self._print_report = kwargs.pop('print_report')
|
|
2393
|
-
if not isinstance(self._print_report, bool):
|
|
2394
|
-
raise ValueError(
|
|
2395
|
-
'Invalid value for keyword argument print_report'
|
|
2396
|
-
f'({self._print_report})')
|
|
2397
|
-
if 'plot' in kwargs:
|
|
2398
|
-
self._plot = kwargs.pop('plot')
|
|
2399
|
-
if not isinstance(self._plot, bool):
|
|
2400
|
-
raise ValueError(
|
|
2401
|
-
'Invalid value for keyword argument plot'
|
|
2402
|
-
f'({self._plot})')
|
|
2403
|
-
if 'skip_init' in kwargs:
|
|
2404
|
-
self._skip_init = kwargs.pop('skip_init')
|
|
2405
|
-
if not isinstance(self._skip_init, bool):
|
|
2406
|
-
raise ValueError(
|
|
2407
|
-
'Invalid value for keyword argument skip_init'
|
|
2408
|
-
f'({self._skip_init})')
|
|
2409
|
-
|
|
2410
|
-
# Apply mask if supplied:
|
|
2411
|
-
if 'mask' in kwargs:
|
|
2412
|
-
self._mask = kwargs.pop('mask')
|
|
2413
|
-
if self._mask is not None:
|
|
2414
|
-
self._mask = np.asarray(self._mask).astype(bool)
|
|
2415
|
-
if self._x.size != self._mask.size:
|
|
2416
|
-
raise ValueError(
|
|
2417
|
-
f'Inconsistent x and mask dimensions ({self._x.size} vs '
|
|
2418
|
-
f'{self._mask.size})')
|
|
2419
|
-
|
|
2420
|
-
# Add constant offset for a normalized single component model
|
|
2421
|
-
if self._result is None and self._norm is not None and self._norm[0]:
|
|
2422
|
-
self.add_model(
|
|
2423
|
-
'constant',
|
|
2424
|
-
prefix='tmp_normalization_offset_',
|
|
2425
|
-
parameters={
|
|
2426
|
-
'name': 'c',
|
|
2427
|
-
'value': -self._norm[0],
|
|
2428
|
-
'vary': False,
|
|
2429
|
-
'norm': True,
|
|
2430
|
-
})
|
|
2431
|
-
# 'value': -self._norm[0]/self._norm[1],
|
|
2432
|
-
# 'vary': False,
|
|
2433
|
-
# 'norm': False,
|
|
2434
|
-
|
|
2435
|
-
# Adjust existing parameters for refit:
|
|
2436
|
-
if 'parameters' in kwargs:
|
|
2437
|
-
parameters = kwargs.pop('parameters')
|
|
2438
|
-
if isinstance(parameters, dict):
|
|
2439
|
-
parameters = (parameters, )
|
|
2440
|
-
elif not is_dict_series(parameters):
|
|
2441
|
-
raise ValueError(
|
|
2442
|
-
'Invalid value for keyword argument parameters'
|
|
2443
|
-
f'({parameters})')
|
|
2444
|
-
for par in parameters:
|
|
2445
|
-
name = par['name']
|
|
2446
|
-
if name not in self._parameters:
|
|
2447
|
-
raise ValueError(
|
|
2448
|
-
f'Unable to match {name} parameter {par} to an '
|
|
2449
|
-
'existing one')
|
|
2450
|
-
if self._parameters[name].expr is not None:
|
|
2451
|
-
raise ValueError(
|
|
2452
|
-
f'Unable to modify {name} parameter {par} '
|
|
2453
|
-
'(currently an expression)')
|
|
2454
|
-
value = par.get('value')
|
|
2455
|
-
vary = par.get('vary')
|
|
2456
|
-
if par.get('expr') is not None:
|
|
2457
|
-
raise KeyError(
|
|
2458
|
-
f'Invalid "expr" key in {name} parameter {par}')
|
|
2459
|
-
self._parameters[name].set(
|
|
2460
|
-
value=value, vary=vary, min=par.get('min'),
|
|
2461
|
-
max=par.get('max'))
|
|
2462
|
-
# Overwrite existing best values for fixed parameters
|
|
2463
|
-
# when a value is specified
|
|
2464
|
-
if isinstance(value, (int, float)) and vary is False:
|
|
2465
|
-
for i, nname in enumerate(self._best_parameters):
|
|
2466
|
-
if nname == name:
|
|
2467
|
-
self._best_values[i] = value
|
|
2442
|
+
self._redchi_cutoff *= self._y_range**2
|
|
2468
2443
|
|
|
2469
|
-
#
|
|
2470
|
-
|
|
2471
|
-
if par.expr is None:
|
|
2472
|
-
value = par.value
|
|
2473
|
-
if value is None or np.isinf(value) or np.isnan(value):
|
|
2474
|
-
value = 1.0
|
|
2475
|
-
if self._norm is None or name not in self._parameter_norms:
|
|
2476
|
-
self._parameters[name].set(value=value)
|
|
2477
|
-
elif self._parameter_norms[name]:
|
|
2478
|
-
self._parameters[name].set(value=value*self._norm[1])
|
|
2444
|
+
# Setup the fit
|
|
2445
|
+
self._setup_fit(config)
|
|
2479
2446
|
|
|
2480
2447
|
# Create the best parameter list, consisting of all varying
|
|
2481
2448
|
# parameters plus the expression parameters in order to
|
|
@@ -2511,15 +2478,12 @@ class FitMap(Fit):
|
|
|
2511
2478
|
assert self._best_values is not None
|
|
2512
2479
|
assert self._best_values.shape[0] == num_best_parameters
|
|
2513
2480
|
assert self._best_values.shape[1:] == self._map_shape
|
|
2514
|
-
if self._transpose is not None:
|
|
2515
|
-
self._best_values = np.transpose(
|
|
2516
|
-
self._best_values, [0]+[i+1 for i in self._transpose])
|
|
2517
2481
|
self._best_values = [
|
|
2518
2482
|
np.reshape(self._best_values[i], self._map_dim)
|
|
2519
2483
|
for i in range(num_best_parameters)]
|
|
2520
2484
|
if self._norm is not None:
|
|
2521
2485
|
for i, name in enumerate(self._best_parameters):
|
|
2522
|
-
if self.
|
|
2486
|
+
if name in self._linear_parameters:
|
|
2523
2487
|
self._best_values[i] /= self._norm[1]
|
|
2524
2488
|
|
|
2525
2489
|
# Normalize the initial parameters
|
|
@@ -2564,7 +2528,7 @@ class FitMap(Fit):
|
|
|
2564
2528
|
np.zeros(self._map_dim, dtype=np.float64)
|
|
2565
2529
|
for _ in range(num_new_parameters)]
|
|
2566
2530
|
else:
|
|
2567
|
-
self._memfolder = '
|
|
2531
|
+
self._memfolder = 'joblib_memmap'
|
|
2568
2532
|
try:
|
|
2569
2533
|
mkdir(self._memfolder)
|
|
2570
2534
|
except FileExistsError:
|
|
@@ -2671,25 +2635,31 @@ class FitMap(Fit):
|
|
|
2671
2635
|
|
|
2672
2636
|
# Renormalize the initial parameters for external use
|
|
2673
2637
|
if self._norm is not None and self._normalized:
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2638
|
+
if hasattr(self._result, 'init_values'):
|
|
2639
|
+
init_values = {}
|
|
2640
|
+
for name, value in self._result.init_values.items():
|
|
2641
|
+
if (name in self._nonlinear_parameters
|
|
2642
|
+
or self._parameters[name].expr is not None):
|
|
2643
|
+
init_values[name] = value
|
|
2644
|
+
else:
|
|
2645
|
+
init_values[name] = value*self._norm[1]
|
|
2646
|
+
self._result.init_values = init_values
|
|
2647
|
+
if (hasattr(self._result, 'init_params')
|
|
2648
|
+
and self._result.init_params is not None):
|
|
2649
|
+
for name, par in self._result.init_params.items():
|
|
2650
|
+
if par.expr is None and name in self._linear_parameters:
|
|
2651
|
+
_min = par.min
|
|
2652
|
+
_max = par.max
|
|
2653
|
+
value = par.value*self._norm[1]
|
|
2654
|
+
if not np.isinf(_min) and abs(_min) != FLOAT_MIN:
|
|
2655
|
+
_min *= self._norm[1]
|
|
2656
|
+
if not np.isinf(_max) and abs(_max) != FLOAT_MIN:
|
|
2657
|
+
_max *= self._norm[1]
|
|
2658
|
+
par.set(value=value, min=_min, max=_max)
|
|
2659
|
+
if self._code == 'scipy':
|
|
2660
|
+
setattr(par, '_init_value', par.value)
|
|
2661
|
+
else:
|
|
2662
|
+
par.init_value = par.value
|
|
2693
2663
|
|
|
2694
2664
|
# Remap the best results
|
|
2695
2665
|
self._out_of_bounds = np.copy(np.reshape(
|
|
@@ -2736,9 +2706,9 @@ class FitMap(Fit):
|
|
|
2736
2706
|
self._parameters[name].set(min=par['min'], max=par['max'])
|
|
2737
2707
|
self._normalized = False
|
|
2738
2708
|
if self._norm is not None:
|
|
2739
|
-
for name
|
|
2709
|
+
for name in self._linear_parameters:
|
|
2740
2710
|
par = self._parameters[name]
|
|
2741
|
-
if par.expr is None
|
|
2711
|
+
if par.expr is None:
|
|
2742
2712
|
value = par.value*self._norm[1]
|
|
2743
2713
|
_min = par.min
|
|
2744
2714
|
_max = par.max
|
|
@@ -2757,57 +2727,62 @@ class FitMap(Fit):
|
|
|
2757
2727
|
self._fit(n_start+n, current_best_values, **kwargs)
|
|
2758
2728
|
|
|
2759
2729
|
def _fit(self, n, current_best_values, return_result=False, **kwargs):
|
|
2760
|
-
#
|
|
2761
|
-
if
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2730
|
+
# Do not attempt a fit if the data is entirely below the cutoff
|
|
2731
|
+
if (self._rel_height_cutoff is not None
|
|
2732
|
+
and self._ymap_norm[n].max() < self._rel_height_cutoff):
|
|
2733
|
+
logger.debug(f'Skipping fit for n = {n} (rel norm = '
|
|
2734
|
+
f'{self._ymap_norm[n].max():.5f})')
|
|
2735
|
+
if self._code == 'scipy':
|
|
2736
|
+
from CHAP.utils.fit import ModelResult
|
|
2737
|
+
|
|
2738
|
+
result = ModelResult(self._model, deepcopy(self._parameters))
|
|
2739
|
+
else:
|
|
2740
|
+
from lmfit.model import ModelResult
|
|
2741
|
+
|
|
2742
|
+
result = ModelResult(self._model, deepcopy(self._parameters))
|
|
2743
|
+
result.success = False
|
|
2744
|
+
# Renormalize the data and results
|
|
2745
|
+
self._renormalize(n, result)
|
|
2746
|
+
return result
|
|
2771
2747
|
|
|
2772
2748
|
# Regular full fit
|
|
2773
2749
|
result = self._fit_with_bounds_check(n, current_best_values, **kwargs)
|
|
2774
2750
|
|
|
2775
|
-
if
|
|
2751
|
+
if self._rel_height_cutoff is not None:
|
|
2776
2752
|
# Third party modules
|
|
2777
2753
|
from lmfit.models import (
|
|
2778
2754
|
GaussianModel,
|
|
2779
2755
|
LorentzianModel,
|
|
2780
2756
|
)
|
|
2781
2757
|
|
|
2782
|
-
# Check for low
|
|
2783
|
-
|
|
2758
|
+
# Check for low heights peaks and refit without them
|
|
2759
|
+
heights = []
|
|
2784
2760
|
names = []
|
|
2785
2761
|
for component in result.components:
|
|
2786
2762
|
if isinstance(component, (GaussianModel, LorentzianModel)):
|
|
2787
2763
|
for name in component.param_names:
|
|
2788
|
-
if '
|
|
2789
|
-
|
|
2764
|
+
if 'height' in name:
|
|
2765
|
+
heights.append(result.params[name].value)
|
|
2790
2766
|
names.append(name)
|
|
2791
|
-
if
|
|
2767
|
+
if heights:
|
|
2792
2768
|
refit = False
|
|
2793
|
-
|
|
2769
|
+
max_height = max(heights)
|
|
2794
2770
|
parameters_save = deepcopy(self._parameters)
|
|
2795
|
-
for i, (name,
|
|
2796
|
-
if
|
|
2797
|
-
self._parameters[
|
|
2798
|
-
|
|
2771
|
+
for i, (name, height) in enumerate(zip(names, heights)):
|
|
2772
|
+
if height < self._rel_height_cutoff*max_height:
|
|
2773
|
+
self._parameters[
|
|
2774
|
+
name.replace('height', 'amplitude')].set(
|
|
2775
|
+
value=0.0, min=0.0, vary=False)
|
|
2799
2776
|
self._parameters[
|
|
2800
|
-
name.replace('
|
|
2777
|
+
name.replace('height', 'center')].set(
|
|
2801
2778
|
vary=False)
|
|
2802
2779
|
self._parameters[
|
|
2803
|
-
name.replace('
|
|
2780
|
+
name.replace('height', 'sigma')].set(
|
|
2804
2781
|
value=0.0, min=0.0, vary=False)
|
|
2805
2782
|
refit = True
|
|
2806
2783
|
if refit:
|
|
2807
2784
|
result = self._fit_with_bounds_check(
|
|
2808
2785
|
n, current_best_values, **kwargs)
|
|
2809
|
-
# for name in names:
|
|
2810
|
-
# result.params[name].error = 0.0
|
|
2811
2786
|
# Reset fixed amplitudes back to default
|
|
2812
2787
|
self._parameters = deepcopy(parameters_save)
|
|
2813
2788
|
|
|
@@ -2827,8 +2802,10 @@ class FitMap(Fit):
|
|
|
2827
2802
|
current_best_values[par.name] = par.value
|
|
2828
2803
|
else:
|
|
2829
2804
|
logger.warning(f'Fit for n = {n} failed: {result.lmdif_message}')
|
|
2805
|
+
|
|
2830
2806
|
# Renormalize the data and results
|
|
2831
2807
|
self._renormalize(n, result)
|
|
2808
|
+
|
|
2832
2809
|
if self._print_report:
|
|
2833
2810
|
print(result.fit_report(show_correl=False))
|
|
2834
2811
|
if self._plot:
|
|
@@ -2840,6 +2817,7 @@ class FitMap(Fit):
|
|
|
2840
2817
|
result=result, y=np.asarray(self._ymap[dims]),
|
|
2841
2818
|
plot_comp_legends=True, skip_init=self._skip_init,
|
|
2842
2819
|
title=str(dims))
|
|
2820
|
+
|
|
2843
2821
|
if return_result:
|
|
2844
2822
|
return result
|
|
2845
2823
|
return None
|
|
@@ -2864,13 +2842,8 @@ class FitMap(Fit):
|
|
|
2864
2842
|
elif par.expr is None:
|
|
2865
2843
|
par.set(value=self._best_values[i][n])
|
|
2866
2844
|
self._reset_par_at_boundary()
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
self._ymap_norm[n], self._parameters, x=self._x, **kwargs)
|
|
2870
|
-
else:
|
|
2871
|
-
result = self._model.fit(
|
|
2872
|
-
self._ymap_norm[n][~self._mask], self._parameters,
|
|
2873
|
-
x=self._x[~self._mask], **kwargs)
|
|
2845
|
+
result = self._fit_nonlinear_model(
|
|
2846
|
+
self._x, self._ymap_norm[n], **kwargs)
|
|
2874
2847
|
out_of_bounds = False
|
|
2875
2848
|
for name, par in self._parameter_bounds.items():
|
|
2876
2849
|
if self._parameters[name].vary:
|
|
@@ -2906,13 +2879,8 @@ class FitMap(Fit):
|
|
|
2906
2879
|
elif par.expr is None:
|
|
2907
2880
|
par.set(value=self._best_values[i][n])
|
|
2908
2881
|
self._reset_par_at_boundary()
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
self._ymap_norm[n], self._parameters, x=self._x, **kwargs)
|
|
2912
|
-
else:
|
|
2913
|
-
result = self._model.fit(
|
|
2914
|
-
self._ymap_norm[n][~self._mask], self._parameters,
|
|
2915
|
-
x=self._x[~self._mask], **kwargs)
|
|
2882
|
+
result = self._fit_nonlinear_model(
|
|
2883
|
+
self._x, self._ymap_norm[n], **kwargs)
|
|
2916
2884
|
out_of_bounds = False
|
|
2917
2885
|
for name, par in self._parameter_bounds.items():
|
|
2918
2886
|
if self._parameters[name].vary:
|
|
@@ -2929,41 +2897,50 @@ class FitMap(Fit):
|
|
|
2929
2897
|
return result
|
|
2930
2898
|
|
|
2931
2899
|
def _renormalize(self, n, result):
|
|
2932
|
-
self._redchi_flat[n] = np.float64(result.redchi)
|
|
2933
2900
|
self._success_flat[n] = result.success
|
|
2901
|
+
if result.success:
|
|
2902
|
+
self._redchi_flat[n] = np.float64(result.redchi)
|
|
2934
2903
|
if self._norm is None or not self._normalized:
|
|
2935
|
-
self._best_fit_flat[n] = result.best_fit
|
|
2936
2904
|
for i, name in enumerate(self._best_parameters):
|
|
2937
2905
|
self._best_values_flat[i][n] = np.float64(
|
|
2938
2906
|
result.params[name].value)
|
|
2939
2907
|
self._best_errors_flat[i][n] = np.float64(
|
|
2940
2908
|
result.params[name].stderr)
|
|
2909
|
+
if result.success:
|
|
2910
|
+
self._best_fit_flat[n] = result.best_fit
|
|
2941
2911
|
else:
|
|
2942
|
-
pars = set(self._parameter_norms) & set(self._best_parameters)
|
|
2943
2912
|
for name, par in result.params.items():
|
|
2944
|
-
if name in
|
|
2913
|
+
if name in self._linear_parameters:
|
|
2945
2914
|
if par.stderr is not None:
|
|
2946
|
-
|
|
2915
|
+
if self._code == 'scipy':
|
|
2916
|
+
setattr(par, '_stderr', par.stderr*self._norm[1])
|
|
2917
|
+
else:
|
|
2918
|
+
par.stderr *= self._norm[1]
|
|
2947
2919
|
if par.expr is None:
|
|
2948
2920
|
par.value *= self._norm[1]
|
|
2949
2921
|
if self._print_report:
|
|
2950
2922
|
if par.init_value is not None:
|
|
2951
|
-
|
|
2923
|
+
if self._code == 'scipy':
|
|
2924
|
+
setattr(par, '_init_value',
|
|
2925
|
+
par.init_value*self._norm[1])
|
|
2926
|
+
else:
|
|
2927
|
+
par.init_value *= self._norm[1]
|
|
2952
2928
|
if (not np.isinf(par.min)
|
|
2953
2929
|
and abs(par.min) != FLOAT_MIN):
|
|
2954
2930
|
par.min *= self._norm[1]
|
|
2955
2931
|
if (not np.isinf(par.max)
|
|
2956
2932
|
and abs(par.max) != FLOAT_MIN):
|
|
2957
2933
|
par.max *= self._norm[1]
|
|
2958
|
-
self._best_fit_flat[n] = (
|
|
2959
|
-
result.best_fit*self._norm[1] + self._norm[0])
|
|
2960
2934
|
for i, name in enumerate(self._best_parameters):
|
|
2961
2935
|
self._best_values_flat[i][n] = np.float64(
|
|
2962
2936
|
result.params[name].value)
|
|
2963
2937
|
self._best_errors_flat[i][n] = np.float64(
|
|
2964
2938
|
result.params[name].stderr)
|
|
2965
|
-
if
|
|
2966
|
-
|
|
2967
|
-
result.
|
|
2968
|
-
|
|
2969
|
-
|
|
2939
|
+
if result.success:
|
|
2940
|
+
self._best_fit_flat[n] = (
|
|
2941
|
+
result.best_fit*self._norm[1] + self._norm[0])
|
|
2942
|
+
if self._plot:
|
|
2943
|
+
if not self._skip_init:
|
|
2944
|
+
result.init_fit = (
|
|
2945
|
+
result.init_fit*self._norm[1] + self._norm[0])
|
|
2946
|
+
result.best_fit = np.copy(self._best_fit_flat[n])
|