/
xrf_bgr.py
459 lines (382 loc) · 18 KB
/
xrf_bgr.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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
"""
Methods for fitting background in energy dispersive xray spectra
Authors/Modifications:
----------------------
* Mark Rivers, GSECARS
* See http://cars9.uchicago.edu/software/python/index.html
Notes:
------
This function fits a background to an MCA spectrum. The background is
fitted using an enhanced version of the algorithm published by
Kajfosz, J. and Kwiatek, W .M. (1987) "Non-polynomial approximation of
background in x-ray spectra." Nucl. Instrum. Methods B22, 78-81.
Procedure:
1) At each channel "i" in a spectrum (y[i]) an n'th degree polynomial which
is concave up is fitted. Its equation is
(e(i) - e(j))**n
f(j,i) = y(i) + --------------
top_width**n
where f(j,i) is the fitted counts in channel j for the polynomial
centered in channel i. y(i) is the input counts in channel "i", e(i) is
the energy of channel i, e(j) is the energy of channel j, and
"top_width" and "n" are user-specified parameters. The background count
in channel "j", b(j) is defined as
b(j) = min ((f(j,i), y(j))
i
b(j) is thus the smallest fitted polynomial in channel j, or the raw
data, whichever is smaller.
2) After the concave up polynomials have been fitted, a series of
concave down polynomials are constructed. At each channel "i" an n'th
degree polynomial which is concave up is fitted. The polynomial is slid
up from below until it "just touches" some channel of the spectrum. Call
this channel "i". The maximum height of the polynomial is thus
(e(i) - e(j))**n
height(j) = max ( b(j) + -------------- )
i bottom_width**n
where bottom_width is a user_specified parameter.
3) Once the value of height(i) is known the polynomial is fitted. The
background counts in each channel are then determined from:
(e(i) - e(j))**n
bgr(j) = max ( height(i) + -------------- )
i bottom_width**n
bgr(j) is thus the maximum counts for any of the concave down
polynomials passing though channel j.
Before the concave-down polynomials are fitted the spectrum at each
channel it is possible to subtract out a straight line which is
tangent to the spectrum at that channel. Use the TANGENT qualifier to
do this. This is equivalent to fitting a "tilted" polynomial whose
apex is tangent to the spectrum at that channel. By fitting
polynomials which are tangent rather than vertical the background fit
is much improved on spectra with steep slopes.
Input Parameter Fields:
bottom_width (double, variable):
Specifies the width of the polynomials which are concave downward.
The bottom_width is the full width in energy units at which the
magnitude of the polynomial is 100 counts. The default is 4.
top_width (double, variable):
Specifies the width of the polynomials which are concave upward.
The top_width is the full width in energy units at which the
magnitude of the polynomial is 100 counts. The default is 0, which
means that concave upward polynomials are not used.
tangent (True/False):
Specifies that the polynomials are to be tangent to the slope of the
spectrum. The default is vertical polynomials. This option works
best on steeply sloping spectra. It has trouble in spectra with
big peaks because the polynomials are very tilted up inside the
peaks. Default is False
exponent (int):
Specifies the power of polynomial which is used. The power must be
an integer. The default is 2, i.e. parabolas. Higher exponents,
for example EXPONENT=4, results in polynomials with flatter tops
and steeper sides, which can better fit spectra with steeply
sloping backgrounds.
compress (int):
Compression factor to apply before fitting the background.
Default=4, which means, for example, that a 2048 channel spectrum
will be rebinned to 512 channels before fitting.
The compression is done on a temporary copy of the input spectrum,
so the input spectrum itself is unchanged.
The algorithm works best if the spectrum is compressed before it
is fitted. There are two reasons for this. First, the background
is constrained to never be larger than the data itself. If the
spectrum has negative noise spikes they will cause the fit to be
too low. Compression will smooth out such noise spikes.
Second, the algorithm requires about 3*N**2 operations, so the time
required grows rapidly with the size of the input spectrum. On a
200 MHz Pentium it takes about 3 seconds to fit a 2048 channel
spectrum with COMPRESS=1 (no compression), but only 0.2 seconds
with COMPRESS=4 (the default).
Note - compress needs a data array that integer divisible.
Inputs to calc
data:
The raw data to fit the background
slope:
Slope for the conversion from channel number to energy.
Ie the slope from calibration
Todo:
-----
* fix compress so works for arbitrary factor
(see ana.background)
* get rid of bottom width and top width flags
no reason to optimize these...
"""
#############################################################################
import copy
import numpy as num
#############################################################################
class Background:
"""
Class defining a spectrum background
Attributes:
-----------
These may be set by kw argument upon initialization.
* bottom_width = 4.0 # Bottom width
* bottom_width_flag = 0 # Bottom width flag
# 0 = Optimize
# 1 = Fixed
* top_width = 0.0 # Top width
* top_width_flag = 0 # Top width flag
# 0 = Optimize
# 1 = Fixed
* exponent = 2 # Exponent
* tangent = False # Tangent flag
* compress = 4 # Compress
"""
#########################################################################
def __repr__(self):
""" display """
lout = 'Xrf Background Parameters:\n'
lout = lout + ' bottom_width = %f\n' % self.bottom_width
lout = lout + ' bottom_width_flag = %i\n' % self.bottom_width_flag
lout = lout + ' top_width = %f\n' % self.top_width
lout = lout + ' top_width_flag = %i\n' % self.top_width_flag
lout = lout + ' exponent = %i\n' % self.exponent
lout = lout + ' tangent = %s\n' % str(self.tangent)
lout = lout + ' compress = %i\n' % self.compress
return lout
##########################################################################
def __init__(self,**kws):
"""
Parameters:
-----------
These may be set by kw argument upon initialization.
* bottom_width = 4.0 # Bottom width
* bottom_width_flag = 0 # Bottom width flag
# 0 = Optimize
# 1 = Fixed
* top_width = 0.0 # Top width
* top_width_flag = 0 # Top width flag
# 0 = Optimize
# 1 = Fixed
* exponent = 2 # Exponent
* tangent = False # Tangent flag
* compress = 4 # Compress
"""
self.bgr = [] # Background
# Parameters
self.bottom_width = 4.0 # Bottom width
self.bottom_width_flag = 1 # Bottom width flag
# 0 = Optimize
# 1 = Fixed
self.top_width = 0.0 # Top width
self.top_width_flag = 1 # Top width flag
# 0 = Optimize
# 1 = Fixed
self.exponent = 2 # Exponent
self.tangent = False # Tangent flag
self.compress = 4 # Compress
# init
self.init(params=kws)
###########################################################################
def init(self,params=None):
"""
init based on keyword parameters
"""
# reset bgr array
self.bgr = []
# set parameters
if params:
keys = params.keys()
if 'bottom_width' in keys: self.bottom_width = float(params['bottom_width'])
if 'bottom_width_flag' in keys: self.bottom_width_flag = int(params['bottom_width_flag'])
if 'top_width' in keys: self.top_width = float(params['top_width'])
if 'top_width_flag' in keys: self.top_width_flag = int(params['top_width_flag'])
if 'exponent' in keys: self.exponent = int(params['exponent'])
if 'compress' in keys: self.compress = int(params['compress'])
if 'tangent' in keys: self.tangent = bool(eval(str(params['tangent'])))
# For optimization
self.parinfo = []
# bottom_width
self.parinfo.append({'parname':'bottom_width',
'value':self.bottom_width,
'fixed':self.top_width_flag,
'limited':[0,0], 'limits':[0., 0.], 'step':0.})
# top_width
self.parinfo.append({'parname':'top_width',
'value':self.top_width,
'fixed':self.top_width_flag,
'limited':[0,0], 'limits':[0., 0.], 'step':0.})
##################################################################################
def get_params(self,):
"""
Return a dictionary of parameters
"""
bgr_params = {'bottom_width':self.bottom_width,
'bottom_width_flag':self.bottom_width_flag,
'top_width':self.top_width,
'top_width_flag':self.top_width_flag,
'exponent':self.exponent,
'tangent':self.tangent,
'compress':self.compress}
return bgr_params
##################################################################################
def calc(self, data, slope=1.0):
"""
compute background
Parameters:
-----------
* data is the spectrum
* slope is the slope of conversion channels to energy
"""
REFERENCE_AMPL=100.
TINY = 1.E-20
HUGE = 1.E20
MAX_TANGENT=2
bottom_width = self.bottom_width
top_width = self.top_width
exponent = self.exponent
tangent = self.tangent
compress = self.compress
#bgr = copy.copy(data)
nchans = len(data)
self.bgr = num.zeros(nchans,dtype=num.int)
scratch = copy.copy(data)
# Compress scratch spectrum
if (compress > 1):
tmp = compress_array(scratch, compress)
if tmp == None:
compress = 1
else:
scratch = tmp
slope = slope * compress
nchans = nchans / compress
# Copy scratch spectrum to background spectrum
bckgnd = copy.copy(scratch)
# Find maximum counts in input spectrum. This information is used to
# limit the size of the function lookup table
max_counts = max(scratch)
####################################################
# Fit functions which come down from top
if (top_width > 0.):
# First make a lookup table of this function
chan_width = top_width / (2. * slope)
denom = chan_width**exponent
indices = num.arange(float(nchans*2+1)) - nchans
power_funct = indices**exponent * (REFERENCE_AMPL / denom)
power_funct = num.compress((power_funct <= max_counts), power_funct)
max_index = len(power_funct)/2 - 1
for center_chan in range(nchans):
first_chan = max((center_chan - max_index), 0)
last_chan = min((center_chan + max_index), (nchans-1))
f = first_chan - center_chan + max_index
l = last_chan - center_chan + max_index
test = scratch[center_chan] + power_funct[f:l+1]
sub = bckgnd[first_chan:last_chan+1]
bckgnd[first_chan:last_chan+1] = num.maximum(sub, test)
# Copy this approximation of background to scratch
scratch = copy.copy(bckgnd)
# Find maximum counts in scratch spectrum. This information is used to
# limit the size of the function lookup table
max_counts = max(scratch)
####################################################
# Fit functions which come up from below
bckgnd = num.arange(float(nchans)) - HUGE
# First make a lookup table of this function
chan_width = bottom_width / (2. * slope)
if (chan_width == 0.):
denom = TINY
else:
denom = chan_width**exponent
indices = num.arange(float(nchans*2+1)) - nchans
power_funct = indices**exponent * (REFERENCE_AMPL / denom)
power_funct = num.compress((power_funct <= max_counts), power_funct)
max_index = len(power_funct)/2 - 1
for center_chan in range(nchans-1):
tangent_slope = 0.
if tangent:
# Find slope of tangent to spectrum at this channel
first_chan = max((center_chan - MAX_TANGENT), 0)
last_chan = min((center_chan + MAX_TANGENT), (nchans-1))
denom = center_chan - num.arange(float(last_chan - first_chan + 1))
# is this correct?
denom = max(max(denom), 1)
tangent_slope = (scratch[center_chan] - scratch[first_chan:last_chan+1]) / denom
tangent_slope = num.sum(tangent_slope) / (last_chan - first_chan)
first_chan = max((center_chan - max_index), 0)
last_chan = min((center_chan + max_index), (nchans-1))
last_chan = max(last_chan, first_chan)
nc = last_chan - first_chan + 1
lin_offset = scratch[center_chan] + (num.arange(float(nc)) - nc/2) * tangent_slope
# Find the maximum height of a function centered on this channel
# such that it is never higher than the counts in any channel
f = first_chan - center_chan + max_index
l = last_chan - center_chan + max_index
test = scratch[first_chan:last_chan+1] - lin_offset + power_funct[f:l+1]
height = min(test)
# We now have the function height. Set the background to the
# height of the maximum function amplitude at each channel
test = height + lin_offset - power_funct[f:l+1]
sub = bckgnd[first_chan:last_chan+1]
bckgnd[first_chan:last_chan+1] = num.maximum(sub, test)
####################################################
# Expand spectrum
if (compress > 1):
bckgnd = expand_array(bckgnd, compress)
# Bgr should be positive integers??
bgr = bckgnd.astype(int)
idx = num.where(bgr <= 0)
bgr[idx] = 0
self.bgr = bgr
##################################################################################
def _update(self,parameters):
"""
update for fitting
"""
if len(parameters) != 2:
raise "Wrong number of parameters in background"
self.bottom_width = parameters[0]
self.top_width = parameters[1]
############################################################
def compress_array(array, compress):
"""
Compresses an 1-D array by the integer factor "compress".
Temporary fix until the equivalent of IDL's 'rebin' is found.
"""
alen = len(array)
if ((alen % compress) != 0):
print 'Warning compress must be integer divisor of array length'
return None
temp = num.resize(array, (alen/compress, compress))
return num.sum(temp, 1)/compress
############################################################
def expand_array(array, expand, sample=0):
"""
Expands an 1-D array by the integer factor "expand".
if 'sample' is 1 the new array is created with sampling,
if 0 then the new array is created via interpolation (default)
Temporary fix until the equivalent of IDL's 'rebin' is found.
"""
alen = len(array)
if (expand == 1): return array
if (sample == 1): return num.repeat(array, expand)
kernel = num.ones(expand)/expand
# The following mimic the behavior of IDL's rebin when expanding
temp = num.convolve(num.repeat(array, expand), kernel, mode=2)
# Discard the first "expand-1" entries
temp = temp[expand-1:]
# Replace the last "expand" entries with the last entry of original
for i in range(1,expand): temp[-i]=array[-1]
return temp
########################################################################
########################################################################
########################################################################
def test():
from matplotlib import pyplot
# make some dat
import _test_dat as test_dat
chans = num.arange(2048)
offset = 1.0
slope = .01
en = offset + slope*chans
data = test_dat.data1(en)
pyplot.plot(en,data,'ko')
#
bgr = Background(bottom_width=4.,compress=4)
bgr.calc(data,slope=slope)
print bgr
pyplot.plot(en,bgr.bgr,'r')
#
pyplot.show()
########################################################################
if __name__ == "__main__":
test()