From 06c310a5c1d2ba3c341f62060835ce866c951ab5 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Mon, 16 Dec 2024 01:06:54 +0100 Subject: [PATCH 01/13] Fix bin_width + Bug Fixes --- src/sipm_simple_calibration.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index 58590f7b..b8d6adf4 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -74,11 +74,11 @@ function find_peaks( min_amp = initial_min_amp max_quantile = initial_max_quantile max_amp = quantile(amps, max_quantile) - bin_width = get_friedman_diaconis_bin_width(filter(in(quantile(amps, 0.01)..quantile(amps, 0.9)), amps)) + bin_width = get_friedman_diaconis_bin_width(filter(in(min_amp..quantile(amps, 0.9)), amps)) # Initial peak search h_uncal = fit(Histogram, amps, min_amp:bin_width:max_amp) - h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=true, threshold=peakfinder_threshold) + h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=false, threshold=peakfinder_threshold) # Ensure at least 2 peaks num_peaks = length(peakpos) @@ -98,18 +98,18 @@ function find_peaks( # while less than two peaks found, or second peakpos smaller than 1 pe peakpos, or gain smaller than 1 (peaks too close) while num_peaks < 2 || peakpos[2] <= first_pe_peak_pos || (peakpos[2] - peakpos[1]) <= 1.0 # Adjust σ and recheck peaks - if peakfinder_σ < 10.0 - println("Increasing peakfinder_σ: ", peakfinder_σ) + if peakfinder_σ < 25.0 + @debug "Increasing peakfinder_σ: $peakfinder_σ" peakfinder_σ += 0.5 else # If σ can't increase further, reduce threshold - println("Adjusting peakfinder_threshold: ", peakfinder_threshold) + @debug "Adjusting peakfinder_threshold: $peakfinder_threshold" peakfinder_threshold -= 1.0 peakfinder_σ = 2.0 # Reset σ for new threshold # Safety check to avoid lowering threshold too much if peakfinder_threshold < 2.0 - error("Unable to find two distinct peaks within reasonable quantile range.") + throw(ErrorException("Unable to find two peaks within reasonable quantile range.")) end end @@ -120,7 +120,7 @@ function find_peaks( # Safety check to avoid infinite loops if peakfinder_σ >= 10.0 && peakfinder_threshold < 2.0 - error("Unable to find two peaks within reasonable quantile range.") + throw(ErrorException("Unable to find two peaks within reasonable quantile range.")) end end From 3c67043b3f1e61ccc6b3eee68eb91e351b6fc9ab Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Wed, 18 Dec 2024 23:23:07 +0100 Subject: [PATCH 02/13] Update binning --- src/sipm_simple_calibration.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index b8d6adf4..a4bb87c6 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -67,17 +67,14 @@ end function find_peaks( - amps::Vector{<:Real}; initial_min_amp::Real=1.0, initial_max_quantile::Real=0.99, + amps::Vector{<:Real}; initial_min_amp::Real=1.0, initial_max_amp::Real=100.0, initial_max_bin_width_quantile::Real=0.9, peakfinder_σ::Real=2.0, peakfinder_threshold::Real=10.0 ) # Start with a big window where the noise peak is included - min_amp = initial_min_amp - max_quantile = initial_max_quantile - max_amp = quantile(amps, max_quantile) - bin_width = get_friedman_diaconis_bin_width(filter(in(min_amp..quantile(amps, 0.9)), amps)) + bin_width = get_friedman_diaconis_bin_width(filter(in(initial_min_amp..quantile(amps, initial_max_bin_width_quantile)), amps)) # Initial peak search - h_uncal = fit(Histogram, amps, min_amp:bin_width:max_amp) + h_uncal = fit(Histogram, amps, initial_min_amp:bin_width:initial_max_amp) h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=false, threshold=peakfinder_threshold) # Ensure at least 2 peaks @@ -114,12 +111,12 @@ function find_peaks( end # Find peaks with updated parameters - h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=true, threshold=peakfinder_threshold) + h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=false, threshold=peakfinder_threshold) filter!(x -> x >= first_pe_peak_pos, peakpos) num_peaks = length(peakpos) # Safety check to avoid infinite loops - if peakfinder_σ >= 10.0 && peakfinder_threshold < 2.0 + if peakfinder_σ >= 10.0 && peakfinder_threshold < 0.1 throw(ErrorException("Unable to find two peaks within reasonable quantile range.")) end end From 463d52681f1e4e22e25a476278a2936b1a0d1058 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Mon, 30 Dec 2024 09:36:30 +0100 Subject: [PATCH 03/13] STASH --- src/sipm_simple_calibration.jl | 88 +++++++++++++--------------------- 1 file changed, 34 insertions(+), 54 deletions(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index a4bb87c6..37c8392d 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -27,23 +27,39 @@ function sipm_simple_calibration end export sipm_simple_calibration function sipm_simple_calibration(pe_uncal::Vector{<:Real}; - kwargs...) + min_pe_peak::Int=2, max_pe_peak::Int=5, relative_cut_noise_cut::Real=0.5, n_fwhm_noise_cut::Real=5.0, + initial_min_amp::Real=1.0, initial_max_amp::Real=25.0, initial_max_bin_width_quantile::Real=0.9, + peakfinder_σ::Real=2.0, peakfinder_threshold::Real=10.0, peakfinder_rtol::Real=0.1, peakfinder_α::Real=0.05 +) - h_uncal, peakpos = find_peaks(pe_uncal; kwargs...) + # Start with a big window where the noise peak is included + bin_width = get_friedman_diaconis_bin_width(filter(in(initial_min_amp..quantile(pe_uncal, initial_max_bin_width_quantile)), pe_uncal)) + + # Initial peak search + h_uncal = fit(Histogram, pe_uncal, initial_min_amp:bin_width:initial_max_amp) + + cuts_1pe = cut_single_peak(pe_uncal, initial_min_amp, initial_max_amp, relative_cut=relative_cut_noise_cut) + + h_uncal_cut = fit(Histogram, pe_uncal, cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max):bin_width:initial_max_amp) + + c, h_deconv, peakpos, threshold = RadiationSpectra.determine_calibration_constant_through_peak_ratios(h_uncal_cut, collect(range(min_pe_peak, max_pe_peak, step=1)), + min_n_peaks = 2, max_n_peaks = 2*max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) + # simple calibration sort!(peakpos) - gain = peakpos[2] - peakpos[1] + gain = peakpos[min_pe_peak + 1] - peakpos[min_pe_peak] c = 1/gain - offset = - (peakpos[1] * c - 1) + offset = - (peakpos[min_pe_peak] * c - min_pe_peak) f_simple_calib = x -> x .* c .+ offset f_simple_uncal = x -> (x .- offset) ./ c - pe_simple_cal = pe_uncal .* c .+ offset - peakpos_cal = peakpos .* c .+ offset + + pe_simple_cal = f_simple_calib.(pe_uncal) + peakpos_cal = f_simple_calib.(peakpos) bin_width_cal = get_friedman_diaconis_bin_width(filter(in(0.5..1.5), pe_simple_cal)) - bin_width_uncal = get_friedman_diaconis_bin_width(filter(in( (0.5 - offset) / c .. (1.5 - offset) / c), pe_simple_cal)) + bin_width_uncal = f_simple_uncal(bin_width_cal) - f_simple_uncal(0.0) h_calsimple = fit(Histogram, pe_simple_cal, 0.0:bin_width_cal:6.0) h_uncal = fit(Histogram, pe_uncal, 0.0:bin_width_uncal:(6.0 - offset) / c) @@ -67,59 +83,23 @@ end function find_peaks( - amps::Vector{<:Real}; initial_min_amp::Real=1.0, initial_max_amp::Real=100.0, initial_max_bin_width_quantile::Real=0.9, - peakfinder_σ::Real=2.0, peakfinder_threshold::Real=10.0 + amps::Vector{<:Real}; + min_pe_peak::Real=1.0, max_pe_peak::Real=5.0, n_fwhm_noise_cut::Real=5.0, + initial_min_amp::Real=1.0, initial_max_amp::Real=100.0, initial_max_bin_width_quantile::Real=0.9, + peakfinder_σ::Real=2.0, peakfinder_threshold::Real=10.0, peakfinder_rtol::Real=0.1, peakfinder_α::Real=0.05 ) # Start with a big window where the noise peak is included bin_width = get_friedman_diaconis_bin_width(filter(in(initial_min_amp..quantile(amps, initial_max_bin_width_quantile)), amps)) # Initial peak search h_uncal = fit(Histogram, amps, initial_min_amp:bin_width:initial_max_amp) - h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=false, threshold=peakfinder_threshold) - - # Ensure at least 2 peaks - num_peaks = length(peakpos) - if num_peaks == 0 - error("No peaks found.") - end - - # Determine the 1 p.e. peak position based on the assumption that it is the highest - peakpos_idxs = StatsBase.binindex.(Ref(h_decon), peakpos) - cts_peakpos = h_decon.weights[peakpos_idxs] - first_pe_peak_pos = peakpos[argmax(cts_peakpos)] - - # Remove all peaks with x vals < x pos of 1p.e. peak - filter!(x -> x >= first_pe_peak_pos, peakpos) - num_peaks = length(peakpos) - - # while less than two peaks found, or second peakpos smaller than 1 pe peakpos, or gain smaller than 1 (peaks too close) - while num_peaks < 2 || peakpos[2] <= first_pe_peak_pos || (peakpos[2] - peakpos[1]) <= 1.0 - # Adjust σ and recheck peaks - if peakfinder_σ < 25.0 - @debug "Increasing peakfinder_σ: $peakfinder_σ" - peakfinder_σ += 0.5 - else - # If σ can't increase further, reduce threshold - @debug "Adjusting peakfinder_threshold: $peakfinder_threshold" - peakfinder_threshold -= 1.0 - peakfinder_σ = 2.0 # Reset σ for new threshold - - # Safety check to avoid lowering threshold too much - if peakfinder_threshold < 2.0 - throw(ErrorException("Unable to find two peaks within reasonable quantile range.")) - end - end - - # Find peaks with updated parameters - h_decon, peakpos = peakfinder(h_uncal, σ=peakfinder_σ, backgroundRemove=false, threshold=peakfinder_threshold) - filter!(x -> x >= first_pe_peak_pos, peakpos) - num_peaks = length(peakpos) - - # Safety check to avoid infinite loops - if peakfinder_σ >= 10.0 && peakfinder_threshold < 0.1 - throw(ErrorException("Unable to find two peaks within reasonable quantile range.")) - end - end + + cuts_1pe = cut_single_peak(amps, initial_min_amp, initial_max_amp, relative_cut=0.5) + + h_uncal_cut = fit(Histogram, trig_max, cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max):bin_width:initial_max_amp) + + c, h_deconv, peak_positions, threshold = RadiationSpectra.determine_calibration_constant_through_peak_ratios(h_uncal_cut, collect(range(min_pe_peak, max_pe_peak, step=1)), + min_n_peaks = 2, max_n_peaks = 2*max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) return h_decon, peakpos end \ No newline at end of file From a980c53227a35231e1054d6abb142134f8d6d9c9 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Wed, 22 Jan 2025 04:29:52 +0100 Subject: [PATCH 04/13] Add SiPM filter optimization based on window length sweep --- ext/LegendSpecFitsRecipesBaseExt.jl | 36 ++++++- src/LegendSpecFits.jl | 1 + src/sipm_filter_optimization.jl | 139 ++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 src/sipm_filter_optimization.jl diff --git a/ext/LegendSpecFitsRecipesBaseExt.jl b/ext/LegendSpecFitsRecipesBaseExt.jl index 8878a9fb..8a87e69f 100644 --- a/ext/LegendSpecFitsRecipesBaseExt.jl +++ b/ext/LegendSpecFitsRecipesBaseExt.jl @@ -154,6 +154,36 @@ end end end + +@recipe function f(report:: NamedTuple{(:wl, :min_obj, :gain, :res_1pe, :pos_1pe, :threshold, :a_grid_wl_sg, :obj, :report_simple, :report_fit)}) + xlabel := "Window Length ($(unit(first(report.a_grid_wl_sg))))" + ylabel := "Objective \n σ * √(threshold) \n ______________ \n gain * √(pos_1pe)" + grid := :true + gridcolor := :black + gridalpha := 0.2 + gridlinewidth := 0.5 + ylims := (0, 1.5 * maximum(Measurements.value.(report.obj))) + @series begin + seriestype := :scatter + label := "Obj" + ustrip.(report.a_grid_wl_sg), ustrip.(report.obj) + end + @series begin + seriestype := :hline + label := "Min. Obj $(report.min_obj) (WL: $(report.wl))" + color := :red + linewidth := 2.5 + [ustrip(Measurements.value(report.min_obj))] + end + @series begin + seriestype := :hspan + label := "" + color := :red + alpha := 0.1 + ustrip.([Measurements.value(report.min_obj)-Measurements.uncertainty(report.min_obj), Measurements.value(report.min_obj)+Measurements.uncertainty(report.min_obj)]) + end +end + @recipe function f(report::NamedTuple{(:v, :h, :f_fit, :f_components, :gof)}; show_label=true, show_fit=true, show_components=true, show_residuals=true, f_fit_x_step_scaling=1/100, _subplot=1, x_label="Energy (keV)") f_fit_x_step = ustrip(value(report.v.σ)) * f_fit_x_step_scaling bin_centers = collect(report.h.edges[1])[1:end-1] .+ diff(collect(report.h.edges[1]))[1]/2 @@ -509,14 +539,14 @@ end pps = report.peakpos end xlims := (0, last(first(h.edges))) - min_y = minimum(h.weights) == 0.0 ? 1e-3*maximum(h.weights) : 0.8*minimum(h.weights) + min_y = minimum(h.weights) == 0.0 ? 1e1 : 0.8*minimum(h.weights) ylims --> (min_y, maximum(h.weights)*1.1) @series begin seriestype := :stepbins label := "amps" h end - y_vline = min_y:1:maximum(h.weights)*1.1 + y_vline = [min_y, maximum(h.weights)*1.1] for (i, p) in enumerate(pps) @series begin seriestype := :line @@ -544,7 +574,7 @@ end ylabel := "Counts / $(round_wo_units(report_sipm.bin_width * 1e3, digits=2))E-3 P.E." xlims := (first(first(report_sipm.h_cal.edges)), last(first(report_sipm.h_cal.edges))) xticks := (ceil(first(first(report_sipm.h_cal.edges)))-0.5:0.5:last(first(report_sipm.h_cal.edges))) - min_y = minimum(report_sipm.h_cal.weights) == 0.0 ? 1e-3*maximum(report_sipm.h_cal.weights) : 0.8*minimum(report_sipm.h_cal.weights) + min_y = minimum(report_sipm.h_cal.weights) == 0.0 ? 1e1 : 0.8*minimum(report_sipm.h_cal.weights) ylims := (min_y, maximum(report_sipm.h_cal.weights)*1.1) bin_centers = collect(report_sipm.h_cal.edges[1])[1:end-1] .+ diff(collect(report_sipm.h_cal.edges[1]))[1]/2 @series begin diff --git a/src/LegendSpecFits.jl b/src/LegendSpecFits.jl index 63a84aa8..6d4946b1 100644 --- a/src/LegendSpecFits.jl +++ b/src/LegendSpecFits.jl @@ -81,6 +81,7 @@ include("specfit_functions.jl") include("calfunc.jl") include("sipm_simple_calibration.jl") include("sipmfit.jl") +include("sipm_filter_optimization.jl") abstract type UncertTag end ForwardDiff.:(≺)(::Type{<:ForwardDiff.Tag}, ::Type{UncertTag}) = true ForwardDiff.:(≺)(::Type{UncertTag}, ::Type{<:ForwardDiff.Tag}) = false diff --git a/src/sipm_filter_optimization.jl b/src/sipm_filter_optimization.jl new file mode 100644 index 00000000..3ecb51bb --- /dev/null +++ b/src/sipm_filter_optimization.jl @@ -0,0 +1,139 @@ + +""" + fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRangeLen) + +Fit the SiPM spectrum for different window lengths and return the optimal window length. + +# Arguments +- `trig_max_grid`: grid of trigger maxima for different window lengths +- `e_grid_wl`: range of window lengths to sweep through + +# Returns +- `result`: optimal window length and corresponding gain, resolution and position of 1pe peak +- `report`: report with all window lengths and corresponding gains, resolutions and positions of 1pe peaks +""" +function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRangeLen, thresholds::Vector{<:Real}=zeros(length(e_grid_wl)); + min_pe_peak::Int=1, max_pe_peak::Int=5, n_fwhm_noise_cut::Real=2.0, peakfinder_threshold::Real=5.0, initial_max_amp::Real = 50.0, initial_max_bin_width_quantile::Real=0.9999, + peakfinder_rtol::Real=0.1, peakfinder_α::Real=0.1, peakfinder_σ::Real=-1.0, + min_pe_fit::Real=0.6, max_pe_fit::Real=3.5, Δpe_peak_assignment::Real=0.3) + + gain_wl = Vector{Measurement{Float64}}(undef, length(e_grid_wl)) + res_1pe_wl = Vector{Measurement{Float64}}(undef, length(e_grid_wl)) + pos_1pe_wl = Vector{Measurement{Float64}}(undef, length(e_grid_wl)) + success = Vector{Bool}(zeros(length(e_grid_wl))) + reports_simple = Vector{NamedTuple}(undef, length(e_grid_wl)) + reports_fit = Vector{NamedTuple}(undef, length(e_grid_wl)) + + # for each window lenght, calculate gain, resolution and position of 1pe peak + Threads.@threads for w in eachindex(e_grid_wl) + wl = e_grid_wl[w] + trig_max = filter(isfinite, collect(trig_max_grid[w])) + threshold = thresholds[w] + try + result_simple, report_simple = sipm_simple_calibration(trig_max; initial_min_amp=threshold, initial_max_amp=initial_max_amp, initial_max_bin_width_quantile=initial_max_bin_width_quantile, + min_pe_peak=min_pe_peak, max_pe_peak=max_pe_peak, n_fwhm_noise_cut=n_fwhm_noise_cut, peakfinder_threshold=peakfinder_threshold, + peakfinder_rtol=peakfinder_rtol, peakfinder_α=peakfinder_α, peakfinder_σ=peakfinder_σ) + # min_pe_peak=1, n_fwhm_noise_cut=2.0, peakfinder_threshold=5.0, initial_max_bin_width_quantile=0.9999, peakfinder_rtol=0.1, peakfinder_α=0.1, peakfinder_σ=-1.0) + + result_fit, report_fit = fit_sipm_spectrum(result_simple.pe_simple_cal, min_pe_fit, max_pe_fit; f_uncal=result_simple.f_simple_uncal, Δpe_peak_assignment=Δpe_peak_assignment) + + gain_wl[w] = minimum(result_simple.peakpos) - ifelse(threshold == 0.0, result_simple.noisepeakpos, threshold) + res_1pe_wl[w] = first(result_fit.resolutions) + pos_1pe_wl[w] = first(result_fit.positions) + reports_simple[w] = report_simple + reports_fit[w] = report_fit + success[w] = true + catch e + @warn "Failed to process wl: $wl: $e" + end + end + + thrs = if all(thresholds .== 0.0) ones(length(e_grid_wl)) else thresholds end + obj = sqrt.(res_1pe_wl[success]) .* sqrt.(thrs[success]) ./ gain_wl[success] ./ sqrt.(pos_1pe_wl[success]) + wls = collect(e_grid_wl)[success] + + if isempty(obj) + @error "No valid gain found" + throw(ErrorException("No valid gain found, could not determine optimal window length")) + end + min_obj = minimum(obj) + wl_min_obj = wls[findmin(obj)[2]] + min_res1pe = res_1pe_wl[success][findmin(obj)[2]] + min_gain = gain_wl[success][findmin(obj)[2]] + min_pos1pe = pos_1pe_wl[success][findmin(obj)[2]] + min_threshold = thresholds[success][findmin(obj)[2]] + report_simple = reports_simple[success][findmin(obj)[2]] + report_fit = reports_fit[success][findmin(obj)[2]] + + # generate result and report + result = ( + wl = measurement(wl_min_obj, step(e_grid_wl)), + obj = min_obj, + res_1pe = min_res1pe, + gain = min_gain, + pos_1pe = min_pos1pe, + threshold = min_threshold + ) + report = ( + wl = result.wl, + min_obj = result.obj, + gain = result.gain, + res_1pe = result.res_1pe, + pos_1pe = result.pos_1pe, + threshold = result.threshold, + a_grid_wl_sg = wls, + obj = obj, + report_simple = report_simple, + report_fit = report_fit, + ) + return result, report +end +export fit_sipm_wl + + + +""" + fit_sipm_threshold(thresholds::Vector{<:Real}, min_cut::Real=minimum(thresholds), max_cut::Real=maximum(thresholds); n_bins::Int=-1, relative_cut::Real=0.2, fit_thresholds::Bool=true, uncertainty::Bool=true) + +Fit the SiPM threshold spectrum and return the optimal threshold. + +# Arguments +- `thresholds`: vector of thresholds +- `min_cut`: minimum threshold +- `max_cut`: maximum threshold +- `n_bins`: number of bins for histogram +- `relative_cut`: relative cut for threshold +- `fit_thresholds`: fit thresholds +- `uncertainty`: calculate uncertainty + +# Returns +- `result`: optimal threshold and corresponding gain, resolution and position of 1pe peak +- `report`: report with all thresholds and corresponding gains, resolutions and positions of 1pe peaks +""" +function fit_sipm_threshold(thresholds::Vector{<:Real}, min_cut::Real=minimum(thresholds), max_cut::Real=maximum(thresholds); n_bins::Int=-1, relative_cut::Real=0.2, fit_thresholds::Bool=true, uncertainty::Bool=true) + # cut out thresholds + filter!(in(min_cut .. max_cut), thresholds) + + # get bin_width + h = if n_bins < 1 + fit(Histogram, thresholds, min_cut:get_friedman_diaconis_bin_width(thresholds):max_cut) + else + fit(Histogram, thresholds, n_bins) + end + # get simple thresholds + result_simple = (μ_simple = mean(thresholds), σ_simple = std(thresholds)) + + # fit histogram + result_trig, report_trig = if fit_thresholds + # generate cuts for thresholds + cuts_thres = cut_single_peak(thresholds, min_cut, max_cut; n_bins=n_bins, relative_cut=relative_cut) + # fit histogram + fit_binned_trunc_gauss(h, cuts_thres; uncertainty=uncertainty) + else + (μ = result_simple.μ_simple, σ = result_simple.σ_simple), h + end + # get simple std and mu values + result = merge(result_trig, result_simple) + return result, report_trig +end +export fit_sipm_threshold \ No newline at end of file From 221636bdfef91628886c710b8b2a246cafabb289 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Wed, 22 Jan 2025 04:30:17 +0100 Subject: [PATCH 05/13] Decrease default nIter to 25 --- src/sipmfit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sipmfit.jl b/src/sipmfit.jl index 5d4d76da..283d7f0a 100644 --- a/src/sipmfit.jl +++ b/src/sipmfit.jl @@ -24,7 +24,7 @@ Fit a Gaussian Mixture Model to the given pe calibration data and return the fit - `report`: a tuple with the fit report which can be plotted via a recipe """ function fit_sipm_spectrum(pe_cal::Vector{<:Real}, min_pe::Real=0.5, max_pe::Real=3.5; - n_mixtures::Int=ceil(Int, (max_pe - min_pe) * 4), nIter::Int=50, nInit::Int=50, + n_mixtures::Int=ceil(Int, (max_pe - min_pe) * 4), nIter::Int=25, nInit::Int=50, method::Symbol=:kmeans, kind=:diag, Δpe_peak_assignment::Real=0.3, f_uncal::Function=identity, uncertainty::Bool=true) # first filter peak positions out of amplitude vector From 1910588315d7ad3059bb4db61bcdde243afee5cd Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Wed, 22 Jan 2025 04:30:52 +0100 Subject: [PATCH 06/13] Refactor simple calibration based on cutting away noise peak and usage of peakfinder with peak ratios --- src/sipm_simple_calibration.jl | 90 +++++++++++++++++----------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index 37c8392d..05c94af3 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -27,42 +27,65 @@ function sipm_simple_calibration end export sipm_simple_calibration function sipm_simple_calibration(pe_uncal::Vector{<:Real}; - min_pe_peak::Int=2, max_pe_peak::Int=5, relative_cut_noise_cut::Real=0.5, n_fwhm_noise_cut::Real=5.0, - initial_min_amp::Real=1.0, initial_max_amp::Real=25.0, initial_max_bin_width_quantile::Real=0.9, - peakfinder_σ::Real=2.0, peakfinder_threshold::Real=10.0, peakfinder_rtol::Real=0.1, peakfinder_α::Real=0.05 + min_pe_peak::Int=1, max_pe_peak::Int=5, relative_cut_noise_cut::Real=0.5, n_fwhm_noise_cut::Real=5.0, + initial_min_amp::Real=0.0, initial_max_amp::Real=50.0, initial_max_bin_width_quantile::Real=0.9, + peakfinder_σ::Real=-1.0, peakfinder_threshold::Real=10.0, peakfinder_rtol::Real=0.1, peakfinder_α::Real=0.05 ) - # Start with a big window where the noise peak is included - bin_width = get_friedman_diaconis_bin_width(filter(in(initial_min_amp..quantile(pe_uncal, initial_max_bin_width_quantile)), pe_uncal)) - # Initial peak search - h_uncal = fit(Histogram, pe_uncal, initial_min_amp:bin_width:initial_max_amp) - cuts_1pe = cut_single_peak(pe_uncal, initial_min_amp, initial_max_amp, relative_cut=relative_cut_noise_cut) - - h_uncal_cut = fit(Histogram, pe_uncal, cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max):bin_width:initial_max_amp) - - c, h_deconv, peakpos, threshold = RadiationSpectra.determine_calibration_constant_through_peak_ratios(h_uncal_cut, collect(range(min_pe_peak, max_pe_peak, step=1)), - min_n_peaks = 2, max_n_peaks = 2*max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) - + + bin_width_cut_min = cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max) + bin_width_cut = get_friedman_diaconis_bin_width(filter(in(bin_width_cut_min..quantile(pe_uncal, initial_max_bin_width_quantile)), pe_uncal)) + peakpos = [] + for bin_width_scale in 10 .^ range(0, stop=-3, length=50) + @debug "Using bin width: $(bin_width_cut)" + + bin_width_cut = bin_width_cut * bin_width_scale + h_uncal_cut = fit(Histogram, pe_uncal, bin_width_cut_min:bin_width_cut:initial_max_amp) + if peakfinder_σ <= 0.0 + peakfinder_σ = round(Int, 2*(cuts_1pe.high - cuts_1pe.max) / bin_width_cut / 2.355) + end + @debug "Peakfinder σ: $(peakfinder_σ)" + try + c, h_deconv, peakpos, threshold = RadiationSpectra.determine_calibration_constant_through_peak_ratios(h_uncal_cut, collect(range(min_pe_peak, max_pe_peak, step=1)), + min_n_peaks = 2, max_n_peaks = 2*max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) + catch e + @warn "Failed to find peaks with bin width scale $(bin_width_scale): $(e)" + continue + else + @debug "Found peaks with bin width scale $(bin_width_scale)" + if !isempty(peakpos) + break + end + end + end + + if isempty(peakpos) || length(peakpos) < 2 + throw(ErrorException("Failed to find peaks")) + end # simple calibration sort!(peakpos) - gain = peakpos[min_pe_peak + 1] - peakpos[min_pe_peak] + @debug "Found $(min_pe_peak) PE Peak positions: $(peakpos[1])" + @debug "Found $(min_pe_peak+1) PE Peak positions: $(peakpos[2])" + gain = peakpos[2] - peakpos[1] + @debug "Calculated gain: $(round(gain, digits=2))" c = 1/gain - offset = - (peakpos[min_pe_peak] * c - min_pe_peak) + offset = - (peakpos[1] * c - min_pe_peak) + @debug "Calculated offset: $(round(offset, digits=2))" f_simple_calib = x -> x .* c .+ offset f_simple_uncal = x -> (x .- offset) ./ c pe_simple_cal = f_simple_calib.(pe_uncal) peakpos_cal = f_simple_calib.(peakpos) - - bin_width_cal = get_friedman_diaconis_bin_width(filter(in(0.5..1.5), pe_simple_cal)) + + bin_width_cal = get_friedman_diaconis_bin_width(filter(in(0.5..min_pe_peak), pe_simple_cal)) bin_width_uncal = f_simple_uncal(bin_width_cal) - f_simple_uncal(0.0) - h_calsimple = fit(Histogram, pe_simple_cal, 0.0:bin_width_cal:6.0) - h_uncal = fit(Histogram, pe_uncal, 0.0:bin_width_uncal:(6.0 - offset) / c) + h_calsimple = fit(Histogram, pe_simple_cal, 0.0:bin_width_cal:max_pe_peak + 1) + h_uncal = fit(Histogram, pe_uncal, 0.0:bin_width_uncal:f_simple_uncal(max_pe_peak + 1)) result = ( pe_simple_cal = pe_simple_cal, @@ -70,7 +93,9 @@ function sipm_simple_calibration(pe_uncal::Vector{<:Real}; f_simple_calib = f_simple_calib, f_simple_uncal = f_simple_uncal, c = c, - offset = offset + offset = offset, + noisepeakpos = cuts_1pe.max, + noisepeakwidth = cuts_1pe.high - cuts_1pe.low ) report = ( peakpos = peakpos, @@ -79,27 +104,4 @@ function sipm_simple_calibration(pe_uncal::Vector{<:Real}; h_calsimple = h_calsimple ) return result, report -end - - -function find_peaks( - amps::Vector{<:Real}; - min_pe_peak::Real=1.0, max_pe_peak::Real=5.0, n_fwhm_noise_cut::Real=5.0, - initial_min_amp::Real=1.0, initial_max_amp::Real=100.0, initial_max_bin_width_quantile::Real=0.9, - peakfinder_σ::Real=2.0, peakfinder_threshold::Real=10.0, peakfinder_rtol::Real=0.1, peakfinder_α::Real=0.05 -) - # Start with a big window where the noise peak is included - bin_width = get_friedman_diaconis_bin_width(filter(in(initial_min_amp..quantile(amps, initial_max_bin_width_quantile)), amps)) - - # Initial peak search - h_uncal = fit(Histogram, amps, initial_min_amp:bin_width:initial_max_amp) - - cuts_1pe = cut_single_peak(amps, initial_min_amp, initial_max_amp, relative_cut=0.5) - - h_uncal_cut = fit(Histogram, trig_max, cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max):bin_width:initial_max_amp) - - c, h_deconv, peak_positions, threshold = RadiationSpectra.determine_calibration_constant_through_peak_ratios(h_uncal_cut, collect(range(min_pe_peak, max_pe_peak, step=1)), - min_n_peaks = 2, max_n_peaks = 2*max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) - - return h_decon, peakpos end \ No newline at end of file From 434680ea61aa6f0fc2a0e69fe04c601ce8a80932 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Fri, 31 Jan 2025 13:32:08 +0100 Subject: [PATCH 07/13] Bug Fix calibration constant --- src/sipm_simple_calibration.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index 05c94af3..0598958e 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -49,7 +49,7 @@ function sipm_simple_calibration(pe_uncal::Vector{<:Real}; @debug "Peakfinder σ: $(peakfinder_σ)" try c, h_deconv, peakpos, threshold = RadiationSpectra.determine_calibration_constant_through_peak_ratios(h_uncal_cut, collect(range(min_pe_peak, max_pe_peak, step=1)), - min_n_peaks = 2, max_n_peaks = 2*max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) + min_n_peaks = 2, max_n_peaks = max_pe_peak, threshold=peakfinder_threshold, rtol=peakfinder_rtol, α=peakfinder_α, σ=peakfinder_σ) catch e @warn "Failed to find peaks with bin width scale $(bin_width_scale): $(e)" continue From 3327a02e52022348cd8d2d3d01d73850ffce85d9 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Fri, 31 Jan 2025 13:32:43 +0100 Subject: [PATCH 08/13] Bug Fix resolution calculation --- src/sipmfit.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sipmfit.jl b/src/sipmfit.jl index 283d7f0a..b2e109d6 100644 --- a/src/sipmfit.jl +++ b/src/sipmfit.jl @@ -104,10 +104,10 @@ function fit_sipm_spectrum(pe_cal::Vector{<:Real}, min_pe::Real=0.5, max_pe::Rea # get pe_pos get_pe_pos = pe -> let sel = in.(μ, (-Δpe_peak_assignment..Δpe_peak_assignment) .+ pe) - dot(view(μ,sel), view(w,sel)) / sum(view(w,sel)) + dot(view(μ, sel), view(w, sel)) / sum(view(w, sel)) end get_pe_res = pe -> let sel = in.(μ, (-Δpe_peak_assignment..Δpe_peak_assignment) .+ pe) - dot(view(σ,sel), view(w,sel)) / sum(view(w,sel)) + sqrt(dot(view(σ, sel).^2, view(w, sel).^2)) end n_pos_mixtures = [count(in.(μ, (-Δpe_peak_assignment..Δpe_peak_assignment) .+ pe)) for pe in pes] From e8f0a6ece77657f305aab0b2a6c99dc70cee95f1 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Fri, 31 Jan 2025 13:33:12 +0100 Subject: [PATCH 09/13] Use kwargs for all fit parameters + return correct gain --- src/sipm_filter_optimization.jl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sipm_filter_optimization.jl b/src/sipm_filter_optimization.jl index 3ecb51bb..5057dfd3 100644 --- a/src/sipm_filter_optimization.jl +++ b/src/sipm_filter_optimization.jl @@ -33,11 +33,11 @@ function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRang result_simple, report_simple = sipm_simple_calibration(trig_max; initial_min_amp=threshold, initial_max_amp=initial_max_amp, initial_max_bin_width_quantile=initial_max_bin_width_quantile, min_pe_peak=min_pe_peak, max_pe_peak=max_pe_peak, n_fwhm_noise_cut=n_fwhm_noise_cut, peakfinder_threshold=peakfinder_threshold, peakfinder_rtol=peakfinder_rtol, peakfinder_α=peakfinder_α, peakfinder_σ=peakfinder_σ) - # min_pe_peak=1, n_fwhm_noise_cut=2.0, peakfinder_threshold=5.0, initial_max_bin_width_quantile=0.9999, peakfinder_rtol=0.1, peakfinder_α=0.1, peakfinder_σ=-1.0) result_fit, report_fit = fit_sipm_spectrum(result_simple.pe_simple_cal, min_pe_fit, max_pe_fit; f_uncal=result_simple.f_simple_uncal, Δpe_peak_assignment=Δpe_peak_assignment) - gain_wl[w] = minimum(result_simple.peakpos) - ifelse(threshold == 0.0, result_simple.noisepeakpos, threshold) + # gain_wl[w] = minimum(result_simple.peakpos) - ifelse(threshold == 0.0, result_simple.noisepeakpos, threshold) + gain_wl[w] = minimum(result_simple.peakpos) - result_simple.noisepeakpos res_1pe_wl[w] = first(result_fit.resolutions) pos_1pe_wl[w] = first(result_fit.positions) reports_simple[w] = report_simple @@ -49,7 +49,7 @@ function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRang end thrs = if all(thresholds .== 0.0) ones(length(e_grid_wl)) else thresholds end - obj = sqrt.(res_1pe_wl[success]) .* sqrt.(thrs[success]) ./ gain_wl[success] ./ sqrt.(pos_1pe_wl[success]) + obj = sqrt.(res_1pe_wl[success]) .* sqrt.(thrs[success]) ./ gain_wl[success] wls = collect(e_grid_wl)[success] if isempty(obj) @@ -62,8 +62,8 @@ function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRang min_gain = gain_wl[success][findmin(obj)[2]] min_pos1pe = pos_1pe_wl[success][findmin(obj)[2]] min_threshold = thresholds[success][findmin(obj)[2]] - report_simple = reports_simple[success][findmin(obj)[2]] - report_fit = reports_fit[success][findmin(obj)[2]] + min_report_simple = reports_simple[success][findmin(obj)[2]] + min_report_fit = reports_fit[success][findmin(obj)[2]] # generate result and report result = ( @@ -77,14 +77,14 @@ function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRang report = ( wl = result.wl, min_obj = result.obj, - gain = result.gain, - res_1pe = result.res_1pe, - pos_1pe = result.pos_1pe, - threshold = result.threshold, + gain = gain_wl, + res_1pe = res_1pe_wl, + pos_1pe = pos_1pe_wl, + threshold = thresholds[success], a_grid_wl_sg = wls, obj = obj, - report_simple = report_simple, - report_fit = report_fit, + report_simple = min_report_simple, + report_fit = min_report_fit, ) return result, report end From b1c8fe1b5f14939acf73865e791f883ef204727b Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Mon, 3 Feb 2025 15:10:53 +0100 Subject: [PATCH 10/13] Resolved comments --- src/sipm_filter_optimization.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sipm_filter_optimization.jl b/src/sipm_filter_optimization.jl index 5057dfd3..9ace64b9 100644 --- a/src/sipm_filter_optimization.jl +++ b/src/sipm_filter_optimization.jl @@ -20,7 +20,7 @@ function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRang gain_wl = Vector{Measurement{Float64}}(undef, length(e_grid_wl)) res_1pe_wl = Vector{Measurement{Float64}}(undef, length(e_grid_wl)) pos_1pe_wl = Vector{Measurement{Float64}}(undef, length(e_grid_wl)) - success = Vector{Bool}(zeros(length(e_grid_wl))) + success = falses(length(e_grid_wl)) reports_simple = Vector{NamedTuple}(undef, length(e_grid_wl)) reports_fit = Vector{NamedTuple}(undef, length(e_grid_wl)) @@ -56,14 +56,14 @@ function fit_sipm_wl(trig_max_grid::VectorOfVectors{<:Real}, e_grid_wl::StepRang @error "No valid gain found" throw(ErrorException("No valid gain found, could not determine optimal window length")) end - min_obj = minimum(obj) - wl_min_obj = wls[findmin(obj)[2]] - min_res1pe = res_1pe_wl[success][findmin(obj)[2]] - min_gain = gain_wl[success][findmin(obj)[2]] - min_pos1pe = pos_1pe_wl[success][findmin(obj)[2]] - min_threshold = thresholds[success][findmin(obj)[2]] - min_report_simple = reports_simple[success][findmin(obj)[2]] - min_report_fit = reports_fit[success][findmin(obj)[2]] + min_obj, min_obj_idx = findmin(obj) + wl_min_obj = wls[min_obj_idx] + min_res1pe = res_1pe_wl[success][min_obj_idx] + min_gain = gain_wl[success][min_obj_idx] + min_pos1pe = pos_1pe_wl[success][min_obj_idx] + min_threshold = thresholds[success][min_obj_idx] + min_report_simple = reports_simple[success][min_obj_idx] + min_report_fit = reports_fit[success][min_obj_idx] # generate result and report result = ( From 32e6590ccd3a0ac939f6cd13faaed2501f148c6e Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Mon, 3 Feb 2025 15:19:22 +0100 Subject: [PATCH 11/13] Bug fix iterative bin_width_cut algorithm --- src/sipm_simple_calibration.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index 0598958e..607e9e15 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -38,13 +38,13 @@ function sipm_simple_calibration(pe_uncal::Vector{<:Real}; bin_width_cut_min = cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max) bin_width_cut = get_friedman_diaconis_bin_width(filter(in(bin_width_cut_min..quantile(pe_uncal, initial_max_bin_width_quantile)), pe_uncal)) peakpos = [] - for bin_width_scale in 10 .^ range(0, stop=-3, length=50) + for bin_width_scale in exp10.(range(0, stop=-3, length=50)) # 1e-3 to 1 @debug "Using bin width: $(bin_width_cut)" - bin_width_cut = bin_width_cut * bin_width_scale - h_uncal_cut = fit(Histogram, pe_uncal, bin_width_cut_min:bin_width_cut:initial_max_amp) + bin_width_cut_scaled = bin_width_cut * bin_width_scale + h_uncal_cut = fit(Histogram, pe_uncal, bin_width_cut_min:bin_width_cut_scaled:initial_max_amp) if peakfinder_σ <= 0.0 - peakfinder_σ = round(Int, 2*(cuts_1pe.high - cuts_1pe.max) / bin_width_cut / 2.355) + peakfinder_σ = round(Int, 2*(cuts_1pe.high - cuts_1pe.max) / bin_width_cut_scaled / 2.355) end @debug "Peakfinder σ: $(peakfinder_σ)" try From 5a14d82ff152d38d91f58ea16c73e8d35f00ac7e Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Mon, 3 Feb 2025 15:23:43 +0100 Subject: [PATCH 12/13] Bump GaussianMixtures to 0.3.12 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2e3185c5..80f771bd 100644 --- a/Project.toml +++ b/Project.toml @@ -61,7 +61,7 @@ Distributions = "0.25.87" FillArrays = "1.4.1" Format = "1.2, 1.3" ForwardDiff = "0.10.26" -GaussianMixtures = "0.3.11" +GaussianMixtures = "0.3.12" IntervalSets = "0.7" InverseFunctions = "0.1.8" IrrationalConstants = "0.1.1, 0.2" From 2346c99cd3aae2248a7ba3c27abb8db856aeb2c9 Mon Sep 17 00:00:00 2001 From: Florian Henkes Date: Mon, 3 Feb 2025 15:24:50 +0100 Subject: [PATCH 13/13] Remove range comment --- src/sipm_simple_calibration.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sipm_simple_calibration.jl b/src/sipm_simple_calibration.jl index 607e9e15..85751b6e 100644 --- a/src/sipm_simple_calibration.jl +++ b/src/sipm_simple_calibration.jl @@ -38,7 +38,7 @@ function sipm_simple_calibration(pe_uncal::Vector{<:Real}; bin_width_cut_min = cuts_1pe.max+n_fwhm_noise_cut*(cuts_1pe.high - cuts_1pe.max) bin_width_cut = get_friedman_diaconis_bin_width(filter(in(bin_width_cut_min..quantile(pe_uncal, initial_max_bin_width_quantile)), pe_uncal)) peakpos = [] - for bin_width_scale in exp10.(range(0, stop=-3, length=50)) # 1e-3 to 1 + for bin_width_scale in exp10.(range(0, stop=-3, length=50)) @debug "Using bin width: $(bin_width_cut)" bin_width_cut_scaled = bin_width_cut * bin_width_scale