-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathspecfit.jl
300 lines (268 loc) · 13.3 KB
/
specfit.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
# This file is a part of LegendSpecFits.jl, licensed under the MIT License (MIT).
# helper fucntions for fitting peakshapes
th228_fit_functions = (
f_fit = (x, v) -> gamma_peakshape(x, v.μ, v.σ, v.n, v.step_amplitude, v.skew_fraction, v.skew_width, v.background),
f_sig = (x, v) -> signal_peakshape(x, v.μ, v.σ, v.n, v.skew_fraction),
f_lowEtail = (x, v) -> lowEtail_peakshape(x, v.μ, v.σ, v.n, v.skew_fraction, v.skew_width),
f_bck = (x, v) -> background_peakshape(x, v.μ, v.σ, v.step_amplitude, v.background),
f_sigWithTail = (x, v) -> signal_peakshape(x, v.μ, v.σ, v.n, v.skew_fraction) + lowEtail_peakshape(x, v.μ, v.σ, v.n, v.skew_fraction, v.skew_width)
)
export th228_fit_functions
"""
estimate_single_peak_stats(h::Histogram, calib_type::Symbol=:th228)
Estimate statistics/parameters for a single peak in the given histogram `h`.
`h` must only contain a single peak. The peak should have a Gaussian-like
shape.
`calib_type` specifies the calibration type. Currently only `:th228` is implemented.
If you want get the peak statistics for a PSD calibration, use `:psd`.
# Returns
`NamedTuple` with the fields
* `peak_pos`: estimated position of the peak (in the middle of the peak)
* `peak_fwhm`: full width at half maximum (FWHM) of the peak
* `peak_sigma`: estimated standard deviation of the peak
* `peak_counts`: estimated number of counts in the peak
* `mean_background`: estimated mean background value
"""
function estimate_single_peak_stats(h::Histogram,; calib_type::Symbol=:th228)
if calib_type == :th228
return estimate_single_peak_stats_th228(h)
elseif calib_type == :psd
return estimate_single_peak_stats_psd(h)
else
error("Calibration type not supported")
end
end
export estimate_single_peak_stats
function estimate_single_peak_stats_th228(h::Histogram{T}) where T<:Real
W = h.weights
E = first(h.edges)
peak_amplitude, peak_idx = findmax(W)
fwhm_idx_left = findfirst(w -> w >= (first(W) + peak_amplitude) / 2, W)
fwhm_idx_right = findlast(w -> w >= (last(W) + peak_amplitude) / 2, W)
peak_max_pos = (E[peak_idx] + E[peak_idx+1]) / 2
peak_mid_pos = (E[fwhm_idx_right] + E[fwhm_idx_left]) / 2
peak_pos = (peak_max_pos + peak_mid_pos) / 2.0
peak_fwhm = (E[fwhm_idx_right] - E[fwhm_idx_left]) / 1.0
peak_sigma = peak_fwhm * inv(2*√(2log(2)))
peak_fwqm = NaN
# make sure that peakstats have non-zero sigma and fwhm values to prevent fit priors from being zero
if peak_fwhm == 0
fwqm_idx_left = findfirst(w -> w >= (first(W) + peak_amplitude) / 4, W)
fwqm_idx_right = findlast(w -> w >= (last(W) + peak_amplitude) / 4, W)
peak_fwqm = (E[fwqm_idx_right] - E[fwqm_idx_left]) / 1.0
peak_sigma = peak_fwqm * inv(2*√(2log(4)))
peak_fwhm = peak_sigma * 2*√(2log(2))
end
if peak_sigma == 0
peak_sigma = 1.0
peak_fwhm = 2.0
end
#peak_area = peak_amplitude * peak_sigma * sqrt(2*π)
mean_background = (first(W) + last(W)) / 2
mean_background = ifelse(mean_background == 0, 0.01, mean_background)
peak_counts = inv(0.761) * (sum(view(W,fwhm_idx_left:fwhm_idx_right)) - mean_background * peak_fwhm)
peak_counts = ifelse(peak_counts < 0.0, inv(0.761) * sum(view(W,fwhm_idx_left:fwhm_idx_right)), peak_counts)
if !isnan(peak_fwqm)
peak_counts = inv(0.904) * (sum(view(W,fwqm_idx_left:fwqm_idx_right)) - mean_background * peak_fwqm)
peak_counts = ifelse(peak_counts < 0.0, inv(0.904) * sum(view(W,fwqm_idx_left:fwqm_idx_right)), peak_counts)
end
(
peak_pos = peak_pos,
peak_fwhm = peak_fwhm,
peak_sigma = peak_sigma,
peak_counts = peak_counts,
mean_background = mean_background
)
end
"""
fit_peaks(peakhists::Array, peakstats::StructArray, th228_lines::Array,; calib_type::Symbol=:th228, uncertainty::Bool=true, low_e_tail::Bool=true)
Perform a fit of the peakshape to the data in `peakhists` using the initial values in `peakstats` to the calibration lines in `th228_lines`.
# Returns
* `peak_fit_plots`: array of plots of the peak fits
* `return_vals`: dictionary of the fit results
"""
function fit_peaks(peakhists::Array, peakstats::StructArray, th228_lines::Array,; calib_type::Symbol=:th228, uncertainty::Bool=true, low_e_tail::Bool=true,iterative_fit::Bool=false)
if calib_type == :th228
return fit_peaks_th228(peakhists, peakstats, th228_lines,; uncertainty=uncertainty, low_e_tail=low_e_tail,iterative_fit=iterative_fit)
else
error("Calibration type not supported")
end
end
export fit_peaks
function fit_peaks_th228(peakhists::Array, peakstats::StructArray, th228_lines::Array{T},; uncertainty::Bool=true, low_e_tail::Bool=true, iterative_fit::Bool=false) where T<:Any
# create return and result dicts
result = Dict{T, NamedTuple}()
report = Dict{T, NamedTuple}()
# iterate throuh all peaks
for (i, peak) in enumerate(th228_lines)
# get histogram and peakstats
h = peakhists[i]
ps = peakstats[i]
# fit peak
result_peak, report_peak = fit_single_peak_th228(h, ps, ; uncertainty=uncertainty, low_e_tail=low_e_tail)
# check covariance matrix for being semi positive definite (no negative uncertainties)
if uncertainty
if iterative_fit && !isposdef(result_peak.covmat)
@warn "Covariance matrix not positive definite for peak $peak - repeat fit without low energy tail"
pval_save = result_peak.pval
result_peak, report_peak = fit_single_peak_th228(h, ps, ; uncertainty=uncertainty, low_e_tail=false)
@info "New covariance matrix is positive definite: $(isposdef(result_peak.covmat))"
@info "p-val with low-energy tail p=$(round(pval_save,digits=5)) , without low-energy tail: p=$(round((result_peak.pval),digits=5))"
end
end
# save results
result[peak] = result_peak
report[peak] = report_peak
end
return result, report
end
"""
fit_single_peak_th228(h::Histogram, ps::NamedTuple{(:peak_pos, :peak_fwhm, :peak_sigma, :peak_counts, :mean_background), NTuple{5, T}};, uncertainty::Bool=true, fixed_position::Bool=false, low_e_tail::Bool=true) where T<:Real
Perform a fit of the peakshape to the data in `h` using the initial values in `ps` while using the `gamma_peakshape` with low-E tail.
Also, FWHM is calculated from the fitted peakshape with MC error propagation. The peak position can be fixed to the value in `ps` by setting `fixed_position=true`. If the low-E tail should not be fitted, it can be disabled by setting `low_e_tail=false`.
# Returns
* `result`: NamedTuple of the fit results containing values and errors
* `report`: NamedTuple of the fit report which can be plotted
"""
function fit_single_peak_th228(h::Histogram, ps::NamedTuple{(:peak_pos, :peak_fwhm, :peak_sigma, :peak_counts, :mean_background), NTuple{5, T}};
uncertainty::Bool=true, low_e_tail::Bool=true, fixed_position::Bool=false, pseudo_prior::NamedTupleDist=NamedTupleDist(empty = true),
fit_fun::Symbol=:f_fit) where T<:Real
# create standard pseudo priors
standard_pseudo_prior = NamedTupleDist(
μ = ifelse(fixed_position, ConstValueDist(ps.peak_pos), Uniform(ps.peak_pos-10, ps.peak_pos+10)),
σ = weibull_from_mx(ps.peak_sigma, 2*ps.peak_sigma),
n = weibull_from_mx(ps.peak_counts, 2*ps.peak_counts),
step_amplitude = weibull_from_mx(ps.mean_background, 2*ps.mean_background),
skew_fraction = ifelse(low_e_tail, Uniform(0.005, 0.25), ConstValueDist(0.0)),
skew_width = ifelse(low_e_tail, LogUniform(0.001, 0.1), ConstValueDist(1.0)),
background = weibull_from_mx(ps.mean_background, 2*ps.mean_background),
)
# use standard priors in case of no overwrites given
if !(:empty in keys(pseudo_prior))
# check if input overwrite prior has the same fields as the standard prior set
@assert all(f -> f in keys(standard_pseudo_prior), keys(standard_pseudo_prior)) "Pseudo priors can only have $(keys(standard_pseudo_prior)) as fields."
# replace standard priors with overwrites
pseudo_prior = merge(standard_pseudo_prior, pseudo_prior)
else
# take standard priors as pseudo priors with overwrites
pseudo_prior = standard_pseudo_prior
end
# transform back to frequency space
f_trafo = BAT.DistributionTransform(Normal, pseudo_prior)
# start values for MLE
v_init = mean(pseudo_prior)
# create loglikehood function: f_loglike(v) that can be evaluated for any set of v (fit parameter)
f_loglike = let f_fit=th228_fit_functions[fit_fun], h=h
v -> hist_loglike(Base.Fix2(f_fit, v), h)
end
# MLE
opt_r = optimize((-) ∘ f_loglike ∘ inverse(f_trafo), f_trafo(v_init))
# best fit results
v_ml = inverse(f_trafo)(Optim.minimizer(opt_r))
f_loglike_array = let f_fit=th228_fit_functions[fit_fun], h=h, v_keys = keys(standard_pseudo_prior) #same loglikelihood function as f_loglike, but has array as input instead of NamedTuple
v -> - hist_loglike( x -> f_fit(x,NamedTuple{v_keys}(v)), h)
end
if uncertainty
# Calculate the Hessian matrix using ForwardDiff
H = ForwardDiff.hessian(f_loglike_array, tuple_to_array(v_ml))
# Calculate the parameter covariance matrix
param_covariance_raw = inv(H)
param_covariance = nearestSPD(param_covariance_raw)
# Extract the parameter uncertainties
v_ml_err = array_to_tuple(sqrt.(abs.(diag(param_covariance))), v_ml)
# calculate p-value
pval, chi2, dof = p_value(th228_fit_functions.f_fit, h, v_ml)
# get fwhm of peak
fwhm, fwhm_err = get_peak_fwhm_th228(v_ml, param_covariance)
@debug "Best Fit values"
@debug "μ: $(v_ml.μ) ± $(v_ml_err.μ)"
@debug "σ: $(v_ml.σ) ± $(v_ml_err.σ)"
@debug "n: $(v_ml.n) ± $(v_ml_err.n)"
@debug "p: $pval , chi2 = $(chi2) with $(dof) dof"
@debug "FWHM: $(fwhm) ± $(fwhm_err)"
result = merge(v_ml, (pval = pval, chi2 = chi2, dof = dof, fwhm = fwhm,covmat = param_covariance, covmat_raw = param_covariance_raw,),(err = merge(v_ml_err, (fwhm = fwhm_err,)),))
report = (
v = v_ml,
h = h,
f_fit = x -> Base.Fix2(th228_fit_functions.f_fit, v_ml)(x),
f_sig = x -> Base.Fix2(th228_fit_functions.f_sig, v_ml)(x),
f_lowEtail = x -> Base.Fix2(th228_fit_functions.f_lowEtail, v_ml)(x),
f_bck = x -> Base.Fix2(th228_fit_functions.f_bck, v_ml)(x),
)
else
# get fwhm of peak
fwhm, fwhm_err = get_peak_fwhm_th228(v_ml, v_ml, false)
@debug "Best Fit values"
@debug "μ: $(v_ml.μ)"
@debug "σ: $(v_ml.σ)"
@debug "n: $(v_ml.n)"
@debug "FWHM: $(fwhm)"
result = merge(v_ml, (fwhm = fwhm, ))
report = (
v = v_ml,
h = h,
f_fit = x -> Base.Fix2(th228_fit_functions.f_fit, v_ml)(x),
f_sig = x -> Base.Fix2(th228_fit_functions.f_sig, v_ml)(x),
f_lowEtail = x -> Base.Fix2(th228_fit_functions.f_lowEtail, v_ml)(x),
f_bck = x -> Base.Fix2(th228_fit_functions.f_bck, v_ml)(x)
)
end
return result, report
end
export fit_single_peak_th228
"""
estimate_fwhm(v::NamedTuple, v_err::NamedTuple)
Get the FWHM of a peak from the fit parameters.
# Returns
* `fwhm`: the FWHM of the peak
"""
function estimate_fwhm(v::NamedTuple)
# get FWHM
try
half_max_sig = maximum(Base.Fix2(th228_fit_functions.f_sigWithTail, v).(v.μ - v.σ:0.001:v.μ + v.σ))/2
roots_low = find_zero(x -> Base.Fix2(th228_fit_functions.f_sigWithTail, v)(x) - half_max_sig, v.μ - v.σ, maxiter=100)
roots_high = find_zero(x -> Base.Fix2(th228_fit_functions.f_sigWithTail,v)(x) - half_max_sig, v.μ + v.σ, maxiter=100)
return roots_high - roots_low
catch e
return NaN
end
end
export estimate_fwhm
"""
get_peak_fwhm_th228(v_ml::NamedTuple, v_ml_err::NamedTuple)
Get the FWHM of a peak from the fit parameters while performing a MC error propagation.
# Returns
* `fwhm`: the FWHM of the peak
* `fwhm_err`: the uncertainty of the FWHM of the peak
"""
function get_peak_fwhm_th228(v_ml::NamedTuple, v_ml_err::Union{Matrix,NamedTuple},uncertainty::Bool=true)
# get fwhm for peak fit
fwhm = estimate_fwhm(v_ml)
if !uncertainty
return fwhm, NaN
end
# get MC for FWHM err
if isa(v_ml_err,Matrix)# use correlated fit parameter uncertainties
v_mc = get_mc_value_shapes(v_ml, v_ml_err, 10000)
elseif isa(v_ml_err,NamedTuple) # use uncorrelated fit parameter uncertainties
v_mc = get_mc_value_shapes(v_ml, v_ml_err, 1000)
end
fwhm_mc = estimate_fwhm.(v_mc)
fwhm_err = std(fwhm_mc[isfinite.(fwhm_mc)])
return fwhm, fwhm_err
end
export get_peak_fwhm_th228
"""
fitCalibration
Fit the calibration lines to a linear function.
# Returns
* `slope`: the slope of the linear fit
* `intercept`: the intercept of the linear fit
"""
function fit_calibration(peaks::Array, μ::Array)
@assert length(peaks) == length(μ)
@debug "Fit calibration curve with linear function"
calib_fit_result = linregress(peaks, μ)
return LinearRegression.slope(calib_fit_result)[1], LinearRegression.bias(calib_fit_result)[1]
end
export fit_calibration