/
continuum.jl
348 lines (323 loc) · 12.6 KB
/
continuum.jl
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
using CubicSplines
# Model and fit the continuum
struct ContinuumModel
material::Material
e0::Float64
takeoff::Float64
mc::Type{<:MatrixCorrection}
br::Type{<:NeXLBremsstrahlung}
"""
ContinuumModel(mat::Material, e0::Float64, det::DetectorEfficiency, takeoff::Float64)
Create a continuum model for the specified material, beam energy, detector and take-off angle. Computes
the *detected* quantity of continuum generated in the sample.
"""
ContinuumModel(
material::Material,
e0::Float64,
takeoff::Float64;
matrixcorrection::Type{<:MatrixCorrection}=Riveros1993,
bremsstrahlung::Type{<:NeXLBremsstrahlung}=Castellano2004b
) = new(material, e0, takeoff, matrixcorrection, bremsstrahlung)
end
"""
emitted(cm::ContinuumModel, ea::Float64)
Compute the intensity of the measured continuum emitted from the material and conditions specified in the continuum
model object at the specified measured energy `ea`.
"""
function emitted(cm::ContinuumModel, ea::Float64) #
g = generated(cm, ea)
return g > 0.0 ?
g * correctcontinuum(
continuumcorrection(cm.mc, cm.material, ea, cm.e0),
cm.takeoff,
) : 0.0
end
"""
generated(cm::ContinuumModel, ea::Float64)
Compute the intensity of the measured continuum generated from the material and conditions specified in the continuum
model object at the specified measured energy `ea`.
"""
generated(cm::ContinuumModel, ea::Float64) = #
ea <= cm.e0 ? bremsstrahlung(cm.br, ea, cm.e0, cm.material) : 0.0
"""
fitcontinuum(
spec::Spectrum,
resp::AbstractArray,
rois::Vector{UnitRange};
brem::Type{<:NeXLBremsstrahlung} = Castellano2004a,
mc::Type{<:MatricCorrection} = Riveros1993,
)
Fit a continuum model to the specified range of channels (`rois`). The `resp` argument is a matrix which describes
the detector response on a channel-by-channel basis. It can be calculated from an `EDSDetector` and an
`DetectorEfficiency` using `resp = NeXLSpectrum.detectorresponse(det, eff)`. The `Spectrum` object must have
the :Composition, :BeamEnergy and :TakeOffAngle properties defined.
"""
function fitcontinuum(
spec::Spectrum,
resp::AbstractArray{<:Real,2},
rois::AbstractArray{<:UnitRange,1};
brem::Type{<:NeXLBremsstrahlung}=Castellano2004a,
mc::Type{<:MatrixCorrection}=Riveros1993
)
@assert haskey(spec, :Composition) "The fitcontinuum(...) function requires the spec[:Composition] property."
@assert haskey(spec, :BeamEnergy) "The fitcontinuum(...) function requires the spec[:BeamEnergy] property."
@assert haskey(spec, :TakeOffAngle) "The fitcontinuum(...) function requires the spec[:TakeOffAngle] property."
cmod = ContinuumModel(
spec[:Composition],
spec[:BeamEnergy],
spec[:TakeOffAngle],
matrixcorrection=mc,
bremsstrahlung=brem,
)
minEmod = max(50.0, energy(lld(spec), spec)) # Ensures
model = resp * map(e -> e > minEmod ? emitted(cmod, e) : 0.0, energyscale(spec))
k =
sum(sum(model[roi]) for roi in rois) /
sum(sum(counts(spec, roi, Float64)) for roi in rois)
props = Dict{Symbol,Any}(
:TakeOffAngle => spec[:TakeOffAngle],
:BeamEnergy => spec[:BeamEnergy],
:Composition => spec[:Composition],
:K => k,
:Name => "Brem[Global][$(spec[:Name])]",
)
return Spectrum(spec.energy, model / k, props)
end
"""
continuumrois(elems, det::EDSDetector, minE::Float64, maxE::Float64)
Compute the ROIs for the contiguous continuum regions for the specified elements `elems` on an
`EDSDetector` for the specified range of energies.
"""
function continuumrois(
elems,
det::EDSDetector,
minE::Float64,
maxE::Float64,
)::Vector{UnitRange{Int}}
# Calculate the un-rois
function invert(rois, over)
st, res = over.start, []
for roi in rois
low, hgh = max(st, over.start), min(roi.start, over.stop)
if hgh > low
push!(res, low:hgh)
end
st = roi.stop
end
if st < over.stop
push!(res, st:over.stop)
end
return res
end
extend(roi, n) = roi.start-n:roi.stop+n
# Average channel width between minE and maxE.
avgwidth = let
minC, maxC = channel(minE, det), channel(maxE, det)
(energy(maxC, det) - energy(minC, det)) / (maxC - minC)
end
extra = round(Int, (2.0 * resolution(enx"Mn K-L3", det)) / avgwidth)
rois = mapreduce(elm -> extend.(extents(elm, det, 1.0e-3), extra), append!, elems)
# Join rois into contiguous rois
ascontiguous = let
srois = sort(rois)
res = [srois[1]]
for roi in srois[2:end]
if length(intersect(res[end], roi)) > 0
res[end] = min(roi.start, res[end].start):max(roi.stop, res[end].stop)
else
push!(res, roi)
end
end
res
end
return invert(ascontiguous, channel(minE, det):channel(maxE, det))
end
"""
fitcontinuum(
spec::Spectrum,
det::EDSDetector,
resp::AbstractArray{<:Real,2}; #
minE::Float64 = 1.5e3,
maxE::Float64 = 0.95 * spec[:BeamEnergy],
brem::Type{<:NeXLBremsstrahlung} = Castellano2004a,
mc::Type{<:MatrixCorrection} = Riveros1993,
)
Fit the continuum from ROIs determined from the data within the spectrum (:Composition, :BeamEnergy & :TakeOffAngle).
The ROIs are computed using `continuumrois(...)` and each roi is fit seperately.
"""
function fitcontinuum(
spec::Spectrum,
det::EDSDetector,
resp::AbstractArray{<:Real,2}; #
minE::Float64=1.5e3,
maxE::Float64=0.90 * spec[:BeamEnergy],
width::Int=20,
brem::Type{<:NeXLBremsstrahlung}=Castellano2004a,
mc::Type{<:MatrixCorrection}=Riveros1993
)
@assert haskey(spec, :Composition) "The fitcontinuum(...) function requires the spec[:Composition] property."
@assert haskey(spec, :BeamEnergy) "The fitcontinuum(...) function requires the spec[:BeamEnergy] property."
@assert haskey(spec, :TakeOffAngle) "The fitcontinuum(...) function requires the spec[:TakeOffAngle] property."
cmod = ContinuumModel(
spec[:Composition],
spec[:BeamEnergy],
spec[:TakeOffAngle],
matrixcorrection=mc,
bremsstrahlung=brem,
)
# meas is the raw continuum shape. It needs to be scaled to the unknown.
minEmod = max(50.0, energy(lld(spec), spec))
model = resp * map(e -> e > minEmod ? emitted(cmod, e) : 0.0, energyscale(spec))
chlow, k0 = 1, 0.0
chdata, result = counts(spec, Float64), zeros(Float64, length(spec))
for roi in continuumrois(elms(spec, true), det, minE, maxE)
# Begining of continuum ROI
roi1 = roi.start:(roi.start+min(width, length(roi))-1)
k1 = sum(@view chdata[roi1]) / sum(@view model[roi1])
# End of continuum ROI
roi2 = (roi.stop-min(width, length(roi))+1):roi.stop
k2 = sum(@view chdata[roi2]) / sum(@view model[roi2])
# Model between roi1 and roi2
k1_2(ch) = (k2 - k1) * (ch - roi.start) / length(roi) + k1
result[roi] = k1_2.(roi) .* model[roi]
k0 = chlow == 1 ? k1 : k0
# Model between prev roi and roi
roi0_1 = chlow:(roi.start-1)
k0_1(ch) = (k1 - k0) * (ch - roi0_1.start) / length(roi0_1) + k0
result[roi0_1] = k0_1.(roi0_1) .* model[roi0_1]
chlow, k0 = roi.stop + 1, k2
end
# Fill the final patch
result[chlow:length(result)] = k0 * model[chlow:length(result)]
props = Dict{Symbol,Any}(
:TakeOffAngle => spec[:TakeOffAngle],
:BeamEnergy => spec[:BeamEnergy],
:Composition => spec[:Composition],
:Name => "Brem[Local][$(spec[:Name])]",
)
return Spectrum(spec.energy, result, props)
end
"""
fittedcontinuum(
spec::Spectrum,
det::EDSDetector,
resp::AbstractArray{<:Real,2}; #
mode = :Global [ | :Local ] # Fit to all ROIs simultaneously (:Global) or to each roi independently (:Local)
minE::Float64 = 1.5e3,
maxE::Float64 = 0.95 * spec[:BeamEnergy],
width::Int = 20, # Width of ROI at each end of each patch of continuum that is matched
brem::Type{<:NeXLBremsstrahlung} = Castellano2004a,
mc::Type{<:MatrixCorrection} = Riveros1993,
)::Spectrum
Fit the continuum under the characteristic peaks by fitting the closest continuum ROIs. The low energy peaks are
fit using the continuum immediately higher in energy and the high energy peaks are fit using the continuum on both
sides.
* mode = :Global [ | :Local ] Global fits the model to the data using a single scale factor. :Local tweaks the
global model at ROIs above and below the characteristic peaks.
Typically, :Global produces the overall best fit but :Local fits better around the characteristic peaks and is
better for modeling the continuum under the peaks.
"""
function fittedcontinuum(
spec::Spectrum,
det::EDSDetector,
resp::AbstractArray{<:Real,2}; #
mode::Symbol=:Global,
minE::Float64=1.5e3,
maxE::Float64=0.95 * spec[:BeamEnergy],
brem::Type{<:NeXLBremsstrahlung}=Castellano2004a,
mc::Type{<:MatrixCorrection}=Riveros1993
)
@assert (mode == :Global) || (mode == :Local) "mode must equal :Global | :Local in fitted continuum"
crois = continuumrois(elms(spec, true), det, minE, maxE)
gl = fitcontinuum(spec, resp, crois, brem=brem, mc=mc)
return mode == :Global ? gl : tweakcontinuum(spec, gl, crois)
end
"""
tweakcontinuum(
meas::Spectrum{T},
cont::Spectrum{U},
crois::Vector{UnitRange{Int}};
nch=10,
maxSc=1.5
) where { T<: Real, U <: Real }
Takes a measured spectrum and the associated modeled continuum spectrum and tweaks the continuum spectrum to produce
a better fit to the measured. It focuses on the ends of the continuum ROIs (in `crois`) to match the continuum at
these channels.
* `maxSc` limits how large a tweak is acceptable.
* `nch` determines how many channels to match at the end of each ROI in `crois`
"""
function tweakcontinuum(meas::Spectrum{T}, cont::Spectrum{U}, crois::Vector{UnitRange{Int}}; nch=10, maxSc=1.5) where {T<:Real,U<:Real}
function LinearSpline(x, y)
@assert length(x)==length(y)
function f(xx)
i = findfirst(v -> xx <= v, x)
return if !isnothing(i)
i > firstindex(x) ? ((y[i] - y[i-1]) / (x[i] - x[i-1])) * (xx - x[i-1]) + y[i-1] : y[i]
else
y[lastindex(y)]
end
end
return f
end
x, y, chmax = Float64[1.0], Float64[1.0], channel(0.9*meas[:BeamEnergy], meas)
for croi in crois
if (length(croi) > (3 * nch) / 2) && (croi.start < chmax)
# Add a pivot beginning and end of croi
stroi = croi.start:croi.start+nch-1
push!(x, stroi.start)
push!(y, Base.clamp(integrate(meas, stroi) / max(integrate(cont, stroi),1.0), 1.0 / maxSc, maxSc))
if croi.stop < chmax
endroi = croi.stop-nch+1:min(length(cont), croi.stop)
push!(x, endroi.stop)
push!(y, clamp(integrate(meas, endroi) / max(integrate(cont, endroi), 1.0), 1.0 / maxSc, maxSc))
else
push!(x, chmax)
push!(y, 1.0)
end
end
end
if x[end] < length(cont)
push!(x, length(cont))
push!(y, 1.0)
end
# In case there are too few points in the spline for a cubic spline
spline = length(x) >= 5 ? CubicSpline(x, y) : LinearSpline(x, y)
scale = map(x -> spline(x), eachindex(cont.counts))
props = Dict(
cont.properties...,
:CScale => scale,
:Parent => cont,
:Name => "Tweaked[$(cont[:Name])]"
)
return Spectrum(cont.energy, scale .* cont.counts, props)
end
"""
subtractcontinuum(
spec::Spectrum,
det::EDSDetector,
resp::AbstractArray{<:Real,2}; #
minE::Float64 = 1.5e3,
maxE::Float64 = 0.95 * spec[:BeamEnergy],
brem::Type{<:NeXLBremsstrahlung} = Castellano2004a,
mc::Type{<:MatrixCorrection} = Riveros1993,
)::Spectrum
Computes the characteristic-only spectrum by subtracting the continuum.
"""
function subtractcontinuum(
spec::Spectrum,
det::EDSDetector,
resp::AbstractArray{<:Real,2}; #
minE::Float64=1.5e3,
maxE::Float64=0.95 * spec[:BeamEnergy],
brem::Type{<:NeXLBremsstrahlung}=Castellano2004a,
mc::Type{<:MatrixCorrection}=Riveros1993
)::Spectrum
res = Spectrum(
spec.energy,
counts(spec, Float64) -
fitcontinuum(spec, det, resp, minE=minE, maxE=maxE, brem=brem, mc=mc),
copy(spec.properties),
)
res[:Name] = "CharOnly[$(res[:Name])]"
return res
end