-
Notifications
You must be signed in to change notification settings - Fork 0
/
models.py
319 lines (259 loc) · 8.75 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
"""
Models used to fit mode profiles
Objects intended for public use:
AbstractModelMaker
ModelBaselinePoly
ModelBaselineExp
ModelLineLorentzian
ModelLineVoigt
ModelMakerLorentzian
ModelMakerVoigt
"""
import numpy as np
import scipy.special
import abc
class AbstractModelMaker(abc.ABC):
def __init__(self, poly_order, n_lorentz):
self.poly_order = poly_order
self.n_lines = n_lorentz
self.nparams = (1 + self.poly_order) + self.n_lineparams*self.n_lines
def unpack_params(self, args):
assert len(args) == self.nparams
params_poly = np.array(args[:self.poly_order+1])
params_lorentz = np.reshape(args[self.poly_order+1:], (self.n_lines, self.n_lineparams))
return params_poly, params_lorentz
def pack_params(self, params_poly, params_lorentz):
"""
params_poly: numpy array of length poly_order + 1
params_lorentz: numpy array of shape (n_lines, n_lineparams)
Returns list
"""
assert len(params_poly) == self.poly_order + 1
assert np.shape(params_lorentz) == (self.n_lines, self.n_lineparams)
return tuple([*params_poly, *np.reshape(params_lorentz, self.n_lineparams*self.n_lines)])
@abc.abstractmethod
def baseline(self, om, *params_poly):
raise NotImplementedError
def __call__(self, om, *args):
assert self.pack_params(*self.unpack_params(args)) == args, f"{args = }\n{self.pack_params(*self.unpack_params(args)) = }"
params_poly, params_lorentz = self.unpack_params(args)
ret = self.poly(om, *params_poly)
for i in range(self.n_lines):
ret += self.line(om, *params_lorentz[i], params_poly = params_poly)
return ret
@property
def n_lorentz(self):
"""
For backwards compatibility
"""
return self.n_lines
def lorentzian(self, *args, **kwargs):
"""
For backwards compatibility
"""
return self.line(*args, **kwargs)
def poly(self, *args, **kwargs):
"""
For backwards compatibility
"""
return self.baseline(*args, **kwargs)
@property
@abc.abstractmethod
def n_lineparams(self):
"""
Number of parameters needed to specify the line profile
"""
raise NotImplementedError
@abc.abstractmethod
def line(self, om, *args, **kwargs):
"""
Model for the profile of an individual mode. The first argument is a 1D array of angular frequencies, and the ones that follow are the parameters of the mode profile.
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _ind_line_freq(self):
"""
Index of the central angular frequency of a line in the tuple of line parameters
"""
raise NotImplementedError
@property
def ind_line_freq(self):
return self._ind_line_freq
def get_line_freq(self, *args):
"""
Get the central angular frequency of a line, given the line parameters
"""
if len(args) != self.n_lineparams:
raise ValueError(f"Wrong number of parameters for line (expected {self.n_lineparams}; got {len(args)}")
return args[self._ind_line_freq]
@abc.abstractmethod
def get_line_hwhm(self, *args):
"""
Get the HWHM (half-width at half-maximum) of a line, given the line parameters
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _width_like_params(self):
"""
Returns a list of indices of the line parameters that can be interpreted as widths. This is used by fit_mode to set bounds on the parameters.
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _positive_params(self):
"""
Returns a list of indices of the line parameters that need to be positive. This is used by fit_mode to set bounds on the parameters.
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _amplitude_like_params(self):
"""
Returns a list of indices of the line parameters that should be scaled when the data is scaled. This is used by scale_params
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _baseline_amplitude_like_params(self):
"""
Returns a list of indices of the baseline parameters that should be scaled when the data is scaled. This is used by scale_params
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _baseline_positive_params(self):
"""
Returns a list of indices of the baseline parameters that should be constrained to be positive. This is used by fit_mode to set bounds on the parameters.
"""
raise NotImplementedError
@property
@abc.abstractmethod
def _baseline_ensure_positive(self):
"""
If the function used for the baseline is such that one cannot easily ensure its positivity by constraining the parameters, one can instead set this property to True.
When this property is True, fit_mode uses a modified expression for the least-squares residual that prefers a positive baseline. If False, no such special handling is done.
"""
raise NotImplementedError
def scale_params(self, params, factor):
"""
Given a vector of params, apply a scaling factor to the polynomial and to the amplitudes of the lines, and return the vector of scaled params. This is useful if you want to pass a scaled version of the data to a fitting routine, but want to finally obtain parameters corresponding to the unscaled data.
"""
params_poly, params_lines = self.unpack_params(params)
for i in range(len(params_poly)):
if i in self._baseline_amplitude_like_params:
params_poly[i] = params_poly[i]*factor
for i in range(np.shape(params_lines)[1]):
if i in self._amplitude_like_params:
params_lines[:,i] = params_lines[:,i]*factor
return self.pack_params(params_poly, params_lines)
def _baseline_guesser(self, omt, data):
"""
Given the data, return a guess for the parameters of the baseline.
"""
return np.zeros(self.poly_order + 1)
class ModelBaselinePoly():
_baseline_ensure_positive = True
@property
def _baseline_amplitude_like_params(self):
return list(range(self.poly_order + 1))
@property
def _baseline_positive_params(self):
return []
def baseline(self, om, *params_poly):
assert len(params_poly) == self.poly_order + 1
ret = 0
for i,a in enumerate(params_poly):
ret += a*om**i
return ret
class ModelBaselineExp():
_baseline_ensure_positive = False
@property
def _baseline_amplitude_like_params(self):
return [0]
@property
def _baseline_positive_params(self):
return [0,1]
def baseline(self, om, *params_poly):
if len(params_poly) != 2:
raise ValueError("ModelBaselineExp requires poly_order = 1")
assert len(params_poly) == self.poly_order + 1
a, b = params_poly
return a*np.exp(-b*abs(om))
class ModelBaselinePowerLaw():
_baseline_ensure_positive = False
@property
def _baseline_amplitude_like_params(self):
return [0]
@property
def _baseline_positive_params(self):
return [0,1]
def baseline(self, om, *params_poly):
if len(params_poly) != 2:
raise ValueError("ModelBaselinePowerLaw requires poly_order = 2")
assert len(params_poly) == self.poly_order + 1
a, b = params_poly
return a*abs(om)**(-b)
def _baseline_guesser(self, omt, data):
"""
In the variable names below, prefix l stands for 'log'
"""
if not np.all(omt > 0):
raise ValueError
if np.any(data < 0):
raise ValueError
if np.all(data == 0):
return np.array([0,0])
else:
lP1 = np.log(data[0])
lP2 = np.log(data[-1])
lo1 = np.log(omt[0])
lo2 = np.log(omt[-1])
la = (lo2*lP1 - lo1*lP2)/(lo2-lo1)
b = (la - lP1)/lo1
if b < 0:
#We impose positivity of b.
b = 0
return np.array([np.exp(la), b])
def _get_power_law_slope(self, params_poly):
"""
For use with ModelLineLorentzianWithPowerLawForcing
"""
return params_poly[1]
class ModelLineLorentzian():
n_lineparams = 3
_ind_line_freq = 1
_width_like_params = [2]
_positive_params = [0,2]
_amplitude_like_params = [0]
def line(self, om, A, om_0, gam, **kwargs):
return (A*gam/np.pi)/((om - om_0)**2 + gam**2)
def get_line_hwhm(self, A, om_0, gam):
return gam
class ModelLineVoigt():
n_lineparams = 4
_ind_line_freq = 1
_width_like_params = [2,3]
_positive_params = [0,2,3]
_amplitude_like_params = [0]
def line(self, om, A, om_0, gam, sigma, **kwargs):
return A*scipy.special.voigt_profile(om-om_0, sigma, gam)
def get_line_hwhm(self, A, om_0, gam, sigma):
"""
An approximation formula from https://en.wikipedia.org/wiki/Voigt_profile#The_width_of_the_Voigt_profile (accessed 2:39 PM IST, 09-Jun-2024)
"""
f_L = 2*gam
f_G = 2.3548200450309493*sigma
f = 0.5346*f_L + np.sqrt(0.2166*f_L**2 + f_G**2)
return f/2
class ModelMakerLorentzian(ModelBaselinePoly, ModelLineLorentzian, AbstractModelMaker):
"""
An instance of this class behaves like a function which is the sum of a polynomial of order poly_order and n_lorentz Lorentzians.
"""
pass
class ModelMakerVoigt(ModelBaselinePoly, ModelLineVoigt, AbstractModelMaker):
"""
An instance of this class behaves like a function which is the sum of a polynomial of order poly_order and n_lorentz Voigt functions.
"""
pass