1 | # This program is public domain |
---|
2 | """ |
---|
3 | A park model implementing a multipeak fitter. |
---|
4 | |
---|
5 | *** WARNING *** this example was used to inform the design process, |
---|
6 | and has not yet been updated to correspond to the current implementation. |
---|
7 | Do not use this as a tutorial. |
---|
8 | |
---|
9 | This is an example model showing how to put together a multi-part |
---|
10 | fit objective function. |
---|
11 | |
---|
12 | :group main: Peaks |
---|
13 | :group peaks: Gaussian, Lorentzian, Voigt |
---|
14 | :group backgrounds: Constant, Quadratic, Linear |
---|
15 | """ |
---|
16 | import numpy |
---|
17 | import park |
---|
18 | |
---|
19 | class Peaks(park.Model): |
---|
20 | """Peak fitter""" |
---|
21 | # Increment the minor version when adding new functions |
---|
22 | # Increment the major version if refactoring the class |
---|
23 | __version__ = "0.9" |
---|
24 | peak_types = {} |
---|
25 | |
---|
26 | def __init__(self, datafile=None): |
---|
27 | """ |
---|
28 | Create a new multipeak model. If datafile is present it will |
---|
29 | be loaded directly. If not, then it can be set later with |
---|
30 | self.data.load(datafile), or replaced with a specialized |
---|
31 | data loader for the particular instrument. |
---|
32 | """ |
---|
33 | # The initializer creates self.parameterset which will hold |
---|
34 | # the set of fitting parameters for all models. It will also |
---|
35 | # define self.data = park.data.Data1D() which computes the |
---|
36 | # residuals given an eval function. |
---|
37 | park.Model.__init__(self,filename=datafile) |
---|
38 | self.parameterset = park.ParameterSet() |
---|
39 | self.peaks = {} |
---|
40 | self.peaknum = 0 |
---|
41 | |
---|
42 | def eval(self, x): |
---|
43 | """ |
---|
44 | Returns the raw theory value at x, unconvoluted and unweighted. |
---|
45 | |
---|
46 | The residuals will be calculated by park.Model.residuals by |
---|
47 | calling |
---|
48 | """ |
---|
49 | x = self.data[0] |
---|
50 | y = numpy.empty(x.shape) |
---|
51 | for name,peak in self.peaks.itervalues(): |
---|
52 | y += peak(x) |
---|
53 | return y |
---|
54 | |
---|
55 | def add_peak(self, type, name=None, **kw): |
---|
56 | """ |
---|
57 | Add a peak to the model. |
---|
58 | |
---|
59 | The name of the peak is used to distinguish it from other peaks |
---|
60 | in the model, and appears as part of the parameter name in |
---|
61 | constraint expressions. For example, if name is 'P3' and this |
---|
62 | is part of model 'M1' then the sigma parameter of a gaussian |
---|
63 | peak would be 'M1.P3.sigma'. |
---|
64 | |
---|
65 | The peak can be specified either by peak type and initial arguments |
---|
66 | or by the peak itself, which is a function of x with a parameters |
---|
67 | attribute. |
---|
68 | |
---|
69 | Available peak types include: |
---|
70 | gaussian(scale=1, center=0, sigma=1) |
---|
71 | = scale exp ( -0.5 (x-center)**2 / sigma**2 ) |
---|
72 | lorentzian(scale=1, center=0, gamma=1) |
---|
73 | = scale/pi gamma / ((x-center)**2 + gamma**2)) |
---|
74 | voigt(scale=1, center=0, sigma=1, gamma=1) |
---|
75 | = scale (lorentzian(mu,gamma) * gaussian(mu,sigma))(x-center) |
---|
76 | where * represents convolution |
---|
77 | |
---|
78 | Available background functions include: |
---|
79 | constant(C=0) |
---|
80 | = C |
---|
81 | slope(C=0, B=1) |
---|
82 | = B x + C |
---|
83 | quadratic(A=1, B=0, C=0) |
---|
84 | = A x**2 + B x + C |
---|
85 | |
---|
86 | Additional peak types can be registered. The complete dictionary |
---|
87 | of available types is in Peaks.peak_types. |
---|
88 | """ |
---|
89 | self.peaknum |
---|
90 | if name == None: name = 'P'+str(self.peaknum) |
---|
91 | if isinstance(type,basestring): |
---|
92 | peak = self.peak_types[type](**kw) |
---|
93 | else: |
---|
94 | type = peak |
---|
95 | self.peaks[name] = peak |
---|
96 | self.parameters[name] = peak.parameters |
---|
97 | setattr(self,name,peak) |
---|
98 | |
---|
99 | def remove_peak(self, name): |
---|
100 | """ |
---|
101 | Remove the named peak from the model. |
---|
102 | """ |
---|
103 | del self.peaks[name] |
---|
104 | del self.parameterset[name] |
---|
105 | |
---|
106 | @classmethod |
---|
107 | def register_peak_generator(cls, type, factory): |
---|
108 | """ |
---|
109 | Register a new peak generator. This will return a callable |
---|
110 | object with a parameter set that can be used in the peak |
---|
111 | fitting class. |
---|
112 | |
---|
113 | The individual peak functions a number of attributes: |
---|
114 | |
---|
115 | __name__ |
---|
116 | the name to be displayed in the list of peaks; the |
---|
117 | default is __class__.__name__ |
---|
118 | __factory__ |
---|
119 | the name of a factory function in the python |
---|
120 | namespace which returns a new peak. These parameters |
---|
121 | will be set from the stored parameter list when the |
---|
122 | peak model is reloaded. This can be omitted if it |
---|
123 | is simply the module and name of the class. |
---|
124 | __doc__ |
---|
125 | the tool tip to be displayed with the peak |
---|
126 | __call__(x) |
---|
127 | evaluate the peak function at x |
---|
128 | """ |
---|
129 | cls.peak_types[type]=factory |
---|
130 | |
---|
131 | |
---|
132 | |
---|
133 | # ================== Peaks and backgrounds =================== |
---|
134 | class Gaussian(object): |
---|
135 | """Gaussian peak: scale exp ( -0.5 (x-center)**2 / sigma**2 )""" |
---|
136 | def __init__(self, name, scale=1., center=0, sigma=1): |
---|
137 | park.define_parameters(name, ['scale','center','sigma'], |
---|
138 | scale=scale,center=center,sigma=sigma) |
---|
139 | self.parameters['scale'].deriv = self.dscale |
---|
140 | self.parameters['center'].deriv = self.dcenter |
---|
141 | self.parameters['sigma'].deriv = self.dsigma |
---|
142 | def __call__(self, x): |
---|
143 | return self.scale * numpy.exp(-((x-self.center)/self.sigma)**2) |
---|
144 | def dscale(self, x): |
---|
145 | return self(x)/self.scale |
---|
146 | def dcenter(self, x): |
---|
147 | return 2*(x-self.center)/self.sigma*self(x) |
---|
148 | def dsigma(self, x): |
---|
149 | return 2*(x-self.center)/self.sigma**3*self(x) |
---|
150 | |
---|
151 | class Lorentzian(object): |
---|
152 | """Lorentzian peak (HWHM): scale/pi gamma/((x-center)**2 + gamma**2)""" |
---|
153 | def __init__(self, name, scale=1., center=0, gamma=1): |
---|
154 | park.define_parameters(name, ['scale','center','gamma'], |
---|
155 | scale=scale,center=center,gamma=gamma) |
---|
156 | self.parameters['scale'].deriv = self.dscale |
---|
157 | self.parameters['center'].deriv = self.dcenter |
---|
158 | self.parameters['gamma'].deriv = self.dgamma |
---|
159 | def __call__(self, x): |
---|
160 | return self.scale/numpy.pi * self.gamma/((x-self.center)**2 + self.gamma**2) |
---|
161 | def dscale(self, x): |
---|
162 | return self(x)/self.scale |
---|
163 | def dcenter(self, x): |
---|
164 | return 2*(x-self.center)/((x-self.center)**2+self.gamma**2)*self(x) |
---|
165 | def dgamma(self, x): |
---|
166 | return self(x)*(1/self.gamma - self.gamma/((x-self.center)**2 + self.gamma**2)) |
---|
167 | |
---|
168 | class Voigt(object): |
---|
169 | """Voigt peak (HWHM,sigma): A [G(sigma) * L(gamma)](x-center)""" |
---|
170 | def __init__(self, name, scale=1, center=0, sigma=1, gamma=1): |
---|
171 | park.define_parameters(name, ['scale','center','sigma','gamma'], |
---|
172 | scale=scale, center=center, sigma=sigma, |
---|
173 | gamma=gamma) |
---|
174 | def __call__(self, x): |
---|
175 | return self.scale*voigt(x-self.center, |
---|
176 | sigma=self.sigma, gamma=self.gamma) |
---|
177 | |
---|
178 | class Quadratic(object): |
---|
179 | """Quadratic background: A x**2 + B x + C""" |
---|
180 | name = "Background: quadratic" |
---|
181 | def __init__(self, name, A=1, B=0, C=0): |
---|
182 | park.define_parameters(name, ['A', 'B', 'C'], A=A, B=B, C=C) |
---|
183 | self.parameters['A'].deriv = self.dA |
---|
184 | self.parameters['B'].deriv = self.dB |
---|
185 | self.parameters['C'].deriv = self.dC |
---|
186 | def __call__(self, x): |
---|
187 | return numpy.polyval([self.a.value, self.b.value, self.c.value],x) |
---|
188 | def dA(self, x): return x**2 |
---|
189 | def dB(self, x): return x |
---|
190 | def dC(self, x): return numpy.ones(x.shape) |
---|
191 | |
---|
192 | class Linear(object): |
---|
193 | """Linear background: B x**2 + C""" |
---|
194 | name = "Background: linear" |
---|
195 | def __init__(self, name, B=0, C=0): |
---|
196 | park.define_parameters(name, ['B', 'C'], B=B, C=C) |
---|
197 | self.parameters['B'].deriv = self.dB |
---|
198 | self.parameters['C'].deriv = self.dC |
---|
199 | def __call__(self, x): |
---|
200 | return numpy.polyval([self.a.value, self.b.value, self.c.value],x) |
---|
201 | def dB(self, x): return x |
---|
202 | def dC(self, x): return numpy.ones(x.shape) |
---|
203 | |
---|
204 | class Constant(object): |
---|
205 | """Constant background: C""" |
---|
206 | name = "Background: constant" |
---|
207 | def __init__(self, name, C=0): |
---|
208 | park.define_parameters(name, ['C'], C=C) |
---|
209 | self.parameters['C'].deriv = self.dC |
---|
210 | def __call__(self, x): |
---|
211 | return numpy.polyval([self.a.value, self.b.value, self.c.value],x) |
---|
212 | def dC(self, x): return numpy.ones(x.shape) |
---|
213 | |
---|
214 | |
---|
215 | def voigt(x, sigma, gamma): |
---|
216 | """ |
---|
217 | Return the voigt function, which is the convolution of a Lorentz |
---|
218 | function with a Gaussian. |
---|
219 | |
---|
220 | :Parameters: |
---|
221 | gamma : real |
---|
222 | The half-width half-maximum of the Lorentzian |
---|
223 | sigma : real |
---|
224 | The 1-sigma width of the Gaussian, which is one standard deviation. |
---|
225 | |
---|
226 | Ref: W.I.F. David, J. Appl. Cryst. (1986). 19, 63-64 |
---|
227 | |
---|
228 | Note: adjusted to use stddev and HWHM rather than FWHM parameters |
---|
229 | """ |
---|
230 | # wofz function = w(z) = Fad[d][e][y]eva function = exp(-z**2)erfc(-iz) |
---|
231 | from scipy.special import wofz |
---|
232 | z = (x+1j*gamma)/(sigma*sqrt(2)) |
---|
233 | V = wofz(z)/(sqrt(2*pi)*sigma) |
---|
234 | return V.real |
---|
235 | |
---|
236 | |
---|
237 | def init(): |
---|
238 | """ |
---|
239 | Register peak types with the Peaks model. |
---|
240 | """ |
---|
241 | for cls in [Gaussian, Lorentzian, Voigt, Quadratic, Linear, Constant]: |
---|
242 | Peaks.register_peak_generator(cls.__name__,cls) |
---|
243 | init() |
---|
244 | |
---|
245 | # ======================================== |
---|
246 | |
---|
247 | def test(): |
---|
248 | x = numpy.linspace(-5,5,10) |
---|
249 | |
---|
250 | pass |
---|
251 | |
---|
252 | if __name__ == "__main__": test() |
---|